Recording & Playback

Capture every gesture, then redraw it with 99.9% accuracy

Contents

  1. Your brush has an invisible camera
  2. What gets recorded? Event list
  3. JSON: the "tape" format
  4. Why replay is hard
  5. Crandom: taming randomness
  6. Event sync: one thing per frame
  7. Delayed pen-up: don’t drop the last draw
  8. 99.9% consistency: how?
  9. Demo mode: automatic playback
  10. URL parameters: the address-bar remote
1

Your brush has an invisible camera

While you draw, an invisible camera records every action—where you pressed, where you moved, when you released, which color and brush size. It doesn’t record the screen; it writes an "action script."

InkField records actions, not pixels. On replay, the app re-runs that script—like a robot redrawing your moves.

Why not just record video?

Because video files are huge and you cannot change resolution or effects. Recording actions gives you:

  • Tiny files: A recording can be tens of KB (video would be tens of MB)
  • Perfect replay: The replayed drawing matches the original
  • Change settings: Replay at different canvas size or background
  • Like a performance: Viewers see the whole creation process, not just the result
2

What gets recorded? Event list

Each "action" in the program is called an event. The recording system records these event types:

Event type overview

CodeFull nameMeaningPayload
mpmousePressedMouse down (pen down)Position x, y + stroke settings
mdmouseDraggedMouse drag (stroke)Position x, y
mrmouseReleasedMouse up (pen up)Position x, y
kpkeyPressedKeyboard keyWhich key
eceffectControlEffect setting changeSetting and new value
Imagine teaching a robot to draw. You can't hand it a photo and say "copy this"; you have to instruct step by step: "Put the pen here, drag right, lift, switch to white…" That's what the event list does.

Each event has a timestamp in milliseconds. On replay, the program knows when each action should happen.

t = 0ms
mousePressed at (234, 567) — start first stroke
Payload: brush size=3, mode=1, color=black
t = 16ms
mouseDragged to (240, 560) — move up-right
t = 33ms
mouseDragged to (255, 548) — continue
t = 50ms
mouseDragged to (278, 532) — accelerating…
t = 200ms
mouseReleased at (412, 489) — pen up
t = 800ms
keyPressed — key 'w' (switch to white brush)
t = 1200ms
mousePressed at (300, 400) — start second stroke

mousePressed payload: strokeData

On each pen down (mousePressed), the event records not only position but a full stroke snapshot:

  • Brush size (initialSize)
  • Brush mode (brushMode, brushColorMode)
  • Brush color (brushColorMode)
  • Ink effect (useSharpen)
  • White brush mode (whiteBrushMode)
  • Flow (flow, flowSpeed)
  • Distort (distort, distortMode)
  • Blend (keyBlendMode, blend)
  • …and more

So even if settings change mid-session, each stroke start has a full snapshot for exact replay.

3

JSON: the "tape" format

When recording finishes, all actions are saved as a JSON file. JSON is like a tidy notebook—braces and brackets structure the data so both humans and machines can read it.

A minimal recording looks like this:

{
  "version": "1.0",
  "randomSeed": 42857,     // seed (critical for replay!)
  "canvasSize": { "width": 1920, "height": 1080 },
  "canvasBackgroundColor": [255, 255, 255],
  "initialBrushColorMode": 0,  // start: black
  "initialEffectControl": {
    "shapeType": 0,
    "metallicStrength": 85,
    "metallicTintType": "copper"
  },
  "events": [
    { "m": "mp", "t": 0, "x": 234, "y": 567,
      "strokeData": { "initialSize": 3, "brushMode": 1, ... } },
    { "m": "md", "t": 16, "x": 240, "y": 560 },
    { "m": "md", "t": 33, "x": 255, "y": 548 },
    { "m": "mr", "t": 200, "x": 412, "y": 489 }
  ]
}

Field reference

FieldMeaning
versionFormat version for future upgrades
randomSeedRandom seed—key to replay consistency (Ch5)
canvasSizeCanvas dimensions, restored on replay
canvasBackgroundColorBackground RGB
events[].mEvent type (mp/md/mr/kp/ec)
events[].tTimestamp (ms)
events[].x, .yMouse position
events[].strokeDataFull stroke snapshot on pen down

Event types use abbreviations: mp for mousePressed, md for mouseDragged, etc. A single drawing can have tens of thousands of mouseDragged events; shortening names saves a lot of file size.

4

Why replay is hard

Sounds simple—just replay the event list. But the devil is in the details:

Challenge 1: Random numbers

The brush uses random() everywhere—splash positions, texture, split hairs… Each call returns a different number.

Like two people rolling dice with the same rules but different outcomes. Record might get 3, 6, 1, 4…; replay might get 5, 2, 6, 3…—visually different.

Challenge 2: Frame rate

Your machine doesn't run at a constant speed. Recording might be 60fps; replay might be 45fps under load. Different events per frame change stroke detail.

Challenge 3: Conditional branches

Code has many "if… then…" branches. E.g. "if speed > threshold, call random() again for texture." Tiny float differences can send record down one branch and replay down another—random sequence drift.

Two people follow the same recipe: "add a pinch of salt." Their pinches differ. Later: "taste; if bland, add another pinch." One adds, one doesn't—from there the two cakes diverge.
5

Crandom: taming randomness

The fix is a seed. Imagine a die with a lock. Enter the same code (seed), and the sequence is always the same: 3, 6, 1, 4, 2, 5… Re-enter the same code and the sequence repeats from the start.

That's what js/crandom.js does. The Crandom class wraps p5's random() and adds counting:

// Record start randomSeed(recordingSeed); // lock sequence noiseSeed(recordingSeed); // same for noise crandom.reset(); // reset counter // On each crandom.random() // 1. counter +1 // 2. call p5 random() // 3. return result // Replay start randomSeed(recordingData.randomSeed); // same seed! noiseSeed(recordingData.randomSeed); crandom.reset(); // reset counter // Same seed → same sequence: 1st→3, 2nd→6, 3rd→1…
3
6
1
4
2
5
8
7

Same seed = same sequence; record and replay match.

Crandom debug superpower

Besides wrapping random(), Crandom counts calls—you can see how many random() calls so far. Essential for debugging:

// End of record console.log(`Record used ${crandom.getCount()} random()`); // → Record used 12847 random() // End of replay console.log(`Replay used ${crandom.getCount()} random()`); // → Replay used 12847 random() ← match = good

If the counts differ, some branch made record and replay take different paths. CrandomDebugger checkpoints help find where the drift starts.

6

Event sync: one thing per frame

Like someone reading a storybook to you. Too fast (three pages at once) and you lose the thread; too slow (one page forever) and the rhythm breaks. Best: one page at a time.

Replay is the same. A key rule:

maxMouseDraggedPerFrame = 1

const maxMouseDraggedPerFrame = 1; // One mouseDragged per frame—draw loop count matches record

Each draw() updates brush physics (spring, damping) once. If one frame processes three events, physics updates once; if record had one event per frame, physics updated three times—stroke elasticity and thickness diverge.

Before vs after

MetricBeforeAfter
Draw loop match93%100%
Random consistency93%99.9%
Visual consistencyNoticeable differenceIndistinguishable
7

Delayed pen-up: don't drop the last draw

Another subtle fix concerns pen-up.

Like calligraphy: the last stroke ends with the hand still on the paper. If the robot stops before that final touch is drawn, the last bit of ink is lost.

Problem: mouseReleased too early

If a frame gets both mouseDragged and mouseReleased, processing drag then release seems fine—but release triggers pen-up before the last drag's draw() has run.

// Fix: delay mouseReleased to next frame let processedMouseDraggedThisFrame = false; if (isMouseReleased && processedMouseDraggedThisFrame) { break; // pen-up next frame } if (isMouseDragged) { processedMouseDraggedThisFrame = true; }

This ensures the last draw runs, so the final pen-up stroke isn't dropped.

8

99.9% consistency: how?

Putting it all together, replay consistency works like this:

1. Restore initial state Canvas size, background, brush and effect settings
2. Lock seed randomSeed() + noiseSeed() with the same seed as record
3. Reset state Crandom counter, physics, shader init
4. Play event by event At most 1 mouseDragged per frame; delay mouseReleased
5. Restore strokeData per stroke On each mousePressed, restore full snapshot
6. Remove conditional random Ensure every random() runs in both record and replay

What is step 6?

Some code looked like this:

// Bad: random() sometimes called, sometimes not if (crandom.random(0, 1) > 0.8 && baseBrushSize >= 2.0) { currentSteps = int(crandom.random(20, 40)); // ↑ only 20% chance to call → sequence drift }

Fix: remove conditional random, or ensure random() is always called (result may be ignored).

Final scorecard

Brush sizerandom() diffConsistency
Size 3.0 (10 strokes)±2599.9%
Size 5.0 (4 strokes)±15099.8%

±25 calls sounds a lot, but a full drawing uses tens of thousands; 25 is visually negligible.

9

Demo mode: automatic playback

Like a gallery screen looping the artist's process. Viewers see the journey from blank canvas to finished work. That's Demo mode.

Demo flow:

Load recording (JSON) From URL if needed; no manual file pick
Restore canvas and background Resize and reload if dimensions differ
Auto start playback 500ms delay so canvas is ready
Playback complete Save result PNG

startPlayback() init list

On replay start the app does a long init (like pre-show setup):

  • Clear canvas
  • Restore background
  • Set random seed
  • Restore brush color mode
  • Restore effects (metallic, shape, etc.)
  • Regenerate forceMap
  • Re-init shader
  • Reset state (paths, flags, blur, …)
  • Set camera tracking

Skip any step and replay can diverge. That's why you see long if (typeof xxx !== 'undefined') chains—every state must be reset.

Design philosophy

Don't record the result; record the process.
Don't rely on luck; rely on the seed.
Don't do many things at once; one step per frame.
Don't skip details; reset everything.

These four principles get a highly random art program to 99.9% replay consistency.

10

URL parameters: the address-bar remote

Imagine a remote control that lets you preset the channel, volume, and picture quality before turning on the TV. URL parameters are that remote—add commands to the address bar and the page loads with your settings applied.

Basic syntax

Parameters go after ?, separated by _, in key:value format:

https://your-site.com/?_pix:2.0_wd:0.5_gr:0.3_console:1
Important rule: As soon as any URL parameter is present, the system turns all toggles off first, then enables only the ones you explicitly specify. So ?_wd:0.5 means "only White Dot on, everything else off."

Toggle parameters (1 = on / 0 = off)

Param Function Example
pathFuture path preview_path:1
gridGrid overlay_grid:1
consoleScreen text (Art System Log)_console:1
paperPaper texture_paper:1
cameraCamera movement (doMoving)_camera:1
loopLoop playback_loop:1
distortDistort shader_distort:1
rsRS effect_rs:1
clCellular effect_cl:1

Numeric parameters

These accept numbers; any value > 0 automatically enables the corresponding effect:

Param Function Slider range Actual value Example
pix Pixel Density Used directly (0.5–5.0) _pix:2.0
wd White Dot Density 0–1.0 display × 0.1 _wd:1.0 → actual 0.1
gr Grain Amount 0–1.0 display × 0.1 _gr:0.5 → actual 0.05

Common combinations

// Exhibition: 2x quality + White Dot + Grain + loop + screen text
?_pix:2.0_wd:1.0_gr:0.5_loop:1_console:1

// Mobile power-saving: 1x quality + Grain only
?_pix:1.0_gr:0.3

// High-quality replay: record at low pix, play at high pix
// (path density = recording FPS)
// Recording: ?_pix:1.0
// Playback: ?_pix:3.0_wd:0.8_gr:0.4_loop:1

pix and playback smoothness

Recording events (mouseDragged) are generated inside the draw() loop, so recording FPS = path point density.

Recording at high _pix (e.g. 4.0) means low FPS → sparse path → jerky playback. Solution: record at low pix, play back at any pix.

Mobile auto-override

Mobile devices (iPhone/iPad/Android) automatically cap pixel density at 1.0 for performance. The URL _pix parameter has the highest priority and overrides this default.

Priority: URL _pix > mobile override > desktop default

← Previous: Blend & Flow   |   Next: AI Log →

InkField Tutorial Series — Understanding digital ink, explained simply