{
  "schemaVersion": 1,
  "id": "split-flap",
  "version": "1.0.0",
  "status": "published",
  "kind": "text",
  "title": "Split-Flap Display",
  "summary": "Your message on an airport departure board, every letter flipping into place, then changing to a second message.",
  "description": "A split-flap board like the ones in old stations and airports. Each little module flips through its letters, the flaps falling and slapping down, until your words settle in a ripple from left to right; then the board flips on to a second message. Type up to three lines for each message, pick the flap and letter colours, the flip speed, and a straight-on or angled view.",
  "role": "Title or sign (Three.js 3D): a split-flap board that spells and changes your message.",
  "collections": [
    "kinetic-text",
    "satisfying"
  ],
  "tags": [
    "split flap",
    "departure board",
    "flip board",
    "solari",
    "airport",
    "train station",
    "retro",
    "mechanical",
    "title",
    "reveal"
  ],
  "synonyms": [
    "flip clock text",
    "solari board",
    "departures board",
    "flap display",
    "airport sign",
    "station board",
    "split flap text",
    "flip letters"
  ],
  "useCases": [
    "A title reveal for travel, trip and holiday videos",
    "Announcements: \"Now boarding\", \"Coming soon\", a launch date",
    "A score or countdown that flips when it changes"
  ],
  "featured": null,
  "publishedAt": "2026-10-05",
  "implementation": {
    "target": "three",
    "runtime": "browser",
    "language": "javascript",
    "module": "esm",
    "entry": "src/split-flap.js",
    "exports": [
      {
        "name": "createSplitFlapBoard",
        "signature": "createSplitFlapBoard({ messages, times?, rows?, columns?, align?, flipRate?, ripple?, jitter?, flapColor?, textColor?, housingColor?, fontFamily?, seed?, anisotropy? }) => { group, width, height, rows, columns, settledAt, update(time), dispose() }",
        "description": "The board as a THREE.Group centred at the origin, facing +z; update(time in seconds) every frame."
      },
      {
        "name": "splitLines",
        "signature": "splitLines(text) => string[]",
        "description": "'NOW BOARDING|GATE 12' → ['NOW BOARDING', 'GATE 12']."
      },
      {
        "name": "toDrum",
        "signature": "toDrum(text) => string[]",
        "description": "Text as the characters the drum has (capitals, accents dropped, the rest blank)."
      },
      {
        "name": "DRUM",
        "signature": "DRUM: string",
        "description": "The characters on every module, in flipping order."
      }
    ],
    "reusable": [
      "src/split-flap.js"
    ],
    "demo": "demo.js",
    "harness": "index.html"
  },
  "dependencies": [
    {
      "id": "three",
      "version": "0.185.1",
      "reason": "Renderer, geometry and materials."
    },
    {
      "id": "chrome-material",
      "version": "1.1.0",
      "scope": "demo",
      "reason": "Studio environment light for the demo."
    },
    {
      "id": "apw-runtime",
      "version": "1.0.0",
      "scope": "demo",
      "reason": "Demo harness."
    }
  ],
  "params": [
    {
      "key": "text1",
      "label": "First message",
      "help": "Use | to start a new line (up to 3 lines).",
      "type": "text",
      "default": "NOW|BOARDING|GATE 12",
      "maxLength": 80,
      "sensitive": true
    },
    {
      "key": "text2",
      "label": "Then",
      "help": "A second message to flip to. Leave empty to keep the first.",
      "type": "text",
      "default": "FINAL|CALL|LISBON",
      "maxLength": 80,
      "sensitive": true
    },
    {
      "key": "changeAt",
      "label": "Change at (seconds)",
      "type": "number",
      "min": 2,
      "max": 8,
      "step": 0.1,
      "default": 4.6
    },
    {
      "key": "flapColor",
      "label": "Flap colour",
      "type": "color",
      "default": "#1d1d21"
    },
    {
      "key": "textColor",
      "label": "Letter colour",
      "type": "color",
      "default": "#f2eee4"
    },
    {
      "key": "speed",
      "label": "Flips per second",
      "type": "number",
      "min": 8,
      "max": 30,
      "step": 1,
      "default": 18
    },
    {
      "key": "ripple",
      "label": "Ripple",
      "help": "Delay from one column to the next, in seconds.",
      "type": "number",
      "min": 0,
      "max": 0.12,
      "step": 0.005,
      "default": 0.035
    },
    {
      "key": "columns",
      "label": "Columns",
      "help": "0 fits the longest line.",
      "type": "integer",
      "min": 0,
      "max": 24,
      "step": 1,
      "default": 0
    },
    {
      "key": "align",
      "label": "Alignment",
      "type": "select",
      "default": "center",
      "options": [
        {
          "value": "center",
          "label": "Centred"
        },
        {
          "value": "left",
          "label": "Left"
        }
      ]
    },
    {
      "key": "camera",
      "label": "View",
      "type": "select",
      "default": "angled",
      "options": [
        {
          "value": "angled",
          "label": "Angled, drifting"
        },
        {
          "value": "front",
          "label": "Straight on"
        }
      ]
    },
    {
      "key": "seed",
      "label": "Variation",
      "help": "Changes when each module starts.",
      "type": "integer",
      "min": 1,
      "max": 9999,
      "step": 1,
      "default": 1
    }
  ],
  "presets": [
    {
      "id": "default",
      "label": "Now boarding",
      "params": {}
    },
    {
      "id": "airport",
      "label": "Airport yellow",
      "params": {
        "text1": "DEPARTURES|LISBON  08:45|TOKYO   09:10",
        "text2": "DEPARTURES|LISBON  DELAYED|TOKYO   BOARDING",
        "flapColor": "#141414",
        "textColor": "#ffc93c",
        "align": "left",
        "columns": 17
      }
    },
    {
      "id": "station",
      "label": "Station, white flaps",
      "params": {
        "text1": "PLATFORM 7|14:32",
        "text2": "PLATFORM 7|ON TIME",
        "flapColor": "#f1f0ec",
        "textColor": "#15161a",
        "camera": "front"
      }
    },
    {
      "id": "score",
      "label": "Scoreboard",
      "params": {
        "text1": "HOME  0|AWAY  0",
        "text2": "HOME  1|AWAY  0",
        "flapColor": "#13321f",
        "textColor": "#f4f1e6",
        "changeAt": 3.5,
        "align": "left"
      }
    }
  ],
  "output": {
    "width": 1080,
    "height": 1080,
    "fps": 30,
    "durationSeconds": 9,
    "posterSeconds": 3.6,
    "alpha": false,
    "motion": "procedural",
    "loop": {
      "seamless": false,
      "method": "Starts blank, spells the first message, then flips to the second and holds."
    },
    "seed": {
      "param": "seed",
      "behavior": "Seed sets the small random delay of each module: same seed, same ripple."
    }
  },
  "composition": {
    "inputs": "Messages (lines of text) and when each starts. Time in seconds for each frame.",
    "outputs": "A THREE.Group: the board with its housing, centred at the origin and facing +z.",
    "coordinates": "World units: each module is 0.62 wide and 1 tall, 0.7 apart; width and height report the whole board.",
    "lighting": "Standard materials: light it like any scene (one shadow-casting light shows the falling flaps best). The demo adds the studio environment from Chrome Material.",
    "timing": "update(time) is a pure function of time. settledAt tells you when every module has stopped.",
    "compatibleWith": [
      "chrome-material",
      "orbit-camera",
      "stop-motion-feel"
    ],
    "notes": [
      "Messages are treated as private: not stored in shared packs unless you choose to include them.",
      "Letters are drawn with the system's bold sans-serif (Helvetica or Arial where present); pass fontFamily and load the font first for identical renders.",
      "Each module flips forward through the drum (blank, A–Z, 0–9, punctuation), so letters further along take longer, as on a real board."
    ]
  },
  "reuse": [
    "Trip video opener: your destination and date as the first message, \"NOW BOARDING\" as the second.",
    "Score update: two messages that differ by one digit; only that module flips.",
    "In a 3D scene: add board.group to a wall in your own scene and light it with a spotlight."
  ],
  "integration": {
    "newProject": "Create a scene with a light, add createSplitFlapBoard({ messages }).group, and call board.update(frame / fps) before each render.",
    "existingProject": "Copy src/split-flap.js. Add the group to your scene, scale or rotate it as you like, and call update(time) every frame."
  },
  "performance": {
    "notes": "One shared texture and a few hundred small meshes; only the flaps that are moving change each frame. Smooth at 1080p on laptop GPUs.",
    "limits": "Best up to about 4 rows of 24 columns. Software WebGL (no GPU) renders slowly with shadows on."
  },
  "limitations": [
    "Capital letters, digits and common punctuation only (as on a real board); other characters show as blanks.",
    "No sound: add the clatter in your editor."
  ],
  "license": {
    "spdx": "MIT",
    "notice": "Copyright (c) 2026 AI Pixel Wizard. Released under the MIT License. See LICENSE in this folder.",
    "thirdParty": [
      {
        "name": "three.js",
        "version": "0.185.1",
        "license": "MIT",
        "url": "https://github.com/mrdoob/three.js"
      }
    ]
  },
  "provenance": "Adapted from the owner's threejs-sketches/mechanical-kinetic/flip-clock (MIT), rebuilt for AI Pixel Wizard in October 2026 as a full split-flap board for your own messages: a character drum per module, gravity-eased flaps with a settling bounce, a left-to-right ripple, a second message, and the frame/time contract.",
  "changelog": [
    {
      "version": "1.0.0",
      "date": "2026-10-05",
      "notes": "Initial release."
    }
  ]
}
