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();
 ...