commit 2c750ff48ae7a1cc5126084df9970b428f2fd748
parent 77cba162654c5447803afec92b58b967571b4339
Author: Hunter
Date: Wed, 22 Jul 2026 23:46:12 -0400
tune brush and zoom gesture sensitivity
Diffstat:
| M | index.html | | | 67 | ++++++++++++++++++++++++++++++++++++++++++++++++++----------------- |
| M | readme.md | | | 2 | +- |
2 files changed, 51 insertions(+), 18 deletions(-)
diff --git a/index.html b/index.html
@@ -144,10 +144,11 @@
let dpr = window.devicePixelRatio || 1;
let cssW = 0, cssH = 0;
- // zoomF accumulates fractional pinch input; zoom is the snapped integer used for rendering.
- let zoomF = 2;
- let zoom = 2;
- const MIN_ZOOM = 1, MAX_ZOOM = 64;
+ const ZOOM_STOPS = [2, 3, 4, 5, 6, 8, 11, 15, 21, 30];
+ const MIN_ZOOM = ZOOM_STOPS[0];
+ const ZOOM_SENSITIVITY = 3.5; // fractional levels per unit log(pinch factor)
+ let zoomIdx = 0;
+ let zoom = MIN_ZOOM;
let camX = 0, camY = 0;
@@ -806,6 +807,16 @@
requestDraw();
});
+ // brush resize curve: linear (fine, precise) up to BRUSH_LINEAR_MAX, then a
+ // power-law ramp beyond, tuned by BRUSH_ACCEL.
+ function brushFromDrag(start, dyPx) {
+ const A = BRUSH_DRAG_PX_PER_STEP, T = BRUSH_LINEAR_MAX, c = BRUSH_ACCEL;
+ const seam = T * A; // drag distance from size 0 to the seam
+ const brushToPos = (b) => b <= T ? b * A : seam + (seam / c) * (Math.pow(b / T, c) - 1);
+ const posToBrush = (p) => p <= seam ? p / A : T * Math.pow(1 + c * (p - seam) / seam, 1 / c);
+ return posToBrush(brushToPos(start) + dyPx);
+ }
+
// modifier-drag: shift/cmd/RGB held -> vertical motion from the anchor
// drives brush size / roundness / color channels. drag up (dy negative)
// -> increase. when a value hits its bound and the drag continues past,
@@ -814,10 +825,10 @@
function applyDrags(clientY) {
if (dragBrush) {
const dyPx = (dragBrush.anchorY - clientY) * dpr;
- const target = dragBrush.start + Math.trunc(dyPx / BRUSH_DRAG_PX_PER_STEP);
- const clamped = clamp(target, 1, 256);
+ const target = brushFromDrag(dragBrush.start, dyPx);
+ const clamped = clamp(Math.round(target), 1, MAX_BRUSH);
brush = clamped;
- if (target !== clamped) {
+ if (target < 1 || target > MAX_BRUSH) {
dragBrush.start = clamped;
dragBrush.anchorY = clientY;
}
@@ -1185,6 +1196,11 @@
fileInput.click();
return;
}
+ if ((e.metaKey || e.ctrlKey) && !e.altKey) {
+ if (e.key === '=' || e.key === '+') { e.preventDefault(); stepZoom(1); return; }
+ if (e.key === '-' || e.key === '_') { e.preventDefault(); stepZoom(-1); return; }
+ if (e.key === '0') { e.preventDefault(); resetZoom(); return; }
+ }
if (!e.metaKey && !e.ctrlKey && !e.altKey && !e.repeat) {
const lk = e.key.toLowerCase();
if (e.key === 'ArrowLeft') { e.preventDefault(); tapArrow(-1); return; }
@@ -1266,19 +1282,33 @@
function applyZoom(factor, ax, ay) {
const worldAtAnchorX = camX + ax / zoom;
const worldAtAnchorY = camY + ay / zoom;
- zoomF = clamp(zoomF * factor, MIN_ZOOM, MAX_ZOOM);
- const STEP_RATIO = 1.2;
- let newZoom = zoom;
- if (zoomF > zoom * STEP_RATIO) newZoom = Math.min(MAX_ZOOM, zoom + 1);
- else if (zoomF < zoom / STEP_RATIO) newZoom = Math.max(MIN_ZOOM, zoom - 1);
+ zoomIdx = clamp(zoomIdx + Math.log(factor) * ZOOM_SENSITIVITY, 0, ZOOM_STOPS.length - 1);
+ const newZoom = ZOOM_STOPS[Math.round(zoomIdx)];
if (newZoom !== zoom) {
zoom = newZoom;
- zoomF = zoom; // resync so next step needs the same delta
camX = worldAtAnchorX - ax / zoom;
camY = worldAtAnchorY - ay / zoom;
}
}
+ function zoomTo(newZoom) {
+ if (newZoom === zoom) return;
+ const ax = cssW / 2, ay = cssH / 2;
+ const worldAtAnchorX = camX + ax / zoom;
+ const worldAtAnchorY = camY + ay / zoom;
+ zoom = newZoom;
+ zoomIdx = ZOOM_STOPS.indexOf(zoom);
+ camX = worldAtAnchorX - ax / zoom;
+ camY = worldAtAnchorY - ay / zoom;
+ requestDraw();
+ }
+
+ function stepZoom(dir) {
+ const i = ZOOM_STOPS.indexOf(zoom);
+ zoomTo(ZOOM_STOPS[clamp(i + dir, 0, ZOOM_STOPS.length - 1)]);
+ }
+ function resetZoom() { zoomTo(MIN_ZOOM); }
+
// wheel gesture lock: once a wheel gesture starts in a given mode,
// subsequent events in the same burst stay in that mode until the wheel
// goes idle or the controlling modifier set changes. this keeps trackpad
@@ -1288,9 +1318,12 @@
let wheelIdleTimer = null;
let panning = false;
- const BRUSH_DRAG_PX_PER_STEP = 4; // device px per +/- 1 brush px
- const ROUNDNESS_DRAG_FULL_PX = 200; // device px to traverse 0 to 1
- const COLOR_DRAG_FULL_PX = 256; // device px to traverse 0 to 255
+ const BRUSH_DRAG_PX_PER_STEP = 25; // device px per +/- 1 brush px (below the seam)
+ const BRUSH_LINEAR_MAX = 6; // brush px below which resize stays linear/fine
+ const BRUSH_ACCEL = 0.5;
+ const MAX_BRUSH = 150;
+ const ROUNDNESS_DRAG_FULL_PX = 200;
+ const COLOR_DRAG_FULL_PX = 256;
// each drag carries its own anchor so any combination can run at once
let dragBrush = null; // {anchorY, start}
let dragRoundness = null; // {anchorY, start}
@@ -1340,7 +1373,7 @@
if (wheelMode === 'zoom') {
// exponential zoom on the float accumulator so small pinches add up
- applyZoom(Math.exp(-e.deltaY * 0.04), e.clientX, e.clientY);
+ applyZoom(Math.exp(-e.deltaY * 0.02), e.clientX, e.clientY);
requestDraw();
return;
}
diff --git a/readme.md b/readme.md
@@ -25,7 +25,7 @@ an infinite canvas for radical digital painting.
- hold any of the , , and/or  keys and move your cursor up/down to change the , , and/or  of the active color
- hold any combination of the above to change size, shape, and/or color at the same time
- hold `C` and click anywhere to pick up the color underneath your brush
-- pinch with two fingers to zoom in/out
+- pinch with two fingers to zoom in / out (or use `⌘ +` / `⌘ -`)
- drag with two fingers to pan around the canvas
### animation