Add brush cursor preview, keyboard shortcuts, and command system
This commit is contained in:
parent
ce49530ff4
commit
3517f61219
1 changed files with 164 additions and 20 deletions
|
|
@ -82,11 +82,23 @@
|
||||||
border-width: 4px;
|
border-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas {
|
#canvas-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas, #overlay-canvas {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#overlay-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.actions button {
|
.actions button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -275,7 +287,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
<div id="canvas-wrapper">
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
|
<canvas id="overlay-canvas"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -306,9 +321,12 @@
|
||||||
let lastPaintedCell = null;
|
let lastPaintedCell = null;
|
||||||
let brushSize = 1;
|
let brushSize = 1;
|
||||||
let brushShape = 'circle';
|
let brushShape = 'circle';
|
||||||
|
let lastHoveredCell = { x: -1, y: -1 }; // for cursor overlay
|
||||||
|
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
const overlayCanvas = document.getElementById('overlay-canvas');
|
||||||
|
const overlayCtx = overlayCanvas.getContext('2d');
|
||||||
const widthInput = document.getElementById('width');
|
const widthInput = document.getElementById('width');
|
||||||
const heightInput = document.getElementById('height');
|
const heightInput = document.getElementById('height');
|
||||||
const m1Slot = document.getElementById('m1');
|
const m1Slot = document.getElementById('m1');
|
||||||
|
|
@ -355,6 +373,9 @@
|
||||||
canvas.width = gridWidth;
|
canvas.width = gridWidth;
|
||||||
canvas.height = gridHeight;
|
canvas.height = gridHeight;
|
||||||
}
|
}
|
||||||
|
// Sync overlay canvas size
|
||||||
|
overlayCanvas.width = canvas.width;
|
||||||
|
overlayCanvas.height = canvas.height;
|
||||||
fitCanvas();
|
fitCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,6 +396,9 @@
|
||||||
}
|
}
|
||||||
canvas.style.width = Math.floor(cssW) + 'px';
|
canvas.style.width = Math.floor(cssW) + 'px';
|
||||||
canvas.style.height = Math.floor(cssH) + 'px';
|
canvas.style.height = Math.floor(cssH) + 'px';
|
||||||
|
// Sync overlay canvas CSS size
|
||||||
|
overlayCanvas.style.width = Math.floor(cssW) + 'px';
|
||||||
|
overlayCanvas.style.height = Math.floor(cssH) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', fitCanvas);
|
window.addEventListener('resize', fitCanvas);
|
||||||
|
|
@ -441,6 +465,34 @@
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCellsInBrush(cx, cy) {
|
||||||
|
const cells = [];
|
||||||
|
if (brushSize === 1) {
|
||||||
|
cells.push({ x: cx, y: cy });
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let dy = -(brushSize - 1); dy < brushSize; dy++) {
|
||||||
|
for (let dx = -(brushSize - 1); dx < brushSize; dx++) {
|
||||||
|
const nx = cx + dx;
|
||||||
|
const ny = cy + dy;
|
||||||
|
|
||||||
|
if (nx < 0 || nx >= gridWidth || ny < 0 || ny >= gridHeight) continue;
|
||||||
|
|
||||||
|
if (brushShape === 'circle') {
|
||||||
|
if (dx * dx + dy * dy < brushSize * brushSize) {
|
||||||
|
cells.push({ x: nx, y: ny });
|
||||||
|
}
|
||||||
|
} else { // square
|
||||||
|
if (Math.abs(dx) < brushSize && Math.abs(dy) < brushSize) {
|
||||||
|
cells.push({ x: nx, y: ny });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
function paintBrush(x, y, symbol) {
|
function paintBrush(x, y, symbol) {
|
||||||
if (brushSize === 1) {
|
if (brushSize === 1) {
|
||||||
paintCell(x, y, symbol);
|
paintCell(x, y, symbol);
|
||||||
|
|
@ -451,23 +503,9 @@
|
||||||
const oldLastPainted = lastPaintedCell;
|
const oldLastPainted = lastPaintedCell;
|
||||||
lastPaintedCell = null;
|
lastPaintedCell = null;
|
||||||
|
|
||||||
for (let dy = -(brushSize - 1); dy < brushSize; dy++) {
|
const cells = getCellsInBrush(x, y);
|
||||||
for (let dx = -(brushSize - 1); dx < brushSize; dx++) {
|
for (const cell of cells) {
|
||||||
const nx = x + dx;
|
grid[cell.y][cell.x] = symbol;
|
||||||
const ny = y + dy;
|
|
||||||
|
|
||||||
if (nx < 0 || nx >= gridWidth || ny < 0 || ny >= gridHeight) continue;
|
|
||||||
|
|
||||||
if (brushShape === 'circle') {
|
|
||||||
if (dx * dx + dy * dy < brushSize * brushSize) {
|
|
||||||
grid[ny][nx] = symbol;
|
|
||||||
}
|
|
||||||
} else { // square
|
|
||||||
if (Math.abs(dx) < brushSize && Math.abs(dy) < brushSize) {
|
|
||||||
grid[ny][nx] = symbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastPaintedCell = `${x},${y}`;
|
lastPaintedCell = `${x},${y}`;
|
||||||
|
|
@ -823,6 +861,7 @@ ${paletteStr}"""
|
||||||
symbols.m1 = entry.char;
|
symbols.m1 = entry.char;
|
||||||
m1Slot.textContent = entry.char;
|
m1Slot.textContent = entry.char;
|
||||||
updateSymbolSlotColors();
|
updateSymbolSlotColors();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -833,6 +872,7 @@ ${paletteStr}"""
|
||||||
symbols.m2 = entry.char;
|
symbols.m2 = entry.char;
|
||||||
m2Slot.textContent = entry.char;
|
m2Slot.textContent = entry.char;
|
||||||
updateSymbolSlotColors();
|
updateSymbolSlotColors();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -881,6 +921,36 @@ ${paletteStr}"""
|
||||||
document.getElementById('palette-form').style.display = 'none';
|
document.getElementById('palette-form').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCursorOverlay(cellX, cellY) {
|
||||||
|
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||||
|
|
||||||
|
if (cellX < 0 || cellY < 0) return;
|
||||||
|
|
||||||
|
const cells = getCellsInBrush(cellX, cellY);
|
||||||
|
|
||||||
|
let fillColor;
|
||||||
|
if (currentTool === 'paintbrush') {
|
||||||
|
const activeChar = symbols[activeSlot];
|
||||||
|
const entry = palette.find(e => e.char === activeChar);
|
||||||
|
const rgb = entry ? hexToRgb(entry.color) : [255, 255, 255];
|
||||||
|
fillColor = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.35)`;
|
||||||
|
} else {
|
||||||
|
fillColor = 'rgba(220, 50, 50, 0.35)';
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayCtx.fillStyle = fillColor;
|
||||||
|
|
||||||
|
if (viewMode === 'text') {
|
||||||
|
for (const cell of cells) {
|
||||||
|
overlayCtx.fillRect(cell.x * CELL_SIZE, cell.y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const cell of cells) {
|
||||||
|
overlayCtx.fillRect(cell.x, cell.y, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function savePaletteEntry() {
|
function savePaletteEntry() {
|
||||||
const form = document.getElementById('palette-form');
|
const form = document.getElementById('palette-form');
|
||||||
const char = document.getElementById('palette-char').value;
|
const char = document.getElementById('palette-char').value;
|
||||||
|
|
@ -951,11 +1021,24 @@ ${paletteStr}"""
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', (e) => {
|
canvas.addEventListener('mousemove', (e) => {
|
||||||
|
const { x, y } = getGridCoords(e);
|
||||||
|
lastHoveredCell = { x, y };
|
||||||
|
renderCursorOverlay(x, y);
|
||||||
|
|
||||||
if (isDrawing) {
|
if (isDrawing) {
|
||||||
handlePaint(e, drawButton);
|
handlePaint(e, drawButton);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mouseenter', () => {
|
||||||
|
overlayCanvas.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mouseleave', () => {
|
||||||
|
overlayCanvas.style.display = 'none';
|
||||||
|
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||||
|
});
|
||||||
|
|
||||||
// Listen on document so releasing outside canvas still stops drawing
|
// Listen on document so releasing outside canvas still stops drawing
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
if (isDrawing) {
|
if (isDrawing) {
|
||||||
|
|
@ -980,6 +1063,7 @@ ${paletteStr}"""
|
||||||
document.querySelectorAll('input[name="tool"]').forEach(radio => {
|
document.querySelectorAll('input[name="tool"]').forEach(radio => {
|
||||||
radio.addEventListener('change', (e) => {
|
radio.addEventListener('change', (e) => {
|
||||||
currentTool = e.target.value;
|
currentTool = e.target.value;
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -987,18 +1071,21 @@ ${paletteStr}"""
|
||||||
document.getElementById('brushSize').addEventListener('input', (e) => {
|
document.getElementById('brushSize').addEventListener('input', (e) => {
|
||||||
brushSize = parseInt(e.target.value);
|
brushSize = parseInt(e.target.value);
|
||||||
document.getElementById('brushSizeValue').textContent = brushSize;
|
document.getElementById('brushSizeValue').textContent = brushSize;
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('brushCircle').addEventListener('click', () => {
|
document.getElementById('brushCircle').addEventListener('click', () => {
|
||||||
brushShape = 'circle';
|
brushShape = 'circle';
|
||||||
updateBrushUI();
|
updateBrushUI();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('brushSquare').addEventListener('click', () => {
|
document.getElementById('brushSquare').addEventListener('click', () => {
|
||||||
brushShape = 'square';
|
brushShape = 'square';
|
||||||
updateBrushUI();
|
updateBrushUI();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1006,16 +1093,72 @@ ${paletteStr}"""
|
||||||
m1Slot.addEventListener('click', () => {
|
m1Slot.addEventListener('click', () => {
|
||||||
activeSlot = 'm1';
|
activeSlot = 'm1';
|
||||||
updateActiveSlot();
|
updateActiveSlot();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
m2Slot.addEventListener('click', () => {
|
m2Slot.addEventListener('click', () => {
|
||||||
activeSlot = 'm2';
|
activeSlot = 'm2';
|
||||||
updateActiveSlot();
|
updateActiveSlot();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard input for symbols
|
// commands - add new commands here
|
||||||
|
const commands = {
|
||||||
|
'brush-size-up': () => {
|
||||||
|
if (brushSize < 8) {
|
||||||
|
brushSize++;
|
||||||
|
const slider = document.getElementById('brushSize');
|
||||||
|
const valueDisplay = document.getElementById('brushSizeValue');
|
||||||
|
slider.value = brushSize;
|
||||||
|
valueDisplay.textContent = brushSize;
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'brush-size-down': () => {
|
||||||
|
if (brushSize > 1) {
|
||||||
|
brushSize--;
|
||||||
|
const slider = document.getElementById('brushSize');
|
||||||
|
const valueDisplay = document.getElementById('brushSizeValue');
|
||||||
|
slider.value = brushSize;
|
||||||
|
valueDisplay.textContent = brushSize;
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'swap-symbols': () => {
|
||||||
|
const temp = symbols.m1;
|
||||||
|
symbols.m1 = symbols.m2;
|
||||||
|
symbols.m2 = temp;
|
||||||
|
m1Slot.textContent = symbols.m1;
|
||||||
|
m2Slot.textContent = symbols.m2;
|
||||||
|
updateSymbolSlotColors();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
|
saveState();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// keybinding map - key string to command name
|
||||||
|
const keybindings = {
|
||||||
|
']': 'brush-size-up',
|
||||||
|
'[': 'brush-size-down',
|
||||||
|
'x': 'swap-symbols',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard event handlers
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
const tag = document.activeElement.tagName;
|
||||||
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
||||||
|
|
||||||
|
const command = keybindings[e.key];
|
||||||
|
if (command && commands[command]) {
|
||||||
|
e.preventDefault();
|
||||||
|
commands[command]();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('keypress', (e) => {
|
document.addEventListener('keypress', (e) => {
|
||||||
if (e.target.tagName === 'INPUT') return;
|
if (e.target.tagName === 'INPUT') return;
|
||||||
|
|
||||||
|
|
@ -1028,6 +1171,7 @@ ${paletteStr}"""
|
||||||
m2Slot.textContent = char;
|
m2Slot.textContent = char;
|
||||||
}
|
}
|
||||||
updateSymbolSlotColors();
|
updateSymbolSlotColors();
|
||||||
|
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||||
saveState();
|
saveState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue