diff options
Diffstat (limited to 'js/pixel-art')
-rw-r--r-- | js/pixel-art/app.js | 683 | ||||
-rw-r--r-- | js/pixel-art/apple-icon-180x180.png | bin | 0 -> 11517 bytes | |||
-rw-r--r-- | js/pixel-art/index.html | 27 | ||||
-rw-r--r-- | js/pixel-art/pixel/app.js | 561 | ||||
-rw-r--r-- | js/pixel-art/pixel/index.html | 213 |
5 files changed, 1107 insertions, 377 deletions
diff --git a/js/pixel-art/app.js b/js/pixel-art/app.js index 9e501fe..816ee09 100644 --- a/js/pixel-art/app.js +++ b/js/pixel-art/app.js @@ -1,443 +1,388 @@ -// Inspired by https://eloquentjavascript.net/19_paint.html - -class Picture { // Honestly? I hate classes in javascript, if I were doing this not from a book I wouldn't do this like this - constructor(width, height, pixels) { - this.width = width; - this.height = height; - this.pixels = pixels; - } - static empty(width, height, color) { - let pixels = new Array(width * height).fill(color); - return new Picture(width, height, pixels); - } - pixel(x, y) { - return this.pixels[x + y * this.width]; - } - draw(pixels) { - let copy = this.pixels.slice(); - for (let { - x, - y, - color - } - of pixels) { - copy[x + y * this.width] = color; - } - return new Picture(this.width, this.height, copy); - } +// mostly from https://eloquentjavascript.net/19_paint.html + +class Picture { + constructor(width, height, pixels) { + this.width = width; + this.height = height; + this.pixels = pixels; + } + static empty(width, height, color) { + let pixels = new Array(width * height).fill(color); + return new Picture(width, height, pixels); + } + pixel(x, y) { + return this.pixels[x + y * this.width]; + } + draw(pixels) { + let copy = this.pixels.slice(); + for (let { x, y, color } of pixels) { + copy[x + y * this.width] = color; + } + return new Picture(this.width, this.height, copy); + } } function updateState(state, action) { - return { - ...state, - ...action - }; + return Object.assign({}, state, action); } function elt(type, props, ...children) { - let dom = document.createElement(type); - if (props) Object.assign(dom, props); - for (let child of children) { - if (typeof child != "string") dom.appendChild(child); - else dom.appendChild(document.createTextNode(child)); - } - return dom; + let dom = document.createElement(type); + if (props) Object.assign(dom, props); + for (let child of children) { + if (typeof child != "string") dom.appendChild(child); + else dom.appendChild(document.createTextNode(child)); + } + return dom; } -const scale = 10; +const scale = 38; class PictureCanvas { - constructor(picture, pointerDown) { - this.dom = elt("canvas", { - onmousedown: event => this.mouse(event, pointerDown), - ontouchstart: event => this.touch(event, pointerDown) - }); - this.syncState(picture); - } - syncState(picture) { - if (this.picture == picture) return; - this.picture = picture; - drawPicture(this.picture, this.dom, scale); - } + constructor(picture, pointerDown) { + this.dom = elt("canvas", { + onmousedown: (event) => this.mouse(event, pointerDown), + ontouchstart: (event) => this.touch(event, pointerDown), + }); + this.syncState(picture); + } + syncState(picture) { + if (this.picture == picture) return; + this.picture = picture; + drawPicture(this.picture, this.dom, scale); + } } function drawPicture(picture, canvas, scale) { - canvas.width = picture.width * scale; - canvas.height = picture.height * scale; - let cx = canvas.getContext("2d"); - - picture.pixels.forEach((color, index) => { - let x = index % picture.width; - let y = Math.floor(index / picture.width); - cx.fillStyle = color; - cx.fillRect(x * scale, y * scale, scale, scale); - }); + canvas.width = picture.width * scale; + canvas.height = picture.height * scale; + let cx = canvas.getContext("2d"); + + for (let y = 0; y < picture.height; y++) { + for (let x = 0; x < picture.width; x++) { + cx.fillStyle = picture.pixel(x, y); + cx.fillRect(x * scale, y * scale, scale, scale); + } + } } -PictureCanvas.prototype.mouse = function(downEvent, onDown) { - if (downEvent.button != 0) return; - let pos = pointerPosition(downEvent, this.dom); - let onMove = onDown(pos); - if (!onMove) return; - let move = moveEvent => { - if (moveEvent.buttons == 0) { - this.dom.removeEventListener("mousemove", move); - } else { - let newPos = pointerPosition(moveEvent, this.dom); - if (newPos.x == pos.x && newPos.y == pos.y) return; - pos = newPos; - onMove(newPos); - } - }; - this.dom.addEventListener("mousemove", move); +PictureCanvas.prototype.mouse = function (downEvent, onDown) { + if (downEvent.button != 0) return; + let pos = pointerPosition(downEvent, this.dom); + let onMove = onDown(pos); + if (!onMove) return; + let move = (moveEvent) => { + if (moveEvent.buttons == 0) { + this.dom.removeEventListener("mousemove", move); + } else { + let newPos = pointerPosition(moveEvent, this.dom); + if (newPos.x == pos.x && newPos.y == pos.y) return; + pos = newPos; + onMove(newPos); + } + }; + this.dom.addEventListener("mousemove", move); }; function pointerPosition(pos, domNode) { - let rect = domNode.getBoundingClientRect(); - return { - x: Math.floor((pos.clientX - rect.left) / scale), - y: Math.floor((pos.clientY - rect.top) / scale) - }; + let rect = domNode.getBoundingClientRect(); + return { + x: Math.floor((pos.clientX - rect.left) / scale), + y: Math.floor((pos.clientY - rect.top) / scale), + }; } -PictureCanvas.prototype.touch = function(startEvent, - onDown) { - let pos = pointerPosition(startEvent.touches[0], this.dom); - let onMove = onDown(pos); - startEvent.preventDefault(); - if (!onMove) return; - let move = moveEvent => { - let newPos = pointerPosition(moveEvent.touches[0], - this.dom); - if (newPos.x == pos.x && newPos.y == pos.y) return; - pos = newPos; - onMove(newPos); - }; - let end = () => { - this.dom.removeEventListener("touchmove", move); - this.dom.removeEventListener("touchend", end); - }; - this.dom.addEventListener("touchmove", move); - this.dom.addEventListener("touchend", end); +PictureCanvas.prototype.touch = function (startEvent, onDown) { + let pos = pointerPosition(startEvent.touches[0], this.dom); + let onMove = onDown(pos); + startEvent.preventDefault(); + if (!onMove) return; + let move = (moveEvent) => { + let newPos = pointerPosition(moveEvent.touches[0], this.dom); + if (newPos.x == pos.x && newPos.y == pos.y) return; + pos = newPos; + onMove(newPos); + }; + let end = () => { + this.dom.removeEventListener("touchmove", move); + this.dom.removeEventListener("touchend", end); + }; + this.dom.addEventListener("touchmove", move); + this.dom.addEventListener("touchend", end); }; class PixelEditor { - constructor(state, config) { - let { - tools, - controls, - dispatch - } = config; - this.state = state; - - this.canvas = new PictureCanvas(state.picture, pos => { - let tool = tools[this.state.tool]; - let onMove = tool(pos, this.state, dispatch); - if (onMove) return pos => onMove(pos, this.state); - }); - this.controls = controls.map( - Control => new Control(state, config)); - this.dom = elt("div", {}, this.canvas.dom, elt("br"), - ...this.controls.reduce( - (a, c) => a.concat(" ", c.dom), [])); - } - syncState(state) { - this.state = state; - this.canvas.syncState(state.picture); - for (let ctrl of this.controls) ctrl.syncState(state); - } + constructor(state, config) { + let { tools, controls, dispatch } = config; + this.state = state; + + this.canvas = new PictureCanvas(state.picture, (pos) => { + let tool = tools[this.state.tool]; + let onMove = tool(pos, this.state, dispatch); + if (onMove) return (pos) => onMove(pos, this.state); + }); + this.controls = controls.map((Control) => new Control(state, config)); + this.dom = elt( + "div", + {}, + this.canvas.dom, + elt("br"), + ...this.controls.reduce((a, c) => a.concat(" ", c.dom), []) + ); + } + syncState(state) { + this.state = state; + this.canvas.syncState(state.picture); + for (let ctrl of this.controls) ctrl.syncState(state); + } } class ToolSelect { - constructor(state, { - tools, - dispatch - }) { - this.select = elt("select", { - onchange: () => dispatch({ - tool: this.select.value - }) - }, ...Object.keys(tools).map(name => elt("option", { - selected: name == state.tool - }, name))); - this.dom = elt("label", null, "🖌 Tool: ", this.select); - } - syncState(state) { - this.select.value = state.tool; - } + constructor(state, { tools, dispatch }) { + this.select = elt( + "select", + { + onchange: () => dispatch({ tool: this.select.value }), + }, + ...Object.keys(tools).map((name) => + elt( + "option", + { + selected: name == state.tool, + }, + name + ) + ) + ); + this.dom = elt("label", null, "🖌 Tool: ", this.select); + } + syncState(state) { + this.select.value = state.tool; + } } class ColorSelect { - constructor(state, { - dispatch - }) { - this.input = elt("input", { - type: "color", - value: state.color, - onchange: () => dispatch({ - color: this.input.value - }) - }); - this.dom = elt("label", null, "🎨 Color: ", this.input); - } - syncState(state) { - this.input.value = state.color; - } + constructor(state, { dispatch }) { + this.input = elt("input", { + type: "color", + value: state.color, + onchange: () => dispatch({ color: this.input.value }), + }); + this.dom = elt("label", null, "🎨 Color: ", this.input); + } + syncState(state) { + this.input.value = state.color; + } } function draw(pos, state, dispatch) { - function drawPixel({ - x, - y - }, state) { - let drawn = { - x, - y, - color: state.color - }; - dispatch({ - picture: state.picture.draw([drawn]) - }); - } - drawPixel(pos, state); - return drawPixel; + function drawPixel({ x, y }, state) { + let drawn = { x, y, color: state.color }; + dispatch({ picture: state.picture.draw([drawn]) }); + } + drawPixel(pos, state); + return drawPixel; } function rectangle(start, state, dispatch) { - function drawRectangle(pos) { - let xStart = Math.min(start.x, pos.x); - let yStart = Math.min(start.y, pos.y); - let xEnd = Math.max(start.x, pos.x); - let yEnd = Math.max(start.y, pos.y); - let drawn = []; - for (let y = yStart; y <= yEnd; y++) { - for (let x = xStart; x <= xEnd; x++) { - drawn.push({ - x, - y, - color: state.color - }); - } - } - dispatch({ - picture: state.picture.draw(drawn) - }); - } - drawRectangle(start); - return drawRectangle; + function drawRectangle(pos) { + let xStart = Math.min(start.x, pos.x); + let yStart = Math.min(start.y, pos.y); + let xEnd = Math.max(start.x, pos.x); + let yEnd = Math.max(start.y, pos.y); + let drawn = []; + for (let y = yStart; y <= yEnd; y++) { + for (let x = xStart; x <= xEnd; x++) { + drawn.push({ x, y, color: state.color }); + } + } + dispatch({ picture: state.picture.draw(drawn) }); + } + drawRectangle(start); + return drawRectangle; } -const around = [{ - dx: -1, - dy: 0 - }, { - dx: 1, - dy: 0 - }, - { - dx: 0, - dy: -1 - }, { - dx: 0, - dy: 1 - } +const around = [ + { dx: -1, dy: 0 }, + { dx: 1, dy: 0 }, + { dx: 0, dy: -1 }, + { dx: 0, dy: 1 }, ]; -function fill({ - x, - y -}, state, dispatch) { - let targetColor = state.picture.pixel(x, y); - let drawn = [{ - x, - y, - color: state.color - }]; - for (let done = 0; done < drawn.length; done++) { - for (let { - dx, - dy - } - of around) { - let x = drawn[done].x + dx, - y = drawn[done].y + dy; - if (x >= 0 && x < state.picture.width && - y >= 0 && y < state.picture.height && - state.picture.pixel(x, y) == targetColor && - !drawn.some(p => p.x == x && p.y == y)) { - drawn.push({ - x, - y, - color: state.color - }); - } - } - } - dispatch({ - picture: state.picture.draw(drawn) - }); +function fill({ x, y }, state, dispatch) { + let targetColor = state.picture.pixel(x, y); + let drawn = [{ x, y, color: state.color }]; + for (let done = 0; done < drawn.length; done++) { + for (let { dx, dy } of around) { + let x = drawn[done].x + dx, + y = drawn[done].y + dy; + if ( + x >= 0 && + x < state.picture.width && + y >= 0 && + y < state.picture.height && + state.picture.pixel(x, y) == targetColor && + !drawn.some((p) => p.x == x && p.y == y) + ) { + drawn.push({ x, y, color: state.color }); + } + } + } + dispatch({ picture: state.picture.draw(drawn) }); } function pick(pos, state, dispatch) { - dispatch({ - color: state.picture.pixel(pos.x, pos.y) - }); + dispatch({ color: state.picture.pixel(pos.x, pos.y) }); } class SaveButton { - constructor(state) { - this.picture = state.picture; - this.dom = elt("button", { - onclick: () => this.save() - }, "💾 Save"); - } - save() { - let canvas = elt("canvas"); - drawPicture(this.picture, canvas, 1); - let link = elt("a", { - href: canvas.toDataURL(), - download: "pixelart.png" - }); - document.body.appendChild(link); - link.click(); - link.remove(); - } - syncState(state) { - this.picture = state.picture; - } + constructor(state) { + this.picture = state.picture; + this.dom = elt( + "button", + { + onclick: () => this.save(), + }, + "💾 Save" + ); + } + save() { + let canvas = elt("canvas"); + drawPicture(this.picture, canvas, 1); + let link = elt("a", { + href: canvas.toDataURL(), + download: "pixelart.png", + }); + document.body.appendChild(link); + link.click(); + link.remove(); + } + syncState(state) { + this.picture = state.picture; + } } class LoadButton { - constructor(_, { - dispatch - }) { - this.dom = elt("button", { - onclick: () => startLoad(dispatch) - }, "📁 Load"); - } - syncState() {} + constructor(_, { dispatch }) { + this.dom = elt( + "button", + { + onclick: () => startLoad(dispatch), + }, + "📁 Load" + ); + } + syncState() {} } function startLoad(dispatch) { - let input = elt("input", { - type: "file", - onchange: () => finishLoad(input.files[0], dispatch) - }); - document.body.appendChild(input); - input.click(); - input.remove(); + let input = elt("input", { + type: "file", + onchange: () => finishLoad(input.files[0], dispatch), + }); + document.body.appendChild(input); + input.click(); + input.remove(); } function finishLoad(file, dispatch) { - if (file == null) return; - let reader = new FileReader(); - reader.addEventListener("load", () => { - let image = elt("img", { - onload: () => dispatch({ - picture: pictureFromImage(image) - }), - src: reader.result - }); - }); - reader.readAsDataURL(file); + if (file == null) return; + let reader = new FileReader(); + reader.addEventListener("load", () => { + let image = elt("img", { + onload: () => + dispatch({ + picture: pictureFromImage(image), + }), + src: reader.result, + }); + }); + reader.readAsDataURL(file); } function pictureFromImage(image) { - let width = Math.min(100, image.width); - let height = Math.min(100, image.height); - let canvas = elt("canvas", { - width, - height - }); - let cx = canvas.getContext("2d"); - cx.drawImage(image, 0, 0); - let pixels = []; - let { - data - } = cx.getImageData(0, 0, width, height); - - function hex(n) { - return n.toString(16).padStart(2, "0"); - } - for (let i = 0; i < data.length; i += 4) { - let [r, g, b] = data.slice(i, i + 3); - pixels.push("#" + hex(r) + hex(g) + hex(b)); - } - return new Picture(width, height, pixels); + let width = Math.min(16, image.width); + let height = Math.min(16, image.height); + let canvas = elt("canvas", { width, height }); + let cx = canvas.getContext("2d"); + cx.drawImage(image, 0, 0); + let pixels = []; + let { data } = cx.getImageData(0, 0, width, height); + + function hex(n) { + return n.toString(16).padStart(2, "0"); + } + for (let i = 0; i < data.length; i += 4) { + let [r, g, b] = data.slice(i, i + 3); + pixels.push("#" + hex(r) + hex(g) + hex(b)); + } + return new Picture(width, height, pixels); } function historyUpdateState(state, action) { - if (action.undo == true) { - if (state.done.length == 0) return state; - return { - ...state, - picture: state.done[0], - done: state.done.slice(1), - doneAt: 0 - }; - } else if (action.picture && state.doneAt < Date.now() - 1000) { - return { - ...state, - ...action, - done: [state.picture, ...state.done], - doneAt: Date.now() - }; - } else { - return { - ...state, - ...action - }; - } + if (action.undo == true) { + if (state.done.length == 0) return state; + return Object.assign({}, state, { + picture: state.done[0], + done: state.done.slice(1), + doneAt: 0, + }); + } else if (action.picture && state.doneAt < Date.now() - 1000) { + return Object.assign({}, state, action, { + done: [state.picture, ...state.done], + doneAt: Date.now(), + }); + } else { + return Object.assign({}, state, action); + } } - class UndoButton { - constructor(state, { - dispatch - }) { - this.dom = elt("button", { - onclick: () => dispatch({ - undo: true - }), - disabled: state.done.length == 0 - }, "⮪ Undo"); - } - syncState(state) { - this.dom.disabled = state.done.length == 0; - } + constructor(state, { dispatch }) { + this.dom = elt( + "button", + { + onclick: () => dispatch({ undo: true }), + disabled: state.done.length == 0, + }, + "⮪ Undo" + ); + } + syncState(state) { + this.dom.disabled = state.done.length == 0; + } } const startState = { - tool: "draw", - color: "#000000", - picture: Picture.empty(16, 16, "#f0f0f0"), - done: [], - doneAt: 0 + tool: "draw", + color: "#000000", + picture: Picture.empty(16, 16, "#f0f0f0"), + done: [], + doneAt: 0, }; -const baseTools = { - draw, - fill, - rectangle, - pick -}; +const baseTools = { draw, fill, rectangle, pick }; const baseControls = [ - ToolSelect, ColorSelect, SaveButton, LoadButton, UndoButton + ToolSelect, + ColorSelect, + SaveButton, + LoadButton, + UndoButton, ]; function startPixelEditor({ - state = startState, - tools = baseTools, - controls = baseControls + state = startState, + tools = baseTools, + controls = baseControls, }) { - let app = new PixelEditor(state, { - tools, - controls, - dispatch(action) { - state = historyUpdateState(state, action); - app.syncState(state); - } - }); - return app.dom; -} \ No newline at end of file + let app = new PixelEditor(state, { + tools, + controls, + dispatch(action) { + state = historyUpdateState(state, action); + app.syncState(state); + }, + }); + return app.dom; +} diff --git a/js/pixel-art/apple-icon-180x180.png b/js/pixel-art/apple-icon-180x180.png new file mode 100644 index 0000000..22e9372 --- /dev/null +++ b/js/pixel-art/apple-icon-180x180.png Binary files differdiff --git a/js/pixel-art/index.html b/js/pixel-art/index.html index abae244..87f240e 100644 --- a/js/pixel-art/index.html +++ b/js/pixel-art/index.html @@ -6,19 +6,30 @@ <title>pixel art</title> <script type="text/javascript" src="app.js"></script> <style type="text/css"> - .editor { - max-width: 80em; - padding: 1em; + body { + padding: 1.75em 1em; margin: 0 auto; display: block; } </style> + <link rel="icon" href="apple-icon-180x180.png" type="image/x-icon"> </head> <body> -<div class="editor"></div> -<script> - document.querySelector("div") - .appendChild(startPixelEditor({})); -</script> + <!-- <input type="number" name="size" value="16" min="8" max="200"> --> + <div class="editor"></div> + <script> + document.querySelector("div") + .appendChild(startPixelEditor({})); + </script> + <p><a href="./pixel/index.html">Check out a more advanced pixel art tool that I made after this one.</a></p> + <p> + Gameboy colors: + <ul> + <li><span style="background-color: #071821;"> </span> <code>#071821</code></li> + <li><span style="background-color: #86c06c;"> </span> <code>#86c06c</code></li> + <li><span style="background-color: #e0f8cf;"> </span> <code>#e0f8cf</code></li> + <li><span style="background-color: #65ff00;"> </span> <code>#65ff00</code></li> + </ul> + </p> </body> </html> \ No newline at end of file diff --git a/js/pixel-art/pixel/app.js b/js/pixel-art/pixel/app.js new file mode 100644 index 0000000..2d83997 --- /dev/null +++ b/js/pixel-art/pixel/app.js @@ -0,0 +1,561 @@ +const canvas = document.getElementById('canvas'); +const ctx = canvas.getContext('2d'); +const defaultGridWidth = 16; +const defaultGridHeight = 16; +let gridWidth = defaultGridWidth; +let gridHeight = defaultGridHeight; +let cellSize = 16; +let colorHistory = [ + '#000000', + '#ae8ce2', + '#2d5d9e', + '#43bef2', + '#99b213', + '#e5b42e', + '#c00f68', + '#ffffff' +]; +let currentColor = '#000000'; +let grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); +let offsetX = 0; +let offsetY = 0; +const paletteToggle = document.getElementById('palette-toggle'); +const palette = document.getElementById('palette'); +let isPaletteVisible = true; +let isDrawing = false; +let lastX = null; +let lastY = null; +let lastCell = null; +const MIN_CELL_SIZE = 4; +const MAX_CELL_SIZE = 64; +let canvases = []; +let currentCanvasIndex = 0; +let globalOffsetX = 0; +let globalOffsetY = 0; + +canvas.addEventListener('mousedown', handleInputStart); +canvas.addEventListener('mousemove', handleInputMove); +canvas.addEventListener('mouseup', handleInputEnd); +canvas.addEventListener('touchstart', handleInputStart); +canvas.addEventListener('touchmove', handleInputMove); +canvas.addEventListener('touchend', handleInputEnd); +document.getElementById('colorPicker').addEventListener('input', handleColorChange); +document.getElementById('gridWidth').addEventListener('change', updateGridSize); +document.getElementById('gridHeight').addEventListener('change', updateGridSize); +document.getElementById('resetBtn').addEventListener('click', handleReset); +document.getElementById('exportBtn').addEventListener('click', exportToPNG); +window.addEventListener('keydown', handlePan); +paletteToggle.addEventListener('click', togglePalette); +document.getElementById('zoomInBtn').addEventListener('click', () => handleZoom(1.25)); +document.getElementById('zoomOutBtn').addEventListener('click', () => handleZoom(0.75)); +document.getElementById('panUpBtn').addEventListener('click', () => handlePanButton('up')); +document.getElementById('panDownBtn').addEventListener('click', () => handlePanButton('down')); +document.getElementById('panLeftBtn').addEventListener('click', () => handlePanButton('left')); +document.getElementById('panRightBtn').addEventListener('click', () => handlePanButton('right')); +document.getElementById('centerViewBtn').addEventListener('click', resetView); +document.getElementById('newCanvasBtn').addEventListener('click', addNewCanvas); +document.getElementById('saveProjectBtn').addEventListener('click', saveProject); +document.getElementById('loadProjectBtn').addEventListener('click', loadProject); + + +resizeCanvas(); +loadFromLocalStorage(); +renderColorHistory(); + + +function addNewCanvas() { + canvases.push({ + grid: Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)), + offsetX: 0, + offsetY: 0, + hasPixels: false + }); + currentCanvasIndex = canvases.length - 1; + centerGrid(); + drawGrid(); + saveToLocalStorage(); +} + +function initializeGrid() { + if (canvases.length > 0) { + canvases[currentCanvasIndex].grid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); + } +} + +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + centerGrid(); + drawGrid(); +} + +function centerGrid() { + if (canvases.length === 0) return; + + canvases.forEach((canvasData, index) => { + canvasData.offsetY = Math.max((canvas.height - (gridHeight * cellSize)) / 2, 0); + if (index === 0) { + canvasData.offsetX = Math.max((canvas.width - (gridWidth * cellSize * canvases.length) - (cellSize * (canvases.length - 1))) / 2, 0); + } else { + // Position each canvas one cell width apart + const previousCanvas = canvases[index - 1]; + canvasData.offsetX = previousCanvas.offsetX + (gridWidth * cellSize) + cellSize; + } + }); +} + +function drawGrid() { + ctx.fillStyle = 'teal'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + canvases.forEach((canvasData, index) => { + const xOffset = canvasData.offsetX + globalOffsetX; + + for (let x = 0; x < gridWidth; x++) { + for (let y = 0; y < gridHeight; y++) { + const cellX = x * cellSize + xOffset; + const cellY = y * cellSize + canvasData.offsetY + globalOffsetY; + + // Fill cell background + ctx.fillStyle = canvasData.grid[x][y] || '#f7f7f7'; + ctx.fillRect(cellX, cellY, cellSize, cellSize); + + // Draw cell border + ctx.strokeStyle = '#888888'; + ctx.strokeRect(cellX, cellY, cellSize, cellSize); + + // Draw diagonal line for empty cells + if (!canvasData.grid[x][y]) { + ctx.beginPath(); + ctx.strokeStyle = '#bfbfbf'; + ctx.moveTo(cellX, cellY); + ctx.lineTo(cellX + cellSize, cellY + cellSize); + ctx.stroke(); + ctx.strokeStyle = '#888888'; + } + } + } + }); +} + +function addToColorHistory(color) { + if (colorHistory.includes(color)) return; + if (colorHistory.length >= 10) colorHistory.shift(); + colorHistory.push(color); + renderColorHistory(); +} + +function renderColorHistory() { + const historyDiv = document.getElementById('colorHistory'); + historyDiv.innerHTML = ''; + colorHistory.forEach(color => { + const colorDiv = document.createElement('div'); + colorDiv.style.backgroundColor = color; + colorDiv.addEventListener('click', () => { + currentColor = color; + document.getElementById('colorPicker').value = color; + }); + historyDiv.appendChild(colorDiv); + }); +} + +function handleColorChange() { + currentColor = document.getElementById('colorPicker').value; + addToColorHistory(currentColor); + saveToLocalStorage(); +} + +function handleReset() { + const confirmReset = confirm("Are you sure you want to reset? This will clear your drawing and settings."); + + if (confirmReset) { + gridWidth = defaultGridWidth; + gridHeight = defaultGridHeight; + cellSize = 16; + globalOffsetX = 0; + globalOffsetY = 0; + colorHistory = []; + renderColorHistory(); + + canvases = []; + addNewCanvas(); + + document.getElementById('gridWidth').disabled = false; + document.getElementById('gridHeight').disabled = false; + document.getElementById('gridWidth').value = gridWidth; + document.getElementById('gridHeight').value = gridHeight; + + localStorage.removeItem('pixelArtConfig'); + + alert("Reset complete. You can now adjust the grid size until you place your first pixel."); + } +} + +function handlePan(e) { + const step = cellSize; + if (canvases.length === 0) return; + + if (e.key === 'ArrowUp') globalOffsetY += step; + if (e.key === 'ArrowDown') globalOffsetY -= step; + if (e.key === 'ArrowLeft') globalOffsetX += step; + if (e.key === 'ArrowRight') globalOffsetX -= step; + drawGrid(); +} + +function updateGridSize() { + const newWidth = parseInt(document.getElementById('gridWidth').value); + const newHeight = parseInt(document.getElementById('gridHeight').value); + + // Validate input + if (newWidth <= 0 || newHeight <= 0 || newWidth > 100 || newHeight > 100) return; + + gridWidth = newWidth; + gridHeight = newHeight; + + // Update all existing canvases with new dimensions + canvases.forEach(canvasData => { + const newGrid = Array(gridWidth).fill().map(() => Array(gridHeight).fill(null)); + + // Preserve existing pixel data where possible + const minWidth = Math.min(canvasData.grid.length, gridWidth); + const minHeight = Math.min(canvasData.grid[0].length, gridHeight); + + for (let x = 0; x < minWidth; x++) { + for (let y = 0; y < minHeight; y++) { + newGrid[x][y] = canvasData.grid[x][y]; + } + } + + canvasData.grid = newGrid; + }); + + centerGrid(); + drawGrid(); + saveToLocalStorage(); +} + +function saveToLocalStorage() { + const gridData = { + gridWidth, + gridHeight, + cellSize, + colorHistory, + currentColor, + canvases, + isPaletteVisible, + globalOffsetX, + globalOffsetY + }; + localStorage.setItem('pixelArtConfig', JSON.stringify(gridData)); +} + +function loadFromLocalStorage() { + const savedData = localStorage.getItem('pixelArtConfig'); + if (savedData) { + const gridData = JSON.parse(savedData); + gridWidth = gridData.gridWidth || defaultGridWidth; + gridHeight = gridData.gridHeight || defaultGridHeight; + cellSize = gridData.cellSize || 16; + colorHistory = gridData.colorHistory || []; + currentColor = gridData.currentColor || '#000000'; + canvases = gridData.canvases || []; + globalOffsetX = gridData.globalOffsetX || 0; + globalOffsetY = gridData.globalOffsetY || 0; + + // Set input values + document.getElementById('gridWidth').value = gridWidth; + document.getElementById('gridHeight').value = gridHeight; + document.getElementById('colorPicker').value = currentColor; + + // Disable grid size inputs if there's saved data + document.getElementById('gridWidth').disabled = true; + document.getElementById('gridHeight').disabled = true; + + isPaletteVisible = gridData.isPaletteVisible ?? true; + if (!isPaletteVisible) { + palette.classList.add('hidden'); + paletteToggle.classList.add('hidden'); + paletteToggle.innerHTML = '🎨'; + } + } else { + // No saved data, create default canvas + gridWidth = defaultGridWidth; + gridHeight = defaultGridHeight; + addNewCanvas(); + } + + // Ensure there's at least one canvas + if (canvases.length === 0) { + addNewCanvas(); + } + + centerGrid(); + drawGrid(); + renderColorHistory(); +} + +function exportToPNG() { + // Prompt for filename + const filename = prompt("Enter a name for your file(s)", "pixel-art"); + if (!filename) return; // Cancelled + + // An array of promises for each canvas + const exportPromises = canvases.map((canvasData, index) => { + return new Promise(resolve => { + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = gridWidth * cellSize; + tempCanvas.height = gridHeight * cellSize; + + // Draw the canvas content + for (let x = 0; x < gridWidth; x++) { + for (let y = 0; y < gridHeight; y++) { + tempCtx.fillStyle = canvasData.grid[x][y] || 'transparent'; + tempCtx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + } + + // Convert to data URL (trying to work around a webkit bug where blobs don't work so well) + const dataURL = tempCanvas.toDataURL('image/png'); + const paddedNumber = String(index + 1).padStart(2, '0'); + const finalFilename = canvases.length > 1 + ? `${filename}-${paddedNumber}.png` + : `${filename}.png`; + + resolve({ dataURL, filename: finalFilename }); + }); + }); + + // Process exports sequentially with delay + Promise.all(exportPromises).then(exports => { + exports.forEach((exportData, index) => { + setTimeout(() => { + const link = document.createElement('a'); + link.href = exportData.dataURL; + link.download = exportData.filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, index * 1000); // 1 second delay between each download + }); + }); +} + +function handleInputStart(e) { + e.preventDefault(); + isDrawing = true; + const coords = getInputCoordinates(e); + const cell = getCellFromCoords(coords); + + if (cell) { + lastCell = cell; + currentCanvasIndex = cell.canvasIndex; + + // Lock grid size on first pixel placement + if (!document.getElementById('gridWidth').disabled) { + document.getElementById('gridWidth').disabled = true; + document.getElementById('gridHeight').disabled = true; + } + + if (canvases[currentCanvasIndex].grid[cell.x][cell.y]) { + canvases[currentCanvasIndex].grid[cell.x][cell.y] = null; + } else { + canvases[currentCanvasIndex].grid[cell.x][cell.y] = currentColor; + } + drawGrid(); + saveToLocalStorage(); + } +} + +function handleInputMove(e) { + e.preventDefault(); + if (!isDrawing) return; + + const coords = getInputCoordinates(e); + const cell = getCellFromCoords(coords); + + if (cell && (!lastCell || cell.x !== lastCell.x || cell.y !== lastCell.y)) { + lastCell = cell; + // When dragging, always draw (don't erase) + canvases[currentCanvasIndex].grid[cell.x][cell.y] = currentColor; + drawGrid(); + saveToLocalStorage(); + } +} + +function handleInputEnd(e) { + e.preventDefault(); + isDrawing = false; + lastCell = null; +} + +function getInputCoordinates(e) { + const rect = canvas.getBoundingClientRect(); + if (e.touches) { + // Touch event + return { + x: e.touches[0].clientX - rect.left, + y: e.touches[0].clientY - rect.top + }; + } else { + // Mouse event + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; + } +} + +function getCellFromCoords(coords) { + if (canvases.length === 0) return null; + + for (let i = 0; i < canvases.length; i++) { + const canvasData = canvases[i]; + const canvasLeft = canvasData.offsetX + globalOffsetX; + const canvasRight = canvasLeft + (gridWidth * cellSize); + + if (coords.x >= canvasLeft && coords.x < canvasRight) { + const x = Math.floor((coords.x - canvasLeft) / cellSize); + const y = Math.floor((coords.y - (canvasData.offsetY + globalOffsetY)) / cellSize); + + if (x >= 0 && x < gridWidth && y >= 0 && y < gridHeight) { + return { x, y, canvasIndex: i }; + } + } + } + return null; +} + +function togglePalette() { + isPaletteVisible = !isPaletteVisible; + + if (isPaletteVisible) { + palette.classList.remove('hidden'); + paletteToggle.classList.remove('hidden'); + paletteToggle.innerHTML = '☰'; + } else { + palette.classList.add('hidden'); + paletteToggle.classList.add('hidden'); + paletteToggle.innerHTML = '🎨'; + } +} + +function handleZoom(factor) { + const newCellSize = Math.max(MIN_CELL_SIZE, Math.min(MAX_CELL_SIZE, cellSize * factor)); + + if (newCellSize === cellSize) return; + + cellSize = newCellSize; + + centerGrid(); + + drawGrid(); + saveToLocalStorage(); +} + +function handlePanButton(direction) { + if (canvases.length === 0) return; + + const step = cellSize; + switch(direction) { + case 'up': + globalOffsetY += step; + break; + case 'down': + globalOffsetY -= step; + break; + case 'left': + globalOffsetX += step; + break; + case 'right': + globalOffsetX -= step; + break; + } + drawGrid(); +} + +function resetView() { + cellSize = 16; // Reset to default zoom + globalOffsetX = 0; + globalOffsetY = 0; + if (canvases.length > 0) { + centerGrid(); + drawGrid(); + saveToLocalStorage(); + } +} + +function saveProject() { + const now = new Date(); + const formattedDate = now.toISOString().slice(0, 16).replace('T', '-').replace(':', '-'); + + const projectName = prompt("Enter a name for your project", formattedDate); + if (!projectName) return; // User cancelled + + // First save to localStorage to ensure all current state is saved + saveToLocalStorage(); + + // Get the data from localStorage and add our special header + const projectData = JSON.parse(localStorage.getItem('pixelArtConfig')); + const exportData = { + __projectHeader: "pppppp_v1", // Add special header + timestamp: new Date().toISOString(), + data: projectData + }; + + // Create and trigger download + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${projectName}.json`; + link.click(); + URL.revokeObjectURL(link.href); +} + +function loadProject() { + // AAAAH! Data loss! + const confirmLoad = confirm("Loading a project will replace your current work. Are you sure you want to proceed?"); + if (!confirmLoad) return; + + // Create file input + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.json'; + + fileInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const importedData = JSON.parse(e.target.result); + + // Check for our super special header + if (!importedData.__projectHeader || + importedData.__projectHeader !== "pppppp_v1") { + throw new Error('This file is not a valid Pixel Art Project file'); + } + + const projectData = importedData.data; + + // Validate the data has expected properties + if (!projectData.gridWidth || !projectData.gridHeight || !projectData.canvases) { + throw new Error('Invalid project file format'); + } + + // Save to localStorage + localStorage.setItem('pixelArtConfig', JSON.stringify(projectData)); + + // Reload the page to apply changes + window.location.reload(); + + } catch (error) { + alert('Error loading project file: ' + error.message); + } + }; + reader.readAsText(file); + }); + + fileInput.click(); +} \ No newline at end of file diff --git a/js/pixel-art/pixel/index.html b/js/pixel-art/pixel/index.html new file mode 100644 index 0000000..7e3df56 --- /dev/null +++ b/js/pixel-art/pixel/index.html @@ -0,0 +1,213 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <title>Pixel Pixel Pixel Pixel Pixel Pixel</title> + <meta name="description" content="Draw me a goblin, please."> + <style> + body, html { + margin: 0; + padding: 0; + overflow: hidden; + background-color: teal; + } + + button { + padding: 0.5rem 0.75rem; + font-size: 1rem; + cursor: pointer; + min-height: 2.75rem; + } + + button.reset-btn { + cursor: pointer; + margin: 0; + padding: 0.25rem 0.5rem; + border: none; + background-color: tomato; + color: #fafafa; + min-height: 2rem; + font-size: 0.875rem; + border-radius: 0.25rem; + margin-top: 0.5rem; + } + + #canvas { + display: block; + } + + #palette { + position: fixed; + top: 0.625rem; + right: 0.625rem; + background-color: beige; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 0.25rem; + width: 10.625rem; + transition: transform 0.3s ease-out; + max-height: 90vh; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + box-sizing: border-box; + } + + #palette.hidden { + transform: translateX(calc(100% + 1.25rem)); + } + + #palette-toggle { + position: fixed; + top: 1.025rem; + right: 0.025rem; + background-color: beige; + border: 1px solid #ccc; + border-radius: 0.25rem 0 0 0.25rem; + padding: 0.75rem; + cursor: pointer; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease-out; + min-width: 2.75rem; + min-height: 2.75rem; + font-size: 1.125rem; + } + + #palette-toggle:not(.hidden) { + transform: translateX(-11.25rem); + } + + #palette input, + #palette button { + display: block; + margin-bottom: 0.75rem; + width: 100%; + } + + .color-history { + display: flex; + flex-wrap: wrap; + margin-top: 0.625rem; + } + + .color-history div { + width: 1.25rem; + height: 1.25rem; + border: 1px solid #000; + cursor: pointer; + margin-right: 0.3125rem; + margin-bottom: 0.3125rem; + } + + .zoom-controls { + display: flex; + gap: 0.3125rem; + margin-top: 0.625rem; + justify-content: space-between; + width: 100%; + } + + .zoom-controls button { + flex: 1; + padding: 0.25rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + min-height: unset; + width: unset; + } + + .pan-controls { + display: flex; + flex-direction: column; + gap: 0.3125rem; + margin-top: 0.625rem; + align-items: center; + } + + .pan-middle { + display: flex; + gap: 0.3125rem; + width: 100%; + } + + .pan-controls button { + padding: 0.25rem; + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + min-height: unset; + } + + #centerViewBtn { + margin-top: 0.625rem; + } + + @media (max-width: 48rem) { + #palette { + width: 12.5rem; + top: auto; + bottom: 0.625rem; + } + + #palette-toggle { + top: auto; + right: 0.625rem; + bottom: 0.625rem; + } + + #palette-toggle:not(.hidden) { + transform: translateX(-12.5rem); + } + } + </style> +</head> +<body> + <canvas id="canvas"></canvas> + <button id="palette-toggle">☰</button> + <div id="palette"> + <div style="display: flex; gap: 5px; margin-bottom: 10px;"> + <div style="flex: 1;"> + <label for="gridWidth">Width:</label> + <input type="number" id="gridWidth" value="16" min="1" max="100" style="width: 90%;"> + </div> + <div style="flex: 1;"> + <label for="gridHeight">Height:</label> + <input type="number" id="gridHeight" value="16" min="1" max="100" style="width: 90%;"> + </div> + </div> + <label for="colorPicker">Select Color:</label> + <input type="color" id="colorPicker" value="#000000"> + <div id="colorHistory" class="color-history"></div> + <div class="zoom-controls"> + <button id="zoomInBtn">🔍+</button> + <button id="zoomOutBtn">🔍-</button> + </div> + <div class="pan-controls"> + <button id="panUpBtn">⬆️</button> + <div class="pan-middle"> + <button id="panLeftBtn">⬅️</button> + <button id="panRightBtn">➡️</button> + </div> + <button id="panDownBtn">⬇️</button> + </div> + <button id="centerViewBtn">🎯 Center View</button> + <button id="newCanvasBtn">➕ New Canvas</button> + <hr> + <button id="exportBtn">Export as PNG</button> + <button id="saveProjectBtn">Save Project</button> + <button id="loadProjectBtn">Load Project</button> + <button id="resetBtn" class="reset-btn">Reset</button> + </div> + <script src="app.js"></script> +</body> +</html> |