Opacity added.
This commit is contained in:
parent
802c3cd028
commit
641ae1470d
1 changed files with 64 additions and 43 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue