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