CANVAS: iPad Pencil and touch drawing (§60) #978

Open
opened 2026-10-03 06:38:53 +00:00 by kayg · 11 comments
Owner

Owner decision (2026-10-03, #509 grill Q6)

"I agree this would be amazing!" Contract: docs/DESIGN.md §60 "Touch and Pencil in v1". Depends on the Canvas core issue.

Scope

  • iPad: Excalidraw pen mode is on automatically when a Pencil touches the screen. The Pencil draws, a finger pans or pinches, and palm rejection is on. Pressure is kept on freehand strokes.
  • Phone and tablet chrome follows §34: the toolbar is reachable by thumb, there are no hover-only controls, touch targets are at least 44 pt, and swipe actions include Copy link on elements.
  • No page scroll or rubber-band while drawing (see #718). The Safari address bar must not steal strokes at the edges.
  • Reduced motion and screen reader rules apply (element list announced, tools labelled).

Verification

Use the macOS test VM (calternal@10.69.69.21, macvm.lock) with the iOS/iPadOS simulator against staging. Jobs never configure the VM: list any setup steps instead. Record Pencil-style input (simulator stylus) and finger pan in screenshots or a short screen recording. Measure stroke latency (pointer to paint) on a 5k-element canvas.

## Owner decision (2026-10-03, #509 grill Q6) "I agree this would be amazing!" Contract: `docs/DESIGN.md` §60 "Touch and Pencil in v1". Depends on the Canvas core issue. ## Scope - iPad: Excalidraw pen mode is on automatically when a Pencil touches the screen. The Pencil draws, a finger pans or pinches, and palm rejection is on. Pressure is kept on freehand strokes. - Phone and tablet chrome follows §34: the toolbar is reachable by thumb, there are no hover-only controls, touch targets are at least 44 pt, and swipe actions include Copy link on elements. - No page scroll or rubber-band while drawing (see #718). The Safari address bar must not steal strokes at the edges. - Reduced motion and screen reader rules apply (element list announced, tools labelled). ## Verification Use the macOS test VM (`calternal@10.69.69.21`, macvm.lock) with the iOS/iPadOS simulator against staging. Jobs never configure the VM: list any setup steps instead. Record Pencil-style input (simulator stylus) and finger pan in screenshots or a short screen recording. Measure stroke latency (pointer to paint) on a 5k-element canvas.
Author
Owner

Owner decision C10 (2026-10-03, DESIGN §60 'Open mode'): a canvas with content opens in edit mode on desktop/iPad and read mode on phones; an empty canvas opens in read mode everywhere except right after New canvas or Sketch (edit). One Edit tap switches; mode is per open, never stored in the file. e2e must cover all four cases (phone/desktop × empty/content) plus New canvas.

Owner decision C10 (2026-10-03, DESIGN §60 'Open mode'): a canvas with content opens in edit mode on desktop/iPad and read mode on phones; an empty canvas opens in read mode everywhere except right after New canvas or Sketch (edit). One Edit tap switches; mode is per open, never stored in the file. e2e must cover all four cases (phone/desktop × empty/content) plus New canvas.
Author
Owner

Starting #978 on job/canvas-pencil-978 from Canvas core commit beb3bbf4b0. Working tree is clean. I am checking the current web canvas/editor path, then will merge origin/dev before final gates.

Starting #978 on job/canvas-pencil-978 from Canvas core commit beb3bbf4b0487200af4a596b771e6690c452bfc3. Working tree is clean. I am checking the current web canvas/editor path, then will merge origin/dev before final gates.
Author
Owner

Finding: the pinned @excalidraw/excalidraw 0.18.1 already turns penMode on at the first pointerType=pen event and records PointerEvent.pressure on freehand points (upstream v0.18.1 App.tsx, lines 6417–6425 and 7431–7449). Its touch suppression is also built in while penMode is active. I am preserving that input path and adding the missing touch-pan/page-overscroll integration plus focused production coverage.

Finding: the pinned @excalidraw/excalidraw 0.18.1 already turns penMode on at the first pointerType=pen event and records PointerEvent.pressure on freehand points (upstream v0.18.1 App.tsx, lines 6417–6425 and 7431–7449). Its touch suppression is also built in while penMode is active. I am preserving that input path and adding the missing touch-pan/page-overscroll integration plus focused production coverage.
Author
Owner

Finding: current Canvas chrome exposes Copy link for a selected element, but has no element list for screen-reader users. I am adding a paged Canvas elements sheet from the existing ⋯ actions; each row selects and scrolls to its stable element ID, has a labelled Copy link button, and supports left-swipe Copy link on touch. Pages are limited to 50 items so a 20k-element scene does not create 20k UI rows at once.

Finding: current Canvas chrome exposes Copy link for a selected element, but has no element list for screen-reader users. I am adding a paged Canvas elements sheet from the existing ⋯ actions; each row selects and scrolls to its stable element ID, has a labelled Copy link button, and supports left-swipe Copy link on touch. Pages are limited to 50 items so a 20k-element scene does not create 20k UI rows at once.
Author
Owner

Finding: the focused production Canvas flow stopped before server startup because sharp could not load without libstdc++.so.6 on the build host. I replaced only the screenshot crop helper with Playwright's built-in screenshot clip, which removes this native runtime dependency. Retrying the production flow now.

Finding: the focused production Canvas flow stopped before server startup because `sharp` could not load without `libstdc++.so.6` on the build host. I replaced only the screenshot crop helper with Playwright's built-in screenshot clip, which removes this native runtime dependency. Retrying the production flow now.
Author
Owner

Finding: the production probe creates and renames a Canvas successfully, and GET /api/v1/notes/{id} returns HTTP 200 with a valid empty scene, but the collaboration socket is refused before the view reaches ready. This is visible as data-canvas-collab=refused; I am checking the socket response before continuing the touch assertions.

Finding: the production probe creates and renames a Canvas successfully, and `GET /api/v1/notes/{id}` returns HTTP 200 with a valid empty scene, but the collaboration socket is refused before the view reaches ready. This is visible as `data-canvas-collab=refused`; I am checking the socket response before continuing the touch assertions.
Author
Owner

E2E findings fixed: the synthetic stylus helper targeted Excalidraw's non-interactive preview canvas, so it never exercised the real pointer handlers. It now targets the interactive scene canvas; the production flow verifies Pencil pressure, one-finger pan, and two-finger pinch. The test origin also needs Chrome's local-network permission for its loopback collaboration WebSocket. Playwright permission grants replaced the clipboard grants, so both are now granted together; Copy link reads the clipboard in the same real browser flow.

E2E findings fixed: the synthetic stylus helper targeted Excalidraw's non-interactive preview canvas, so it never exercised the real pointer handlers. It now targets the interactive scene canvas; the production flow verifies Pencil pressure, one-finger pan, and two-finger pinch. The test origin also needs Chrome's local-network permission for its loopback collaboration WebSocket. Playwright permission grants replaced the clipboard grants, so both are now granted together; Copy link reads the clipboard in the same real browser flow.
Author
Owner

Screenshot-run finding: after repeated production route/theme captures, the live Canvas preview timed out before reaching its ready state and the browser recorded three collaboration WebSocket handshake responses with status 429. The screenshot helper now waits, with a three-second bound, for the previous page's collaboration socket to close before it starts the next theme capture. I am rerunning the production screenshot flow to verify this fixture connection churn is gone.

Screenshot-run finding: after repeated production route/theme captures, the live Canvas preview timed out before reaching its ready state and the browser recorded three collaboration WebSocket handshake responses with status 429. The screenshot helper now waits, with a three-second bound, for the previous page's collaboration socket to close before it starts the next theme capture. I am rerunning the production screenshot flow to verify this fixture connection churn is gone.
Author
Owner

READY FOR MERGE: yes — branch-level implementation and focused gates are complete. The remaining integrated-build and device checks are listed under “For the merge round”.

Built

  • Pencil Mode suppresses drawing under a finger, which can pan the Canvas. Two-finger pinch temporarily uses Hand and restores the previous drawing tool. Pen pressure is preserved on freehand strokes.
  • Added the accessible, paged Canvas elements surface with stable-ID selection/reveal, Copy link, context actions, touch swipe Copy link, keyboard controls, and announced loading/empty/error states.
  • Added a 5k-element Canvas performance profile for stylus paint and the element list.

Commits:

  • 2353688a81d576dcd7f2689a26be8f7a708d3b02 — Pencil gestures and element actions.
  • 4ef59ea109826dcbbb2708cb61e53bcf869b08cb — 5k Pencil and outline profile.

Head SHA: 4ef59ea109826dcbbb2708cb61e53bcf869b08cb.

Files

  • apps/web/src/lib/canvas/CanvasReact.tsx
  • apps/web/src/lib/canvas/CanvasView.svelte
  • apps/web/src/lib/canvas/canvas.css
  • apps/web/src/lib/canvas/scene.ts
  • apps/web/src/lib/canvas/scene.test.ts
  • apps/web/e2e/canvas-976.mjs
  • bench/canvas-976.mjs

UX gaps closed

  • Pencil/finger interaction, pressure, pan and pinch are covered by the production Canvas flow.
  • The element list works with pointer, touch and keyboard; each row has a stable link and context action. State is announced to assistive technology.
  • Canvas editing disables page scroll and rubber-band within its gesture surface.

UX gaps left

  • No real iPadOS/Safari Pencil session was run in this branch. The merge round must verify pressure, palm rejection, finger pan/pinch, and edge strokes in the iOS/iPadOS simulator.
  • The broader screenshot/embed tail reached three HTTP 429 responses while opening additional collaboration rooms. The required Canvas visual set passed in visual-only mode; the focused production flow passed separately. Re-run the integrated full E2E once in the merge round.

Decisions

  • DESIGN §60 does not specify the element-list presentation or page size. The list uses the existing Canvas actions surface and pages 50 stable-ID rows at a time.
  • During a two-finger pinch, switch to Hand for the gesture, then restore the prior freedraw tool. This preserves the active Pencil drawing mode without asking the User to change tools.

Verification

bun run check passed:

User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-pencil-978/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Focused Vitest passed:

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/canvas-pencil-978/apps/web

 Test Files  1 passed (1)
      Tests  15 passed (15)
   Start at  02:19:13
   Duration  489ms (transform 68%, import 16%, worker 10%, tests 6%, environment 1%)

Focused production Canvas flow passed:

Import verified json
Import verified markdown
Import verified compressed
Import verified inline
Canvas User flow: create, draw, save, reload, event and element-link checks passed.

Production screenshots passed with macOS platform emulation at 390, 820 and 1440 px in light and dark. All 18 required screenshots are attached to this issue:

node --check passed for apps/web/e2e/canvas-976.mjs and bench/canvas-976.mjs; git diff --check passed. No Rust files changed, so per-crate Rust gates do not apply. Cleanup output:

Removed 7238 files, 4.6GiB total

Performance

The local debug profile covered 5,500 elements (5,000 shapes and 500 bound labels). On the shared local host (load average at start: 8.84, 14.06, 14.45), stylus pointer-to-paint p50/p95 was 523.1/798.4 ms (10 samples); element-list open p50/p95 was 501.96/723.01 ms (5 samples). Cold open was 6.86 s; warm open p50/p95 was 4.55/4.78 s. Mean/peak server RSS was 954.5 MB/1.138 GB; mean/peak CPU was 227.9%/407.87%. These are local debug diagnostics under host load, not a release baseline. No Canvas metric exists in docs/perf/baseline.json. The perf VM lock was busy for the measurement attempt. Local debug export returned 503, so export measurements were skipped locally; the default profile includes export cases.

For the merge round

  • cd apps/web && bun run test — run the full Vitest suite on the combined branch.
  • cd apps/web && bun run build then cd apps/web && CALTERNAL_E2E_ASSET_OVERRIDE=1 bun e2e/canvas-976.mjs with the shared prebuilt server selected via /mnt/hdd/targets/shared-server/CURRENT — run the complete Canvas production flow, export round-trips and embed tail; confirm no 429s and retain the screenshot set.
  • XUSER_MATRIX_ONLY=1 tests/adversarial/run.sh, AUTHZ_MATRIX_ONLY=1 tests/adversarial/run.sh, and ADVERSARIAL_API_ONLY=1 tests/adversarial/run.sh — run the merge-round cross-User, authorization and robustness matrices.
  • flock /root/perf.lock bash -c 'uptime; source /mnt/hdd/targets/shared-server/CURRENT; CALTERNAL_E2E_ASSET_OVERRIDE=1 bun bench/canvas-976.mjs --json artifacts/canvas-978-perf.json' — measure release hot paths under the perf lock and record host load; compare when the integrated Canvas baseline is established.
  • flock -w 7200 ~/.local/state/codex-jobs/calternal/macvm.lock netbird ssh --no-browser calternal@10.69.69.21 — hold the Mac VM lock for staging iOS/iPadOS simulator verification. Confirm Pencil pressure and palm rejection, finger pan/pinch, and Safari edge strokes; capture the device evidence.
READY FOR MERGE: yes — branch-level implementation and focused gates are complete. The remaining integrated-build and device checks are listed under “For the merge round”. ## Built - Pencil Mode suppresses drawing under a finger, which can pan the Canvas. Two-finger pinch temporarily uses Hand and restores the previous drawing tool. Pen pressure is preserved on freehand strokes. - Added the accessible, paged Canvas elements surface with stable-ID selection/reveal, Copy link, context actions, touch swipe Copy link, keyboard controls, and announced loading/empty/error states. - Added a 5k-element Canvas performance profile for stylus paint and the element list. Commits: - `2353688a81d576dcd7f2689a26be8f7a708d3b02` — Pencil gestures and element actions. - `4ef59ea109826dcbbb2708cb61e53bcf869b08cb` — 5k Pencil and outline profile. Head SHA: `4ef59ea109826dcbbb2708cb61e53bcf869b08cb`. ## Files - `apps/web/src/lib/canvas/CanvasReact.tsx` - `apps/web/src/lib/canvas/CanvasView.svelte` - `apps/web/src/lib/canvas/canvas.css` - `apps/web/src/lib/canvas/scene.ts` - `apps/web/src/lib/canvas/scene.test.ts` - `apps/web/e2e/canvas-976.mjs` - `bench/canvas-976.mjs` ## UX gaps closed - Pencil/finger interaction, pressure, pan and pinch are covered by the production Canvas flow. - The element list works with pointer, touch and keyboard; each row has a stable link and context action. State is announced to assistive technology. - Canvas editing disables page scroll and rubber-band within its gesture surface. ## UX gaps left - No real iPadOS/Safari Pencil session was run in this branch. The merge round must verify pressure, palm rejection, finger pan/pinch, and edge strokes in the iOS/iPadOS simulator. - The broader screenshot/embed tail reached three HTTP 429 responses while opening additional collaboration rooms. The required Canvas visual set passed in visual-only mode; the focused production flow passed separately. Re-run the integrated full E2E once in the merge round. ## Decisions - DESIGN §60 does not specify the element-list presentation or page size. The list uses the existing Canvas actions surface and pages 50 stable-ID rows at a time. - During a two-finger pinch, switch to Hand for the gesture, then restore the prior freedraw tool. This preserves the active Pencil drawing mode without asking the User to change tools. ## Verification `bun run check` passed: ```text User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-pencil-978/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Focused Vitest passed: ```text RUN v5.0.1 /home/kayg/Developer/calternal-wt/canvas-pencil-978/apps/web Test Files 1 passed (1) Tests 15 passed (15) Start at 02:19:13 Duration 489ms (transform 68%, import 16%, worker 10%, tests 6%, environment 1%) ``` Focused production Canvas flow passed: ```text Import verified json Import verified markdown Import verified compressed Import verified inline Canvas User flow: create, draw, save, reload, event and element-link checks passed. ``` Production screenshots passed with macOS platform emulation at 390, 820 and 1440 px in light and dark. All 18 required screenshots are attached to this issue: - Drawing: [390 light](https://git.kayg.org/attachments/4b5e3d37-4217-420e-9f39-0b801d86b1d0), [390 dark](https://git.kayg.org/attachments/3991febd-c400-4486-9c55-b4182625f486), [820 light](https://git.kayg.org/attachments/8d6f0154-ae10-45a2-9acd-79697c659e28), [820 dark](https://git.kayg.org/attachments/afc1fc38-e701-4b09-9adb-5bf55085a33e), [1440 light](https://git.kayg.org/attachments/fc58b409-1b9f-4b65-86ea-1e25959a2c79), [1440 dark](https://git.kayg.org/attachments/2adce601-6430-4803-b4f6-0b9ed00baac6). - Element list: [390 light](https://git.kayg.org/attachments/1e80927a-2dc1-4af7-bbc9-0feb693b6243), [390 dark](https://git.kayg.org/attachments/476b4dcb-8556-4b49-8b36-704f15065c53), [820 light](https://git.kayg.org/attachments/b4459680-2e84-458c-878c-54ff4ab315e0), [820 dark](https://git.kayg.org/attachments/96f62c01-53e8-4855-8180-bc7e7c037b0d), [1440 light](https://git.kayg.org/attachments/ceb49f61-1537-42b6-8ca0-49e3e1c3a9dd), [1440 dark](https://git.kayg.org/attachments/ae8cfba2-8984-412b-9aa4-8d2630d4a425). - Pencil edit: [390 light](https://git.kayg.org/attachments/73233b6c-359b-4262-8666-4f476954942b), [390 dark](https://git.kayg.org/attachments/0ec1a32e-7416-4a4b-9c98-885128bbf2e4), [820 light](https://git.kayg.org/attachments/dd3c4c6f-4dfb-4e66-8112-88ca8a3f8133), [820 dark](https://git.kayg.org/attachments/aa6b7aa5-b7a0-4a3f-b9fc-6bd11de5b9c2), [1440 light](https://git.kayg.org/attachments/11b1c197-c72e-4654-a727-342a4c3307f4), [1440 dark](https://git.kayg.org/attachments/64e28052-b5c5-4fd2-87a3-f39a59f69ec4). `node --check` passed for `apps/web/e2e/canvas-976.mjs` and `bench/canvas-976.mjs`; `git diff --check` passed. No Rust files changed, so per-crate Rust gates do not apply. Cleanup output: ```text Removed 7238 files, 4.6GiB total ``` ## Performance The local debug profile covered 5,500 elements (5,000 shapes and 500 bound labels). On the shared local host (load average at start: 8.84, 14.06, 14.45), stylus pointer-to-paint p50/p95 was 523.1/798.4 ms (10 samples); element-list open p50/p95 was 501.96/723.01 ms (5 samples). Cold open was 6.86 s; warm open p50/p95 was 4.55/4.78 s. Mean/peak server RSS was 954.5 MB/1.138 GB; mean/peak CPU was 227.9%/407.87%. These are local debug diagnostics under host load, not a release baseline. No Canvas metric exists in `docs/perf/baseline.json`. The perf VM lock was busy for the measurement attempt. Local debug export returned 503, so export measurements were skipped locally; the default profile includes export cases. ## For the merge round - `cd apps/web && bun run test` — run the full Vitest suite on the combined branch. - `cd apps/web && bun run build` then `cd apps/web && CALTERNAL_E2E_ASSET_OVERRIDE=1 bun e2e/canvas-976.mjs` with the shared prebuilt server selected via `/mnt/hdd/targets/shared-server/CURRENT` — run the complete Canvas production flow, export round-trips and embed tail; confirm no 429s and retain the screenshot set. - `XUSER_MATRIX_ONLY=1 tests/adversarial/run.sh`, `AUTHZ_MATRIX_ONLY=1 tests/adversarial/run.sh`, and `ADVERSARIAL_API_ONLY=1 tests/adversarial/run.sh` — run the merge-round cross-User, authorization and robustness matrices. - `flock /root/perf.lock bash -c 'uptime; source /mnt/hdd/targets/shared-server/CURRENT; CALTERNAL_E2E_ASSET_OVERRIDE=1 bun bench/canvas-976.mjs --json artifacts/canvas-978-perf.json'` — measure release hot paths under the perf lock and record host load; compare when the integrated Canvas baseline is established. - `flock -w 7200 ~/.local/state/codex-jobs/calternal/macvm.lock netbird ssh --no-browser calternal@10.69.69.21` — hold the Mac VM lock for staging iOS/iPadOS simulator verification. Confirm Pencil pressure and palm rejection, finger pan/pinch, and Safari edge strokes; capture the device evidence.
Author
Owner

Merge round 7c starts on job/merge-round-7c, base 4082669f7. The current owner job authorizes integration and full verification; it supersedes the original read-only #867 brief.

Canvas order: core → files → collab → Sketch → Pencil → cards. Migrations follow the combined 7b schema. No pushes or deploys. Final report will include verbatim gates, screenshots, defensive renderer review and staging readiness.

Merge round 7c starts on `job/merge-round-7c`, base `4082669f7`. The current owner job authorizes integration and full verification; it supersedes the original read-only #867 brief. Canvas order: core → files → collab → Sketch → Pencil → cards. Migrations follow the combined 7b schema. No pushes or deploys. Final report will include verbatim gates, screenshots, defensive renderer review and staging readiness.
Author
Owner

Finished merge-round-7c integration at 094d22e44507bf8bdd87dd8ffd460c254cb6329c. READY FOR STAGING: no.

Core production visual flow passes emulated Pencil pressure, single-finger pan, two-finger pinch and accessible element-list actions. Real iPad/Pencil remains an owner check.

Production macOS evidence is attached to #867: 390/820/1440, light/dark. Screenshots remain outside git.

Focused Canvas viewer authority regression:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 248 filtered out; finished in 2.52s

Svelte check:

svelte-check found 0 errors and 4 warnings in 3 files

The full report, renderer boundary review, migration upgrade evidence, exact gate excerpts and decisions are in docs/audits/merge-round-7c.md and the final #867 comment. Staging blockers include the Notes process SIGSEGV after 278 passing assertions (#1069), stale performance exception pins, the retained thumbnail test conflict, Sketch save and unfinished verification. No pushes or deployments.

Finished merge-round-7c integration at `094d22e44507bf8bdd87dd8ffd460c254cb6329c`. READY FOR STAGING: no. Core production visual flow passes emulated Pencil pressure, single-finger pan, two-finger pinch and accessible element-list actions. Real iPad/Pencil remains an owner check. Production macOS evidence is attached to [#867](https://git.kayg.org/kayg/calternal/issues/867): 390/820/1440, light/dark. Screenshots remain outside git. Focused Canvas viewer authority regression: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 248 filtered out; finished in 2.52s ``` Svelte check: ```text svelte-check found 0 errors and 4 warnings in 3 files ``` The full report, renderer boundary review, migration upgrade evidence, exact gate excerpts and decisions are in `docs/audits/merge-round-7c.md` and the final #867 comment. Staging blockers include the Notes process SIGSEGV after 278 passing assertions (#1069), stale performance exception pins, the retained thumbnail test conflict, Sketch save and unfinished verification. No pushes or deployments.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#978
No description provided.