Sprite Stacking Editor
by Ben Gillbanks
HTML
<div class="panel">
<h3>Editor</h3>
<canvas id="editor" width="256" height="256"></canvas>
<div class="toolbar">
<div class="palette" id="palette"></div>
<button id="eraseTool">Erase</button>
<button id="addLayer" type="button">Add Layer</button>
<button id="duplicateLayer" type="button">Duplicate Layer</button>
</div>
<div id="layers"></div>
<div class="note">
Left click draws. Right click erases.
</div>
</div>
<div class="panel">
<h3>Preview</h3>
<canvas id="preview" width="130" height="100"></canvas>
<div class="note">
Drag to rotate and tilt. Mouse wheel zooms.
</div>
</div>
CSS
body {
margin: 0;
padding: 16px;
font-family: sans-serif;
background: #f3f3f3;
color: #222;
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.panel {
background: #fff;
border: 1px solid #ccc;
padding: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
h3 {
margin: 0 0 12px 0;
font-size: 16px;
}
canvas {
border: 1px solid #999;
image-rendering: pixelated;
image-rendering: crisp-edges;
background: #fff;
display: block;
}
#preview {
width: 520px;
height: 400px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
align-items: center;
}
.palette {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.swatch {
width: 28px;
height: 28px;
border: 2px solid #999;
cursor: pointer;
box-sizing: border-box;
padding: 0;
}
.swatch.selected {
border-color: #000;
outline: 2px solid #fff;
box-shadow: 0 0 0 2px #000;
}
button {
padding: 6px 10px;
cursor: pointer;
}
#layers {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
}
.layer-button.active {
font-weight: bold;
border: 2px solid #000;
}
.note {
margin-top: 10px;
font-size: 13px;
color: #555;
max-width: 460px;
}
JavaScript
const GRID_SIZE = 16;
const EDITOR_SCALE = 16;
const PALETTE = [
'#000000',
'#ffffff',
'#d7263d',
'#f49d37',
'#3f88c5',
'#1a936f',
'#7b2cbf',
'#f2e94e',
];
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
const editorContext = editor.getContext('2d');
const previewContext = preview.getContext('2d');
previewContext.imageSmoothingEnabled = false
previewContext.webkitImageSmoothingEnabled = false
previewContext.mozImageSmoothingEnabled = false
previewContext.msImageSmoothingEnabled = false
let layers = [];
let currentLayerIndex = 0;
let selectedColour = 0;
let isDrawing = false;
let drawMode = 'draw';
let tool = "draw"
let cameraYaw = Math.PI / 4;
let cameraPitch = 0.75;
let cameraZoom = 3;
let layerHeight = 1;
let isDraggingPreview = false;
let lastPointerX = 0;
let lastPointerY = 0;
function createEmptyLayer() {
const pixels = [];
for (let y = 0; y < GRID_SIZE; y++) {
const row = [];
for (let x = 0; x < GRID_SIZE; x++) {
row.push(-1);
}
pixels.push(row);
}
const canvas = document.createElement('canvas');
canvas.width = GRID_SIZE;
canvas.height = GRID_SIZE;
return {
pixels: pixels,
canvas: canvas,
context: canvas.getContext('2d'),
dirty: true,
};
}
function cloneLayer(sourceLayer) {
const layer = createEmptyLayer();
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
layer.pixels[y][x] = sourceLayer.pixels[y][x];
}
}
layer.dirty = true;
return layer;
}
function addLayer() {
layers.push(createEmptyLayer());
currentLayerIndex = layers.length - 1;
renderLayerButtons();
drawEditor();
drawPreview();
}
function duplicateCurrentLayer() {
const source = layers[currentLayerIndex];
layers.push(cloneLayer(source));
currentLayerIndex = layers.length - 1;
renderLayerButtons();
...