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;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
#canvas-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#canvas, #overlay-canvas {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
#overlay-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
@ -275,7 +287,10 @@
|
|||
</div>
|
||||
|
||||
<div id="main">
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="canvas-wrapper">
|
||||
<canvas id="canvas"></canvas>
|
||||
<canvas id="overlay-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
@ -306,9 +321,12 @@
|
|||
let lastPaintedCell = null;
|
||||
let brushSize = 1;
|
||||
let brushShape = 'circle';
|
||||
let lastHoveredCell = { x: -1, y: -1 }; // for cursor overlay
|
||||
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
const overlayCanvas = document.getElementById('overlay-canvas');
|
||||
const overlayCtx = overlayCanvas.getContext('2d');
|
||||
const widthInput = document.getElementById('width');
|
||||
const heightInput = document.getElementById('height');
|
||||
const m1Slot = document.getElementById('m1');
|
||||
|
|
@ -355,6 +373,9 @@
|
|||
canvas.width = gridWidth;
|
||||
canvas.height = gridHeight;
|
||||
}
|
||||
// Sync overlay canvas size
|
||||
overlayCanvas.width = canvas.width;
|
||||
overlayCanvas.height = canvas.height;
|
||||
fitCanvas();
|
||||
}
|
||||
|
||||
|
|
@ -375,6 +396,9 @@
|
|||
}
|
||||
canvas.style.width = Math.floor(cssW) + '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);
|
||||
|
|
@ -441,6 +465,34 @@
|
|||
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) {
|
||||
if (brushSize === 1) {
|
||||
paintCell(x, y, symbol);
|
||||
|
|
@ -451,23 +503,9 @@
|
|||
const oldLastPainted = lastPaintedCell;
|
||||
lastPaintedCell = null;
|
||||
|
||||
for (let dy = -(brushSize - 1); dy < brushSize; dy++) {
|
||||
for (let dx = -(brushSize - 1); dx < brushSize; dx++) {
|
||||
const nx = x + dx;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
const cells = getCellsInBrush(x, y);
|
||||
for (const cell of cells) {
|
||||
grid[cell.y][cell.x] = symbol;
|
||||
}
|
||||
|
||||
lastPaintedCell = `${x},${y}`;
|
||||
|
|
@ -823,6 +861,7 @@ ${paletteStr}"""
|
|||
symbols.m1 = entry.char;
|
||||
m1Slot.textContent = entry.char;
|
||||
updateSymbolSlotColors();
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
saveState();
|
||||
});
|
||||
|
||||
|
|
@ -833,6 +872,7 @@ ${paletteStr}"""
|
|||
symbols.m2 = entry.char;
|
||||
m2Slot.textContent = entry.char;
|
||||
updateSymbolSlotColors();
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
saveState();
|
||||
});
|
||||
|
||||
|
|
@ -881,6 +921,36 @@ ${paletteStr}"""
|
|||
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() {
|
||||
const form = document.getElementById('palette-form');
|
||||
const char = document.getElementById('palette-char').value;
|
||||
|
|
@ -951,11 +1021,24 @@ ${paletteStr}"""
|
|||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const { x, y } = getGridCoords(e);
|
||||
lastHoveredCell = { x, y };
|
||||
renderCursorOverlay(x, y);
|
||||
|
||||
if (isDrawing) {
|
||||
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
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDrawing) {
|
||||
|
|
@ -980,6 +1063,7 @@ ${paletteStr}"""
|
|||
document.querySelectorAll('input[name="tool"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
currentTool = e.target.value;
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -987,18 +1071,21 @@ ${paletteStr}"""
|
|||
document.getElementById('brushSize').addEventListener('input', (e) => {
|
||||
brushSize = parseInt(e.target.value);
|
||||
document.getElementById('brushSizeValue').textContent = brushSize;
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
saveState();
|
||||
});
|
||||
|
||||
document.getElementById('brushCircle').addEventListener('click', () => {
|
||||
brushShape = 'circle';
|
||||
updateBrushUI();
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
saveState();
|
||||
});
|
||||
|
||||
document.getElementById('brushSquare').addEventListener('click', () => {
|
||||
brushShape = 'square';
|
||||
updateBrushUI();
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
saveState();
|
||||
});
|
||||
|
||||
|
|
@ -1006,16 +1093,72 @@ ${paletteStr}"""
|
|||
m1Slot.addEventListener('click', () => {
|
||||
activeSlot = 'm1';
|
||||
updateActiveSlot();
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
saveState();
|
||||
});
|
||||
|
||||
m2Slot.addEventListener('click', () => {
|
||||
activeSlot = 'm2';
|
||||
updateActiveSlot();
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
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) => {
|
||||
if (e.target.tagName === 'INPUT') return;
|
||||
|
||||
|
|
@ -1028,6 +1171,7 @@ ${paletteStr}"""
|
|||
m2Slot.textContent = char;
|
||||
}
|
||||
updateSymbolSlotColors();
|
||||
renderCursorOverlay(lastHoveredCell.x, lastHoveredCell.y);
|
||||
saveState();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue