{
  "schemaVersion": 1,
  "id": "cassini-ovals",
  "version": "1.0.0",
  "status": "published",
  "kind": "background",
  "title": "Cassini Ovals",
  "summary": "The glowing family of Cassini ovals flowing outwards from two foci while one bright curve morphs from two loops through a figure-eight to a single oval. Loops perfectly.",
  "description": "The curves of Giovanni Cassini, the points whose distances to two fixed foci multiply to a constant. As the constant grows they morph from two separate loops, through the figure-eight lemniscate, to a single peanut and finally a nearly circular oval. A whole family of them glows on a dark field and flows outwards from the foci like ripples while one bright curve breathes through every shape of the family and back. Choose the colours (electric, amber, violet or mono), how far apart the foci are, the lines' thickness, the flow and the grid.",
  "role": "Background (WebGL shader, full frame): a precise, hypnotic mathematical backdrop for science, tech and title content.",
  "collections": [
    "backgrounds",
    "hypnotic-loops"
  ],
  "tags": [
    "cassini oval",
    "lemniscate",
    "curves",
    "mathematics",
    "foci",
    "geometry",
    "glow",
    "morph",
    "loop",
    "background"
  ],
  "synonyms": [
    "oval of cassini",
    "lemniscate morph",
    "math curves",
    "focal curves",
    "bernoulli lemniscate",
    "geometry animation"
  ],
  "useCases": [
    "A maths or physics explainer backdrop",
    "A tech or data-science title scene",
    "A hypnotic generative loop"
  ],
  "featured": null,
  "publishedAt": "2026-10-06",
  "implementation": {
    "target": "three",
    "runtime": "browser",
    "language": "javascript",
    "module": "esm",
    "entry": "src/cassini-ovals.js",
    "exports": [
      {
        "name": "createCassini",
        "signature": "createCassini({ palette?, spread?, lines?, width?, flow?, grid? }) => { scene, camera, setSize(width, height), update(time, loopSeconds), dispose() }",
        "description": "The family of curves as one full-screen shader; render its scene with its camera, 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."
      },
      {
        "name": "PALETTES",
        "signature": "PALETTES: { electric, amber, violet, mono }",
        "description": "Colours of the family, the moving curve and the foci (sRGB hex)."
      }
    ],
    "reusable": [
      "src/cassini-ovals.js"
    ],
    "demo": "demo.js",
    "harness": "index.html"
  },
  "dependencies": [
    {
      "id": "three",
      "version": "0.185.1",
      "reason": "Renderer, geometry and shader materials."
    },
    {
      "id": "apw-runtime",
      "version": "1.0.0",
      "scope": "demo",
      "reason": "Demo harness."
    }
  ],
  "params": [
    {
      "key": "palette",
      "label": "Colours",
      "type": "select",
      "default": "electric",
      "options": [
        {
          "value": "electric",
          "label": "Electric"
        },
        {
          "value": "amber",
          "label": "Amber"
        },
        {
          "value": "violet",
          "label": "Violet"
        },
        {
          "value": "mono",
          "label": "Mono"
        }
      ]
    },
    {
      "key": "spread",
      "label": "Foci apart",
      "type": "number",
      "min": 0.6,
      "max": 1.4,
      "step": 0.05,
      "default": 1
    },
    {
      "key": "width",
      "label": "Line width",
      "type": "number",
      "min": 0.5,
      "max": 2,
      "step": 0.05,
      "default": 1
    },
    {
      "key": "flow",
      "label": "Flow",
      "help": "Whole rungs of the family that creep outwards per loop.",
      "type": "integer",
      "min": 0,
      "max": 3,
      "step": 1,
      "default": 1
    },
    {
      "key": "grid",
      "label": "Grid",
      "type": "number",
      "min": 0,
      "max": 1.5,
      "step": 0.05,
      "default": 1
    }
  ],
  "presets": [
    {
      "id": "default",
      "label": "Electric, one rung a loop",
      "params": {}
    },
    {
      "id": "amber",
      "label": "Amber, wide foci",
      "params": {
        "palette": "amber",
        "spread": 1.3,
        "flow": 2
      }
    },
    {
      "id": "violet",
      "label": "Violet, fine lines",
      "params": {
        "palette": "violet",
        "width": 0.6,
        "flow": 3
      }
    },
    {
      "id": "mono",
      "label": "Mono, still",
      "params": {
        "palette": "mono",
        "flow": 0,
        "grid": 0.4
      }
    }
  ],
  "output": {
    "width": 1080,
    "height": 1080,
    "fps": 30,
    "durationSeconds": 12,
    "posterSeconds": 2,
    "alpha": false,
    "motion": "procedural",
    "loop": {
      "seamless": true,
      "cycleSeconds": 12,
      "method": "The bright curve's constant is b = a(1 + 0.62 sin(2π·phase)), the ladder of levels creeps a whole number of rungs per loop and everything else is static, so the last frame meets the first."
    },
    "seed": null
  },
  "composition": {
    "inputs": "Time within the loop; the colours, foci spread, line width, flow and grid.",
    "outputs": "A full frame, drawn by one shader on a full-screen quad.",
    "coordinates": "The plane spans about 1.55 half-heights either way; the foci are at ±0.62 · spread on the x axis.",
    "lighting": "Self-lit; the bloom gives the glow.",
    "timing": "update(time, loopSeconds) sets the loop phase; frames render in any order.",
    "compatibleWith": [
      "kinetic-title",
      "number-reveal"
    ],
    "notes": [
      "The family's levels are a geometric ladder of |z² − a²| (ratio 1.28) drawn as lines of even pixel width by dividing by the gradient's length; the bright curve is the level b² with b swinging from 0.38a to 1.62a, passing through the lemniscate at b = a."
    ]
  },
  "reuse": [
    "Maths explainer: Electric, one rung a loop.",
    "Warm science title: Amber, wide foci.",
    "Still diagram backdrop: Mono, still."
  ],
  "integration": {
    "newProject": "Create a WebGLRenderer, createCassini() and createBloomPass(renderer, { width, height }); call cassini.setSize(width, height), then each frame cassini.update(time, loopSeconds) and bloom.render(cassini.scene, cassini.camera).",
    "existingProject": "Copy src/cassini-ovals.js; render cassini.scene with cassini.camera into a render target (or through the bloom pass) as a backdrop."
  },
  "performance": {
    "notes": "One full-screen pass with a few evaluations of a complex square per pixel and a small bloom. A couple of milliseconds a frame at 1080p on an integrated Intel GPU.",
    "limits": "Software WebGL renders in under a second a frame."
  },
  "limitations": [
    "The picture is a pure 2D contour plot: no depth, no 3D surface.",
    "The foci are always on the x axis."
  ],
  "license": {
    "spdx": "MIT",
    "notice": "Copyright (c) 2026 AI Pixel Wizard. Released under the MIT License. See LICENSE in this folder.",
    "thirdParty": []
  },
  "provenance": "Adapted from the owner's threejs-sketches/geometry-math/cassini-ovals (MIT) and rebuilt for AI Pixel Wizard in October 2026 as an original shader: gradient-normalised contour lines of |z² − a²| on a creeping geometric ladder, a morphing bright oval through the lemniscate and an exact loop.",
  "changelog": [
    {
      "version": "1.0.0",
      "date": "2026-10-05",
      "notes": "Initial release."
    }
  ]
}
