* Toolbar.js
*
* Renders the left-side tool dock. The icons are tiny canvases drawn
* inline (no external icon fonts), matching the rest of the asset style.
*/
import { playUiClick } from './Audio.js';
const TOOL_ICONS = {
place: drawPlaceIcon,
fill: drawFillIcon,
erase: drawEraseIcon,
pan: drawPanIcon,
grid: drawGridIcon,
save: drawSaveIcon,
reset: drawResetIcon,
};
const TOOL_BUTTONS = [
{ id: 'place', label: 'Place' },
{ id: 'fill', label: 'Fill' },
{ id: 'erase', label: 'Erase' },
{ id: 'pan', label: 'Pan' },
{ id: 'grid', label: 'Grid' },
{ id: 'save', label: 'Save' },
{ id: 'reset', label: 'Reset' },
];
export class Toolbar {
constructor(rootEl, game) {
this.root = rootEl;
this.game = game;
this.buttons = new Map();
this._build();
}
_build() {
this.root.innerHTML = '';
const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
const dprFactor = Math.max(1, Math.min(3, Math.ceil(dpr)));
const backing = 44 * dprFactor;
for (const def of TOOL_BUTTONS) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'tool';
btn.dataset.toolId = def.id;
const cv = document.createElement('canvas');
cv.className = 'ti';
cv.width = backing;
cv.height = backing;
const ctx = cv.getContext('2d');
if (dprFactor !== 1) ctx.scale(dprFactor, dprFactor);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const drawer = TOOL_ICONS[def.id];
if (drawer) drawer(ctx);
const label = document.createElement('span');
label.className = 'label';
label.textContent = def.label;
btn.appendChild(cv);
btn.appendChild(label);
btn.addEventListener('click', () => this._onClick(def.id));
this.root.appendChild(btn);
this.buttons.set(def.id, btn);
}
this.update();
}
_onClick(id) {
playUiClick();
switch (id) {
case 'place': this.game.setTool('place'); break;
case 'erase': this.game.setTool('erase'); break;
case 'pan': this.game.setTool('pan'); break;
case 'grid': this.game.toggleGrid(); break;
case 'save': this.game.save(); break;
case 'reset': this.game.reset(); break;
case 'fill': this.game.fillGrass(); break;
}
}
update() {
const tool = this.game.tool;
const grid = this.game.renderer.showGrid;
for (const [id, btn] of this.buttons) {
btn.classList.toggle('active',
(id === 'place' && tool === 'place')
|| (id === 'erase' && tool === 'erase')
|| (id === 'pan' && tool === 'pan')
|| (id === 'grid' && grid)
);
}
}
}
*
* Each icon is drawn on its own 44×44 canvas, centered at (22, 22).
* The shapes use universally-recognised metaphors so the user can
* understand what each tool does without reading the label:
*
* place → map pin (Google-Maps-style "drop here")
* fill → paint bucket with green paint inside
* erase → classic pencil eraser block (rubber + metal collar)
* pan → four-direction move arrows (Figma / Photoshop move tool)
* grid → 3×3 mesh
* save → floppy disk with sliding shutter + label area
* reset → circular arrow (refresh / undo)
*
* All icons share the cobalt ink colour so the rail reads as one set.
*/
const INK = '#1b5ba8';
const INK_DARK = '#134680';
const PAPER = '#fafaf5';
const GRASS = '#7eaa5f';
const GRASS_DK = '#5c8a44';
const ERASER_PINK = '#e89a9a';
function drawPlaceIcon(ctx) {
ctx.save();
ctx.translate(22, 22);
ctx.fillStyle = INK;
ctx.beginPath();
ctx.moveTo(0, 14);
ctx.bezierCurveTo(-10, 4, -10, -10, 0, -10);
ctx.bezierCurveTo(10, -10, 10, 4, 0, 14);
ctx.closePath();
ctx.fill();
ctx.fillStyle = PAPER;
ctx.beginPath();
ctx.arc(0, -3, 3.6, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawFillIcon(ctx) {
ctx.save();
ctx.translate(22, 22);
ctx.fillStyle = INK;
ctx.beginPath();
ctx.moveTo(-9, -3);
ctx.lineTo(9, -3);
ctx.lineTo(7, 11);
ctx.lineTo(-7, 11);
ctx.closePath();
ctx.fill();
ctx.fillStyle = GRASS;
ctx.beginPath();
ctx.ellipse(0, -3, 9, 2.6, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = GRASS_DK;
ctx.beginPath();
ctx.ellipse(0, -3, 7, 1.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = INK;
ctx.lineWidth = 1.8;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(0, -3, 11, Math.PI * 1.2, Math.PI * 1.8, false);
ctx.stroke();
ctx.restore();
}
function drawEraseIcon(ctx) {
ctx.save();
ctx.translate(22, 22);
ctx.rotate(-0.5);
ctx.fillStyle = INK;
ctx.beginPath();
ctx.moveTo(-2, -7);
ctx.lineTo(11, -7);
ctx.lineTo(11, 7);
ctx.lineTo(-2, 7);
ctx.closePath();
ctx.fill();
ctx.fillStyle = ERASER_PINK;
ctx.beginPath();
ctx.moveTo(-11, -7);
ctx.lineTo(-2, -7);
ctx.lineTo(-2, 7);
ctx.lineTo(-11, 7);
ctx.closePath();
ctx.fill();
ctx.fillStyle = PAPER;
ctx.fillRect(-3, -7, 1.5, 14);
ctx.restore();
ctx.save();
ctx.translate(22, 22);
ctx.fillStyle = INK;
ctx.fillRect(-12, 11, 3, 1.5);
ctx.fillRect(-7, 13, 2, 1.2);
ctx.fillRect(-3, 11, 2, 1.2);
ctx.restore();
}
function drawPanIcon(ctx) {
ctx.save();
ctx.translate(22, 22);
ctx.fillStyle = INK;
ctx.fillRect(-2, -7, 4, 14);
ctx.fillRect(-7, -2, 14, 4);
ctx.beginPath();
ctx.moveTo(0, -12);
ctx.lineTo(-5, -7);
ctx.lineTo(5, -7);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, 12);
ctx.lineTo(-5, 7);
ctx.lineTo(5, 7);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(-12, 0);
ctx.lineTo(-7, -5);
ctx.lineTo(-7, 5);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(12, 0);
ctx.lineTo(7, -5);
ctx.lineTo(7, 5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawGridIcon(ctx) {
ctx.save();
ctx.translate(22, 22);
ctx.strokeStyle = INK;
ctx.lineWidth = 1.6;
ctx.lineCap = 'square';
ctx.beginPath();
for (let i = -9; i <= 9; i += 6) {
ctx.moveTo(-9, i);
ctx.lineTo(9, i);
ctx.moveTo(i, -9);
ctx.lineTo(i, 9);
}
ctx.stroke();
ctx.restore();
}
function drawSaveIcon(ctx) {
ctx.save();
ctx.translate(22, 22);
ctx.fillStyle = INK;
ctx.beginPath();
ctx.moveTo(-11, -11);
ctx.lineTo(8, -11);
ctx.lineTo(11, -8);
ctx.lineTo(11, 11);
ctx.lineTo(-11, 11);
ctx.closePath();
ctx.fill();
ctx.fillStyle = PAPER;
ctx.fillRect(-7, -11, 13, 7);
ctx.fillStyle = INK;
ctx.fillRect(2, -10, 2.5, 5);
ctx.fillStyle = PAPER;
ctx.fillRect(-7, -1, 14, 9);
ctx.fillStyle = INK;
ctx.fillRect(-5, 2, 10, 1);
ctx.fillRect(-5, 5, 10, 1);
ctx.restore();
}
function drawResetIcon(ctx) {
ctx.save();
ctx.translate(22, 22);
ctx.strokeStyle = INK;
ctx.lineWidth = 2.2;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(0, 0, 9, Math.PI * 0.2, Math.PI * 1.75);
ctx.stroke();
ctx.fillStyle = INK;
ctx.beginPath();
ctx.moveTo(2, -6);
ctx.lineTo(10, -3);
ctx.lineTo(8, -11);
ctx.closePath();
ctx.fill();
ctx.restore();
}