Opacity added.

This commit is contained in:
Yaro Kasear 2025-12-16 15:22:23 -06:00
parent 802c3cd028
commit 641ae1470d

View file

@ -44,11 +44,11 @@
<div id="toolBar" <div id="toolBar"
class="btn-toolbar bg-light border border-bottom-0 rounded-bottom-0 border-secondary-subtle rounded p-1 align-items-center flex-nowrap overflow-auto"> class="btn-toolbar bg-light border border-bottom-0 rounded-bottom-0 border-secondary-subtle rounded p-1 align-items-center flex-nowrap overflow-auto">
<div class="input-group input-group-sm w-auto flex-nowrap"> <div class="input-group input-group-sm w-auto flex-nowrap">
<span class="input-group-text">Grid Size:</span> <span class="input-group-text">Cell Size:</span>
<input type="number" min="1" value="25" name="gridSize" id="gridSize" class="form-control form-control-sm"> <input type="number" min="1" value="25" name="cellSize" id="cellSize" class="form-control form-control-sm">
<span class="input-group-text">Fill Opacity:</span> <span class="input-group-text">Fill Opacity:</span>
<input type="number" min="0" max="1" step=".01" name="fillOpacity" id="fillOpacity" <input type="number" min="0" max="1" step=".01" value="0.15" name="fillOpacity" id="fillOpacity"
class="form-control form-control-sm"> class="form-control form-control-sm">
</div> </div>
<div class="vr mx-1"></div> <div class="vr mx-1"></div>
@ -187,7 +187,7 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
const DEFAULT_DOC = { version: 1, gridSize: 25, shapes: [] }; const DEFAULT_DOC = { version: 1, cellSize: 25, shapes: [] };
const canvasEl = document.getElementById('overlay'); const canvasEl = document.getElementById('overlay');
const clearEl = document.getElementById('clear'); const clearEl = document.getElementById('clear');
@ -199,18 +199,20 @@ const exportEl = document.getElementById('export');
const gridEl = document.getElementById('grid'); const gridEl = document.getElementById('grid');
const importButtonEl = document.getElementById('importButton'); const importButtonEl = document.getElementById('importButton');
const importEl = document.getElementById('import'); const importEl = document.getElementById('import');
const gridSizeEl = document.getElementById('gridSize'); const cellSizeEl = document.getElementById('cellSize');
const gridWrapEl = document.getElementById('gridWrap'); const gridWrapEl = document.getElementById('gridWrap');
const toolBarEl = document.getElementById('toolBar'); const toolBarEl = document.getElementById('toolBar');
const fillOpacityEl = document.getElementById('fillOpacity');
let doc = loadDoc(); let doc = loadDoc();
let gridSize = Number(doc.gridSize) || 25; let cellSize = Number(doc.cellSize) || 25;
gridSizeEl.value = gridSize; cellSizeEl.value = cellSize;
let dotSize = Math.floor(Math.max(gridSize * 1.25, 32)); let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
let ctx; let ctx;
let dpr = 1; let dpr = 1;
let selectedColor; let selectedColor;
let currentOpacity = clamp01(fillOpacity?.value ?? 0.15, 0.15);
let currentShape = null; let currentShape = null;
let shapes = Array.isArray(doc.shapes) ? doc.shapes : []; let shapes = Array.isArray(doc.shapes) ? doc.shapes : [];
@ -218,7 +220,7 @@ let shapes = Array.isArray(doc.shapes) ? doc.shapes : [];
let sizingRAF = 0; let sizingRAF = 0;
let lastApplied = { w: 0, h: 0 }; let lastApplied = { w: 0, h: 0 };
const ro = new ResizeObserver(scheduleSnappedGridSize); const ro = new ResizeObserver(scheduleSnappedcellSize);
ro.observe(gridWrapEl); ro.observe(gridWrapEl);
const savedTool = localStorage.getItem('gridTool'); const savedTool = localStorage.getItem('gridTool');
@ -235,7 +237,12 @@ resizeAndSetupCanvas();
window.addEventListener('resize', resizeAndSetupCanvas); window.addEventListener('resize', resizeAndSetupCanvas);
setGrid(); setGrid();
scheduleSnappedGridSize(); scheduleSnappedcellSize();
function clamp01(n, fallback = 0.15) {
const x = Number(n);
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
}
function isFiniteNum(n) { return Number.isFinite(Number(n)); } function isFiniteNum(n) { return Number.isFinite(Number(n)); }
@ -248,6 +255,9 @@ function sanitizeShapes(list) {
if (!s.color) s.color = '#000000'; if (!s.color) s.color = '#000000';
if (s.opacity == null) s.opacity = 0.15;
s.opacity = clamp01(s.opacity, 0.15);
if (s.type === 'line') { if (s.type === 'line') {
return ['x1','y1','x2','y2'].every(k => isFiniteNum(s[k])); return ['x1','y1','x2','y2'].every(k => isFiniteNum(s[k]));
} else { } else {
@ -270,10 +280,10 @@ function snapDown(n, step) {
return Math.floor(n / step) * step; return Math.floor(n / step) * step;
} }
function applySnappedGridSize() { function applySnappedcellSize() {
sizingRAF = 0; sizingRAF = 0;
const grid = gridSize; const grid = cellSize;
if (!Number.isFinite(grid) || grid < 1) return; if (!Number.isFinite(grid) || grid < 1) return;
const w = gridWrapEl.clientWidth; const w = gridWrapEl.clientWidth;
@ -294,29 +304,29 @@ function applySnappedGridSize() {
resizeAndSetupCanvas(); resizeAndSetupCanvas();
} }
function scheduleSnappedGridSize() { function scheduleSnappedcellSize() {
if (sizingRAF) return; if (sizingRAF) return;
sizingRAF = requestAnimationFrame(applySnappedGridSize); sizingRAF = requestAnimationFrame(applySnappedcellSize);
} }
function applyGridSize(newSize) { function applycellSize(newSize) {
const n = Number(newSize); const n = Number(newSize);
if (!Number.isFinite(n) || n < 1) return; if (!Number.isFinite(n) || n < 1) return;
gridSize = n; cellSize = n;
saveDoc({ ...doc, shapes, gridSize }); saveDoc({ ...doc, shapes, cellSize });
dotSize = Math.floor(Math.max(gridSize * 1.25, 32)); dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
dotSVGEl.setAttribute('width', dotSize); dotSVGEl.setAttribute('width', dotSize);
dotSVGEl.setAttribute('height', dotSize); dotSVGEl.setAttribute('height', dotSize);
setGrid(); setGrid();
scheduleSnappedGridSize(); scheduleSnappedcellSize();
} }
function pxToGrid(v) { function pxToGrid(v) {
return v / gridSize; return v / cellSize;
} }
function getActiveTool() { function getActiveTool() {
@ -348,7 +358,7 @@ function setActiveType(typeId) {
function snapToGrid(x, y) { function snapToGrid(x, y) {
/* /*
Shapes are stored in grid units (document units), not pixels. Shapes are stored in grid units (document units), not pixels.
1 unit renders as gridSize pixels, so changing gridSize rescales (zooms) the whole drawing. 1 unit renders as cellSize pixels, so changing cellSize rescales (zooms) the whole drawing.
Grid modes only affect snapping/visuals; storage is always in document units for portability. Grid modes only affect snapping/visuals; storage is always in document units for portability.
*/ */
@ -359,7 +369,7 @@ function snapToGrid(x, y) {
const localX = clampedX - rect.left; const localX = clampedX - rect.left;
const localY = clampedY - rect.top; const localY = clampedY - rect.top;
const grid = gridSize; const grid = cellSize;
const maxIx = Math.floor(rect.width / grid); const maxIx = Math.floor(rect.width / grid);
const maxIy = Math.floor(rect.height / grid); const maxIy = Math.floor(rect.height / grid);
@ -402,7 +412,8 @@ function normalizeRect(shape) {
w: Math.abs(x2 - x1), w: Math.abs(x2 - x1),
h: Math.abs(y2 - y1), h: Math.abs(y2 - y1),
color: shape.color, color: shape.color,
fill: shape.fill fill: shape.fill,
opacity: clamp01(shape.opacity, 0.15)
}; };
} }
@ -456,7 +467,7 @@ function redrawAll() {
function drawShape(shape) { function drawShape(shape) {
if (!ctx) return; if (!ctx) return;
const toPx = (v) => v * gridSize; const toPx = (v) => v * cellSize;
ctx.save(); ctx.save();
ctx.strokeStyle = shape.color || '#000000'; ctx.strokeStyle = shape.color || '#000000';
@ -478,7 +489,7 @@ function drawShape(shape) {
} }
if (shape.fill) { if (shape.fill) {
ctx.globalAlpha = 0.15; ctx.globalAlpha = clamp01(shape.opacity, 0.15);
ctx.fillStyle = shape.color; ctx.fillStyle = shape.color;
if (shape.type === 'rect') { if (shape.type === 'rect') {
ctx.fillRect(x, y, w, h); ctx.fillRect(x, y, w, h);
@ -524,13 +535,13 @@ function setGrid() {
gridEl.style.backgroundImage = gridEl.style.backgroundImage =
"linear-gradient(to right, #ccc 1px, transparent 1px)," + "linear-gradient(to right, #ccc 1px, transparent 1px)," +
"linear-gradient(to bottom, #ccc 1px, transparent 1px)"; "linear-gradient(to bottom, #ccc 1px, transparent 1px)";
gridEl.style.backgroundSize = `${gridSize}px ${gridSize}px`; gridEl.style.backgroundSize = `${cellSize}px ${cellSize}px`;
gridEl.style.boxShadow = "inset 0 0 0 1px #ccc"; // full frame gridEl.style.boxShadow = "inset 0 0 0 1px #ccc"; // full frame
} else if (type === 'horizontalGrid') { } else if (type === 'horizontalGrid') {
gridEl.style.backgroundImage = gridEl.style.backgroundImage =
"linear-gradient(to bottom, #ccc 1px, transparent 1px)"; "linear-gradient(to bottom, #ccc 1px, transparent 1px)";
gridEl.style.backgroundSize = `100% ${gridSize}px`; gridEl.style.backgroundSize = `100% ${cellSize}px`;
// left + right borders only // left + right borders only
gridEl.style.boxShadow = gridEl.style.boxShadow =
@ -539,7 +550,7 @@ function setGrid() {
} else if (type === 'verticalGrid') { } else if (type === 'verticalGrid') {
gridEl.style.backgroundImage = gridEl.style.backgroundImage =
"linear-gradient(to right, #ccc 1px, transparent 1px)"; "linear-gradient(to right, #ccc 1px, transparent 1px)";
gridEl.style.backgroundSize = `${gridSize}px 100%`; gridEl.style.backgroundSize = `${cellSize}px 100%`;
// top + bottom borders only // top + bottom borders only
gridEl.style.boxShadow = gridEl.style.boxShadow =
@ -570,8 +581,8 @@ document.querySelectorAll('input[name="gridType"]').forEach(input => {
}); });
}); });
gridSizeEl.addEventListener('input', () => applyGridSize(gridSizeEl.value)); cellSizeEl.addEventListener('input', () => applycellSize(cellSizeEl.value));
gridSizeEl.addEventListener('change', () => applyGridSize(gridSizeEl.value)); cellSizeEl.addEventListener('change', () => applycellSize(cellSizeEl.value));
importButtonEl.addEventListener('click', () => importEl.click()); importButtonEl.addEventListener('click', () => importEl.click());
@ -584,9 +595,9 @@ importEl.addEventListener('change', (e) => {
try { try {
const data = JSON.parse(reader.result); const data = JSON.parse(reader.result);
if (Number.isFinite(Number(data.gridSize)) && Number(data.gridSize) >= 1) { if (Number.isFinite(Number(data.cellSize)) && Number(data.cellSize) >= 1) {
gridSizeEl.value = data.gridSize; cellSizeEl.value = data.cellSize;
applyGridSize(data.gridSize); applycellSize(data.cellSize);
} }
const loadedShapes = Array.isArray(data) ? data : data.shapes; const loadedShapes = Array.isArray(data) ? data : data.shapes;
@ -596,7 +607,7 @@ importEl.addEventListener('change', (e) => {
doc = { doc = {
version: Number(data?.version) || 1, version: Number(data?.version) || 1,
gridSize: Number(data?.gridSize) || gridSize, cellSize: Number(data?.cellSize) || cellSize,
shapes shapes
}; };
@ -612,7 +623,7 @@ importEl.addEventListener('change', (e) => {
exportEl.addEventListener('click', () => { exportEl.addEventListener('click', () => {
const payload = { const payload = {
version: 1, version: 1,
gridSize: gridSize, cellSize: cellSize,
shapes shapes
}; };
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
@ -625,11 +636,11 @@ exportEl.addEventListener('click', () => {
}); });
clearEl.addEventListener('click', () => { clearEl.addEventListener('click', () => {
gridSize = 25; cellSize = 25;
shapes = []; shapes = [];
saveDoc({...doc, shapes, gridSize}); saveDoc({...doc, shapes, cellSize});
gridSizeEl.value = 25; cellSizeEl.value = 25;
applyGridSize(25); applycellSize(25);
redrawAll(); redrawAll();
}); });
@ -648,7 +659,7 @@ document.addEventListener('keydown', (e) => {
e.preventDefault(); e.preventDefault();
if (shapes.length > 0) { if (shapes.length > 0) {
shapes.pop(); shapes.pop();
saveDoc({ ...doc, shapes, gridSize }); saveDoc({ ...doc, shapes, cellSize });
redrawAll(); redrawAll();
} }
} }
@ -659,6 +670,14 @@ document.addEventListener('keydown', (e) => {
} }
}); });
fillOpacityEl?.addEventListener('input', () => {
currentOpacity = clamp01(fillOpacityEl.value, 0.15);
});
fillOpacityEl?.addEventListener('change', () => {
currentOpacity = clamp01(fillOpacityEl.value, 0.15);
});
gridEl.addEventListener('pointercancel', () => { gridEl.addEventListener('pointercancel', () => {
currentShape = null; currentShape = null;
redrawAll(); redrawAll();
@ -766,7 +785,8 @@ gridEl.addEventListener('pointerdown', (e) => {
x2: snapX, x2: snapX,
y2: snapY, y2: snapY,
color: selectedColor, color: selectedColor,
fill: (tool === 'filled' || tool === 'filledEllipse') fill: (tool === 'filled'),
opacity: currentOpacity
}; };
} else if (tool === 'outlineEllipse' || tool === 'filledEllipse') { } else if (tool === 'outlineEllipse' || tool === 'filledEllipse') {
currentShape = { currentShape = {
@ -776,7 +796,8 @@ gridEl.addEventListener('pointerdown', (e) => {
x2: snapX, x2: snapX,
y2: snapY, y2: snapY,
color: selectedColor, color: selectedColor,
fill: (tool === 'filledEllipse') fill: (tool === 'filledEllipse'),
opacity: currentOpacity
}; };
} }
}); });
@ -814,7 +835,7 @@ window.addEventListener('pointerup', (e) => {
if (finalShape) { if (finalShape) {
shapes.push(finalShape); shapes.push(finalShape);
saveDoc({...doc, shapes, gridSize}); saveDoc({...doc, shapes, cellSize});
} }
clearCanvas(); clearCanvas();