Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Spirit dragon

New

A painted dusk forest where a glowing white dragon swims between the trees, in front of some and behind others.

spirit-dragon/spirit-dragon.tsx
Live · interactive

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A hero or a splash screen that sets a mood before any words: a story, a game, a launch page.
  • An empty or waiting screen with room to breathe, where a slow loop keeps the page alive without asking for anything.
  • A themed moment that should follow the page between day and dusk: the painting takes the page’s color-scheme.
  • A scene that has to stay sharp at any size: every piece is vector, cropped to a 4:3 stage and scaled by its width.

When not to use

  • Behind text that has to be read: the trees, the dragon and the mist fight the words. Use a flat or blurred backdrop.
  • A small thumbnail or an icon: under about 280 px the child and the fox turn to specks. Use a still illustration.
  • A dense working screen (a dashboard, a form): the swimming pulls the eye away. Use a static header image.
  • A scene the reader should step through or play with: this one only swims in place. Use the moon dragon, with its chapters.
  • Several scenes on one screen at once: each redraws its dragon every frame. Show one live, the rest with autoplay={false}.

You can add this component with the shadcn CLI or by hand:

Into components/spirit-dragon of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/spirit-dragon.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

hero.tsx
import { SpiritDragon } from './spirit-dragon';export function Hero() {  return <SpiritDragon speed={0.8} glow={0.9} />;}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

A teal forest at dusk, painted in SVG: a translucent spirit dragon holds its pose between the trees and swims in place, its ribbon body undulating, its long mane, whiskers, crest and feathered tail fan streaming, while a child holds up a flickering lantern, the reeds bend in the wind and a small fox flicks its tail. Paper grain over everything keeps it looking painted.

  • The picture is four stacked layers in one 2000 × 1500 frame, cropped to the stage: the forest behind the dragon, the dragon, the forest in front of it, and a paper grain (fractal noise, blended in overlay). Only the dragon’s layer is drawn again on each frame; the rest are still SVG painted once.
  • Every trunk, bough and twig is a middle line with a width at each point, turned into a tapering outline with a smooth spline. Its shaded side, light and dark brush streaks, warm rust streaks and dark knot holes are laid along that same line, so they follow the grain without a clip path; the bare twigs that cross the sky grow from a few roots, forking twice. The hills carry loose brush sweeps and a pale rim, each clipped to its hill.
  • The dragon’s pose is a centripetal Catmull-Rom spline through hand-placed points, from the tip of the tail to the back of the head, each with the body’s width there. It never leaves it: the spine slides a few units back and forth along it, a wave runs down the body from neck to tail, and the whole dragon drifts a little, so it swims in place.
  • Each frame rebuilds the body from 281 spine points. The ribbon’s width breathes a little, as if it twisted, with a soft halo and bright rims along its edges; the scales are staggered rows of arcs fixed to spine points, so they ride the wave. The mane is a pale mass lying back over the neck with strands drawn over it, each strand following the spine further down and flaring off it; the tail ends in a fan of six blades spread like a wing’s feathers, a membrane between them. The head, with its crest of soft spikes, its ruff, glowing eye and whiskers, is one drawing turned to the neck.
  • Depth: where the dragon passes behind (the band across the middle, under the navy trunk and the teal boughs), the trees are drawn a second time over it, clipped to that part of the picture. So it weaves in front of one trunk and behind the next with no masks to recompute.
  • The rest moves by CSS: the lantern swings and flickers (its glow, the glass and the pool of light on the ground), the hair and hood stream in the wind, the reeds sway, the fox flicks its tail every few seconds, mist drifts across and motes float; the dragon also sheds motes of its own that drift up and fade.
  • It plays only while on screen in a visible tab; every animation then holds still. Under reduced motion it shows the still frame until the reader presses play.
  • Every colour is a light-dark() token: by day a misty grey-pink sky over teal trees, at night a violet dusk with deeper trees and a brighter glow. Chrome does not repaint SVG painted from such tokens when only the colour scheme changes, so a hidden probe’s light-dark() colour is read again whenever an attribute on the component or any of its ancestors changes, or the system setting does, and on a new scheme the layers are drawn anew.
PropTypeDefaultDescription
autoplaybooleantrueSwims on its own while on screen. With false it shows the still frame until the play button is pressed.
speednumber1How fast the dragon swims and everything sways: 1 is the painting’s own pace.
glownumber0.7How brightly the dragon glows, from 0 (no halo) to 1.
colorScheme'light' | 'dark' | 'system'—Forces day or dusk; without it, the scene follows the page’s color-scheme.
classNamestring—Added to the root element.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

spirit-dragon.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useEffect,  useId,  useLayoutEffect,  useRef,  useState,  type CSSProperties,  type RefObject,} from 'react';import { drawDragon, HEAD_DETAIL, HEAD_EYE, HEAD_SHAPE, type Frame } from './dragon';import { BackScene, FrontScene, MOTES } from './scene';import { useAutoplay } from './use-autoplay';import styles from './spirit-dragon.module.css';/* * Spirit dragon: a painted teal forest at dusk, where a translucent white dragon swims between the * trees above a child holding up a lantern, and a small fox. * * - The picture is four stacked layers in the same 2000 × 1500 frame, cropped to the stage: the *   painting behind the dragon (sky, trees, hills), the dragon, the painting in front of it, and a
dragon.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.import { smooth, Track, type P } from './geometry';/* * The spirit dragon, drawn fresh for every frame. It holds the painting's pose and swims in place. * * - The pose is a smooth line through hand-placed points, from the tip of the tail to the back of *   the head (plus a little past each end). The spine is SEGMENTS + 1 points spread along it *   behind the head; each frame the whole spine slides a few units back and forth along the line, *   a wave runs down it from neck to tail, and the dragon drifts a little as a whole. So the body *   undulates through the same curves without ever leaving them. * - Everything hangs off the spine. The body is a ribbon around it, its width set at each control *   point; the scales are staggered rows of small arcs fixed to spine points, so they ride the *   wave; the mane is long, fine strands that start on the back of the neck and lie back along it, *   each strand following the spine further down, flaring away from it and waving on its own *   clock; the tail ends in a fan: a pale membrane spread between bright ribs. * - The head is one drawing in its own units (the neck joint at 0,0, the neck running to +x), *   moved and turned to the first spine point; its crest of soft spikes and its whiskers wave. *//** The pose, from just past the tip of the tail to just past the head; [x, y, body width]. The *  first and last two points only extend the line, so the spine can slide along it. */const POSE: [number, number, number][] = [
scene.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.import {  brushStrokes,  knotHoles,  limb,  limbBand,  seeded,  smooth,  twig,  type P,  type W,} from './geometry';import styles from './spirit-dragon.module.css';/* * The painting the dragon swims through, in the scene's units (2000 × 1500), as still SVG drawn * once: the sky, the far trees and hills, the trees the dragon weaves between, the near hills, * and in front of it all the big dark trunk, the reeds, the child with the lantern and the fox. * * Every trunk and branch is a limb (a middle line with a width at each point), so its brush * streaks can follow it. Fills are classes, so the colours are the stylesheet's light-dark() * tokens. The few parts that move (hair, hood, lantern, fox's tail, reeds, motes) are their own * elements, animated by CSS transforms in the stylesheet.
geometry.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * Drawing helpers for the spirit dragon's forest, all in the scene's own units (a 2000 × 1500 * painting): * * - smooth() turns a list of points into a soft curve through them (Catmull-Rom as cubic Béziers), *   so a hand-placed outline reads as painted rather than cut. * - limb() draws a trunk or a branch from its middle line and its width at each point, tapering to *   a round tip, and brushStrokes() lays the painterly streaks along it, so no clip path is needed *   to keep them inside. * - Track is the dragon's closed swimming loop: a centripetal Catmull-Rom spline through its *   control points, resampled every few units so a point at any distance along it costs two *   look-ups. */export type P = readonly [number, number];/** A middle-line point of a limb, and the limb's width there. */export type W = readonly [number, number, number];const r1 = (value: number) => Math.round(value * 10) / 10;/** A smooth path through `points`: closed, or open from the first to the last. */export function smooth(points: readonly P[], closed = true): string {
use-autoplay.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(
spirit-dragon.module.css
/* Spirit dragon (spirit-dragon.tsx): a painted forest at dusk, 4:3, cropped to the stage. By day a   misty grey-pink sky over teal trees and hills; at night a violet dusk, the trees deeper, the   dragon and the lantern brighter against them.   Colours are the tokens below, light-dark(light, dark): the component takes the color-scheme it   inherits from the page (light unless the page says otherwise), or the one its colorScheme prop   sets (data-color-scheme). The painting's paths take them through classes.   Memory. The stage is one layout boundary sized from its width. The painting behind the dragon,   the one in front and the grain are still layers of their own, so the dragon's layer is the only   one painted again on each frame; the mist moves by transforms on small layers. */.root {  /* The sky, top to bottom, and the pink haze low in it. */  --sky-top: #acbfc2;  --sky-mid: #b3bfc2;  --sky-low: #c5b6bb;  --haze: #d5bec1;  /* The far trees, pale with distance. */  --far-1: #47827d;  --far-2: #327973;  --far-2-shade: #2a6e68;  --far-3: #376180;  /* The trees the dragon weaves between, and their shaded sides. */
  • The picture is one image (role="img") with a description of the scene; its layers are hidden from assistive technology.
  • A play and pause button sits in its corner, reachable by keyboard, with a visible focus ring and a label that says what it will do.
  • Under reduced motion it stays on the still frame until the reader chooses to play it; off screen or in a hidden tab, everything stops.