Skip to content

Instantly share code, notes, and snippets.

@mattura
Last active November 6, 2023 22:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattura/1b365a1fa21e5b9927cbd0c1c45fcbeb to your computer and use it in GitHub Desktop.
Save mattura/1b365a1fa21e5b9927cbd0c1c45fcbeb to your computer and use it in GitHub Desktop.
<!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