← Learn · Build · 12 November 2026 · 6 min

Toran: Five Indian Doorways You Can Walk Through

How I built /lab/toran: Codex-generated doorway lintels, Matter.js strands made of each region's welcome in its own script, and bells that ring when you swipe.

The output, liveavivashishta.com/lab/toran
Open the live page ↗

I saw a page go viral: a Chinese palace roof on cream paper, with a curtain of characters hanging under it that swung when the cursor passed through. Then a Japanese eave, then a yurt. The physics was simple. What made it work was that the picture looked handmade and the text hung from it like it belonged there.

I wanted my own version of that, closer to home. It is live at avivashishta.com/lab/toran: five Indian doorways, each with a toran, the garland hung over a door to welcome guests. The strands are made of the welcome itself, written in that region's script, and they ring when you swipe through them.

The short version: every lintel, wall and floor is one Codex image, cut out with rembg. The strands are Matter.js chains pinned to the lintel's bottom edge. The welcome is drawn once into a long strip of correctly shaped text, and each strand bends that strip along its links, so Indic letters stay joined. A fast swipe plays a clip from my sound library, pitched per strand to a five-note scale.

The five doors

Door Welcome Script Bead Sound
Rajasthan पधारो म्हारे देस (come to my land) Devanagari Marigold Ghungroo bells
Kerala സ്വാഗതം Malayalam Brass ring Temple brass
Bengal স্বাগতম Bengali Shola pith Bright ping
Punjab ਜੀ ਆਇਆਂ ਨੂੰ (you are welcome here) Gurmukhi Phulkari thread Wooden bead clack
Varanasi स्वागतम् (Sanskrit) Devanagari Rudraksha One deep bell

Step 1: the images, from Codex

Every image comes from the Codex CLI. The trick to making five images look like one set is a shared style block in every prompt, with only the object changing:

A single isolated object, photographed like a premium handmade artefact, front-on, perfectly centred, wide (about 3:2), soft studio light from the top left, plain flat warm off-white background, no text, no people, nothing hanging below it. The bottom edge of the object must be clean and horizontal.

That last line matters most. The strands hang from the bottom edge, so it has to be flat. The walls got their own rule: a plain surface with nothing in the centre, pale and low contrast, so text stays readable on top.

Two things I learned running it:

  • Long prompts as an argument kill Codex (exit 137, no output). Write the prompt to a file and pipe it in: codex exec --skip-git-repo-check -C out - < prompt.txt.
  • Run one job at a time. Parallel jobs failed silently.

Each lintel was then cut out with rembg. The default model made two of Kerala's brass lamp cups see-through, so that one used isnet-general-use instead.

Step 2: strands that hang

Each strand is a chain of small circles joined by constraints, the first pinned to the lintel. Beads never collide with each other (they share a negative collision group), which keeps the curtain calm instead of tangling:

const beads = Array.from({ length: L }, (_, j) =>
  Bodies.circle(x, y + (j + 1) * sp, 5, { frictionAir: 0.05, collisionFilter: { group: -1 } }));
const links = [Constraint.create({ pointA: pin, bodyB: beads[0], length: sp, stiffness: 1 })];
for (let j = 1; j < L; j++) links.push(Constraint.create({ bodyA: beads[j - 1], bodyB: beads[j], length: sp, stiffness: 1 }));

The pointer isn't a body. It pushes like wind: every bead near it gets a force along the pointer's velocity, so a slow hover sways the strands and a fast swipe throws them, and nothing ever sticks to the cursor.

Step 3: letters that stay joined

My first version put one letter on each bead, which is how the original works with Chinese characters. It looked broken. Hindi, Bengali and Punjabi letters hang from a shared top line, and vowel signs attach to the letter before them. Split स्वागतम् into single characters and the pieces fall apart.

The fix is to never split the text. The phrase is drawn once, properly shaped by the browser, into a long horizontal strip. Each strand then draws that strip slice by slice along its chain, one slice per link, rotated to the link's angle:

for (let k = 0; k < count; k++) {
  const a = pts[k], b = pts[k + 1];
  ctx.save(); ctx.translate(a.x, a.y); ctx.rotate(Math.atan2(b.y - a.y, b.x - a.x));
  ctx.drawImage(strip, sx + k * sp, 0, sp, h, 0, -h / 2, sp, h); // true width, never the link's length
  ctx.restore();
}

The text now bends with the strand like a ribbon. The comment on the drawImage line is the second lesson: I first stretched each slice to fit its link, and when links pulled apart the letters stretched too. Draw them at their real size and they can't.

Beads go only in the gaps between repeats of the phrase, and each region gets its own: marigolds, brass rings, white pith, phulkari diamonds, rudraksha. Every strand ends in a tassel, lamp, flower or bell.

Step 4: sound

The sounds are short clips from my own SFX library, not generated. Each strand plays the same clip at a different pitch, mapped across a five-note scale, so sweeping across the curtain plays a little run:

const SCALE = [1, 9/8, 5/4, 3/2, 5/3, 2, 9/4];
const note = SCALE[Math.round(i / (n - 1) * (SCALE.length - 1))];
src.playbackRate.value = stop.pitch * note;

A strand only rings if the pointer is moving fast and that strand hasn't rung in the last 160ms. Slow hovering stays quiet. Browsers block audio until you interact, so the first click on the page turns it on.

Step 5: leaving a door

The strands roll back up into the lintel like a blind, outer strands first, with the tassel riding up on the end. Then the lintel and wall fade, the next door slides in, and its strands unroll from the centre out. I first let them drop with gravity, which looked like a mess of stretched letters. Rolling them up is calmer and reads as the door closing.

Build your own

Build a page with one generated object at the top of a pale paper stage (a transparent PNG, its bottom edge flat) and a curtain of text strands hanging from that edge. Use Matter.js: each strand is a chain of small circle bodies joined by constraints with stiffness 1, the first pinned to a point on the object's bottom edge, all bodies in one negative collision group so they never collide. Make the outer strands a little shorter. Don't use a mouse constraint: on pointer move, push every body within about 40px along the pointer's velocity, scaled by distance.

Draw on a canvas, not with Matter's renderer. Render the strand's phrase once into a long offscreen strip (repeated with gaps), then draw each strand by slicing that strip along the chain, one slice per link, rotated to the link's angle and drawn at its true width so the letters never stretch. Put a small bead in each gap and an ornament at the end of every strand. Add a soft offset shadow. When the pointer crosses a strand fast, play a short sound clip with Web Audio, pitched to a pentatonic scale by the strand's index, with a cooldown per strand. Start audio on the first click. Unroll new strands from the centre out, and roll them back up before switching to the next object. Respect prefers-reduced-motion.