iPad/Apple Pencil tweaks
Hunter
Wednesday, July 22nd, 2026 at 2:55 PM
153
154 let curX = 0, curY = 0;
155 // used to keep the brush pinned under the real cursor while panning, since
- // the OS does not emit mousemove events when only the camera moves.
156 + // the OS does not emit pointermove events when only the camera moves.
157 let curClientX = null, curClientY = null;
158 let mouseInside = true;
159 // brush preview fades in when it appears and out after the last touch lifts
791
792 window.addEventListener('resize', resize);
793
- window.addEventListener('mouseover', (e) => {
794 + // mouse and pen share the pointer-event path; finger touches are handled
795 + // by the touch-event path below
796 + function hoverPointer(e) { return e.pointerType === 'mouse' || e.pointerType === 'pen'; }
797 +
798 + window.addEventListener('pointerover', (e) => {
799 + if (!hoverPointer(e)) return;
800 showCursor();
801 curClientX = e.clientX;
802 curClientY = e.clientY;
860 }
861 }
862
- window.addEventListener('mousemove', (e) => {
863 + window.addEventListener('pointermove', (e) => {
864 + if (!hoverPointer(e)) return;
865 + // a pen only proves it can hover by moving while lifted. without hover
866 + // it gets the finger treatment: modifiers adjust instead of marking
867 + if (e.pointerType === 'pen' && e.buttons === 0) penHover = true;
868 showCursor();
869 curClientX = e.clientX;
870 curClientY = e.clientY;
889 requestDraw();
890 });
891
- window.addEventListener('mouseleave', () => {
892 + window.addEventListener('pointerleave', (e) => {
893 + if (!hoverPointer(e)) return;
894 hideCursorNow();
895 requestDraw();
896 });
897
- window.addEventListener('mousedown', (e) => {
898 + // a pen without hover support sends no move events before contact, so the
899 + // down event must place the cursor itself
900 + let penDown = false;
901 + let penHover = false;
902 + window.addEventListener('pointerdown', (e) => {
903 + if (!hoverPointer(e)) return;
904 if (e.button !== 0) return;
905 e.preventDefault();
906 + stopInertia();
907 + if (e.pointerType === 'pen') penDown = true;
908 + showCursor();
909 + curClientX = e.clientX;
910 + curClientY = e.clientY;
911 + const p = clientToWorld(e.clientX, e.clientY);
912 + curX = p.x;
913 + curY = p.y;
914 + // hoverless pen + modifier held -> the contact adjusts, like a finger
915 + if (e.pointerType === 'pen' && !penHover && !keys['c'] && (dragBrush || dragRoundness || dragColor)) {
916 + anchorDrags(e.clientY);
917 + requestDraw();
918 + return;
919 + }
920 if (keys['c']) {
921 picking = true;
922 pickAt(curX, curY);
930 requestDraw();
931 });
932
- window.addEventListener('mouseup', (e) => {
- if (e.button === 0) {
933 + function pointerUp(e) {
934 + if (!hoverPointer(e)) return;
935 + if (e.button === 0 || e.type === 'pointercancel') {
936 painting = false;
937 picking = false;
938 lastPaintX = null;
939 lastPaintY = null;
940 + if (e.pointerType === 'pen') {
941 + penDown = false;
942 + // hoverless pens get no more events after lift, so fade like touch
943 + if (mouseInside) setCursorFadeTarget(0);
944 + requestDraw();
945 + }
946 }
- });
-
- // on hoverless screens, the finger is the only pointer, so while a modifier
- // is held the finger adjusts instead of marking.
- const hoverMQ = window.matchMedia ? matchMedia('(any-hover: hover)') : null;
- let sawHoverPointer = false;
- function hoverCapable() { return sawHoverPointer || (hoverMQ !== null && hoverMQ.matches); }
- window.addEventListener('pointermove', (e) => {
- if (e.pointerType === 'mouse' || (e.pointerType === 'pen' && e.buttons === 0)) sawHoverPointer = true;
- });
947 + }
948 + window.addEventListener('pointerup', pointerUp);
949 + window.addEventListener('pointercancel', pointerUp);
950
951 // touch: one finger paints, two fingers pan/pinch-zoom. once a second
952 // finger lands the whole touch becomes a gesture (no painting) until all
953 // fingers lift, so a stray finger during a pan never leaves a mark.
954 + // stylus touches are excluded here
955 let touchMode = null; // 'paint' | 'adjust' | 'gesture'
956 let touchPainted = false;
957 let pinchDist = 0, pinchMidX = 0, pinchMidY = 0;
999 return { x: x / touches.length, y: y / touches.length };
1000 }
1001
1002 + function fingerTouches(list) {
1003 + const out = [];
1004 + for (const t of list) if (t.touchType !== 'stylus') out.push(t);
1005 + return out;
1006 + }
1007 +
1008 // re-anchor active drags to a new y and re-baseline to the current values
1009 // so successive drag strokes accumulate instead of snapping back
1010 function anchorDrags(clientY) {
1024
1025 window.addEventListener('touchstart', (e) => {
1026 e.preventDefault();
1027 + const fingers = fingerTouches(e.touches);
1028 + // fingers are ignored while the pen is down so a resting hand can't
1029 + // hijack or extend the pen's stroke
1030 + if (fingers.length === 0 || penDown) return;
1031 stopInertia();
- if (e.touches.length === 1 && touchMode === null) {
- const t = e.touches[0];
1032 + if (fingers.length === 1 && touchMode === null) {
1033 + const t = fingers[0];
1034 curClientX = t.clientX;
1035 curClientY = t.clientY;
- if (!keys['c'] && !hoverCapable() && (dragBrush || dragRoundness || dragColor)) {
1036 + if (!keys['c'] && (dragBrush || dragRoundness || dragColor)) {
1037 touchMode = 'adjust';
1038 const p = clientToWorld(t.clientX, t.clientY);
1039 curX = p.x;
1065 panVX = 0;
1066 panVY = 0;
1067 panLastT = 0;
- anchorGesture(e.touches);
1068 + anchorGesture(fingers);
1069 }
1070 requestDraw();
1071 }, { passive: false });
1072
1073 window.addEventListener('touchmove', (e) => {
1074 e.preventDefault();
1075 + const fingers = fingerTouches(e.touches);
1076 + if (fingers.length === 0) return;
1077 if (touchMode === 'paint') {
- const t = e.touches[0];
1078 + const t = fingers[0];
1079 curClientX = t.clientX;
1080 curClientY = t.clientY;
1081 // on hover devices held keys adjust during the stroke, like mouse
1092 curX = p.x;
1093 curY = p.y;
1094 } else if (touchMode === 'adjust') {
- const t = e.touches[0];
1095 + const t = fingers[0];
1096 curClientX = t.clientX;
1097 curClientY = t.clientY;
1098 applyDrags(t.clientY);
1100 curX = p.x;
1101 curY = p.y;
1102 } else if (touchMode === 'gesture') {
- const m = touchCentroid(e.touches);
1103 + const m = touchCentroid(fingers);
1104 const dxs = m.x - pinchMidX;
1105 const dys = m.y - pinchMidY;
1106 camX -= dxs / zoom;
1112 panVY = panVY * 0.5 + (dys / dt) * 0.5;
1113 }
1114 panLastT = nowT;
- if (e.touches.length >= 2) {
- const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
1115 + if (fingers.length >= 2) {
1116 + const dist = Math.hypot(fingers[0].clientX - fingers[1].clientX, fingers[0].clientY - fingers[1].clientY);
1117 if (pinchDist > 0) applyZoom(dist / pinchDist, m.x, m.y);
1118 pinchDist = dist;
1119 }
1127
1128 function touchEnd(e) {
1129 e.preventDefault();
- if (e.touches.length === 0) {
1130 + if (touchMode === null) return;
1131 + const fingers = fingerTouches(e.touches);
1132 + if (fingers.length === 0) {
1133 // a tap that never moved still paints its dot (or picks its point)
1134 if (touchMode === 'paint' && !touchPainted) {
1135 if (picking) pickAt(curX, curY);
1146 requestDraw();
1147 } else if (touchMode === 'gesture') {
1148 // re-anchor to the remaining fingers so the camera doesn't jump
- anchorGesture(e.touches);
1149 + anchorGesture(fingers);
1150 }
1151 }
1152 window.addEventListener('touchend', touchEnd, { passive: false });
1157 if (mode === 'brush' && !dragBrush) dragBrush = { anchorY, start: brush };
1158 else if (mode === 'roundness' && !dragRoundness) dragRoundness = { anchorY, start: roundness };
1159 else if (mode === 'color' && !dragColor) dragColor = { anchorY, r0: r, g0: g, b0: b };
- // hoverless screen: a modifier pressed mid-stroke ends the stroke and
- // converts the touch into an adjust drag for the rest of the touch
- if (touchMode === 'paint' && !hoverCapable()) {
- touchMode = 'adjust';
1160 + // hoverless input: a modifier pressed mid-stroke ends the stroke and
1161 + // the rest of the contact becomes an adjust drag. hover-capable pens
1162 + // keep drawing (they can adjust while lifted instead)
1163 + if (touchMode === 'paint' || (penDown && !penHover)) {
1164 + if (touchMode === 'paint') touchMode = 'adjust';
1165 painting = false;
1166 picking = false;
1167 lastPaintX = null;
1234 }
1235 }
1236 // releasing c mid-pick-drag -> back to adjusting if a modifier is still
- // held on a hoverless screen, else seamlessly switch to painting
1237 + // held on a hoverless contact, else seamlessly switch to painting
1238 if (k === 'c' && picking) {
1239 picking = false;
- if (touchMode !== null && !hoverCapable() && (dragBrush || dragRoundness || dragColor)) {
- touchMode = 'adjust';
1240 + const hoverless = touchMode !== null || (penDown && !penHover);
1241 + if (hoverless && (dragBrush || dragRoundness || dragColor)) {
1242 + if (touchMode !== null) touchMode = 'adjust';
1243 + painting = false;
1244 if (curClientY !== null) anchorDrags(curClientY);
1245 } else {
1246 painting = true;
1256 dragBrush = null; dragRoundness = null; dragColor = null;
1257 painting = false;
1258 picking = false;
1259 + penDown = false;
1260 lastPaintX = null;
1261 lastPaintY = null;
1262 });