Blog · · 9 min read

Wood rings, cloth gives: shared materials for touchable illustrations

Sink In teaches English phrasal verbs with small engraved animations, and many of them now react to a finger. For a while each scene invented its own way to do that. This is how they ended up sharing one set of materials instead: wood that knocks, brass that rings, cloth that gives.

See Sink In →

Four engraved scenes: a man with a guitar case under a street lantern, a gaming table with chips under a hanging lamp, a bedroom with a reading lamp, and a desk with a lamp, a house on a book and a pencil cup. Coloured areas cover about half of each scene.
Four touchable scenes with their tap areas showing. A finger anywhere in the coloured parts lands on something; on the black the page scrolls as usual.

Every meaning in Sink In has a scene. Base on is a house standing on a book. Sleep in is someone staying under the covers while the sun climbs past the window. Put together is a bench whose loose parts join up. They are drawn in code, in Flutter, as light lines on a black page, like a scratchboard engraving.

Then the scenes started to answer touch. You can knock on the house, press the blanket, drag the desk lamp and watch its light and shadows move. Touch makes a picture feel like a thing, and a thing is easier to remember than a picture. But the way it grew was a mess, and this post is about cleaning that up.

Material feels
8
Scene families on the kit
17
Kinds of haptic
3
A fingertip
44 pt

Every scene had its own physics

The first touchable scenes were built one at a time, and each one grew its own small engine. The sponge had a spring. The seesaw had another spring with other numbers. The ball had its own squash. Four files counted haptic clicks by hand. A tap on a table did one thing in the workshop and something else in the café.

None of that was wrong on its own. Together it meant that a learner who touched ten scenes met ten slightly different worlds, and every new scene started from nothing. The fix was to notice that the scenes kept re-implementing the same question: what is this thing made of, and how does that feel?

A material says how it feels

The scenes already shared materials for drawing. An EngravedMaterial says how a surface is cut and how light falls on it: wood has grain, plaster only shows where light lands, glass has no body and two glints. So the touch went there too. Every material got a MaterialFeel.

The split that made it work: the scene decides what a touch means, the material decides how it feels. Knocking on a house and knocking on a brass lamp shade are the same gesture. The wood gives a short dull jolt; the brass rings.

static const wood = MaterialFeel(
  stiffness: 900, damping: 30,   // a stiff, quiet spring
  give: 0.05, wobble: 0.10,      // barely dents, barely shivers
  knock: 0.45,                   // how far a tap moves it
  flash: 0.7, flashSeconds: 0.38,// light a tap strikes out
  wave: 9,                       // how fast that light runs out
  bounce: 0.3, carries: 0.35,    // what it does to things on it
);

static const cloth = MaterialFeel(
  stiffness: 70, damping: 17,    // soft, creeps back without passing rest
  give: 0.9, folds: 1,           // a deep dent with creases round it
  lag: 0.12,                     // pulled along behind a dragging finger
  tap: MaterialHaptic.detent,    // a soft click in the hand
  bounce: 0, carries: 0.05,      // soaks a knock up
);

There are eight feels now. Each also picks one of three haptics, so the hand feels the material too.

FeelWhat a touch doesHaptic
WoodBarely gives. A short dull jolt and a quick pale flash.contact
MetalDoesn’t give. Rings for about a second, its highlight shivering.impact
Glaze, glassHard, a short ring, the glints flare.contact
StoneDead hard, the dullest jolt. A short flash runs fast through the piece.contact
PaperGives a little and springs back past rest.detent
LeatherFirm, creases under a press, comes back slowly. Worn leather gives more.detent
ClothA deep dent with folds, dragged behind the finger, creeps back.detent
SoftA sponge or cushion: squashes deep, springs back past rest and back.detent

Walls, tabletops and open ground have no feel at all, on purpose. The place a scene happens in is never touchable; only the things in it are.

One spring for everything

Every feel is one damped spring. A press holds it down, letting go lets it come back, a tap gives it a kick. Stiff, lightly damped metal rings; soft, heavily damped cloth creeps back without overshooting. The whole engine is a few lines:

void step(double dt) {
  var left = dt;
  while (left > 0) {
    final h = min(left, 1 / 120);          // small fixed steps
    velocity += (-stiffness * value - damping * velocity) * h;
    value += velocity * h;
    left -= h;
  }
  if (value.abs() < 0.002 && velocity.abs() < 0.02) {
    value = 0; velocity = 0;                // snap to rest, stop ticking
  }
}

Time comes in as dt from the interaction’s own tick, never from the wall clock. That keeps tests deterministic, and it lets a recorder play a scripted finger through a scene and write a video frame by frame. Every clip in this post was made that way.

Around the spring sit a few more shared pieces: MaterialTouch (press, drag, release, knock), a counter for the clicks a press makes as it goes down, and a set of rings for water. The rule in the codebase is short: never hand-roll another spring.

On scratchboard, a touch is light

The hard part wasn’t the physics. It was showing a touch in this style. There is no colour to tint and no blur to use. On a scratchboard, light is line: more light means a wider, paler line, and shade is just thinner lines or none. So a touch has to show up as a change in the lines.

  • A dent bends the hatching. Under a press the hatch lines pinch in toward the finger and kink across the folds. That reads in any light, even on a surface in shadow.
  • A tap strikes light out. A lit surface is already drawn at full width, so making its lines wider shows nothing. A tap instead adds a short burst of pale crossing lines, like a spark on the surface.
  • A wobble moves the highlight. Stiff things don’t dent. They shiver, and the light sliding across them shows it.

All of it lives inside the same paintMaterial call the scenes already used. A scene hands each thing only its own touches, so a knock on the house doesn’t dent the book. With no touches the picture is byte-identical to before, which made the rollout safe: a test can prove an untouched scene didn’t change.

Base on, the same script with and without touch. The house is timber, the book is paper, the cup is glaze, and each answers the same tap in its own way.

The finger hides the touch

The first version looked fine in videos with a ring drawn where the finger was. On a phone it was almost invisible, and my note after trying it was: “the area of effect is quite small, it will be under the finger.” Of course it is. A fingertip covers the exact spot it touches. On a scene the width of a phone, that spot is a good part of the house.

So the rule now is that every touch needs a response away from the contact. There are three kinds, and each scene uses at least one:

  1. A front that runs out across the thing. A tap’s flash is a ring travelling outward at the material’s wave speed: fast through stone, slower through paper. Cloth sends a soft ripple to its edges when you press it and again when you let go.
  2. The whole thing moving. The house squashes and the book sinks with it. The rocket rocks on its pad. The rowing boat lifts on the rings in the river, level, never tipping.
  3. Loose things nearby reacting. Dust falls where the house meets the book. Pencils rattle in a tapped cup. A brick knocked on the wall sheds grit from the mortar.

A local dent alone stays under the finger, so on its own it doesn’t count.

Sleep in: the covers are cloth. Pressing sends a ripple across the blanket. On a blanket this small even the ripple is quiet, which is a limit of the scene, not of the material.

What stands on a struck table

Knock on a table and the cup on it jumps a little, a crumb jumps high, a heavy radio barely moves. That used to be a hand-written list in each scene. Now a struck surface remembers its recent knocks (where, how hard, how long ago), and anything standing on it asks one question while it is drawn: how high am I right now?

final lift = touch.liftAt(cupX,
  gravity: gravity,           // canvas pixels per second squared
  speed: throwSpeed,          // how fast a full knock right beside it throws
  reach: tableWidth / 2,      // further along the table, less
  lightness: 1,               // 1 a cup, more for a crumb, less for a plate
  bounce: MaterialFeel.glaze.bounce,
);
final tilt = touch.rockAt(cupX, tilt: 0.05);  // rocks away from the knock

The answer is worked out from the knocks alone, so the painter keeps no state of its own. A thing already in the air ignores a new knock. A knock too soft to beat gravity throws nothing. Bounces follow the material: a cup clatters, a paper bag flops. A knock can also be passed on to what something stands on, weaker by how well the material carries it: timber carries a knock, cloth swallows it.

The touch has to agree with the meaning

This is a language app, so a touch is never just decoration. It can teach the meaning, or it can argue with it. The best case so far is put together. While the bench is in pieces, tapping a loose piece rings that piece alone. Once it is joined, a tap anywhere rings the whole bench as one. The feel is the meaning.

Put together: apart, each piece rings alone; joined, the bench rings as one.

The same thinking decides what stays hands-off:

  • In the bedroom, sleep in is touchable but wake up and rouse are not. A hand pressing someone who stays asleep would fight both.
  • The count down rocket rocks when you knock its pad. It never launches early.
  • Blow up, tune in and the people in the café can’t be touched. Each family lists only the things a touch doesn’t argue with.
  • Leather in break in gives as much as the story has worn it: stiff at the start, soft by the end.

Tap areas bigger than the drawing

A finger is about 44 points wide on any phone. A pencil cup in a scene is drawn much narrower than that, and a lantern on a street lamp is smaller still. Aiming at those is frustrating, and nothing in a still picture says the lantern can move. My two notes on this were that small details must be bigger than they’re drawn, and that in the lamp scenes about half of the picture should react, so people find the lamp by poking around.

The shared tap map does both with one call:

  • Every small thing is at least a fingertip. It takes a finger within 22 points of its drawn shape, however thin it is drawn.
  • Small things win. The order is: on a small thing’s drawing, then within its fingertip, then on a big thing’s drawing, then plain nearness. So the cup wins over the table it stands on.
  • In the lamp scenes, half the canvas answers. Each point belongs to the nearest thing, so the canvas splits into cells round the things. Out to the distance that covers half the canvas, a touch anywhere in a thing’s cell lands on it. Past that, the page scrolls. Other scenes keep just the fingertip rule, so scrolling past them stays easy.
  • Hands-off parts measure infinity. They never take a finger, so a big table doesn’t steal taps from the person sitting at it.

The picture at the top of this post is that map, drawn over four scenes with lamps you can drag. A tap that lands beside a thing strikes it at the nearest point on its edge, so the flash still starts on the object and not in the air.

How it was built

Like the map in the last post, this was written by Claude Code, an AI coding agent, with me deciding and judging. It went in three steps, all in one day:

  1. A pilot on three scenes. The desk (wood, paper, glaze), the bedroom (cloth) and the sponge. The sponge already had its own spring, so it was moved onto the kit and its video compared frame by frame. It came out byte-identical, which proved the kit could replace the old code.
  2. A rollout to five scenes by five subagents working in parallel, each owning its own files: the clock, the drawing desk, the brick wall, the bench and the coffee mug.
  3. A second batch of ten from one written brief: rowing, shoes, a hut, a rocket, a café, a stove, a workshop, a gaming table, a radio and the sea.

Each scene came back as a side-by-side video, the same scripted finger with touch on and off, like the clips above. That is what I reviewed. Screenshots don’t show a spring.

The subagents also sent requests back to the kit, and the useful ones were folded in the same day: struck light for surfaces a scene draws its own way, glass glints that flare and shiver, dust that stays where it lands, and the hop and rock helpers above.

What I’d keep

  • Split meaning from feel. The scene says what a touch is for. The material says what it is like. Neither needs to know much about the other.
  • Make the response bigger than the finger. If the only change is under the contact, there is no change.
  • Keep the untouched picture identical. It turns a risky refactor of twenty scenes into a test that passes or fails.
  • Judge motion as video, side by side. A still frame can’t tell timber from brass.
  • Sometimes the right answer is no touch. A gesture that argues with the meaning is worse than none.

Sink In is on iPhone, Android and Mac at sink-in.app. If a scene feels wrong under your finger, I’d like to hear about it: hello@pomisoft.dev.