Last active
November 6, 2023 22:18
-
-
Save mattura/1b365a1fa21e5b9927cbd0c1c45fcbeb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Pixel Paint - Pixel Art By Numbers</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
font-size: 10pt; | |
background-color: #f4f4f4; /* Light background color */ | |
margin: 0; | |
padding: 0; | |
} | |
h1 { | |
color: #333; /* Dark text color for headings */ | |
margin: 0; | |
max-width: 400px; | |
overflow: hidden; | |
max-height: 72px; | |
} | |
h2, h3 { | |
color: #333; /* Dark text color for headings */ | |
margin: 10px 0 10px 0; | |
} | |
.container { | |
background-color: #fff; | |
display: flex; | |
justify-content: space-around; | |
align-items: flex-start; /* Align items at the top */ | |
flex-wrap: nowrap; | |
padding: 20px; /* Add padding as needed */ | |
max-width: 1024px; | |
margin: 0 auto; | |
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | |
} | |
.buttons { | |
display: inline-block; | |
padding: 5px 25px; /*10px 30px;*/ | |
background-color: #3498db; /* Blue button background color */ | |
color: #fff; /* White button text color */ | |
border: none; | |
cursor: pointer; | |
font-weight: bold; | |
text-transform: uppercase; /* Uppercase button text */ | |
transition: background-color 0.3s; | |
border-radius: 10px; | |
} | |
.buttons:hover { | |
background-color: #1c6bb7; /* Darker blue on hover */ | |
} | |
.mutex-buttons input[type="radio"] { | |
display: none; | |
} | |
.mutex-buttons label { | |
display: inline-block; | |
padding: 10px 20px; | |
background-color: #C0C0C0;/* #3498db; /* Blue button background color */ | |
color: #000; /* White button text color */ | |
border: none; | |
font-weight: bold; | |
/* border: solid 1px #1c6bb7; */ | |
text-transform: uppercase; /* Uppercase button text */ | |
transition: background-color 0.3s, border 0.3s; /* Transition for background and border */ | |
border-radius: 10px; | |
margin: 5px; | |
} | |
.mutex-buttons label:hover { | |
background-color: #1c6bb7;/*#3498db;*/ | |
color: #000; | |
} | |
.mutex-buttons input[type="radio"]:checked + label { | |
color: #fff; | |
background-color: #1c6bb7;/*#3498db;*/ | |
} | |
a { | |
color: #3498db; /* Blue link color */ | |
text-decoration: none; /* Remove underlines from links */ | |
transition: color 0.3s; /* Smooth color transition on hover */ | |
} | |
a:hover { | |
color: #1c6bb7; /* Darker blue on hover */ | |
} | |
.grid { | |
display: grid; | |
gap: 0; | |
margin: 5px 0 5px 0; /* top and button margin */ | |
} | |
.square { | |
width: 50px; | |
height: 50px; | |
border: 1px solid #000; | |
cursor: pointer; | |
position: relative; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
/* Force background graphics to be displayed in print dialogue */ | |
-webkit-print-color-adjust: exact; /* Chrome/Safari/Edge/Opera */ | |
color-adjust: exact; /* Firefox */ | |
touch-action: none; /* for preventDefault() alternative*/ | |
} | |
.square .colour-text { | |
text-align: center; | |
} | |
/* Prevent text selection on specific elements */ | |
.square, .colour-text, .colour, .toggle-buttons, .small-grid, .saved-title, .used-colour-box, .mutex-buttons label { | |
cursor: pointer; | |
user-select: none; | |
} | |
/* Add vertical spacing between palette and grid */ | |
.palette { | |
display: grid; | |
grid-template-columns: repeat(8, 1fr); | |
grid-gap: 5px; | |
justify-content: center; | |
align-items: center; | |
margin: 12px 0 12px 0; | |
box-sizing: border-box; | |
} | |
.colour { | |
height: 50px; | |
border: 1px solid #000; | |
box-sizing: border-box; | |
border-radius: 20px; | |
} | |
.colour.selected { | |
border: 3px solid #1c6bb7; /* Thicker border for the selected colour */ | |
box-shadow: 0 0 10px rgba(52, 152, 219, 0.8); | |
box-sizing: border-box; | |
} | |
.used-colours { | |
margin-top: 5px; | |
display: grid; | |
grid-auto-rows: 30px; | |
grid-template-columns: repeat(3,1fr); | |
grid-row-gap: 5px; | |
} | |
.used-colour { | |
display: flex; | |
align-items: center; | |
} | |
.used-colour input { | |
width: 40px; /* Reduce the input width */ | |
padding: 5px; /* same as box */ | |
border: 2px solid #000; | |
border-right: 0px; | |
text-align: right; | |
font-weight: bold; | |
box-sizing: border-box; | |
-moz-appearance: textfield; | |
} | |
.used-colour input[type="number"]::-webkit-inner-spin-button, | |
.used-colour input[type="number"]::-webkit-outer-spin-button { | |
-webkit-appearance: none; | |
appearance: none; | |
margin: 0; /* Optional: You can adjust margins if needed */ | |
} | |
.used-colour-box { | |
width: 100px; | |
padding: 5px; | |
margin-right: 10px; | |
flex-shrink: 0; /* Ensure the colour box doesn't shrink */ | |
border: 2px solid #000; /* Add a 2-pixel border */ | |
border-left: 0px; | |
font-weight: bold; | |
box-sizing: border-box; | |
/* Force background graphics to be displayed in print dialogue */ | |
-webkit-print-color-adjust: exact; /* Chrome/Safari/Edge/Opera */ | |
color-adjust: exact; /* Firefox */ | |
} | |
.side-panel, .preset-panel { | |
display: flex; | |
flex-direction: column; | |
text-align: center; | |
} | |
.saved-grids, .preset-grids { | |
width: auto; | |
display: grid; | |
grid-template-columns: repeat(2,1fr); | |
grid-auto-rows: 72px; | |
gap: 30px 15px; | |
overflow: scroll; | |
height: 576px; | |
} | |
.saved-title { | |
font-size: 6pt; | |
height: 20px; | |
max-width: 72px; | |
} | |
.saved-box { | |
} | |
.small-grid { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 0px; | |
box-sizing: border-box; | |
} | |
.small-square { | |
border: 0.5px solid #000; | |
width: 8px; | |
height: 8px; | |
display: flex; | |
box-sizing: border-box; | |
} | |
@media screen and (max-width: 880px) { | |
.square { | |
width: 30px; | |
height: 30px; | |
} | |
.container { | |
flex-wrap: wrap; | |
padding:5px; | |
justify-content: center; | |
column-gap: 15px; | |
} | |
.container > div { | |
/*max-width: calc(100% / 3);*/ | |
} | |
.container > div:first-child { | |
flex-basis: 100%; | |
max-width: 100%; | |
align-self: center; | |
display: grid; /* Utilising Flexbox for centre alignment */ | |
justify-content: center; /* Horizontally centre-aligns child elements */ | |
align-items: center; | |
} | |
.palette { | |
margin: 0px; | |
grid-gap: 2px; | |
} | |
.mutex-buttons label { | |
padding: 10px 8px; | |
margin: 0px; | |
} | |
.buttons { | |
width: auto; | |
padding: 7px 10px; | |
} | |
.colour { | |
height: 30px; | |
} | |
.used-colours { | |
grid-template-columns: repeat(2, 1fr); | |
} | |
.used-colour { | |
justify-content: center; | |
} | |
.used-colour input { | |
border-width: 1px; | |
} | |
.used-colour-box { | |
width: 85px; | |
border-width: 1px; | |
} | |
.saved-grids, .preset-grids { | |
overflow: scroll; | |
height: 300px; | |
} | |
} | |
/* Define the style for print media */ | |
@media print { | |
body { | |
display: block; | |
justify-content: center; | |
align-items: center; | |
height: auto; | |
margin: 0 0 0 10px; | |
/*page-break-after: always;*/ | |
background-color: #FFF; | |
} | |
.container { | |
box-shadow: unset; | |
} | |
.grid .square { | |
background-color: transparent !important; | |
color: #000000 !important; | |
/*width: 50px; | |
height: 50px; | |
position: relative;*/ | |
} | |
.grid { | |
/*grid-template-columns: repeat(9, 50px);*/ | |
} | |
.palette, .buttons, .side-panel, .preset-panel, .mutex-buttons { | |
display: none; | |
} | |
} | |
.palgrid { | |
/*display:flex; | |
justify-content: flex-start; | |
align-items: flex-start; | |
flex-wrap: nowrap;*/ | |
} | |
.control-buttons { | |
/*max-width: 100px;*/ | |
display: flex; | |
justify-content: space-between; | |
margin: 6px 0 6px 0; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div> | |
<h1 id='artname' contenteditable="true">Pixel Painting</h1> | |
<div class='palgrid'> | |
<div class="control-buttons"> | |
<button class="buttons" id="clearButton">Clear</button> | |
<button class="buttons" id="printButton">Print</button> | |
<button class="buttons" id="paletteButton">Palette</button> | |
<button class="buttons" id="shareButton">Share</button> | |
<button class="buttons" id="saveButton">Save</button> | |
</div> | |
<div id="paletteContainer" class="palette"> | |
</div> | |
</div> | |
<div class="grid" id="grid-container"> | |
</div> | |
<div class="mutex-buttons"> | |
<input type="radio" value="showColours" name="mutex" id="showColours" checked> | |
<label for="showColours">Colours Only</label> | |
<input type="radio" value="showNumbers" name="mutex" id="showNumbers"> | |
<label for="showNumbers">Numbers</label> | |
<input type="radio" value="showAdditions" name="mutex" id="showAdditions"> | |
<label for="showAdditions">Sums</label> | |
</div> | |
<div class="used-colours"><!-- Section to display used colours --> | |
</div> | |
</div> | |
<div class="side-panel"> | |
<h2>Saved</h2> | |
<div class="saved-grids"><!-- localStorage saved layouts --> | |
</div> | |
</div> | |
<div class="preset-panel"> | |
<h2>Presets</h2> | |
<div class="preset-grids"><!-- Preset layouts --> | |
</div> | |
</div> | |
</div> | |
<script> | |
const MAX_RECENT_LAYOUTS = 10; | |
const MAX_COLOURS = 16; | |
const MIPMAP_SIZE = 8; | |
const localKey = "SavedGrids" | |
let grid_width = 9; | |
let grid_height = 9; | |
let paintMode = 'showNumbers'; | |
let paletteIndex = 0; | |
let selectedColour = 0; // First in palette | |
const colourPalettes = [ | |
[ | |
{"#000000": "Black"}, | |
{"#FF0000": "Red"}, | |
{"#A000A0": "Purple"}, | |
{"#0000FF": "Blue"}, | |
{"#006400": "Green"}, //dark green | |
{"#8B4513": "Brown"}, | |
{"#FFC096": "Pale Tone"}, | |
{"#909090": "Grey"}, | |
{"#FFFFFF": "White"}, | |
{"#FFA500": "Orange"}, | |
{"#FF60CF": "Pink"}, | |
{"#00FFFF": "Cyan"}, | |
{"#00FF00": "Lime"}, | |
{"#CC8E69": "Rich Tone"}, | |
{"#FFDAB9": "Peach"}, | |
{"#FFFF00": "Yellow"}, | |
], | |
[ | |
{"#FFFFFF": "White"}, | |
{"#000000": "Black"}, | |
{"#FF0000": "Red"}, | |
{"#00FF00": "Lime"}, | |
{"#0000FF": "Blue"}, | |
{"#FFFF00": "Yellow"}, | |
{"#FFA500": "Orange"}, | |
{"#A000A0": "Purple"}, | |
{"#FF60CF": "Pink"}, | |
{"#006400": "Green"}, //dark green | |
{"#00FFFF": "Cyan"}, | |
{"#909090": "Grey"}, | |
{"#FFDAB9": "Peach"}, | |
{"#FFC096": "Pale Tone"}, | |
//{"#F5DEB3": "Wheat"}, | |
{"#CC8E69": "Rich Tone"}, | |
//{"#B87333": "Copper"}, | |
{"#8B4513": "Brown"}, | |
] | |
]; | |
let colourToName = colourPalettes[paletteIndex]; | |
function updateGridStyles() { | |
const grid = document.querySelector(".grid"); // Apply the number of columns using JavaScript | |
const windowWidth = window.innerWidth; | |
if (windowWidth < 880) { | |
grid.style.gridTemplateColumns = 'repeat(9, 30px)'; | |
} else { | |
grid.style.gridTemplateColumns = 'repeat(9, 50px)'; | |
} | |
} | |
updateGridStyles(); | |
const presets = [ | |
"Mario&099iIEYiIiIERGIiIVmaIiIYAiIiBMTGIiBY2GIiIMziIiIODiIiFWFWIA", | |
"Princess&099iI7oiIiP--iIiP7viIj-7v_Ij-qv_IiOIuiIiuIuqIiqqqqIiqqqqIA", | |
"Cherry&099iACAiIgMwFCIiAxcCIgBEREIAYgREQAYEREQARERgQgBEYEIiAAACIA", | |
"Crab&099u7u7u7uIuIu7uAuAu7sbsbu7EREbEREQARG7EREbERGxsbu7u7u7u7A", | |
"Bee&099u7u3d7u7t3e7--97u7Dw-w8L---w8Pu-Dw8Lu7Dw_7u7u7u7u7u7u7A", | |
"Pikachu&099gAiIgIiPmIiYiI--_YmY8P8ImYj-_YhY_ZmIhfn5_IiPlVmIiIiIiIA", | |
"Love%20heart%20flower&099MxMxMzsRERG7sR-xG7sR-xG7uxERu7u7Ebu7REzEREREzEREREzEREA", | |
"Dog&099u7VVu7tVVVVbVVVVVVVQVVBVVQiIBVVYiIhVVYgIhVtbiItbu7sbu7A", | |
"Witch&099_IgAAAj4jwCAiIRECI_IhAiIiIAAiIBAgACIAAgACIAAgAAICAgICIA", | |
"Pumpkin&099iIREiIiZmZmYmQmZCZmQCQCZmZmZmZmQkJCZmQAACZiZCQmYiJmZmIA", | |
"Robin&099u7u1W7u7tQZVu7tWZru7VWYbu7VmEbu1VhEbtVZhG7VWa1tbu7u1tbA", | |
"Santa&099u7u7EYu7sRG7uxERG7sREREbtmZmZruGBgaLuIiIiLuIAAiLu4iIi7A", | |
"Lady&099iIVVWIiIZmVViIVlVViIZmVViIZmVViMLCyIjILCjIyILCiMaILCiGA", | |
]; | |
const svg_header = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'; | |
const button_icons = { | |
"new": svg_header + '<path d="M20 11.08V8l-6-6H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h6"/><path d="M14 3v5h5M18 21v-6M15 18h6"/></svg>', | |
"clear": svg_header + '<polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>', | |
"save": svg_header + '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>', | |
"palette": svg_header + '<g fill="#FFFFFF"><path d="M 1 11 a 9.6 8.4 0 1 1 9.6 9.6 q -3.5 0 -2.4 -2.4 q 2.4 -3.6 -2.4 -3.6 q -5.2 0 -4.8 -3.6 z" fill="transparent"></path><circle cx="6" cy="10" r="1"/><circle cx="10" cy="7.8" r="0.8"/><circle cx="14.4" cy="8.5" r="0.6"/><circle cx="16.6" cy="12" r="0.5"/><circle cx="12.7" cy="16" r="1.6" fill="transparent"/></g></svg>', | |
"print": svg_header + '<polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>', | |
"share": svg_header + '<circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>', | |
}; | |
function compressGrid(arr) { | |
let compressed = ''; | |
for (let i = 0; i < arr.length; i += 2) { | |
const firstNibble = arr[i]; | |
const secondNibble = arr[i + 1] || 0; | |
if (firstNibble > 15 || firstNibble < 0 || secondNibble > 15 || secondNibble < 0) { | |
throw new Error('Array elements must be between 0 and 15.'); | |
} | |
const packedByte = (firstNibble << 4) | secondNibble; | |
compressed += String.fromCharCode(packedByte); | |
} | |
var base64 = btoa(compressed); | |
//URI-safe Base64 encoding: Replace '+' with '_', '/' with '-', and remove '=' | |
return base64.replace(/\+/g, '_').replace(/\//g, '-').replace(/=+$/, ''); | |
} | |
function decompressGrid(base64URI) { | |
const decompressed = []; | |
base64 = base64URI.replace(/_/g, '+').replace(/-/g, '/'); | |
while (base64.length % 4) { | |
base64 += '='; | |
} | |
const compressedStr = atob(base64); | |
for (const char of compressedStr) { | |
const packedByte = char.charCodeAt(0); | |
const firstNibble = (packedByte & 0xF0) >> 4; | |
const secondNibble = packedByte & 0x0F; | |
decompressed.push(firstNibble, secondNibble); | |
} | |
return decompressed; | |
} | |
function findColourPosition(hexColour) { | |
// Iterate through the array to find the position of the hex color | |
for (let i = 0; i < colourToName.length; i++) { | |
const entry = colourToName[i]; | |
const colourHex = Object.keys(entry)[0]; // Get the hex color value | |
if (colourHex === hexColour) { | |
return i; // Return the index if found | |
} | |
} | |
return -1; // Return -1 if not found | |
} | |
function luminance(hexcolour) { | |
hex = hexcolour.replace(/^#/, ''); | |
const bigint = parseInt(hex, 16); | |
const r = (bigint >> 16) & 255; | |
const g = (bigint >> 8) & 255; | |
const b = bigint & 255; | |
return 0.299 * r + 0.587 * g + 0.114 * b; | |
} | |
function colourIt(square, index) { | |
const sqColour = getColourByNumber(index) | |
const oldColour = square.getAttribute("data-index", index); | |
square.setAttribute("data-index", index); | |
addUsedColour(index); | |
const inputNumber = getSquareText(usedColoursInput[index]); | |
square.querySelector(".colour-text").textContent = inputNumber; | |
//Set text white or black depending on luminance of background: | |
if (luminance(sqColour)<127) {square.style.color = '#FFFFFF'} | |
else {square.style.color = '#000000';} | |
square.style.backgroundColor = sqColour; | |
//Remove unused colours: | |
let removeIt = true; | |
const squares = document.querySelectorAll(".square"); | |
squares.forEach((square) => { | |
const colourIndex = square.getAttribute("data-index"); | |
if (colourIndex == oldColour) {removeIt = false;} | |
}); | |
if (removeIt) { | |
usedColoursSet.delete(Object.keys(colourToName[oldColour])[0]); | |
const usedColours = document.querySelectorAll(".used-colours .used-colour"); | |
usedColours.forEach((usedColourDiv) => { | |
const bgColourDiv = usedColourDiv.querySelector('[data-bgcolour]'); | |
const oldColourHex = Object.keys(colourToName[oldColour])[0]; | |
if (bgColourDiv && bgColourDiv.getAttribute('data-bgcolour') === oldColourHex) { | |
usedColourDiv.parentNode.removeChild(usedColourDiv); | |
} | |
}); | |
} | |
} | |
function createPalette() { | |
const paletteSelector = document.querySelector(".palette"); | |
for (const index in colourToName) { | |
const colour = Object.keys(colourToName[index])[0] | |
const paletteColor = document.createElement("div"); | |
paletteColor.classList.add("colour"); | |
paletteColor.style.backgroundColor = colour; | |
paletteSelector.appendChild(paletteColor); | |
} | |
} | |
function createGrid(rows, cols) { | |
//Mouse/finger up events outside of the squares still stop painting | |
document.addEventListener("touchend", (event) => { | |
isTouchPressed = false; | |
event.preventDefault(); | |
}); | |
document.addEventListener("mouseup", () => { | |
isTouchPressed = false; | |
}); | |
document.addEventListener("touchmove", (event) => { //drag paint | |
if (isTouchPressed) { | |
const touch = event.touches[0]; | |
const x = touch.clientX; | |
const y = touch.clientY; | |
const elementUnderFinger = document.elementFromPoint(x, y); | |
if (elementUnderFinger && elementUnderFinger.classList.contains("square")) { | |
colourIt(elementUnderFinger, selectedColour); | |
} | |
} | |
//event.preventDefault(); //not necessary: touch-action:none | |
}); | |
for (let i = 0; i < rows * cols; i++) { | |
const square = document.createElement("div"); | |
square.classList.add("square"); | |
square.addEventListener("touchstart", (event) => { | |
isTouchPressed = true; | |
colourIt(square, selectedColour); | |
//event.preventDefault();//Chrome smooth scroll doesn't like this | |
}); | |
square.addEventListener("mousedown", () => { | |
isTouchPressed = true; | |
colourIt(square, selectedColour); | |
}); | |
square.addEventListener("mousemove", () => { | |
if (isTouchPressed) { | |
colourIt(square, selectedColour); | |
} | |
}); | |
const colourText = document.createElement("div"); | |
colourText.classList.add("colour-text"); | |
square.appendChild(colourText); | |
gridContainer.appendChild(square); | |
} | |
clearGrid(); | |
} | |
function getColourByNumber(number) { | |
return Object.keys(colourToName[number])[0] | |
} | |
function pressUsedColour(event) { | |
const hexColour = event.target.getAttribute("data-bgcolour"); | |
const paletteColours = document.querySelectorAll(".colour"); | |
paletteColours.forEach((colour, index) => { | |
colour.classList.remove("selected"); | |
if (hexColour == Object.keys(colourToName[index])[0]) { | |
selectedColour = index; //Select paint colour | |
colour.classList.add("selected"); //put class on selected colour only | |
} | |
}); | |
} | |
function addUsedColour(colourIndex) { | |
const colour = getColourByNumber(colourIndex); | |
if (!usedColoursSet.has(colour)) { | |
usedColoursSet.add(colour); | |
const colourName = Object.values(colourToName[colourIndex])[0] | |
const usedColoursSection = document.querySelector(".used-colours"); | |
const usedColourContainer = document.createElement("div"); | |
usedColourContainer.classList.add("used-colour"); | |
const usedColourBox = document.createElement("div"); | |
usedColourBox.classList.add("used-colour-box"); | |
usedColourBox.style.backgroundColor = colour; | |
usedColourBox.setAttribute("data-bgcolour", colour); | |
usedColourBox.textContent = colourName; | |
usedColourBox.addEventListener("click", pressUsedColour); | |
usedColourBox.addEventListener("touchstart", pressUsedColour); | |
if (luminance(colour)<127) {usedColourBox.style.color = '#FFFFFF'} | |
else {usedColourBox.style.color = '#000000';} | |
const colourTextInput = document.createElement("input"); | |
colourTextInput.type = "text"; //"number"; | |
usedColoursInput[colourIndex] = getNextFreeNumber(); //usedColoursSet.size; | |
colourTextInput.value = usedColoursInput[colourIndex]; //usedColoursSet.size; | |
colourTextInput.addEventListener("input", () => { | |
if (!isNaN(parseInt(colourTextInput.value))) { | |
usedColoursInput[colourIndex] = parseInt(colourTextInput.value); | |
const gridSquares = document.querySelectorAll(".square"); | |
gridSquares.forEach((square) => { | |
const dataIndex = square.getAttribute("data-index"); | |
if (parseInt(dataIndex) == colourIndex) { //set all squares to the new number: | |
square.querySelector(".colour-text").textContent = getSquareText(usedColoursInput[colourIndex]); | |
} | |
}); | |
} | |
}); | |
//If the number is already in use, change it to the lowest available number: | |
colourTextInput.addEventListener('blur', (event) => { | |
let inputValue = parseInt(event.target.value); // value of the blurred input | |
event.target.value = inputValue; //integers only | |
const usedColourInputs = document.querySelectorAll('.used-colours .used-colour input'); | |
const usedValues = []; | |
usedColourInputs.forEach((otherInput) => { | |
if (otherInput !== event.target) { //only push the other input values | |
usedValues.push(parseInt(otherInput.value, 10)); | |
} | |
}); | |
let uniqueValue = 1; //find next unused positive integer: | |
while (usedValues.includes(uniqueValue)) {uniqueValue++;} | |
if (isNaN(inputValue)) {inputValue = 1;} //NaN -> next free | |
if (usedValues.includes(inputValue)) { //If the number is already in use: | |
event.target.value = uniqueValue.toString(); //Set input value to next unused | |
//Set all the squares data-index and text content to the new index: | |
usedColoursInput[colourIndex] = uniqueValue; | |
//setTextFromIndex(colourTextInput, colourIndex); | |
const inputColour = colourTextInput.parentElement.querySelector('.used-colour-box').getAttribute('data-bgcolour'); | |
const gridSquares = document.querySelectorAll(".square"); | |
gridSquares.forEach((square) => { | |
const squareIndex = parseInt(square.getAttribute('data-index')); | |
if (Object.keys(colourToName[squareIndex])[0] == inputColour) { | |
square.querySelector(".colour-text").textContent = getSquareText(usedColoursInput[colourIndex]); | |
} | |
}); | |
} | |
//Now re-order the used colours by number if necessary: | |
const usedColourElements = document.querySelectorAll('.used-colours .used-colour'); | |
const elementsWithValues = []; | |
usedColourElements.forEach((usedColourElement) => { | |
const textInput = usedColourElement.querySelector('input[type="text"]'); | |
elementsWithValues.push({ element: usedColourElement, value: textInput.value }); | |
}); | |
elementsWithValues.sort((a, b) => a.value - b.value); //sort numerically | |
const usedColoursContainer = document.querySelector('.used-colours'); | |
usedColoursContainer.innerHTML = ''; | |
elementsWithValues.forEach((elementWithValue) => { | |
usedColoursContainer.appendChild(elementWithValue.element); //append in order | |
});//*/ | |
}); | |
//Add the elements: | |
usedColourContainer.appendChild(colourTextInput); | |
usedColourContainer.appendChild(usedColourBox); | |
usedColoursSection.appendChild(usedColourContainer); | |
} | |
} | |
function setTextFromIndex(colourTextInput, colourIndex) { | |
const inputColour = colourTextInput.parentElement.querySelector('.used-colour-box').getAttribute('data-bgcolour'); | |
const gridSquares = document.querySelectorAll(".square"); | |
gridSquares.forEach((square) => { | |
const squareIndex = parseInt(square.getAttribute('data-index')); | |
if (Object.keys(colourToName[squareIndex])[0] == inputColour) { | |
square.setAttribute("data-index", usedColoursInput[colourIndex]); | |
square.querySelector(".colour-text").textContent = getSquareText(usedColoursInput[colourIndex]); | |
} | |
}); | |
} | |
function getNextFreeNumber(exception = undefined) { | |
const usedColourInputs = document.querySelectorAll('.used-colours .used-colour input'); | |
const usedValues = []; | |
usedColourInputs.forEach((otherInput) => { | |
if (otherInput !== exception) { //only push the other input values | |
usedValues.push(parseInt(otherInput.value, 10)); | |
} | |
}); | |
let uniqueValue = 1; | |
while (usedValues.includes(uniqueValue)) { | |
uniqueValue++; | |
} | |
return uniqueValue; | |
} | |
function getSquareText(index) { | |
if (paintMode == 'showNumbers') { | |
return index; //numbers only | |
} else if (paintMode == 'showAdditions'){ | |
const randomNumber1 = Math.floor(Math.random() * index); | |
const randomNumber2 = index - randomNumber1; | |
return `${randomNumber1} + ${randomNumber2}`; | |
} else { //colours only | |
return ""; | |
} | |
} | |
function getAllSquaresTexts() { | |
const squares = gridContainer.querySelectorAll(".square"); | |
squares.forEach((square) => { | |
const colourText = square.querySelector(".colour-text"); | |
const squareBackgroundColour = square.style.backgroundColor; | |
const colourIndex = square.getAttribute("data-index"); | |
addUsedColour(colourIndex); | |
colourText.textContent = getSquareText(usedColoursInput[colourIndex]); | |
}); | |
} | |
function getGridState() { | |
const squares = gridContainer.querySelectorAll(".square"); | |
const gridData = []; | |
const artname = document.querySelector("#artname").textContent; | |
let isGridBlank = true; | |
squares.forEach((square) => { | |
const colourIndex = square.getAttribute("data-index"); | |
const text = square.querySelector(".colour-text").textContent; | |
if (colourIndex || text) { | |
isGridBlank = false; | |
} | |
gridData.push(colourIndex); // Save the index value instead of colour | |
}); | |
if (isGridBlank) { // Don't save if the grid is blank | |
return undefined; | |
} | |
const gridState = encodeURIComponent(artname) + '&' + paletteIndex.toString(36) + grid_width.toString(36) + grid_height.toString(36) + compressGrid(gridData); | |
return gridState; | |
} | |
function saveGridState() { | |
let savedData = JSON.parse(localStorage.getItem(localKey)) || []; | |
savedData.push(getGridState()); | |
//check length | |
console.log(savedData.length) | |
if (savedData.length > MAX_RECENT_LAYOUTS) { | |
savedData.shift(); | |
} | |
localStorage.setItem(localKey, JSON.stringify(savedData)); | |
updateSavedLayouts(); | |
//unshift local TODO | |
} | |
//function loadGridState(n = 0, ispreset=false) { // Load recent layouts or presets into grid | |
/* | |
let newlayout = presets; | |
if (!ispreset) { | |
newlayout = JSON.parse(localStorage.getItem(localKey)); | |
} | |
if (!newlayout || newlayout.length <= n) {return;} | |
*/ | |
function loadGridState(savedData) { | |
clearGrid(); | |
if (!savedData) {return} | |
//const savedData = parseURIData(newlayout[n]); | |
//Load the palette first: | |
colourToName = savedData.palette; | |
const palContainer = document.getElementById("paletteContainer"); | |
const palette = palContainer.querySelectorAll(".colour"); | |
palette.forEach((psquare, index) => { | |
const colour = Object.keys(colourToName[index])[0] | |
psquare.style.backgroundColor = colour; | |
}); | |
//load the grid: | |
const gridData = savedData.grid; | |
const squares = gridContainer.querySelectorAll(".square"); | |
squares.forEach((square, index) => { | |
const colourIndex = gridData[index]; | |
if (colourIndex !== null && colourIndex !== undefined) { | |
colourIt(square, colourIndex); | |
} | |
}); | |
let artname = savedData.name; //set the name | |
document.querySelector('#artname').textContent=artname; | |
} | |
function shareGrid() { | |
const artname = document.querySelector("#artname").textContent; | |
const gridState = getGridState(); | |
const shareURL = window.location.origin + window.location.pathname + '?load=' + gridState; | |
console.log('share') | |
if ('share' in navigator) { | |
navigator.share({ | |
title: artname, | |
url: shareURL | |
}).then(() => { | |
console.log('Thanks for sharing!'); | |
}) | |
.catch(console.error); | |
} else { | |
if (navigator.clipboard) { | |
navigator.clipboard.writeText(shareURL); | |
alert('Copied URL to clipboard!') | |
} else { | |
alert('Could not copy to clipboard') | |
} | |
} | |
}; | |
function parseURIData(URIdata) { | |
if (typeof(URIdata)=="object") { | |
if (["name","palette","x","y","grid"].every(k=>URIdata.hasOwnProperty(k))) {return URIdata;} | |
return; | |
} | |
const datarray = URIdata.split('&'); | |
if (datarray.length!=2) {return;} | |
const data = { | |
"name": decodeURIComponent(datarray[0]), | |
"palette": colourPalettes[parseInt(datarray[1][0], 36)], | |
"x": parseInt(datarray[1][1], 36), | |
"y": parseInt(datarray[1][2], 36), | |
"grid": decompressGrid(datarray[1].substr(3)) | |
} | |
while (data.grid.length > data.x * data.y) {data.grid.pop();} //odd grids will be rounded up otherwise | |
return data; | |
} | |
function gridLayout(URIdata, index, ispreset=false) { | |
const data = parseURIData(URIdata); | |
const savedlayout = document.createElement("div"); | |
savedlayout.classList.add("saved-box"); | |
const mipmap = document.createElement("div"); | |
const savedtitle = document.createElement("div"); | |
savedtitle.classList.add("saved-title"); | |
savedtitle.textContent = data.name | |
mipmap.classList.add("small-grid"); | |
mipmap.style.width = MIPMAP_SIZE * data.x + 'px'; | |
for (let i = 0; i < data.grid.length; i++) { | |
var col = data.grid[i]; | |
const sq = document.createElement("div"); | |
sq.classList.add("small-square"); | |
sq.style.width = MIPMAP_SIZE + 'px'; | |
sq.style.height = MIPMAP_SIZE + 'px'; | |
sq.style.backgroundColor = Object.keys(data.palette[col])[0]; | |
mipmap.appendChild(sq); | |
} | |
savedlayout.appendChild(mipmap); | |
savedlayout.appendChild(savedtitle); | |
savedlayout.dataset.layoutIndex = index; // Store the layout index as a data attribute | |
// Add click event to load the layout when clicked | |
let newlayout = presets; | |
if (!ispreset) { | |
newlayout = JSON.parse(localStorage.getItem(localKey)); | |
} | |
if (!newlayout || newlayout.length <= index) {return;} | |
savedlayout.addEventListener("click", function() { | |
loadGridState(data); | |
}); | |
let touched = false; | |
savedlayout.addEventListener("touchstart", function() { | |
touched = true; | |
initialY = event.touches[0].clientY; | |
}, false); | |
savedlayout.addEventListener("touchend", function() { //no longer touchstart - to easy to false hit | |
if (touched) { | |
loadGridState(data); | |
} | |
}); | |
savedlayout.addEventListener('touchmove', function(event) { | |
const currentY = event.touches[0].clientY; | |
if (Math.abs(initialY - currentY) > 2) { | |
touched = false; | |
} | |
}, false); | |
return savedlayout; | |
} | |
function updateSavedLayouts() { | |
const savedLayoutsPanel = document.querySelector(".saved-grids"); | |
savedLayoutsPanel.innerHTML = ""; //blank panel | |
const recentLayouts = JSON.parse(localStorage.getItem(localKey)) || []; | |
if (recentLayouts.length==0) {return;} | |
//for (let index = recentLayouts.length - 1; index >= recentLayouts.length-MAX_RECENT_LAYOUTS; index--) { | |
for (let index = 0; index < recentLayouts.length; index++) { | |
//if (index < MAX_RECENT_LAYOUTS) { | |
const data = recentLayouts[recentLayouts.length - index - 1]; | |
const savedlayout = gridLayout(data, index);//recentLayouts.length - index - 1); | |
savedLayoutsPanel.appendChild(savedlayout); | |
//} | |
} | |
} | |
function updatePresetLayouts() { | |
const presetGridsPanel = document.querySelector(".preset-grids"); | |
presetGridsPanel.innerHTML = ""; // Clear existing preset grids | |
// Load preset grids from const presets | |
const presetGrids = presets || []; | |
presetGrids.forEach((data, index) => { | |
const presetGridRender = gridLayout(data, index, preset=true); | |
presetGridsPanel.appendChild(presetGridRender); | |
}); | |
} | |
function clearGrid() { | |
gridContainer.querySelectorAll(".square").forEach((square) => { | |
square.style.backgroundColor = '#FFFFFF'; // Reset to colour 0 (white) | |
square.querySelector(".colour-text").textContent = ""; // Update text to 0 | |
//square.removeAttribute("data-index"); | |
//Reset to white (find it in palette) | |
square.setAttribute("data-index", findColourPosition('#FFFFFF')); | |
square.style.color = '#000000'; | |
}); | |
usedColoursSet.clear(); | |
const usedColoursSection = document.querySelector(".used-colours"); | |
usedColoursSection.innerHTML = ''; | |
}; | |
/* | |
// Function to resize the grid without clearing | |
function resizeGrid() { | |
// Calculate the new width and height for the grid container | |
const newWidth = grid_width * 50 + "px"; // Adjust as needed | |
const newHeight = grid_height * 50 + "px"; // Adjust as needed | |
// Set the new width and height for the grid container | |
gridContainer.style.width = newWidth; | |
gridContainer.style.height = newHeight; | |
// Adjust the CSS for the grid squares | |
const squares = gridContainer.querySelectorAll(".square"); | |
squares.forEach((square) => { | |
square.style.width = 100 / grid_width + "%"; | |
square.style.height = 100 / grid_height + "%"; | |
}); | |
} | |
*/ | |
// Get the grid container | |
const gridContainer = document.getElementById("grid-container"); | |
// Store the state of the mouse button (mousedown/up) | |
let isTouchPressed = false; | |
// Define an object to store used colors along with their indices | |
const usedColoursSet = new Set(); | |
usedColoursInput = {} | |
//Initialise | |
createPalette(); | |
createGrid(grid_width, grid_height); | |
updateSavedLayouts(); | |
updatePresetLayouts(); | |
// Add event listeners to the palette colours to change the selected colour | |
function pressPaletteColour(colour, index) { | |
paletteColours.forEach((c) => c.classList.remove("selected")); | |
selectedColour = index; | |
colour.classList.add("selected"); //put class on selected colour only | |
} | |
const paletteColours = document.querySelectorAll(".colour"); | |
paletteColours.forEach((colour, index) => { | |
colour.addEventListener("click", function() { | |
pressPaletteColour(colour, index); | |
}); | |
colour.addEventListener("touchstart", function() { | |
pressPaletteColour(colour, index); | |
}); | |
}); | |
// Add event listeners to each radio button | |
const radioButtons = document.querySelectorAll(".mutex-buttons input[type='radio']"); | |
radioButtons.forEach((radio) => { | |
radio.addEventListener("change", (event) => { | |
//console.log(`${this.value}`) | |
paintMode = event.target.id; | |
getAllSquaresTexts(); | |
}); | |
}); | |
//as the radios are hidden, add touchend events to the labels | |
const labels = document.querySelectorAll('.mutex-buttons label'); | |
labels.forEach((label, index) => { | |
label.addEventListener('touchend', function(event) { | |
event.preventDefault(); | |
radioButtons[index].checked = true; | |
// Dispatch a change event on the radio button | |
const changeEvent = new Event('change', { 'bubbles': true }); | |
radioButtons[index].dispatchEvent(changeEvent); | |
}); | |
}); | |
document.getElementById(paintMode).checked = true; | |
//Top Button listeners: | |
const clearButton = document.getElementById("clearButton"); | |
clearButton.innerHTML = button_icons["clear"]; //replace text with icon | |
clearButton.addEventListener("click", clearGrid); | |
clearButton.addEventListener("touchstart", clearGrid); | |
function pressPrint() { | |
if (paintMode == 'showColours') { //Ensure a paint mode for printing! | |
paintMode = 'showNumbers'; | |
document.getElementById(paintMode).checked = true; | |
} | |
getAllSquaresTexts(); | |
const grid = document.querySelector(".grid"); // Apply the number of columns using JavaScript | |
grid.style.gridTemplateColumns = 'repeat(9, 50px)'; //set squares to big size again | |
window.print(); // Trigger browser print | |
updateGridStyles(); //reset | |
} | |
const printButton = document.getElementById("printButton"); | |
printButton.innerHTML = button_icons["print"]; //replace text with icon | |
printButton.addEventListener("click", pressPrint); | |
printButton.addEventListener("touchstart", pressPrint); | |
function pressPalette() { | |
paletteIndex++; | |
if (paletteIndex >= colourPalettes.length) {paletteIndex = 0;} | |
const palButton = document.getElementById("paletteButton"); | |
//palButton.textContent = "Palette "+paletteIndex; | |
colourToName = colourPalettes[paletteIndex]; | |
//redraw the palette: | |
const palContainer = document.getElementById("paletteContainer"); | |
const palette = palContainer.querySelectorAll(".colour"); | |
palette.forEach((psquare, index) => { | |
const colour = Object.keys(colourToName[index])[0] | |
psquare.style.backgroundColor = colour; | |
}); | |
//redraw the grid: | |
const squares = gridContainer.querySelectorAll(".square"); | |
usedColoursSet.clear(); | |
const usedColoursSection = document.querySelector(".used-colours"); | |
usedColoursSection.innerHTML = ''; | |
squares.forEach((square, index) => { | |
colourIt(square, square.getAttribute("data-index")); | |
}); | |
} | |
const palButton = document.getElementById("paletteButton"); | |
palButton.innerHTML = button_icons["palette"]; //replace text with icon | |
palButton.addEventListener("click", pressPalette); | |
palButton.addEventListener("touchstart", pressPalette); | |
const saveButton = document.getElementById("saveButton"); | |
saveButton.innerHTML = button_icons["save"]; //replace text with icon | |
saveButton.addEventListener("click", saveGridState); | |
saveButton.addEventListener("touchstart", saveGridState); | |
const shareButton = document.getElementById("shareButton"); | |
shareButton.innerHTML = button_icons["share"]; //replace text with icon | |
shareButton.addEventListener('click', shareGrid); | |
shareButton.addEventListener('touchstart', shareGrid); | |
/* | |
// Event listener for increasing the grid size | |
const increaseSizeButton = document.getElementById("increase-size"); | |
increaseSizeButton.addEventListener("click", () => { | |
if (grid_width < 16) { | |
grid_width++; | |
resizeGrid(); | |
} | |
}); | |
// Event listener for decreasing the grid size | |
const decreaseSizeButton = document.getElementById("decrease-size"); | |
decreaseSizeButton.addEventListener("click", () => { | |
if (grid_width > 3) { | |
grid_width--; | |
resizeGrid(); | |
} | |
});*/ | |
//Selected styling for paint: | |
document.querySelectorAll(".colour")[selectedColour].classList.add('selected'); | |
//Add resize listener | |
window.addEventListener('resize', updateGridStyles); | |
//Does this even work? attempt to reset the grid after a print dialogue | |
/* | |
window.addEventListener("afterprint", (event) => { | |
updateGridStyles(); | |
console.log("After print"); | |
});*/ | |
if (window.location.search!="" && window.location.search.split('?load=').length==2) { | |
//parse share link: | |
const data = window.location.search.split('?load=')[1] | |
loadGridState(parseURIData(data)); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment