use z/x/c instead of shift/command/option

Hunter
Thursday, July 16th, 2026 at 5:24 PM

index.html+74-83
readme.md+4-3

index.html
711  	}
712  
713  	async function importFile(file) {
    -		if (dirty && !confirm('Importing will discard the current animation. Continue?')) return;
714 +		if (dirty && !confirm('Importing will discard the current painting. Continue?')) return;
715  		if (playing) stopPlayback();
716  		// ImageDecoder gives us every frame of an animated gif; fall back to
717  		// single-image import where unsupported

765  		// modifier-drag: shift/cmd/RGB held -> vertical motion from the anchor
766  		// drives brush size / roundness / color channels. drag up (dy negative)
767  		// -> increase. when a value hits its bound and the drag continues past,
    -		// re-anchor so reversing direction responds immediately.
    -		if (dragMode !== null) {
    -			const dyPx = (dragAnchorY - e.clientY) * dpr;
    -			if (dragMode === 'brush') {
    -				const targetSteps = Math.trunc(dyPx / BRUSH_DRAG_PX_PER_STEP);
    -				const target = dragBrushStart + targetSteps;
    -				const clamped = clamp(target, 1, 256);
    -				brush = clamped;
    -				if (target !== clamped) {
    -					dragBrushStart = clamped;
    -					dragAnchorY = e.clientY;
    -				}
    -			} else if (dragMode === 'roundness') {
    -				const target = dragRoundnessStart - dyPx / ROUNDNESS_DRAG_FULL_PX;
    -				const clamped = clamp(target, 0, 1);
    -				roundness = clamped;
    -				if (target !== clamped) {
    -					dragRoundnessStart = clamped;
    -					dragAnchorY = e.clientY;
    -				}
    -			} else if (dragMode === 'color') {
    -				const delta = (dyPx / COLOR_DRAG_FULL_PX) * 255;
    -				let nr = r, ng = g, nb = b;
    -				let overshoot = 0;
    -				if (keys['r']) {
    -					const t = dragRStart + delta;
    -					nr = clamp(t, 0, 255);
    -					if (t !== nr) overshoot = Math.max(overshoot, Math.abs(t - nr));
    -				}
    -				if (keys['g']) {
    -					const t = dragGStart + delta;
    -					ng = clamp(t, 0, 255);
    -					if (t !== ng) overshoot = Math.max(overshoot, Math.abs(t - ng));
    -				}
    -				if (keys['b']) {
    -					const t = dragBStart + delta;
    -					nb = clamp(t, 0, 255);
    -					if (t !== nb) overshoot = Math.max(overshoot, Math.abs(t - nb));
    -				}
    -				setColor(Math.round(nr), Math.round(ng), Math.round(nb));
    -				if (overshoot > 0) {
    -					dragRStart = nr; dragGStart = ng; dragBStart = nb;
    -					dragAnchorY = e.clientY;
    -				}
768 +		// re-anchor so reversing direction responds immediately. each drag is
769 +		// independent so any combination can run at once.
770 +		if (dragBrush) {
771 +			const dyPx = (dragBrush.anchorY - e.clientY) * dpr;
772 +			const target = dragBrush.start + Math.trunc(dyPx / BRUSH_DRAG_PX_PER_STEP);
773 +			const clamped = clamp(target, 1, 256);
774 +			brush = clamped;
775 +			if (target !== clamped) {
776 +				dragBrush.start = clamped;
777 +				dragBrush.anchorY = e.clientY;
778 +			}
779 +		}
780 +		if (dragRoundness) {
781 +			const dyPx = (dragRoundness.anchorY - e.clientY) * dpr;
782 +			const target = dragRoundness.start - dyPx / ROUNDNESS_DRAG_FULL_PX;
783 +			const clamped = clamp(target, 0, 1);
784 +			roundness = clamped;
785 +			if (target !== clamped) {
786 +				dragRoundness.start = clamped;
787 +				dragRoundness.anchorY = e.clientY;
788 +			}
789 +		}
790 +		if (dragColor) {
791 +			const dyPx = (dragColor.anchorY - e.clientY) * dpr;
792 +			const delta = (dyPx / COLOR_DRAG_FULL_PX) * 255;
793 +			let nr = r, ng = g, nb = b;
794 +			let overshoot = 0;
795 +			if (keys['r']) {
796 +				const t = dragColor.r0 + delta;
797 +				nr = clamp(t, 0, 255);
798 +				if (t !== nr) overshoot = Math.max(overshoot, Math.abs(t - nr));
799 +			}
800 +			if (keys['g']) {
801 +				const t = dragColor.g0 + delta;
802 +				ng = clamp(t, 0, 255);
803 +				if (t !== ng) overshoot = Math.max(overshoot, Math.abs(t - ng));
804 +			}
805 +			if (keys['b']) {
806 +				const t = dragColor.b0 + delta;
807 +				nb = clamp(t, 0, 255);
808 +				if (t !== nb) overshoot = Math.max(overshoot, Math.abs(t - nb));
809 +			}
810 +			setColor(Math.round(nr), Math.round(ng), Math.round(nb));
811 +			if (overshoot > 0) {
812 +				dragColor.r0 = nr; dragColor.g0 = ng; dragColor.b0 = nb;
813 +				dragColor.anchorY = e.clientY;
814  			}
815  		}
816  

838  	window.addEventListener('mousedown', (e) => {
839  		if (e.button !== 0) return;
840  		e.preventDefault();
    -		if (e.altKey) {
841 +		if (keys['c']) {
842  			picking = true;
843  			pickAt(curX, curY);
844  			requestDraw();

861  	});
862  
863  	function startDragMode(mode) {
    -		if (dragMode === mode) return;
    -		dragMode = mode;
    -		dragAnchorY = curClientY !== null ? curClientY : 0;
    -		dragBrushStart = brush;
    -		dragRoundnessStart = roundness;
    -		dragRStart = r; dragGStart = g; dragBStart = b;
864 +		const anchorY = curClientY !== null ? curClientY : 0;
865 +		if (mode === 'brush' && !dragBrush) dragBrush = { anchorY, start: brush };
866 +		else if (mode === 'roundness' && !dragRoundness) dragRoundness = { anchorY, start: roundness };
867 +		else if (mode === 'color' && !dragColor) dragColor = { anchorY, r0: r, g0: g, b0: b };
868  	}
869  	function endDragMode(mode) {
    -		if (dragMode !== mode) return;
    -		dragMode = null;
870 +		if (mode === 'brush') dragBrush = null;
871 +		else if (mode === 'roundness') dragRoundness = null;
872 +		else if (mode === 'color') dragColor = null;
873  	}
874  
875  	window.addEventListener('keydown', (e) => {

900  		const k = e.key.toLowerCase();
901  		const wasDown = keys[k];
902  		keys[k] = true;
    -		// keep Firefox from focusing the menu bar on a bare Alt tap
    -		if (e.key === 'Alt') e.preventDefault();
    -		// z/x are drag modifiers; block browser undo/cut when cmd is also held
    -		if (k === 'z' || k === 'x') e.preventDefault();
    -		if (e.key === 'Meta' || k === 'x') { metaSeq = ++seqCounter; startDragMode('roundness'); }
    -		else if (e.key === 'Shift' || k === 'z') { shiftSeq = ++seqCounter; startDragMode('brush'); }
    -		else if (!wasDown && (k === 'r' || k === 'g' || k === 'b')) startDragMode('color');
903 +		if (!e.metaKey && !e.ctrlKey) {
904 +			if (k === 'z' || k === 'x' || k === 'c' || k === 'r' || k === 'g' || k === 'b') e.preventDefault();
905 +			if (k === 'x') startDragMode('roundness');
906 +			else if (k === 'z') startDragMode('brush');
907 +			else if (!wasDown && (k === 'r' || k === 'g' || k === 'b')) startDragMode('color');
908 +		}
909  	});
910  	window.addEventListener('keyup', (e) => {
911  		const k = e.key.toLowerCase();
912  		keys[k] = false;
    -		if (e.key === 'Alt') e.preventDefault();
    -		if (e.key === 'Meta' || k === 'x') {
    -			if (!keys['meta'] && !keys['x']) { metaSeq = 0; endDragMode('roundness'); }
    -		}
    -		else if (e.key === 'Shift' || k === 'z') {
    -			if (!keys['shift'] && !keys['z']) { shiftSeq = 0; endDragMode('brush'); }
    -		}
913 +		if (k === 'x') endDragMode('roundness');
914 +		else if (k === 'z') endDragMode('brush');
915  		else if (k === 'r' || k === 'g' || k === 'b') {
916  			if (!keys['r'] && !keys['g'] && !keys['b']) endDragMode('color');
    -			else {
917 +			else if (dragColor) {
918  				// still holding at least one rgb key - re-anchor from current
919  				// values so the remaining keys don't jump based on released key's history
    -				dragAnchorY = curClientY !== null ? curClientY : dragAnchorY;
    -				dragRStart = r; dragGStart = g; dragBStart = b;
920 +				dragColor.anchorY = curClientY !== null ? curClientY : dragColor.anchorY;
921 +				dragColor.r0 = r; dragColor.g0 = g; dragColor.b0 = b;
922  			}
923  		}
    -		// releasing alt mid-pick-drag -> seamlessly switch to painting
    -		if ((e.key === 'Alt' || e.key === 'AltGraph') && picking) {
924 +		// releasing c mid-pick-drag -> seamlessly switch to painting
925 +		if (k === 'c' && picking) {
926  			picking = false;
927  			painting = true;
928  			lastPaintX = curX;

933  	});
934  	window.addEventListener('blur', () => {
935  		for (const k in keys) keys[k] = false;
    -		metaSeq = 0; shiftSeq = 0;
936 +		dragBrush = null; dragRoundness = null; dragColor = null;
937  		painting = false;
938  		picking = false;
939  		lastPaintX = null;

946  	// subsequent events in the same burst stay in that mode until the wheel
947  	// goes idle or the controlling modifier set changes. this keeps trackpad
948  	// momentum from leaking into pan after a modifier release, while still
    -	// letting the user swap between modifier-driven modes (shift <-> cmd)
    -	// mid-flick without stale state. between shift and cmd, the one pressed
    -	// most recently wins so they never apply simultaneously.
949 +	// letting the user swap between modes mid-flick without stale state.
950  	let wheelMode = null;
951  	let wheelIdleTimer = null;
952  	let panning = false;
    -	let metaSeq = 0, shiftSeq = 0, seqCounter = 0;
953  
954  	const BRUSH_DRAG_PX_PER_STEP = 4;		// device px per +/- 1 brush px
955  	const ROUNDNESS_DRAG_FULL_PX = 200;		// device px to traverse 0 to 1
956  	const COLOR_DRAG_FULL_PX = 256;			// device px to traverse 0 to 255
    -	let dragMode = null;		// 'brush' | 'roundness' | 'color' | null
    -	let dragAnchorY = 0;
    -	let dragBrushStart = 1;
    -	let dragRoundnessStart = 0;
    -	let dragRStart = 0, dragGStart = 0, dragBStart = 0;
957 +	// each drag carries its own anchor so any combination can run at once
958 +	let dragBrush = null;		// {anchorY, start}
959 +	let dragRoundness = null;	// {anchorY, start}
960 +	let dragColor = null;		// {anchorY, r0, g0, b0}
961  	function touchWheelGesture() {
962  		if (wheelIdleTimer) clearTimeout(wheelIdleTimer);
963  		wheelIdleTimer = setTimeout(() => {
readme.md
20  ### mark-making
21  
22  - click and drag to make marks
   -- hold `shift` (or `Z`) and move your cursor up/down to resize the brush
   -- hold `⌘` (or `X`) and move your cursor up/down to change the shape of the brush
23 +- hold `Z` and move your cursor up/down to resize the brush
24 +- hold `X` and move your cursor up/down to change the shape of the brush
25  - hold any of the ![R](readme_images/R.svg), ![G](readme_images/G.svg), and/or ![B](readme_images/B.svg) keys and move your cursor up/down to change the ![Redness](readme_images/Redness.svg), ![Greenness](readme_images/Greenness.svg), and/or ![Blueness](readme_images/Blueness.svg) of the active color
   -- hold `option` (or `alt`) and click anywhere to pick up the color underneath your brush
26 +- hold any combination of the above to change size, shape, and/or color at the same time
27 +- hold `C` and click anywhere to pick up the color underneath your brush
28  - pinch with two fingers to zoom in/out
29  - drag with two fingers to pan around the canvas
30