{
  "schemaVersion": 1,
  "id": "disco-ball",
  "version": "1.0.0",
  "status": "published",
  "kind": "background",
  "title": "Disco Ball",
  "summary": "A mirror ball turning in a dark room, throwing hundreds of coloured squares of light across the walls and floor. Loops perfectly.",
  "description": "A mirror ball made of hundreds of tiny tilted tiles turning in a dark room, lit by coloured spotlights that circle above it. Every tile flashes as it turns past a light and shows a miniature of the room in its mirror; every tile throws its own square of light across the walls, floor and ceiling, so the room fills with moving dots; the beams of the lights show in a haze. Choose the colour of the lights (warm white, rainbow, blue and violet, or gold), the number of rows of tiles, how many spotlights, the haze, how many turns the ball makes in a loop, the brightness, the sparkle of the tiles and how far the camera sways.",
  "role": "Background (WebGL shader, full frame): a party, music and nightlife backdrop.",
  "collections": [
    "backgrounds",
    "hypnotic-loops"
  ],
  "tags": [
    "disco ball",
    "mirror ball",
    "disco",
    "party",
    "nightclub",
    "dance",
    "lights",
    "sparkle",
    "retro",
    "loop",
    "background"
  ],
  "synonyms": [
    "mirrorball",
    "glitter ball",
    "disco lights",
    "party background",
    "club lights",
    "dance floor",
    "seventies disco",
    "new year party"
  ],
  "useCases": [
    "A party, club or music video backdrop",
    "A new year, birthday or dance event intro",
    "A retro or fashion title scene"
  ],
  "featured": null,
  "publishedAt": "2026-10-06",
  "implementation": {
    "target": "three",
    "runtime": "browser",
    "language": "javascript",
    "module": "esm",
    "entry": "src/disco-ball.js",
    "exports": [
      {
        "name": "createDiscoBall",
        "signature": "createDiscoBall({ palette?, rows?, spots?, haze?, turns?, brightness?, sparkle?, sway?, seed? }) => { scene, camera, material, setSize(width, height), update(time, loopSeconds), dispose() }",
        "description": "The whole room as one full-screen shader: render its scene with its camera after setSize and update, ideally through createBloomPass."
      },
      {
        "name": "createBloomPass",
        "signature": "createBloomPass(renderer, { width, height, glow?, exposure?, grain? }) => { render(scene, camera, output?), dispose() }",
        "description": "A small bloom with filmic tone mapping: draws a scene to the canvas with its glow."
      },
      {
        "name": "PALETTES",
        "signature": "PALETTES: { classic, rainbow, blue, gold }",
        "description": "Five light colours for the spotlights, in order."
      }
    ],
    "reusable": [
      "src/disco-ball.js"
    ],
    "demo": "demo.js",
    "harness": "index.html"
  },
  "dependencies": [
    {
      "id": "three",
      "version": "0.185.1",
      "reason": "Renderer and the full-screen shader."
    },
    {
      "id": "apw-runtime",
      "version": "1.0.0",
      "scope": "demo",
      "reason": "Demo harness."
    }
  ],
  "params": [
    {
      "key": "palette",
      "label": "Lights",
      "type": "select",
      "default": "classic",
      "options": [
        {
          "value": "classic",
          "label": "Warm white"
        },
        {
          "value": "rainbow",
          "label": "Rainbow"
        },
        {
          "value": "blue",
          "label": "Blue and violet"
        },
        {
          "value": "gold",
          "label": "Gold"
        }
      ]
    },
    {
      "key": "rows",
      "label": "Rows of tiles",
      "help": "More rows mean smaller tiles and more, smaller dots.",
      "type": "integer",
      "min": 12,
      "max": 36,
      "step": 1,
      "default": 32
    },
    {
      "key": "spots",
      "label": "Spotlights",
      "type": "integer",
      "min": 1,
      "max": 5,
      "step": 1,
      "default": 4
    },
    {
      "key": "haze",
      "label": "Haze",
      "type": "number",
      "min": 0,
      "max": 1.6,
      "step": 0.05,
      "default": 0.7
    },
    {
      "key": "turns",
      "label": "Turns per loop",
      "type": "integer",
      "min": 1,
      "max": 3,
      "step": 1,
      "default": 1
    },
    {
      "key": "brightness",
      "label": "Brightness",
      "type": "number",
      "min": 0.5,
      "max": 1.6,
      "step": 0.05,
      "default": 1
    },
    {
      "key": "sparkle",
      "label": "Sparkle",
      "type": "number",
      "min": 0,
      "max": 1.6,
      "step": 0.05,
      "default": 1
    },
    {
      "key": "sway",
      "label": "Camera sway",
      "type": "number",
      "min": 0,
      "max": 1.5,
      "step": 0.05,
      "default": 1
    },
    {
      "key": "seed",
      "label": "Seed",
      "type": "integer",
      "min": 1,
      "max": 999,
      "step": 1,
      "default": 3
    }
  ],
  "presets": [
    {
      "id": "default",
      "label": "Warm white disco",
      "params": {}
    },
    {
      "id": "rainbow",
      "label": "Rainbow party",
      "params": {
        "palette": "rainbow",
        "spots": 5,
        "rows": 28,
        "haze": 1.2,
        "sparkle": 1.2,
        "seed": 9
      }
    },
    {
      "id": "blue",
      "label": "Blue club",
      "params": {
        "palette": "blue",
        "spots": 3,
        "rows": 36,
        "haze": 1.4,
        "brightness": 0.9,
        "seed": 14
      }
    },
    {
      "id": "gold",
      "label": "Gold night",
      "params": {
        "palette": "gold",
        "spots": 4,
        "rows": 24,
        "haze": 0.8,
        "sway": 0.6,
        "seed": 5
      }
    }
  ],
  "output": {
    "width": 1080,
    "height": 1080,
    "fps": 30,
    "durationSeconds": 12,
    "posterSeconds": 2,
    "alpha": false,
    "motion": "procedural",
    "loop": {
      "seamless": true,
      "cycleSeconds": 12,
      "method": "The ball turns a whole number of times per loop and the spotlights circle once, their power pulses in whole cycles and the camera sways in a sine of the loop, so the last frame meets the first."
    },
    "seed": {
      "param": "seed",
      "behavior": "Seed gives every tile its own tilt and decides which tiles are dull: same seed, same ball."
    }
  },
  "composition": {
    "inputs": "Time within the loop; the colour of the lights, rows of tiles, spotlights, haze, turns, brightness, sparkle and camera sway.",
    "outputs": "A full frame, drawn by one shader on a full-screen quad.",
    "coordinates": "World units of about a metre: the ball (radius 0.5) hangs at the origin of a box room 8.4 wide, 4.4 tall and 10.5 deep; the camera is 3.6 away.",
    "lighting": "The spotlights circle at height 2.45; each tile reflects them: the light dots on the walls are solved in closed form from the half-way vector between the light and the point, seen from the ball.",
    "timing": "update(time, loopSeconds) sets the loop phase and the ball's angle; frames render in any order.",
    "compatibleWith": [
      "kinetic-title",
      "number-reveal"
    ],
    "notes": [
      "Tiles are rings of equal height, as many to a ring as fit, each with a tilt of its own; a dull tile in seven throws no light.",
      "Nothing is simulated: a point of the room is lit by a spot only if the half-way vector between the directions to the light and to the point falls on a tile."
    ]
  },
  "reuse": [
    "Party or music video: Rainbow party behind a kinetic title.",
    "Club night promo: Blue club, slow.",
    "Retro fashion or new year: Gold night."
  ],
  "integration": {
    "newProject": "Create a WebGLRenderer, createDiscoBall() and createBloomPass(renderer, { width, height }); call disco.setSize(width, height), then each frame disco.update(time, loopSeconds) and bloom.render(disco.scene, disco.camera).",
    "existingProject": "Copy src/disco-ball.js; render disco.scene with disco.camera into a render target (or through the bloom pass) as the backdrop of your scene."
  },
  "performance": {
    "notes": "One full-screen pass: five tile lookups for a wall pixel, about a dozen for a ball pixel, eighteen cheap steps of haze, and a small bloom. About 38 ms a frame at 1080p on an integrated Intel GPU.",
    "limits": "Heavier than most ingredients: lower the haze to speed it up; software WebGL renders it in several seconds a frame."
  },
  "limitations": [
    "The tile footprints on the walls are idealised squares: there is no scattering or falloff of the reflected beams beyond a simple distance fade.",
    "The room is a plain dark box: a dance floor, speakers or people are not drawn."
  ],
  "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/light-atmosphere/webgpu-disco-ball (MIT) and rebuilt for AI Pixel Wizard in October 2026 as an original shader: a mirror ball of tilted tiles in rings, spots solved in closed form from half-way vectors, the tiles' own mirror images of the room, an exact loop and the frame/time contract.",
  "changelog": [
    {
      "version": "1.0.0",
      "date": "2026-10-05",
      "notes": "Initial release."
    }
  ]
}
