Skip to content
Back to skills

Skia Canvas Rendering

ASecurity

Builds hand-drawn Skia canvases - pure worklet draw() in one Picture, scene clock, timelines, canvas text, goldens. Use when drawing a chart, grid or board, or canvas animation freezes. Not for touch (canvas-gestures-and-hit-testing).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentgoreactnodeawstestinggitapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 20 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill skia-canvas-rendering --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Skia Canvas Rendering?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Skia Canvas Rendering
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-skia-canvas-rendering/badge)](https://www.skillsdirectory.com/skills/zakariaf-skia-canvas-rendering)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: skia-canvas-rendering
description: Builds hand-drawn Skia canvases - pure worklet draw() in one Picture, scene clock, timelines, canvas text, goldens. Use when drawing a chart, grid or board, or canvas animation freezes. Not for touch (canvas-gestures-and-hit-testing).
---

# Skia canvas rendering

Every hand-drawn surface of the app (a progress grid, a chart, an editor, a game board) is one Skia
`<Canvas>` whose single `<Picture>` is recorded on the UI thread from the surface's pure `draw()`,
animated by a timeline that a self-stopping scene clock plays, and proven by unit tests, a draw-call
budget and pixel goldens. This skill ships the canvas kit (`src/canvas/`), a template surface and two
checkers that prove the result.

## Rules that must hold

1. **One `<Canvas>` per surface with one `<Picture>` recorded in `useDerivedValue` from the surface's
   `draw()`, and every computed picture lives in the kit or a surface folder.** Why: dynamic pictures
   are Skia's immediate-mode case; one picture per frame keeps cost predictable; a computed canvas
   elsewhere (a Picture sparkline in `components/`, animated Skia nodes in a child component) escapes
   every rule below. Retained nodes only for static art: a declarative gradient, blur or fixed shape
   may live anywhere (sizing it through `onSize` is layout, not motion), and art recorded once
   (`createPicture`) is listed in the contract's `staticCanvases`. An atlas only for hundreds of sprites.
2. **`draw(canvas, frame)` is a pure worklet in a `'worklet'` file:** it reads only `frame` (`view`,
   `fx`, `highlight`, `colors`, `layout`, `kit`), imports Skia types only, allocates no Skia objects,
   reads no clock or random source, and imports values only from other `'worklet'` modules. Why: the
   same function then renders on device, in Jest goldens and in Node with identical pixels, and a plain
   function called on the UI thread crashes at runtime only.
3. **An opaque canvas paints every pixel first.** `SurfaceCanvas` is opaque by default, so `draw()`
   starts with `canvas.drawColor(colors.color.background)`; a canvas that must show what lies behind it
   is declared `"opaque": false` in the contract and every `SurfaceHost`/`SurfaceCanvas` rendering it
   passes a literal `isOpaque={false}`. Why: on Android an opaque canvas is a SurfaceView, neither
   clipped by a rounded parent nor blended with what lies behind it, so unpainted pixels show black on
   device while iOS and the goldens look right; the declaration alone changes nothing on screen.
4. **Push one `scene` shared value per committed change** (`makeScene(seq, view, tracks)`, `startAt:
   NOT_STARTED`); the frame runner stamps `startAt = info.timestamp` on its first frame. Never use
   `timeSinceFirstFrame`. Why: Reanimated resets it to 0 on every re-activation, which froze every
   animation after the first (verified on Reanimated 4.5.1).
5. **The frame callback runs only while something moves** and its body is one call into
   `runSceneFrame`, which holds the `try/catch`; never Skia's `useClock()` or `withRepeat(…, -1)`. Why:
   an idle surface must cost nothing (both of those run every frame forever), an uncaught UI-thread
   error kills the app, and React Compiler bails out of hooks with value blocks inside `try/catch`.
6. **Persist before animating; a new change fast-forwards; only changes committed while the surface is mounted animate.** The store applies and saves, then
   `presentChange` cancels old cues, pushes the final view and schedules new cues; the change present
   at mount is shown without animation. Why: killing the app mid-animation must lose nothing, the view
   is always the final state, and a screen opened again must not replay its last celebration.
7. **`buildTimeline(events, motion)` is pure and honours `'reduced'`:** half durations, no particles or
   shake, no overshoot. Why: reduce motion is an accessibility requirement, and as data it is testable.
8. **An effect layer that is not a tween of something visible (flash, glow, burst, shake) draws only
   while its track plays (`isPlaying`: `ageMs > 0 && progress < 1`).** Why: a sample holds a track's
   `from` before it starts, so an unguarded effect shows at full strength at moment 0.
9. **Selection and hints are UI state the screen passes as `highlight` (`CanvasHighlight`,
   `EMPTY_HIGHLIGHT`), and a drag preview sits exactly on the hovered cell.** The gesture layer applies
   the surface's `dragLiftPt` once; draw adds no offset. Why: stores, undo and persistence never see a
   selection, and the previewed cell must be the cell that receives the drop.
10. **The layout is a pure worklet of the actual canvas size, shared by draw and hit-testing.** Why:
   tablets, landscape, foldables and resizable windows need no special case, and draw and touch cannot
   drift.
11. **Never pixel-flip.** A surface that opts in (`isMirroredInRtl`) mirrors positions in the layout.
    Why: `scaleX(-1)` mirrors letters and digits.
12. **Digits are localised in `toView()` and drawn with `drawText` (`drawFittedText` in a small
    shape); words and complex scripts use a cached Skia `Paragraph`; widths come from glyph widths;
    fonts come from the bundled TTF through one `useFonts` provider on both platforms.** Why:
    `drawText` does no shaping, `measureText` is unimplemented in CanvasKit (Jest, Node), and Skia's
    Android font manager cannot see fonts embedded with the `expo-font` plugin.
13. **Paths are built once with `Skia.PathBuilder` at unit size** and placed with
    `canvas.translate/scale`. Why: the mutable `SkPath` API is deprecated in Skia 2.6 and allocates.
14. **Palettes are semantic tokens in sets with identical keys (`light`, `dark`, plus the opt-in sets
    `contracts/canvas-surfaces.json` lists), beside a `canvas-contrast.json` saying which colour sits on
    which; every declared pair meets WCAG 2.2 AA in every set (text 4.5:1, graphics and shapes 3:1,
    `distinct` kinds apart under colour-blind simulation); meaning is never carried by colour alone.**
    Why: a missing token is a crash on one theme only, and contrast is provable only when the pairs are
    declared and measured; `accessibility-as-code`'s contrast check measures the same file, so its format
    is stable and the pairs are never declared a second time in `contracts/contrast.json`.
15. **Stop everything when the surface may not run, and switch the clock only through
    `scene-clock-state`.** `useCanvasLifecycle` (app active, screen focused and not covered) stops
    the clock, cancels cues and suspends audio; a scene pushed while the surface may not run is held
    until it may; every resume records at least one frame; a done message names its run, and JS never
    reads the scene shared value back. Why: an overlay or an unfocused screen that stays mounted must
    not keep a display link running, no cue or sound may fire once the app leaves the foreground, and a
    stale done read through `scene.get()` froze the picture after an interruption (the case study in
    `timeline-and-clock.md`).
16. **iOS: keep `CADisableMinimumFrameDurationOnPhone` true.** Expo's prebuild template sets it, so a
    Continuous Native Generation app needs no key; never override it with `false` in the config or a
    plugin, and committed native folders keep it in `ios/<App>/Info.plist`. Why: without it iOS caps
    the app at 60 fps on ProMotion iPhones. Android needs no key.
17. **Every canvas is one accessible image:** `accessible`, `accessibilityRole="image"` (or `role="img"`)
    and `accessibilityLabel` (or `aria-label`) = `t()` of `surface.describe(view)`; a purely decorative
    canvas opts out explicitly (`accessible={false}`). Why: Skia content is invisible to VoiceOver and
    TalkBack, labels must be translated, and React Native 0.86 defaults `accessible` to false: iOS
    ignores the label and role of a view that is not accessible.
18. **Every surface is proven three ways:** pure unit tests (timeline, clock, layout), a draw-call
    budget test that also asserts every localised label, and pixel goldens at 3 sizes × moments 0,
    0.5, 1 with 0.1 % tolerance, accepted only after opening every PNG. Why: a two-digit label is
    below the golden tolerance, so pixels alone miss it; goldens catch what assertions forget.
19. **Draw-call budget per surface on the busiest frame, never above 1,000.** Why: counting canvas
    calls is the headless stand-in for GPU cost; a higher number needs an on-device frame report.
20. **Server data becomes changes through `useCommittedChange(data, diffEvents)`** with a pure, tested
    `diffEvents(previous, next)` in the surface folder: the first data and identical refetches animate
    nothing; an optimistic update and its rollback are two changes. Why: a query has no reducer to
    return events, and a chart that replays on every refetch is noise.

## Workflow

1. **Decide and classify.** Read [references/architecture.md](references/architecture.md): draw with
   Skia only when the picture is computed; pick the kind (timeline: this skill end to end;
   simulate-then-replay or live: the loop comes from `realtime-game-loop`, palettes and drawing rules
   from here). A surface that takes touch also needs `canvas-gestures-and-hit-testing` in the same
   session. A game board: also read [references/game-boards.md](references/game-boards.md).
2. **Copy the kit.** Copy `templates/contracts/canvas-surfaces.json` to `contracts/` and fill in the
   first surface, then from the repo root run `node ${CLAUDE_SKILL_DIR}/scripts/check-canvas-files.mjs .`:
   it lists every file to copy. Copy each `kit-file-missing`
   file verbatim from `templates/` to the path its first line names (`templates/src/canvas/**` →
   `src/canvas/`, `templates/scripts/quality/**`, `templates/test/goldens/skia-golden.ts`), and
   `templates/src/hooks/use-is-app-active.ts` (+ test) → `src/hooks/` when the app does not have it
   yet. A `kit-import-unresolved` line names the prerequisite and the skill that creates it (the
   table in architecture.md). The iOS 120 Hz key comes from Expo's template (`plist-120hz` fails only an
   override or a committed `Info.plist` without it); the canonical `jest.config.js` has the golden
   project (`jest-golden-project`).
3. **Read before writing the surface:** [references/timeline-and-clock.md](references/timeline-and-clock.md)
   for `buildTimeline`, the clock and the lifecycle, [references/drawing.md](references/drawing.md) for
   `draw()`, the kit, palettes, layout and text, [references/api-traps.md](references/api-traps.md) for
   the worklet rules and API traps. Imitate the template surface and
   [examples/weekly-chart/](examples/weekly-chart/).
4. **Copy the surface templates** from `templates/src/features/__FEATURE__/canvas/` to
   `src/features/<feature>/canvas/`, the golden test and its nine baselines from `templates/test/goldens/canvas/`,
   and the contract's schema (`templates/contracts/canvas-surfaces.schema.json`). Replace `__FEATURE__`
   (`tasks`), `__SURFACE__` (kebab, `progress-grid`), `__SURFACE_PASCAL__` and `__SURFACE_CAMEL__` in
   file names and contents. The copy compiles and passes as is; then adapt the model, the view, the
   events, the layers, the palettes and the contrast pairs.
5. **Build test-first, in this order:** palettes and `canvas-contrast.json` → `canvas-model.ts` →
   `to-view.ts` → `build-timeline.ts` with its test (budget, reduced motion, cues) → `layout-<id>.ts`
   with its any-size test → `draw-<id>.ts` with the budget, moment-0, highlight and label tests →
   `<id>-surface.ts` (`isMirroredInRtl`, the `describe` key in every locale catalog, `dragLiftPt` and
   `targetsOf` when the surface drags or suggests). Run `npx jest src/features/<feature>/canvas --ci
   --selectProjects unit` after each step (paths first: `--selectProjects` would swallow them).
6. **Wire the screen.** The feature's model hook passes the store's `CommittedChange` (server data:
   `useCommittedChange(data, diffEvents)`, the timeline reference's server section), the `ViewFormat`,
   the live `motion`, `useIsFocused()`, `isInterrupted`, the colours, the kit (built once the
   `useFonts` provider exists, drawing.md) and the highlight to `SurfaceHost` (display-only), or calls
   `useSurfacePlayer` and renders `SurfaceCanvas` with the gesture layer's `input` (interactive). The
   label is `t(surface.describe(view))`. A canvas that shows what lies behind it passes
   `isOpaque={false}` (written out on the element, not through a spread) and declares
   `"opaque": false` in the contract.
7. **Lint, format and type-check:** `npx eslint --fix src/canvas src/features/<feature>/canvas test/goldens scripts/quality`,
   `npx prettier --write` on them (import order and wrapping depend on the names you chose), then
   `npm run typecheck`.
8. **Create the pixel goldens deliberately** ([references/testing-and-goldens.md](references/testing-and-goldens.md)):
   `npx jest test/goldens/canvas/<id>.golden.test.ts --selectProjects golden -u`, open all nine PNGs
   and compare them with the intended look (moment 0: old geometry, no effect that starts later,
   labels from the view already final; 0.5 mid-flight; 1 the final view), then
   `CI=1 npx jest --ci --selectProjects golden`.
9. **Run the checks:** `node ${CLAUDE_SKILL_DIR}/scripts/check-canvas-code.mjs .` and
   `node ${CLAUDE_SKILL_DIR}/scripts/check-canvas-files.mjs .`. Fix every `FAIL` line (each names the
   file, the rule and the fix) and rerun until both print `RESULT: PASS`.
10. **Look at it for real.** A Release build on a named iOS simulator and a named Android emulator,
    screenshots in every locale (one RTL locale at least), then the developer confirms 120 Hz and the
    hitch rate on real phones with a frame-time recorder (simulators are not devices).

## Definition of done

- [ ] The kit is in `src/canvas/` with its tests, plus `test/goldens/skia-golden.ts` and the worklet
      boundary gate in `scripts/quality/`; `npx jest src/canvas scripts/quality --ci --selectProjects unit` passes.
- [ ] Each surface folder has palettes (identical tokens in every set) with `canvas-contrast.json`,
      `to-view.ts`, `build-timeline.ts`, `layout-<id>.ts`, `draw-<id>.ts`, `<id>-surface.ts`, and their
      tests pass; every pair in `canvas-contrast.json` meets its ratio in every palette set (the
      contrast rules of `check-canvas-files.mjs`), and no `contracts/contrast.json` entry names the
      surface's palettes (`contrast-declared-once`).
- [ ] Moment 0 draws no effect that starts later (a draw test and the moment-0 goldens), the
      highlight is drawn, and a surface with a drag lift draws its preview on the dropped cell.
- [ ] The draw test asserts that the first call is `drawColor` (or the surface is declared
      `"opaque": false` and every element rendering it passes `isOpaque={false}`); every computed
      canvas lives in the kit or a surface folder, and no surface file sits outside one.
- [ ] A server-data surface has `diff-events.ts` with its test and feeds `useCommittedChange`;
      reopening the screen replays nothing (`use-surface-player.test.ts` passes).
- [ ] The busiest frame stays within the surface's draw-call budget (≤ 1,000) and every localised
      label is asserted through the recording canvas.
- [ ] Nine golden PNGs (3 sizes × 0, 0.5, 1) exist, were opened and looked at, and
      `CI=1 npx jest --ci --selectProjects golden` passes.
- [ ] `npm run typecheck` and `npx eslint --max-warnings 0` pass; no `__PLACEHOLDER__` is left.
- [ ] Reduced motion removes particles, shake and overshoot; every canvas is `accessible` with an
      image role and a translated `accessibilityLabel` (or opts out as decorative); the surface runs
      only while the app is active, the screen focused and nothing covers it (no `useClock`, no
      endless repeat); nothing turns the iOS 120 Hz key off.
- [ ] `npx jest src/canvas/scene-clock --ci --selectProjects unit` passes (push while paused, resume
      after done, stale done, and both recorded traces).
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-canvas-code.mjs .` prints `RESULT: PASS`.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-canvas-files.mjs .` prints `RESULT: PASS`.

Neither checker prints SKIP lines. Both print `NOT APPLICABLE` only when no `package.json` declares
`@shopify/react-native-skia`, and both stop with exit 2 when a configured `canvasSurfaceDirs` glob
matches no surface folder or `contracts/canvas-surfaces.json` is malformed.

## Anti-patterns

- **Animating with `timeSinceFirstFrame` or a `t0` stored on JS.** The second change freezes. Stamp
  `startAt` from `info.timestamp` in the frame runner.
- **Creating `Skia.Paint()`, `Skia.Color()` or paths inside `draw()` or at module import.** Native
  allocations every frame, and module scope breaks tests. Build once in the kit (`makeDrawKit`,
  `buildPaths`); one recorder per canvas via `useState`.
- **Putting per-frame data in React state or re-rendering a label per frame.** A 120 Hz frame is
  8.3 ms. Positions live in shared values and the picture; React renders on committed changes.
- **Storing only "the change" in the view and reading old state in `draw()`.** The view is the final
  state; anything that vanishes carries its own position in a track.
- **A frame callback that keeps running on an idle surface, on an unfocused screen or under an
  overlay.** Stop on `onDone(run, seq)` through `finishRun` and through `useCanvasLifecycle`.
- **Guarding `onDone` with `scene.get().seq` on the JS thread.** Until the UI applies a push, the JS
  read returns the old scene, so the done of the scene before an interruption stops the next scene on
  its first frame. Compare runs in `scene-clock-state`.
- **Starting the clock for a scene pushed while the surface is covered or unfocused.** Its animation
  plays where nobody sees it; `pushScene` holds it until the surface may run.
- **Reading Reanimated's `useReducedMotion()` in canvas code.** It reads the setting once at app
  start, so a later change is missed; pass the live setting as `motion`.
- **Flipping the canvas with `scaleX: -1` for RTL.** Mirror positions in the layout instead.
- **Leaving the background unpainted on the default opaque canvas.** iOS and the goldens look fine;
  Android shows black corners. Start draw with `drawColor`, or pass `isOpaque={false}` and declare it.
- **Embedding fonts with the `expo-font` plugin and resolving them with `matchFont` or
  `Skia.FontMgr.System()` for the canvas.** Works on iOS only; Android falls back to a system font.
  Load the TTF with `useFonts` and pass the provider everywhere.
- **Presenting the change a screen mounts with, or clearing store events to avoid it.** The player
  shows the mounted change's final view and animates only later `seq`s.
- **Setting `structuralSharing: false`, or putting `Date`, `Map`, `Set` or class instances into the
  query data or `select` result a surface reads.** Only plain JSON keeps its identity across
  refetches, so each refetch becomes a change and the chart replays.
- **A computed `<Canvas>` (a `<Picture>`, shared values, draw calls) in `src/ui/` or `components/`,
  or a surface's files in a folder no surface glob matches.** Make it a surface folder and render it
  with `SurfaceHost`; a static declarative canvas is fine anywhere.
- **Animating a canvas with Skia's `useClock()` or `withRepeat(…, -1)`.** The frame loop runs for as
  long as the canvas is mounted (idle, unfocused, covered); play a timeline through the scene clock.
- **A canvas with an image role and a label but no `accessible`.** TalkBack may read it, VoiceOver never
  focuses it; write all three (`SurfaceCanvas` does).
- **Declaring a surface's colour pairs again in `contracts/contrast.json`.** Two tables drift. The pairs
  live once, in the surface's `canvas-contrast.json`, which this skill and `accessibility-as-code` both
  measure. If that skill reports `canvas-palettes.json` as an undeclared colour table, update it (drawing.md).
- **`isRtl && TextDirection.RTL`.** RTL is 0; choose explicitly.
- **Raising the golden tolerance or re-running `-u` without opening the PNGs.** Fix the code, or
  accept a deliberate change and say why.
- **Calling a plain JS helper from `draw()` or `layout()`.** It throws on the UI thread at runtime
  only; move the helper into a `'worklet'` module (the template's `canvas-ids.ts`).
- **Asserting small text through pixels.** Use the recording canvas; 0.1 % tolerance cannot see a
  missing number.
- **Drawing a flash or glow whenever its entry exists.** It shows at full strength at moment 0; guard
  it with `isPlaying`.
- **Offsetting a drag preview in `draw()` (`row - 1`).** The item lands elsewhere and an edge row
  becomes unreachable; set `dragLiftPt` on the surface and let the gesture layer lift the pointer.
- **Keeping a selection or hint in the store.** Undo and persistence would see it; the screen passes it
  as `highlight`.
- **Measuring text with `measureText`, or drawing golden text without a loaded typeface.** CanvasKit
  returns `undefined` for the first and draws nothing for the second.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/architecture.md](references/architecture.md) | When to use Skia, data flow, file map, the kit's public API, the `CanvasSurface` contract, surface kinds, prerequisites and owners, the contract file and config keys, When multi-package | Workflow step 1, before any canvas work |
| [references/timeline-and-clock.md](references/timeline-and-clock.md) | Tracks, sampling, `buildTimeline`, the startAt fix, `scene-clock-state`, the frozen-surface case study with its traces, presenter, cues, lifecycle on iOS and Android, the clock trace and canvas-frame probe, 120 Hz | Step 3, before writing a timeline or touching the clock |
| [references/drawing.md](references/drawing.md) | `draw()` rules, highlights and effects, kit and palettes, contrast pairs and the colour-blind profile, paths, layout, RTL, text and fonts, particles, atlas, performance, accessibility, the template and the example | Step 3, before writing draw, layout, view or palettes |
| [references/testing-and-goldens.md](references/testing-and-goldens.md) | Test levels, the draw-call budget, golden set-up and acceptance, failures, the device-only marker, headless previews, simulators, emulators and the E2E probe | Step 8, and whenever a golden fails |
| [references/api-traps.md](references/api-traps.md) | Worklet rules, the SDK 57 API churn table, the re-verify list | Step 3, and when something works in Jest but not on a device |
| [references/game-boards.md](references/game-boards.md) | Games appendix: board folders, kinds, Lights Out from the template, selection, hints and drag, recurring needs | Step 1, when the surface is a game board |
| `templates/src/canvas/` | The kit: layout, types, timeline, draw kit, scene, clock decisions (+ the recorded-trace test), frame runner, clock hook, recorder, text, canvas (`isOpaque`), presenter, cues, lifecycle, player, host, server-data change hook, E2E probe, PNG painter, with tests (layout, types, track and sample are canonical synced copies, identical in every skill that carries them) | Step 2 (copy verbatim) |
| `templates/src/hooks/` | `use-is-app-active.ts` (+ test): the shared foreground hook the lifecycle reads, a synced copy of the canonical file `react-components-and-hooks` owns; keep the app's copy if it already has one | Step 2 (copy verbatim, unless the app has it) |
| `templates/src/features/__FEATURE__/canvas/` | The template surface (the Tasks progress grid): palettes, contrast pairs (`canvas-contrast.json`, format 1 in drawing.md), model, ids, view, timeline, layout, draw, surface object, tests | Step 4 |
| `templates/test/goldens/` | `skia-golden.ts` (synced canonical matcher), the `__SURFACE__.golden.test.ts` golden and its nine reviewed baselines | Steps 2, 4 and 8 |
| `templates/scripts/quality/` | `check-worklet-boundary.ts` (+ test) and `worklet-transform.test.ts`: the `npm test` gate for `'worklet'` modules | Step 2 (copy verbatim) |
| `templates/contracts/` | `canvas-surfaces.json` (the app's surfaces, kinds, opacity, palette sets, `staticCanvases`) and its JSON schema | Steps 2 and 4 |
| [examples/weekly-chart/](examples/weekly-chart/) | A second, complete surface folder to imitate: a server-fed bar chart with `diff-events.ts`, tweens, a guarded flash, RTL mirroring, two regions, fitted digits, its tests and a golden test that loads the bundled TTF (no baselines: you accept them in step 8) | Steps 3 and 6 |
| `scripts/check-canvas-code.mjs` | Checks worklets, the frame clock and idle clocks, the worklets API, pure and opaque draw, the opacity contract, per-frame allocation, the Skia API, where computed canvases and surface files live, accessibility, lifecycle, the clock decisions and the reduce-motion source | Step 9 and at the end |
| `scripts/check-canvas-files.mjs` | Checks kit files and their imports, the layout probe's containment, surface files, palettes, the contrast pairs (measured in every set, never declared again in `contracts/contrast.json`), timeline and draw tests, goldens and baselines, the iOS 120 Hz key (no override, committed `Info.plist`), the Jest golden project | Steps 2 and 9 and at the end |
| `scripts/selftest.mjs` | Proves both checkers pass the templates and catch each planted bug | After changing a checker or a template |
| `scripts/lib/` | `canvas-scope.mjs` (kit and surface folders, the contract, declared packages), `canvas-contrast.mjs` (the contrast file's keys, WCAG and colour-blind maths), `source-scan.mjs` (synced: worklet, JSX and import helpers) and `plist.mjs` (synced: reads `Info.plist`) | When changing a checker |
| `scripts/check-lib.mjs` | The shared script library (synced; never edit here) | Never by hand |
| `assets/shared.json` | The shared files this skill copies in | When adding a shared file |
| `assets/template-placeholders.json` | The placeholder values the template checks fill in (`tasks`, `progress-grid`) | When renaming the template surface |
| `tests/bases/app/` | Fixture base beside the templates: stand-ins for files other skills own (`AppText`, `renderWithProviders`, `app.config.ts`, `jest.config.js`), `package.json`, a live surface and the contract | When changing a fixture |
| `tests/fixtures/` | One overlay suite per checker: `good/` plus one planted bug per case (`mutation.json` + `EXPECT.txt`) | When adding a rule to a checker |

## Related skills

- `canvas-gestures-and-hit-testing` - touch on the canvas: gestures, hit-testing, drag, the pointer sample (load with this skill for any interactive surface).
- `networking-and-server-state` - the queries, keys and optimistic updates a server-fed surface reads.
- `realtime-game-loop` - fixed-step loops, simulate-then-replay and live surfaces.
- `game-rules-engine` - rules, events and seeded randomness for game boards.
- `audio-and-haptics-feedback` - the audio and haptics ports the cue scheduler calls.
- `accessibility-as-code` - contrast and colour-blind checks of the palettes, the live reduce-motion value, per-cell actions.
- `golden-and-snapshot-tests` - the golden policy and accepting baselines.
- `performance-budgets` - frame-time recording and on-device budgets.
- `i18n-strings-and-catalogs` and `rtl-and-direction` - the describe keys, digits and direction.
- `unit-and-component-tests` - the Jest projects, the Skia unit mock and the device-only marker.
- `design-system-structure` - the tokens the palettes take their values from, and `AppText`.
- `e2e-maestro` and `debug-menu-and-test-hooks` - flows that read the canvas probe, and the switch that shows it.
- `motion-and-animation` - Reanimated animations of ordinary views.
- `icons-app-icon-and-splash` - code-drawn icons and store art from the same Skia code.
- `expo-sdk-upgrade` - moving Skia, Reanimated and Worklets to the next SDK.

Files in this skill

  • SKILL.md27.4 KB
  • assets/shared.json1.2 KB
  • assets/template-placeholders.json310 B
  • examples/weekly-chart/build-timeline.test.ts1.6 KB
  • examples/weekly-chart/build-timeline.ts1.7 KB
  • examples/weekly-chart/canvas-contrast.json204 B
  • examples/weekly-chart/canvas-model.ts1.2 KB
  • examples/weekly-chart/canvas-palettes.json326 B
  • examples/weekly-chart/canvas-palettes.ts382 B
  • examples/weekly-chart/diff-events.test.ts1.2 KB
  • examples/weekly-chart/diff-events.ts715 B
  • examples/weekly-chart/draw-chart.test.ts4.1 KB
  • examples/weekly-chart/draw-chart.ts4.5 KB
  • examples/weekly-chart/layout-chart.test.ts1.5 KB
  • examples/weekly-chart/layout-chart.ts1.2 KB
  • examples/weekly-chart/to-view.ts746 B
  • examples/weekly-chart/weekly-chart-surface.ts951 B
  • examples/weekly-chart/weekly-chart.golden.test.ts3.6 KB
  • references/api-traps.md9.9 KB
  • references/architecture.md20.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.