#4866 Barrier-Grid Animation (or Scanimation / Kinegram)

#4866 Barrier-Grid Animation (or Scanimation / Kinegram)

#4866 Describe the process of making animation with two sheets, one with pattern and one with cutout slits

This process is known as barrier-grid animation (or scanimation / kinegram). It relies on optical occlusion and the persistence of vision: a moving grid of narrow clear slits exposes one sequential phase of motion at a time while masking the remaining phases behind opaque bars.

Core Geometric Principle

The geometry depends on the total frame count (N):

  • Slit width (w): The width of each transparent aperture.

  • Opaque bar width (B): Must equal (N - 1) x w to fully cover all non-active frames.

  • Grid period / Pitch (P): The distance from the start of one slit to the next:

    P = w + B = N x w

  • Interleaving: If an animation has 6 frames (N = 6), the composite sheet is constructed from repeating 6-slice clusters. Slice 1 belongs to Frame 1, Slice 2 to Frame 2, and so on up to Slice 6. As the top sheet slides across by distance w, the slit shifts to expose the next slice in every cluster simultaneously.

1.Prepare the Animation Sequence:Use high-contrast silhouettes or bold line work.

Create a looping animation sequence with 4 to 8 frames (6 is standard). High contrast (solid black on white) delivers the sharpest optical cutoff and prevents ghostly motion blur between frames. Ensure each frame is identically sized, cropped, and registered to the exact same canvas coordinates.

2.Slice and Interleave the Composite Base:Digital slicing via script or graphics editor.
  1. Define the unit slit width $w$ based on your display resolution (for example, 6 pixels wide, or 0.5 mm in physical dimensions).

  2. Divide each frame into vertical strips of width w, spaced apart by an interval of (N - 1) x w.

  3. Offset each sequential frame by w:

    • Frame 1 strips: columns 0, N, 2N, 3N, ...

    • Frame 2 strips: columns 1, N+1, 2N+1, 3N+1, ...

    • Frame k strips: columns (k-1) + m cdot N

  4. Flatten all sliced layers into a single composite raster image.

3.Construct the Barrier Grid Sheet:Opaque-to-clear ratio must match frame count.
  1. Generate a parallel grating with clear slits of width $w$ and opaque black bars of width (N - 1) x w.

  2. For a 6-frame system, the transparent duty cycle is:

    Duty Cycle = w/P = 1/6 ~ 16.7%
  3. Output the grid onto high-transparency media (such as inkjet transparency film, clear acetate, or laser-cut/etched cast acrylic). The opaque zones must be completely light-blocking (high optical density).

4.Registration and Motion Calibration:
  1. Lay the slit sheet directly over the composite print.

  2. Rotate the slit sheet until the vertical bars align parallel to the raster stripes (eliminating angular moiré fringes).

  3. Move the top sheet horizontally. Translating the sheet by a total distance equal to the pitch P cycles through all N frames, creating fluid, continuous motion.

Key Technical Parameters

ParameterRecommended ValueImpact of Deviation
Frame Count (N)5 to 7 frames<4 causes jerky motion; >8 darkens the image excessively
Slit Width (w)0.3 mm – 0.6 mm (paper/acrylic)Too wide causes cross-frame ghosting; too narrow drops brightness
Grid AlignmentDelta theta < 0.2-degreesRotational misalignment produces diagonal moiré interference bands
Substrate Gap<= 0.5 mm (flush contact)Gap causes parallax error when viewed off-axis

Physical Fabrication & Execution Checklist

  1. Substrate Contact: Ensure the slotted barrier lies directly flat against the composite surface; any air gap or thick spacer introduces parallax distortion that reveals adjacent frames prematurely.

  2. Duty Cycle Verification: Verify with digital calipers or print inspection that the black bar width is exactly (N - 1) times the slit width before running production sheets.

  3. Linear Constraint: If building an interactive enclosure, add side guide rails to restrict movement strictly to the horizontal X-axis and prevent rotational drifting.


Describe a "Hello World!" project and a Texas A&M Marching Band animation project

"Hello World!" Barrier-Grid Project

The introductory barrier-grid project validates mechanical tolerances, slit-to-bar ratios, and alignment precision without the complexity of organic movement.

  • The Visual Subject: A pulsating heart or expanding/contracting geometric ring (such as concentric circles or an oscillating wordmark).

  • Frame Count (N): 4 frames.

  • Physical Dimensions:

    • Slit width (w): 0.5 mm (clear slit).

    • Opaque bar width (B): (4 - 1) x 0.5 mm = 1.5 mm.

    • Grid Pitch (P): 2.0 mm total cycle length (w + B).

    • Total translation needed for one loop: 2.0 mm.

  • Frame Breakdown:

    • Frame 1: Small central circle (10 mm diameter, 2 mm stroke).

    • Frame 2: Medium circle (18 mm diameter).

    • Frame 3: Large circle (26 mm diameter).

    • Frame 4: Outermost ring fading or dissolving into an outer boundary (34 mm diameter).

  • Execution & Diagnostic Check:

    • Interleave the 4 frames into 0.5 mm columns (A, B, C, D, A, B, C, D ...) on high-contrast photo matte paper.

    • Print the barrier grid (1 slit per 3 opaque bars) onto clear transparency film.

    • Verification: Slide the sheet laterally. If ghosts of Frame 3 bleed through during Frame 1, the black bars lack sufficient optical density, or the transparent slit exceeds the nominal 0.5 mm kerf/print stroke.

Texas A&M Fightin' Texas Aggie Band Animation Project

The Aggie Band is renowned for military-style precision drills, countermarches, and complex crisscross pass-throughs executed at a crisp 102 to 120 beats per minute. This project captures the iconic intersecting block movement or the step-by-step formation shift of the crisscross drill.

  • The Visual Subject: A 6-frame looping countermarch sequence showing two interlocking columns of marching cadets stepping cleanly through one another at 90-degree angles.

  • Frame Count (N): 6 frames (matches standard military cadences where complete strides resolve in 6 or 12 counts).

  • Physical Dimensions:

    • Slit width (w): 0.35 mm (clear aperture).

    • Opaque bar width (B): (6 - 1) x 0.35 mm = 1.75 mm.

    • Grid Pitch (P): 6 x 0.35 mm = 2.10 mm.

    • Target Medium: Cell-cast PMMA / acrylic top plate with laser-etched black-infilled bars or high-resolution vector film, laid flush over a white composite plate.

  • Animation Sequence Formulation:

    • Frame 1 (Count 1): Two orthogonal squad columns (rendered as sharp silhouettes of cadets with instruments at carry) approach the central intersection matrix with leading left boots forward.

    • Frames 2–3 (Counts 2–3): Columns enter the shared grid; individual silhouettes interleave corner-to-corner without overlapping.

    • Frame 4 (Count 4 - Zenith): The exact crisscross moment: every second marcher passes through the midpoint gap of the intersecting file.

    • Frames 5–6 (Counts 5–6): Columns clear the intersection zone and reform parallel files on the departure vectors, ready to loop back to Frame 1.

  • Design & Alignment Considerations:

    • Silhouette Density: Render marchers in pure vector black #000000 with uniform line weights. Keep negative space between marching figures equal to or larger than 2 x w (0.70 mm) to prevent optical clustering.

    • Parallax Mitigation: At N = 6, viewing angle tolerance drops. The barrier sheet must maintain direct contact (<= 0.2 mm gap) with the composite base sheet using side-mounted linear rails, ensuring translation stays constrained strictly along the X-axis with zero yaw.

Step-by-Step Implementation Matrix

MetricProject 1: "Hello World" Pulsing RingProject 2: Texas A&M Drill Crisscross
Frame Count (N)4 frames6 frames
Aperture Slit (w)0.50 mm (1.42 pt)0.35 mm (0.99 pt)
Mask Bar Width (B)1.50 mm (4.25 pt)1.75 mm (4.96 pt)
Spatial Period (P)2.00 mm2.10 mm
Optical Duty Cycle25.0% transparent16.7% transparent
Contrast TargetStandard inkjet transparencyHigh optical density (D_max >= 3.0) or UV etched
Motion Stroke2 mm manual slide2.1 mm constrained track

Immediate Next Steps

  1. Vector Preparation: Create the 4 and 6 frames in Illustrator, Inkscape, or code as pure 1-bit monochrome artboard sequences.

  2. Columnar Interleave: Export all frames at matching scale (e.g., 600 DPI), slice into w-width vertical spans, and composite sequentially into repeating sets.

  3. Registration Frame: Add top and bottom alignment ticks matching pitch P outside the active image area to align the slit sheet against the interleaved base before securing the guide rails.


Provide web application requirements to take an input image and create a N-segment animation from that image, final result viewable on screen.

Here are the functional and architectural requirements for a client-side web application that transforms a single input image or frame sequence into an N-segment barrier-grid (scanimation) asset, with a live interactive on-screen simulation.

1. Functional Requirements

1.1 Asset Input & Frame Generation Engine

  • Single-Image Decomposition:

    • Support loading static raster formats (PNG, JPEG, WebP) and vector graphics (SVG).

    • Built-in transformation generator to synthesize N distinct motion phases from a single base silhouette:

      • Planar Translation: Configurable directional vectors (Delta x, Delta y) for marching, sliding, or cyclical loops.

      • Affine Rotation: Angular displacement (Delta theta) about a configurable pivot point.

      • Scale / Pulse: Radial dilation and contraction (Delta s) for pulsing/expanding effects.

      • Bézier / Mesh Deformation: Parametric sinusoidal wave modulation across vertices/pixels.

  • Direct Multi-Frame Ingestion:

    • Accept animated GIF files (extract individual frame delays and canvas layers automatically).

    • Accept multi-file image sequences (frame_01.png through frame_0N.png).

1.2 Interleaving Core (The Synthesizer)

  • Configurable Parameters:

    • Frame Count (N): Integer range 3 <= N <= 12 (default: 6).

    • Slit Width (w): Pixel-space aperture width 1 <= w <= 10 px (default: 2 px to 4 px for standard DPI displays).

    • Mask Bar Width (B): Enforced automatically as B = (N - 1) x w.

    • Pitch (P): Calculated as P = N x w.

    • Orientation: Vertical slits (horizontal translation) or Horizontal slits (vertical translation).

  • Columnar Interleaving Algorithm:

    • Render frames onto hidden off-screen OffscreenCanvas contexts normalized to uniform dimensions (W x H).

    • Interleave pixel columns into the composite plate such that column index x samples from frame k = lfloor (x / w) rfloor pmod N.

1.3 Interactive On-Screen Simulator

  • Composite Canvas Stack:

    • Base Layer (canvas-base): Displays the static interleaved composite plate.

    • Barrier Layer (canvas-slit): Renders the transparent/opaque grating overlay on an absolute-positioned canvas above the base.

  • Playback & Manipulation Controls:

    • Auto-Play Engine: requestAnimationFrame loop driving the barrier layer's lateral offset from 0 to P px at user-defined FPS (e.g., 6–24 FPS).

    • Manual Scrubber / Drag: Pointer and touch drag events on the top sheet allowing manual scrubbing through the motion cycle.

    • Speed Multiplier: 0.25x to 2.0x playback speed slider.

    • Slit Transparency Toggle: Visual toggle to make the black bars translucent (e.g., 50% alpha) to reveal how the underlying composite slices line up under the aperture.

1.4 Export Pipeline

  • Digital Outputs:

    • Composite Base Plate: Full-resolution PNG (image/png) of the sliced image.

    • Slit Mask Sheet: Full-resolution 1-bit transparent PNG or SVG vector of the barrier grid, ready for printing onto clear acetate or transparency film.

    • Print-Ready Calibration Marks: Option to burn top/bottom tick marks spaced exactly at pitch P outside the canvas margins to facilitate manual alignment after printing.

2. Technical Stack & Architecture

LayerRecommended TechnologyTechnical Justification
Markup & UI FrameworkHTML5 + Tailwind CSSZero build-step needed; enables clean single-file distribution and responsive utility layouts.
Rendering EngineNative HTML5 Canvas APILow-overhead pixel manipulation via CanvasRenderingContext2D and ImageData arrays without WebGL setup overhead.
GIF Parsingomggif or gifuct-js (via CDN)Lightweight client-side GIF deconstruction into individual frame arrays and disposal handling.
Offscreen ProcessingOffscreenCanvas & Web WorkersPrevents UI thread blocking during high-resolution slicing across multi-megapixel uploads.
Vector ExportNative SVG String GenerationGenerates mathematically lossless vector slit files for laser cutting or vector plotters.

3. Core Interleaving Mathematical Logic

/**
 * Interleaves N frames into a single composite ImageData buffer.
 * @param {ImageData[]} frames - Array of ImageData objects of equal dimensions (W, H).
 * @param {number} N - Total frame count.
 * @param {number} w - Slit width in pixels.
 * @returns {ImageData} - The composite barrier-grid plate.
 */
function interleaveFrames(frames, N, w) {
  const width = frames[0].width;
  const height = frames[0].height;
  const output = new ImageData(width, height);
  const outData = output.data;

  for (let x = 0; x < width; x++) {
    // Determine active frame index for column x
    const frameIndex = Math.floor(x / w) % N;
    const srcData = frames[frameIndex].data;

    for (let y = 0; y < height; y++) {
      const idx = (y * width + x) * 4;
      outData[idx]     = srcData[idx];     // R
      outData[idx + 1] = srcData[idx + 1]; // G
      outData[idx + 2] = srcData[idx + 2]; // B
      outData[idx + 3] = srcData[idx + 3]; // A
    }
  }
  return output;
}

4. User Interface Wireframe & Component Layout

+---------------------------------------------------------------------------------+
|  BARRIER-GRID ANIMATION LAB                                                     |
+---------------------------------------------------------------------------------+
| [Controls & Parameters]               | [Interactive Viewport]                  |
|                                       |                                         |
| 1. Input Source:                      |   +---------------------------------+   |
|    [ Upload Image / GIF ]             |   |                                 |   |
|                                       |   |   Base Canvas                   |   |
| 2. Decomposition Mode:                |   |   + Slit Grating Overlay        |   |
|    (o) Translate  ( ) Rotate  ( ) GIF |   |   (Draggable horizontally)      |   |
|                                       |   |                                 |   |
| 3. Geometric Parameters:              |   +---------------------------------+   |
|    Frame Count (N): [ 6 ] <----o----> |                                         |
|    Slit Width (w):  [ 3 ] px          | [ Playback Transport ]                  |
|    Bar Width (B):   15 px (calculated)| [ Play / Pause ]  [ Reset ]             |
|    Pitch (P):       18 px             | Scrub: |==========O================|    |
|                                       | Speed: [ 1.0x v ]  Overlay Alpha: [90%] |
| 4. Export Actions:                    |                                         |
|    [ Download Base Plate (PNG) ]      | Registration Marks: [x] Include Ticks   |
|    [ Download Slit Sheet (SVG/PNG) ]  |                                         |
+---------------------------------------------------------------------------------+

5. Implementation Verification & Test Checklist

  • [ ] Sub-pixel Anti-Aliasing Artifacts: Verify that slit widths snap strictly to integer device pixels (Math.floor(x / w)) to prevent blurry seam blending between adjacent frame strips.

  • [ ] Memory Ceiling: Cap max preview canvas resolution to 2048 x 2048 px in the DOM while retaining full resolution on explicit download.

  • [ ] Phase Continuity: Validate that translating the simulated barrier grid by exactly P = N x w pixels causes frame N to cleanly roll over into frame 1 without jump or stutter.

  • [ ] Zero-Flicker Drag: Ensure pointer events use setPointerCapture and translate the barrier sheet using CSS hardware-accelerated transforms (transform: translateX(...)) rather than re-rendering canvas buffers on each drag tick.


Summarize these requirements, assuming the AI-coder will know the math

Core Application Overview

A client-side web application (HTML5, Tailwind CSS, vanilla JavaScript) that ingests an image, decomposes it into N animated frames, slices and interleaves them into a composite base plate, and renders a live, draggable barrier-grid overlay on screen to simulate optical animation.

Key Requirements Summary

1. Input & Motion Synthesis

  • Upload: Accept raster images (PNG, JPEG, WebP), vector assets (SVG), and multi-frame GIFs.

  • Procedural Frame Generator (Single Image): If given a static silhouette, generate N sequential frames using parametric transforms:

    • Linear translation / march cycle (Delta x, Delta y)

    • Angular rotation (Delta theta)

    • Radial pulse / scale (Delta s)

  • Direct Multi-Frame Mode: Parse native frames if an animated GIF or multiple files are supplied.

2. Processing & Canvas Architecture

  • Configurable Controls:

    • Segment / frame count N (3 <= N <= 12, default: 6).

    • Slit width w in pixels (1 <= w <= 10 px).

    • Auto-derive bar width B = (N - 1)w and spatial period P = Nw.

  • Rendering Strategy:

    • Render frames off-screen on hidden OffscreenCanvas contexts.

    • Construct a single interleaved composite canvas via column-wise pixel mapping (k = lfloor (x / w) rfloor pmod N).

    • Snap slit and column boundaries strictly to integer pixel coordinates to avoid anti-aliasing seam artifacts.

3. Interactive Screen Simulation

  • Two-Layer Viewport Stack:

    • Bottom layer: Static canvas displaying the interleaved composite plate.

    • Top layer: Absolutely positioned overlay canvas rendering the opaque/clear barrier grating.

  • Translation Mechanics:

    • Animate the top overlay horizontally using GPU-accelerated CSS transforms (transform: translateX(...)) rather than re-rasterizing on every frame.

    • Auto-Play: Loop offset from 0 to P px at user-defined FPS using requestAnimationFrame.

    • Interactive Scrubber & Drag: Support horizontal mouse/touch scrubbing with setPointerCapture.

    • Inspection Mode: Alpha-opacity toggle for the opaque bars to inspect alignment against the underlying slices.

4. Export Deliverables

  • Base Plate: Download interleaved image as high-resolution PNG.

  • Slit Overlay: Export matching 1-bit barrier grid as printable PNG and vector SVG.

  • Alignment Guides: Include optional registration tick marks spaced at pitch P along margins for physical alignment.

Verification Checklist for Implementation

  1. Integer Snapping: Verify lfloor (x / w) rfloor logic uses clean integer math without subpixel interpolation blur.

  2. Phase Rollover: Ensure translation by exactly P px creates an imperceptible visual loop back to Phase 0.

  3. DOM Performance: Restrict viewport canvas rendering to maximum display bounds (1080p) while maintaining full native resolution on explicit download.


Generate a single-file, production-ready HTML, CSS (Tailwind CDN), and vanilla JavaScript web application that implements this barrier-grid animation generator, complete with single-image deformation, live draggable overlay simulation, and PNG exports.

I will now generate the complete, production-ready Barrier-Grid (Scanimation / Kinegram) Generator and Simulator as a single, self-contained HTML application.



<!DOCTYPE html>
<html lang="en" class="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Scanimation Studio — Barrier-Grid Animation Generator & Simulator</title>
  <!-- Tailwind CSS CDN -->
  <script src="https://cdn.tailwindcss.com"></script>
  <script>
    tailwind.config = {
      darkMode: 'class',
      theme: {
        extend: {
          colors: {
            brand: {
              50: '#f0fdf4',
              500: '#10b981',
              600: '#059669',
              700: '#047857',
            },
            aggie: '#500000',
          },
          fontFamily: {
            mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
            sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
          }
        }
      }
    }
  </script>
  <style>
    /* Custom utility scrollbar & canvas viewport styling */
    ::-webkit-scrollbar {
      width: 6px;
      height: 6px;
    }
    ::-webkit-scrollbar-track {
      background: #090d16;
    }
    ::-webkit-scrollbar-thumb {
      background: #1e293b;
      border-radius: 9999px;
    }
    ::-webkit-scrollbar-thumb:hover {
      background: #334155;
    }
    .checkered-bg {
      background-color: #0f172a;
      background-image:
        linear-gradient(45deg, #1e293b 25%, transparent 25%),
        linear-gradient(-45deg, #1e293b 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, #1e293b 75%),
        linear-gradient(-45deg, transparent 75%, #1e293b 75%);
      background-size: 20px 20px;
      background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
    }
    .touch-none-all {
      touch-action: none;
      user-select: none;
      -webkit-user-select: none;
    }
  </style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col font-sans antialiased overflow-x-hidden select-none">

  <!-- Top Navigation Bar -->
  <header class="border-b border-slate-800 bg-slate-900/80 backdrop-blur-md px-6 py-3.5 flex items-center justify-between z-30 sticky top-0">
    <div class="flex items-center gap-3">
      <div class="w-9 h-9 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-600 flex items-center justify-center shadow-lg shadow-emerald-500/20">
        <svg class="w-5 h-5 text-slate-950" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"/>
        </svg>
      </div>
      <div>
        <h1 class="text-base font-bold tracking-tight text-white flex items-center gap-2">
          Scanimation Studio
          <span class="text-[10px] font-mono px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 uppercase tracking-widest">PRO v2.0</span>
        </h1>
        <p class="text-xs text-slate-400">Barrier-Grid Optical Synthesizer & Physics Simulator</p>
      </div>
    </div>

    <!-- Quick stats header tags -->
    <div class="hidden md:flex items-center gap-4 text-xs font-mono text-slate-300">
      <div class="bg-slate-800/80 px-3 py-1.5 rounded-md border border-slate-700/60 flex items-center gap-2">
        <span class="text-slate-400">Pitch (P):</span>
        <span id="badge-pitch" class="text-emerald-400 font-bold">18px</span>
      </div>
      <div class="bg-slate-800/80 px-3 py-1.5 rounded-md border border-slate-700/60 flex items-center gap-2">
        <span class="text-slate-400">Duty:</span>
        <span id="badge-duty" class="text-teal-400 font-bold">16.7%</span>
      </div>
      <div class="bg-slate-800/80 px-3 py-1.5 rounded-md border border-slate-700/60 flex items-center gap-2">
        <span class="text-slate-400">Bar (B):</span>
        <span id="badge-bar" class="text-indigo-400 font-bold">15px</span>
      </div>
    </div>
  </header>

  <!-- Workspace Grid Layout -->
  <div class="flex-1 grid grid-cols-1 lg:grid-cols-12 min-h-0">
   
    <!-- Controls Sidebar (Left Column: 4 cols) -->
    <aside class="lg:col-span-4 border-r border-slate-800 bg-slate-900/60 p-5 overflow-y-auto space-y-6 max-h-[calc(100vh-61px)]">
     
      <!-- 1. Source Image & Presets Section -->
      <section class="space-y-3">
        <div class="flex items-center justify-between">
          <label class="text-xs font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2">
            <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"/></svg>
            1. Graphic Source
          </label>
          <span class="text-[11px] text-slate-500 font-mono">Silhouette Mode</span>
        </div>

        <!-- Presets quick picker -->
        <div class="grid grid-cols-2 gap-2">
          <button id="preset-aggie" class="preset-btn px-3 py-2 rounded-lg text-xs font-medium bg-red-950/40 hover:bg-red-900/50 text-red-200 border border-red-800/50 transition-all flex items-center gap-2 justify-center">
            <span class="w-2 h-2 rounded-full bg-red-500"></span>
            Aggie Band Marcher
          </button>
          <button id="preset-heart" class="preset-btn px-3 py-2 rounded-lg text-xs font-medium bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition-all flex items-center gap-2 justify-center">
            <span class="w-2 h-2 rounded-full bg-rose-500"></span>
            Pulsing Heart
          </button>
          <button id="preset-gear" class="preset-btn px-3 py-2 rounded-lg text-xs font-medium bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition-all flex items-center gap-2 justify-center">
            <span class="w-2 h-2 rounded-full bg-amber-500"></span>
            Rotating Gear
          </button>
          <button id="preset-runner" class="preset-btn px-3 py-2 rounded-lg text-xs font-medium bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition-all flex items-center gap-2 justify-center">
            <span class="w-2 h-2 rounded-full bg-emerald-500"></span>
            Stride Runner
          </button>
        </div>

        <!-- Custom Upload button -->
        <div class="relative group">
          <input type="file" id="image-upload" accept="image/*" class="absolute inset-0 opacity-0 cursor-pointer w-full h-full z-10">
          <div class="w-full border border-dashed border-slate-700 rounded-lg p-3 text-center bg-slate-800/40 group-hover:bg-slate-800/80 group-hover:border-slate-500 transition-all">
            <p class="text-xs text-slate-300 font-medium">Drop custom silhouette or <span class="text-emerald-400 underline">Browse File</span></p>
            <p class="text-[10px] text-slate-500 mt-0.5">PNG, SVG, or high-contrast JPG</p>
          </div>
        </div>
      </section>

      <!-- 2. Motion Synthesis Mode -->
      <section class="space-y-3 pt-3 border-t border-slate-800">
        <div class="flex items-center justify-between">
          <label class="text-xs font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2">
            <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"/></svg>
            2. Motion Generator
          </label>
        </div>

        <div class="grid grid-cols-4 gap-1.5 p-1 bg-slate-950/70 rounded-lg border border-slate-800 text-xs">
          <button data-motion="march" class="motion-btn py-1.5 rounded font-medium text-slate-300 transition-all hover:bg-slate-800 active-motion bg-slate-800 text-emerald-400">March</button>
          <button data-motion="rotate" class="motion-btn py-1.5 rounded font-medium text-slate-400 transition-all hover:bg-slate-800">Spin</button>
          <button data-motion="pulse" class="motion-btn py-1.5 rounded font-medium text-slate-400 transition-all hover:bg-slate-800">Pulse</button>
          <button data-motion="wave" class="motion-btn py-1.5 rounded font-medium text-slate-400 transition-all hover:bg-slate-800">Wave</button>
        </div>

        <!-- Motion magnitude parameter -->
        <div class="space-y-1.5">
          <div class="flex justify-between text-xs">
            <span class="text-slate-400">Motion Magnitude</span>
            <span id="motion-mag-val" class="font-mono text-emerald-400 font-semibold">18 px</span>
          </div>
          <input type="range" id="motion-mag" min="4" max="60" value="18" class="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-emerald-500">
        </div>
      </section>

      <!-- 3. Interleaving Geometric Parameters -->
      <section class="space-y-3 pt-3 border-t border-slate-800">
        <label class="text-xs font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2">
          <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"/></svg>
          3. Geometric Calibration
        </label>

        <!-- Frame Count N -->
        <div class="space-y-1.5">
          <div class="flex justify-between text-xs">
            <span class="text-slate-300">Phase Frame Count (N)</span>
            <span id="n-val" class="font-mono text-emerald-400 font-bold">6 frames</span>
          </div>
          <input type="range" id="param-n" min="3" max="10" value="6" step="1" class="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-emerald-500">
          <div class="flex justify-between text-[10px] text-slate-500 font-mono">
            <span>3 (Staccato)</span>
            <span>6 (Optimal)</span>
            <span>10 (Fluid/Dim)</span>
          </div>
        </div>

        <!-- Slit Width w -->
        <div class="space-y-1.5">
          <div class="flex justify-between text-xs">
            <span class="text-slate-300">Slit Width (w)</span>
            <span id="w-val" class="font-mono text-emerald-400 font-bold">3 px</span>
          </div>
          <input type="range" id="param-w" min="1" max="8" value="3" step="1" class="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-emerald-500">
          <div class="flex justify-between text-[10px] text-slate-500 font-mono">
            <span>1 px (Hi-DPI)</span>
            <span>3 px (Standard)</span>
            <span>8 px (Macro)</span>
          </div>
        </div>

        <!-- Invert Silhouette checkbox -->
        <div class="flex items-center justify-between pt-1">
          <span class="text-xs text-slate-400">Dark Silhouette on White</span>
          <label class="relative inline-flex items-center cursor-pointer">
            <input type="checkbox" id="invert-colors" checked class="sr-only peer">
            <div class="w-9 h-5 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-emerald-600"></div>
          </label>
        </div>
      </section>

      <!-- 4. Simulator Inspection & Guide Controls -->
      <section class="space-y-3 pt-3 border-t border-slate-800">
        <label class="text-xs font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2">
          <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/></svg>
          4. Inspection & Alignment
        </label>

        <!-- Slit Bar Opacity -->
        <div class="space-y-1.5">
          <div class="flex justify-between text-xs">
            <span class="text-slate-300">Mask Bar Opacity (Inspection)</span>
            <span id="opacity-val" class="font-mono text-emerald-400 font-bold">100%</span>
          </div>
          <input type="range" id="param-opacity" min="20" max="100" value="100" step="5" class="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-emerald-500">
          <div class="text-[10px] text-slate-500">Lower opacity to inspect interleaved frame slices underneath.</div>
        </div>

        <div class="flex items-center justify-between pt-1">
          <span class="text-xs text-slate-400">Registration Ticks</span>
          <label class="relative inline-flex items-center cursor-pointer">
            <input type="checkbox" id="show-ticks" checked class="sr-only peer">
            <div class="w-9 h-5 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-emerald-600"></div>
          </label>
        </div>
      </section>

      <!-- 5. Export Suite -->
      <section class="space-y-3 pt-3 border-t border-slate-800">
        <label class="text-xs font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2">
          <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
          5. Export Fabrication Plates
        </label>

        <div class="space-y-2">
          <button id="btn-export-base" class="w-full py-2.5 px-3 rounded-lg text-xs font-semibold bg-emerald-600 hover:bg-emerald-500 text-slate-950 transition-all flex items-center justify-center gap-2 shadow-lg shadow-emerald-950/40">
            <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
            Export Interleaved Base (PNG)
          </button>
         
          <div class="grid grid-cols-2 gap-2">
            <button id="btn-export-slit-png" class="py-2 px-3 rounded-lg text-xs font-medium bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition-all flex items-center justify-center gap-1.5">
              <span>Slit Sheet (PNG)</span>
            </button>
            <button id="btn-export-slit-svg" class="py-2 px-3 rounded-lg text-xs font-medium bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition-all flex items-center justify-center gap-1.5">
              <span>Vector Slits (SVG)</span>
            </button>
          </div>
        </div>
      </section>

    </aside>

    <!-- Main Interactive Viewport (Right Column: 8 cols) -->
    <main class="lg:col-span-8 flex flex-col bg-slate-950 relative overflow-hidden">
     
      <!-- Viewport Header Toolbar -->
      <div class="px-6 py-2.5 border-b border-slate-800/80 bg-slate-900/40 flex flex-wrap items-center justify-between gap-3 text-xs">
        <div class="flex items-center gap-3">
          <span class="inline-flex items-center gap-1.5 text-slate-400 font-mono">
            <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
            Simulator View
          </span>
          <span class="text-slate-600">|</span>
          <span class="text-slate-400 hidden sm:inline">Drag mouse/touch across canvas to slide barrier sheet</span>
        </div>

        <div class="flex items-center gap-2">
          <!-- Reset Offset -->
          <button id="btn-reset-pos" class="px-2.5 py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300 transition-all font-mono text-[11px] border border-slate-700">
            Reset (0px)
          </button>
        </div>
      </div>

      <!-- Main Stage Area -->
      <div class="flex-1 flex items-center justify-center p-4 md:p-8 checkered-bg overflow-auto relative select-none">
       
        <!-- Interactive Stage Enclosure -->
        <div id="stage-card" class="relative bg-white rounded-xl shadow-2xl p-6 select-none border border-slate-300/20 max-w-full">
         
          <!-- Top Registration Ticks -->
          <div id="top-ticks" class="h-4 w-full mb-1 flex items-end overflow-hidden border-b border-slate-400/40"></div>

          <!-- Canvas Interactive Stack -->
          <div id="canvas-container" class="relative overflow-hidden rounded shadow-inner touch-none-all cursor-ew-resize mx-auto" style="width: 540px; height: 380px;">
           
            <!-- Bottom Layer: Interleaved Base Plate -->
            <canvas id="base-canvas" width="540" height="380" class="absolute inset-0 block bg-white"></canvas>

            <!-- Top Layer: Draggable Slit Overlay Sheet -->
            <canvas id="slit-canvas" width="600" height="380" class="absolute top-0 left-0 pointer-events-none will-change-transform"></canvas>

            <!-- Drag indicator HUD overlay (fades on drag) -->
            <div id="drag-hint" class="absolute inset-0 pointer-events-none flex items-center justify-center bg-black/20 backdrop-blur-[1px] transition-opacity duration-300 opacity-0">
              <div class="bg-slate-900/90 text-white text-xs px-3 py-1.5 rounded-full border border-slate-700 flex items-center gap-2 shadow-xl">
                <svg class="w-4 h-4 text-emerald-400 animate-bounce" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"/></svg>
                <span>Drag horizontally to animate</span>
              </div>
            </div>

          </div>

          <!-- Bottom Registration Ticks -->
          <div id="bottom-ticks" class="h-4 w-full mt-1 flex items-start overflow-hidden border-t border-slate-400/40"></div>

          <div class="mt-2 flex justify-between items-center text-[10px] text-slate-500 font-mono">
            <span>Physical Simulation: 540 x 380 px</span>
            <span id="offset-readout">Offset: 0.00 px (Phase 0/6)</span>
          </div>
        </div>

      </div>

      <!-- Playback Transport Bar (Bottom) -->
      <div class="border-t border-slate-800 bg-slate-900/90 p-4 px-6 flex flex-wrap items-center justify-between gap-4 z-20">
       
        <!-- Play / Pause & Step Controls -->
        <div class="flex items-center gap-2">
          <button id="btn-play" class="w-10 h-10 rounded-full bg-emerald-500 hover:bg-emerald-400 text-slate-950 flex items-center justify-center font-bold shadow-lg shadow-emerald-500/20 transition-all">
            <svg id="play-icon" class="w-5 h-5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
            <svg id="pause-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
          </button>
         
          <button id="btn-step-prev" class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition-all border border-slate-700">
            <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg>
          </button>
          <button id="btn-step-next" class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition-all border border-slate-700">
            <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/></svg>
          </button>
        </div>

        <!-- Scrub Range Slider -->
        <div class="flex-1 max-w-md mx-2 flex items-center gap-3">
          <span class="text-xs font-mono text-slate-400">Scrub:</span>
          <input type="range" id="scrubber" min="0" max="18" step="0.1" value="0" class="flex-1 h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-emerald-500">
        </div>

        <!-- Playback Speed (FPS / Hertz) -->
        <div class="flex items-center gap-3">
          <span class="text-xs text-slate-400">Rate:</span>
          <select id="playback-speed" class="bg-slate-800 text-slate-200 text-xs rounded-lg px-2.5 py-1.5 border border-slate-700 font-mono focus:outline-none focus:border-emerald-500">
            <option value="0.5">0.5 Hz (Slow)</option>
            <option value="1" selected>1.0 Hz (Standard)</option>
            <option value="2">2.0 Hz (Fast Aggie 120bpm)</option>
            <option value="4">4.0 Hz (Hyper)</option>
          </select>
        </div>

      </div>

    </main>

  </div>

  <!-- Notification Toast Modal Container -->
  <div id="toast" class="fixed bottom-6 right-6 bg-slate-900 border border-emerald-500/30 text-white px-4 py-3 rounded-xl shadow-2xl flex items-center gap-3 transition-all duration-300 opacity-0 pointer-events-none translate-y-3 z-50">
    <div class="w-7 h-7 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
      <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>
    </div>
    <span id="toast-msg" class="text-xs font-medium">Export completed successfully.</span>
  </div>

  <!-- Main Application Logic Script -->
  <script>
    /* ==========================================================================
       SCANIMATION STUDIO CORE ENGINE
       Single-File Production Implementation
       ========================================================================== */

    // --- State Management ---
    const state = {
      // Geometric parameters
      N: 6,              // Frame count (phases)
      w: 3,              // Slit width in pixels
      B: 15,             // Opaque bar width = (N - 1) * w
      P: 18,             // Spatial period (pitch) = N * w
     
      // Canvas dimensions
      width: 540,
      height: 380,
     
      // Motion & Synthesis
      motionType: 'march', // 'march', 'rotate', 'pulse', 'wave'
      motionMagnitude: 18,
      invertColors: true,
     
      // Simulation & Playback
      slitOffset: 0,       // Current horizontal translation in px [0 .. P]
      isPlaying: true,
      playbackFrequency: 1.0, // Full cycles per second (Hz)
      maskOpacity: 1.0,
      showTicks: true,
     
      // Source & Buffers
      sourceImage: null,   // HTMLImageElement or offscreen canvas
      isDragging: false,
      dragStartX: 0,
      dragStartOffset: 0,
    };

    // --- DOM Elements Cache ---
    const dom = {
      baseCanvas: document.getElementById('base-canvas'),
      slitCanvas: document.getElementById('slit-canvas'),
      canvasContainer: document.getElementById('canvas-container'),
      paramN: document.getElementById('param-n'),
      paramW: document.getElementById('param-w'),
      nVal: document.getElementById('n-val'),
      wVal: document.getElementById('w-val'),
      badgePitch: document.getElementById('badge-pitch'),
      badgeDuty: document.getElementById('badge-duty'),
      badgeBar: document.getElementById('badge-bar'),
      motionMag: document.getElementById('motion-mag'),
      motionMagVal: document.getElementById('motion-mag-val'),
      motionBtns: document.querySelectorAll('.motion-btn'),
      paramOpacity: document.getElementById('param-opacity'),
      opacityVal: document.getElementById('opacity-val'),
      showTicks: document.getElementById('show-ticks'),
      topTicks: document.getElementById('top-ticks'),
      bottomTicks: document.getElementById('bottom-ticks'),
      invertColors: document.getElementById('invert-colors'),
      btnPlay: document.getElementById('btn-play'),
      playIcon: document.getElementById('play-icon'),
      pauseIcon: document.getElementById('pause-icon'),
      scrubber: document.getElementById('scrubber'),
      playbackSpeed: document.getElementById('playback-speed'),
      btnStepPrev: document.getElementById('btn-step-prev'),
      btnStepNext: document.getElementById('btn-step-next'),
      btnResetPos: document.getElementById('btn-reset-pos'),
      offsetReadout: document.getElementById('offset-readout'),
      imageUpload: document.getElementById('image-upload'),
      btnExportBase: document.getElementById('btn-export-base'),
      btnExportSlitPng: document.getElementById('btn-export-slit-png'),
      btnExportSlitSvg: document.getElementById('btn-export-slit-svg'),
      toast: document.getElementById('toast'),
      toastMsg: document.getElementById('toast-msg'),
      presetAggie: document.getElementById('preset-aggie'),
      presetHeart: document.getElementById('preset-heart'),
      presetGear: document.getElementById('preset-gear'),
      presetRunner: document.getElementById('preset-runner'),
    };

    const baseCtx = dom.baseCanvas.getContext('2d', { willReadFrequently: true });
    const slitCtx = dom.slitCanvas.getContext('2d');

    // --- Procedural Preset Vector Generators ---
    // Generates clean high-contrast silhouette canvases without external asset dependencies

    function createAggieBandMarcherCanvas() {
      // Texas A&M Fightin' Texas Aggie Band Marcher with Trumpet silhouette
      const cvs = document.createElement('canvas');
      cvs.width = state.width;
      cvs.height = state.height;
      const ctx = cvs.getContext('2d');
     
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, cvs.width, cvs.height);
     
      ctx.fillStyle = '#000000';
      ctx.save();
      ctx.translate(cvs.width / 2, cvs.height / 2 + 10);
     
      // Marcher silhouette coordinates
      // Aggie Cadet Cover (shako / peaked military hat)
      ctx.beginPath();
      ctx.rect(-16, -115, 32, 28);
      ctx.fill();
      // Hat Visor
      ctx.beginPath();
      ctx.moveTo(14, -92);
      ctx.lineTo(26, -92);
      ctx.lineTo(16, -88);
      ctx.fill();
     
      // Head silhouette
      ctx.beginPath();
      ctx.arc(0, -78, 14, 0, Math.PI * 2);
      ctx.fill();
     
      // Torso / Midnight Military Tunic & Cross-belts
      ctx.beginPath();
      ctx.moveTo(-18, -64);
      ctx.lineTo(18, -64);
      ctx.lineTo(15, 10);
      ctx.lineTo(-15, 10);
      ctx.closePath();
      ctx.fill();

      // Horizontal Trumpet held at attention
      ctx.fillRect(4, -62, 75, 7);
      // Trumpet bell flare
      ctx.beginPath();
      ctx.moveTo(79, -68);
      ctx.lineTo(92, -76);
      ctx.lineTo(92, -45);
      ctx.lineTo(79, -53);
      ctx.closePath();
      ctx.fill();

      // Arms angled forward holding instrument
      ctx.lineWidth = 10;
      ctx.strokeStyle = '#000000';
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(-6, -58);
      ctx.lineTo(15, -45);
      ctx.lineTo(45, -58);
      ctx.stroke();

      // Marching Stride Legs (Crisp high-step position)
      // Left lead leg lifted high
      ctx.lineWidth = 14;
      ctx.beginPath();
      ctx.moveTo(-8, 8);
      ctx.lineTo(16, 42);
      ctx.lineTo(24, 88); // Boot
      ctx.stroke();

      // Right back leg pushing off
      ctx.beginPath();
      ctx.moveTo(4, 8);
      ctx.lineTo(-24, 46);
      ctx.lineTo(-32, 85);
      ctx.stroke();

      // Military Boot Soles
      ctx.fillRect(16, 85, 22, 10);
      ctx.fillRect(-42, 82, 18, 9);

      // Aggie Chevron accents on sleeve
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 2.5;
      ctx.beginPath();
      ctx.moveTo(8, -48);
      ctx.lineTo(15, -44);
      ctx.lineTo(22, -48);
      ctx.stroke();

      ctx.restore();
      return cvs;
    }

    function createHeartCanvas() {
      const cvs = document.createElement('canvas');
      cvs.width = state.width;
      cvs.height = state.height;
      const ctx = cvs.getContext('2d');
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, cvs.width, cvs.height);

      ctx.fillStyle = '#000000';
      ctx.save();
      ctx.translate(cvs.width / 2, cvs.height / 2 - 15);
     
      ctx.beginPath();
      const topY = -50;
      ctx.moveTo(0, topY + 30);
      ctx.bezierCurveTo(0, topY, -60, topY - 20, -70, topY + 35);
      ctx.bezierCurveTo(-80, topY + 80, -20, topY + 120, 0, topY + 150);
      ctx.bezierCurveTo(20, topY + 120, 80, topY + 80, 70, topY + 35);
      ctx.bezierCurveTo(60, topY - 20, 0, topY, 0, topY + 30);
      ctx.fill();

      // Concentric inner hollow ring
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 10;
      ctx.stroke();

      ctx.restore();
      return cvs;
    }

    function createGearCanvas() {
      const cvs = document.createElement('canvas');
      cvs.width = state.width;
      cvs.height = state.height;
      const ctx = cvs.getContext('2d');
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, cvs.width, cvs.height);

      ctx.fillStyle = '#000000';
      ctx.save();
      ctx.translate(cvs.width / 2, cvs.height / 2);
     
      const numTeeth = 8;
      const radiusOuter = 95;
      const radiusInner = 72;
      ctx.beginPath();
      for (let i = 0; i < numTeeth * 2; i++) {
        const angle = (i * Math.PI) / numTeeth;
        const r = (i % 2 === 0) ? radiusOuter : radiusInner;
        const x = Math.cos(angle) * r;
        const y = Math.sin(angle) * r;
        if (i === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.closePath();
      ctx.fill();

      // Cutout center
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(0, 0, 36, 0, Math.PI * 2);
      ctx.fill();

      // Keyway notch
      ctx.fillRect(-8, -45, 16, 20);

      ctx.restore();
      return cvs;
    }

    function createRunnerCanvas() {
      const cvs = document.createElement('canvas');
      cvs.width = state.width;
      cvs.height = state.height;
      const ctx = cvs.getContext('2d');
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, cvs.width, cvs.height);

      ctx.fillStyle = '#000000';
      ctx.save();
      ctx.translate(cvs.width / 2, cvs.height / 2 + 10);

      // Head
      ctx.beginPath();
      ctx.arc(20, -85, 18, 0, Math.PI * 2);
      ctx.fill();

      // Leaning Torso
      ctx.lineWidth = 20;
      ctx.strokeStyle = '#000000';
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(15, -68);
      ctx.lineTo(-8, 0);
      ctx.stroke();

      // Leading bent leg
      ctx.lineWidth = 14;
      ctx.beginPath();
      ctx.moveTo(-8, 0);
      ctx.lineTo(36, 25);
      ctx.lineTo(24, 75);
      ctx.stroke();

      // Trailing extended leg
      ctx.beginPath();
      ctx.moveTo(-8, 0);
      ctx.lineTo(-45, 20);
      ctx.lineTo(-75, 45);
      ctx.stroke();

      // Pumping Arms
      ctx.lineWidth = 11;
      ctx.beginPath();
      ctx.moveTo(10, -50);
      ctx.lineTo(-30, -35);
      ctx.lineTo(-25, 0);
      ctx.stroke();

      ctx.beginPath();
      ctx.moveTo(10, -50);
      ctx.lineTo(48, -40);
      ctx.lineTo(60, -70);
      ctx.stroke();

      ctx.restore();
      return cvs;
    }

    // --- Math & Geometric Calibration Helpers ---
    function updateCalculatedParameters() {
      state.B = (state.N - 1) * state.w;
      state.P = state.N * state.w;
      const duty = ((1 / state.N) * 100).toFixed(1);

      // Update UI Indicators
      dom.nVal.textContent = `${state.N} frames`;
      dom.wVal.textContent = `${state.w} px`;
      dom.badgePitch.textContent = `${state.P} px`;
      dom.badgeBar.textContent = `${state.B} px`;
      dom.badgeDuty.textContent = `${duty}%`;

      // Update Scrubber Max
      dom.scrubber.max = state.P;
      if (state.slitOffset > state.P) {
        state.slitOffset = 0;
      }
      dom.scrubber.value = state.slitOffset;

      renderRegistrationTicks();
      renderSlitOverlaySheet();
      synthesizeInterleavedBase();
    }

    // --- Frame Deformation & Transform Synthesis ---
    // Takes the base graphic and transforms it into N unique animation phase canvases
    function generateFramePhases() {
      const frames = [];
      const { N, motionType, motionMagnitude } = state;
      const w = state.width;
      const h = state.height;

      for (let i = 0; i < N; i++) {
        const offCanvas = document.createElement('canvas');
        offCanvas.width = w;
        offCanvas.height = h;
        const ctx = offCanvas.getContext('2d');

        // Clean white background
        ctx.fillStyle = state.invertColors ? '#ffffff' : '#000000';
        ctx.fillRect(0, 0, w, h);

        const phaseRatio = i / N; // 0.0 to < 1.0
        const angle = phaseRatio * Math.PI * 2;

        ctx.save();
        ctx.translate(w / 2, h / 2);

        if (motionType === 'march') {
          // Linear continuous marching stride forward with slight cadence bob
          const dx = (phaseRatio - 0.5) * motionMagnitude * 2;
          const dy = -Math.abs(Math.sin(angle)) * (motionMagnitude * 0.4);
          ctx.translate(dx, dy);
        } else if (motionType === 'rotate') {
          // Pure continuous full cycle rotation
          ctx.rotate(angle);
        } else if (motionType === 'pulse') {
          // Radial dilation & contraction
          const scaleFactor = 1.0 + Math.sin(angle) * (motionMagnitude / 100);
          ctx.scale(scaleFactor, scaleFactor);
        } else if (motionType === 'wave') {
          // Sinusoidal vertical bounce and shearing
          const dy = Math.sin(angle) * motionMagnitude;
          ctx.translate(0, dy);
          ctx.rotate((Math.cos(angle) * motionMagnitude * Math.PI) / 360);
        }

        ctx.translate(-w / 2, -h / 2);

        // Draw source graphic
        if (state.sourceImage) {
          ctx.drawImage(state.sourceImage, 0, 0, w, h);
        }

        ctx.restore();

        // Invert to white silhouette if requested
        if (!state.invertColors) {
          const imgData = ctx.getImageData(0, 0, w, h);
          const d = imgData.data;
          for (let p = 0; p < d.length; p += 4) {
            d[p] = 255 - d[p];
            d[p + 1] = 255 - d[p + 1];
            d[p + 2] = 255 - d[p + 2];
          }
          ctx.putImageData(imgData, 0, 0);
        }

        frames.push(ctx.getImageData(0, 0, w, h));
      }
      return frames;
    }

    // --- Core Interleaving Synthesis Algorithm ---
    // Column x draws from frame phase: floor(x / w) % N
    function synthesizeInterleavedBase() {
      const frames = generateFramePhases();
      const w = state.width;
      const h = state.height;
      const N = state.N;
      const slitW = state.w;

      const composite = baseCtx.createImageData(w, h);
      const out = composite.data;

      for (let x = 0; x < w; x++) {
        // Precise integer floor mapping
        const frameIndex = Math.floor(x / slitW) % N;
        const src = frames[frameIndex].data;

        for (let y = 0; y < h; y++) {
          const idx = (y * w + x) * 4;
          out[idx]     = src[idx];
          out[idx + 1] = src[idx + 1];
          out[idx + 2] = src[idx + 2];
          out[idx + 3] = 255;
        }
      }

      baseCtx.putImageData(composite, 0, 0);
    }

    // --- Slit Overlay Sheet Generator ---
    // Generates the matching opaque/clear grating with width B opaque bars and width w transparent slits
    function renderSlitOverlaySheet() {
      const totalWidth = state.width + state.P * 2; // Extra width for smooth horizontal translation
      const h = state.height;
      dom.slitCanvas.width = totalWidth;
      dom.slitCanvas.height = h;

      slitCtx.clearRect(0, 0, totalWidth, h);

      // Draw the black bars and transparent slits
      // In barrier grid: slit = clear, bar = opaque
      slitCtx.fillStyle = `rgba(0, 0, 0, ${state.maskOpacity})`;

      for (let x = 0; x < totalWidth; x += state.P) {
        // Transparent slit from x to x + w
        // Opaque bar from x + w to x + P
        slitCtx.fillRect(x + state.w, 0, state.B, h);
      }
    }

    // --- Alignment Calibration Ticks ---
    function renderRegistrationTicks() {
      if (!state.showTicks) {
        dom.topTicks.style.display = 'none';
        dom.bottomTicks.style.display = 'none';
        return;
      }
      dom.topTicks.style.display = 'flex';
      dom.bottomTicks.style.display = 'flex';

      const pitch = state.P;
      const totalTicks = Math.floor(state.width / pitch);

      let ticksHtml = '';
      for (let i = 0; i <= totalTicks; i++) {
        const isMajor = i % 5 === 0;
        const tickH = isMajor ? 'h-3.5 bg-slate-900 w-[1.5px]' : 'h-2 bg-slate-400 w-[1px]';
        ticksHtml += `<div style="width: ${pitch}px;" class="flex items-end justify-start flex-shrink-0">
          <div class="${tickH}"></div>
        </div>`;
      }
      dom.topTicks.innerHTML = ticksHtml;
      dom.bottomTicks.innerHTML = ticksHtml;
    }

    // --- Simulation & CSS GPU Translation ---
    function applySlitPosition(pxOffset) {
      // Modulo wrap around spatial pitch P
      state.slitOffset = ((pxOffset % state.P) + state.P) % state.P;
     
      // Hardware-accelerated subpixel translation
      dom.slitCanvas.style.transform = `translate3d(${-state.slitOffset}px, 0, 0)`;

      // Update transport feedback
      dom.scrubber.value = state.slitOffset;
      const activePhase = Math.floor((state.slitOffset / state.w) % state.N);
      dom.offsetReadout.textContent = `Offset: ${state.slitOffset.toFixed(2)} px (Phase ${activePhase + 1}/${state.N})`;
    }

    // --- Animation Playback Loop ---
    let lastTimestamp = 0;
    function animationLoop(timestamp) {
      if (!lastTimestamp) lastTimestamp = timestamp;
      const deltaSec = (timestamp - lastTimestamp) / 1000;
      lastTimestamp = timestamp;

      if (state.isPlaying && !state.isDragging) {
        // Velocity: P pixels per complete cycle * frequency
        const step = state.P * state.playbackFrequency * deltaSec;
        applySlitPosition(state.slitOffset + step);
      }

      requestAnimationFrame(animationLoop);
    }

    // --- Toast Notification Feedback ---
    let toastTimer = null;
    function showToast(message) {
      dom.toastMsg.textContent = message;
      dom.toast.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-3');
      dom.toast.classList.add('opacity-100', 'translate-y-0');
      clearTimeout(toastTimer);
      toastTimer = setTimeout(() => {
        dom.toast.classList.add('opacity-0', 'pointer-events-none', 'translate-y-3');
        dom.toast.classList.remove('opacity-100', 'translate-y-0');
      }, 3200);
    }

    // --- Drag & Touch Interaction ---
    function initDragControls() {
      const el = dom.canvasContainer;

      el.addEventListener('pointerdown', (e) => {
        state.isDragging = true;
        state.dragStartX = e.clientX;
        state.dragStartOffset = state.slitOffset;
        el.setPointerCapture(e.pointerId);
        el.classList.add('cursor-grabbing');
        el.classList.remove('cursor-ew-resize');
      });

      el.addEventListener('pointermove', (e) => {
        if (!state.isDragging) return;
        const deltaX = e.clientX - state.dragStartX;
        // Inverse direction: dragging right moves overlay right
        applySlitPosition(state.dragStartOffset - deltaX);
      });

      const stopDrag = (e) => {
        if (!state.isDragging) return;
        state.isDragging = false;
        try { el.releasePointerCapture(e.pointerId); } catch (_) {}
        el.classList.remove('cursor-grabbing');
        el.classList.add('cursor-ew-resize');
      };

      el.addEventListener('pointerup', stopDrag);
      el.addEventListener('pointercancel', stopDrag);
    }

    // --- Export Generators ---
    function exportInterleavedBase() {
      // Create high-res download
      const link = document.createElement('a');
      link.download = `scanimation-base-N${state.N}-w${state.w}.png`;
      link.href = dom.baseCanvas.toDataURL('image/png');
      link.click();
      showToast(`Exported Interleaved Base Plate (${state.width}×${state.height} px)`);
    }

    function exportSlitPng() {
      // Export single-screen matching mask
      const cvs = document.createElement('canvas');
      cvs.width = state.width;
      cvs.height = state.height;
      const ctx = cvs.getContext('2d');
      ctx.fillStyle = '#000000';

      for (let x = 0; x < state.width; x += state.P) {
        ctx.fillRect(x + state.w, 0, state.B, state.height);
      }

      const link = document.createElement('a');
      link.download = `scanimation-slit-mask-N${state.N}-w${state.w}.png`;
      link.href = cvs.toDataURL('image/png');
      link.click();
      showToast('Exported 1-Bit Slit Transparency (PNG)');
    }

    function exportSlitSvg() {
      const w = state.width;
      const h = state.height;
      const p = state.P;
      const barW = state.B;
      const slitW = state.w;

      let rects = '';
      for (let x = 0; x < w; x += p) {
        rects += `  <rect x="${x + slitW}" y="0" width="${barW}" height="${h}" fill="#000000" />\n`;
      }

      const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}px" height="${h}px">
  <desc>Scanimation Barrier Grid: N=${state.N}, w=${slitW}px, B=${barW}px, Pitch=${p}px</desc>
  <!-- Transparent aperture slits of width ${slitW}px with opaque bars of width ${barW}px -->
  <g id="barrier-bars">
${rects}  </g>
</svg>`;

      const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.download = `scanimation-slit-grid-N${state.N}-w${slitW}.svg`;
      link.href = url;
      link.click();
      setTimeout(() => URL.revokeObjectURL(url), 5000);
      showToast('Exported Vector Slit Grid (SVG for Laser Cut / Vinyl)');
    }

    // --- Event Listeners Binding ---
    function initEventListeners() {
      // Parameter Sliders
      dom.paramN.addEventListener('input', (e) => {
        state.N = parseInt(e.target.value, 10);
        updateCalculatedParameters();
      });

      dom.paramW.addEventListener('input', (e) => {
        state.w = parseInt(e.target.value, 10);
        updateCalculatedParameters();
      });

      dom.motionMag.addEventListener('input', (e) => {
        state.motionMagnitude = parseInt(e.target.value, 10);
        dom.motionMagVal.textContent = `${state.motionMagnitude} px`;
        synthesizeInterleavedBase();
      });

      dom.paramOpacity.addEventListener('input', (e) => {
        state.maskOpacity = parseInt(e.target.value, 10) / 100;
        dom.opacityVal.textContent = `${e.target.value}%`;
        renderSlitOverlaySheet();
      });

      dom.invertColors.addEventListener('change', (e) => {
        state.invertColors = e.target.checked;
        synthesizeInterleavedBase();
      });

      dom.showTicks.addEventListener('change', (e) => {
        state.showTicks = e.target.checked;
        renderRegistrationTicks();
      });

      // Motion Type Buttons
      dom.motionBtns.forEach((btn) => {
        btn.addEventListener('click', () => {
          dom.motionBtns.forEach((b) => {
            b.classList.remove('bg-slate-800', 'text-emerald-400');
            b.classList.add('text-slate-400');
          });
          btn.classList.add('bg-slate-800', 'text-emerald-400');
          btn.classList.remove('text-slate-400');
          state.motionType = btn.dataset.motion;
          synthesizeInterleavedBase();
        });
      });

      // Preset Pickers
      dom.presetAggie.addEventListener('click', () => {
        state.sourceImage = createAggieBandMarcherCanvas();
        state.motionType = 'march';
        highlightMotionBtn('march');
        synthesizeInterleavedBase();
        showToast('Loaded Preset: Texas A&M Band Marcher');
      });

      dom.presetHeart.addEventListener('click', () => {
        state.sourceImage = createHeartCanvas();
        state.motionType = 'pulse';
        highlightMotionBtn('pulse');
        synthesizeInterleavedBase();
        showToast('Loaded Preset: Pulsing Heart');
      });

      dom.presetGear.addEventListener('click', () => {
        state.sourceImage = createGearCanvas();
        state.motionType = 'rotate';
        highlightMotionBtn('rotate');
        synthesizeInterleavedBase();
        showToast('Loaded Preset: Rotating Gear');
      });

      dom.presetRunner.addEventListener('click', () => {
        state.sourceImage = createRunnerCanvas();
        state.motionType = 'wave';
        highlightMotionBtn('wave');
        synthesizeInterleavedBase();
        showToast('Loaded Preset: Stride Runner');
      });

      function highlightMotionBtn(type) {
        dom.motionBtns.forEach((b) => {
          const match = b.dataset.motion === type;
          b.classList.toggle('bg-slate-800', match);
          b.classList.toggle('text-emerald-400', match);
          b.classList.toggle('text-slate-400', !match);
        });
      }

      // Custom Upload
      dom.imageUpload.addEventListener('change', (e) => {
        const file = e.target.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = (event) => {
          const img = new Image();
          img.onload = () => {
            state.sourceImage = img;
            synthesizeInterleavedBase();
            showToast(`Loaded Custom Asset: ${file.name}`);
          };
          img.src = event.target.result;
        };
        reader.readAsDataURL(file);
      });

      // Transport Controls
      dom.btnPlay.addEventListener('click', () => {
        state.isPlaying = !state.isPlaying;
        dom.playIcon.classList.toggle('hidden', state.isPlaying);
        dom.pauseIcon.classList.toggle('hidden', !state.isPlaying);
      });

      dom.scrubber.addEventListener('input', (e) => {
        state.isPlaying = false;
        dom.playIcon.classList.remove('hidden');
        dom.pauseIcon.classList.add('hidden');
        applySlitPosition(parseFloat(e.target.value));
      });

      dom.playbackSpeed.addEventListener('change', (e) => {
        state.playbackFrequency = parseFloat(e.target.value);
      });

      dom.btnStepPrev.addEventListener('click', () => {
        state.isPlaying = false;
        dom.playIcon.classList.remove('hidden');
        dom.pauseIcon.classList.add('hidden');
        applySlitPosition(state.slitOffset - state.w);
      });

      dom.btnStepNext.addEventListener('click', () => {
        state.isPlaying = false;
        dom.playIcon.classList.remove('hidden');
        dom.pauseIcon.classList.add('hidden');
        applySlitPosition(state.slitOffset + state.w);
      });

      dom.btnResetPos.addEventListener('click', () => {
        applySlitPosition(0);
      });

      // Exports
      dom.btnExportBase.addEventListener('click', exportInterleavedBase);
      dom.btnExportSlitPng.addEventListener('click', exportSlitPng);
      dom.btnExportSlitSvg.addEventListener('click', exportSlitSvg);
    }

    // --- Application Bootloader ---
    window.addEventListener('load', () => {
      // 1. Initial Preset Graphic
      state.sourceImage = createAggieBandMarcherCanvas();
     
      // 2. Set Initial Geometries
      updateCalculatedParameters();
     
      // 3. Bind Drag & Controls
      initDragControls();
      initEventListeners();
     
      // 4. Start Physics & Animation Render Loop
      requestAnimationFrame(animationLoop);
    });
  </script>
</body>
</html>


Key Architectural Capabilities

  1. Procedural Vector Presets:

    • Includes four built-in high-contrast vector silhouette generators: the Texas A&M Band Marcher with instrument at attention, Pulsing Heart, Rotating Gear, and Stride Runner.

    • Accepts custom image uploads (PNG, SVG, JPEG) with automatic contrast inversion controls.

  2. Parametric Synthesis Modes:

    • Single-image decomposition into N phase frames supporting March (cadence stride with vertical bob), Spin (360-degree rotation), Pulse (radial dilation), and Wave (harmonic oscillation).

  3. Exact Pixel-Column Interleaving:

    • Strict integer math: column x draws from phase frame index lfloor (x / w) rfloor pmod N.

    • Automatically recalibrates slit width (w), bar width (B = (N-1)w), pitch (P = Nw), and transparent duty cycle.

  4. Interactive Hardware-Accelerated Simulator:

    • Live draggable barrier sheet using translateX with pointerdown / setPointerCapture for zero-lag touch and mouse scrubbing.

    • Continuous playback transport with speed multiplier, step controls, and an inspection mode slider to adjust black bar opacity and inspect the underlying interleaved strips.

    • Top and bottom registration alignment ticks with matching period P.

  5. Multi-Format Export Suite:

    • Exports the interleaved composite base plate as high-resolution PNG.

    • Exports the barrier grid mask as a 1-bit transparent PNG or mathematically pure SVG for laser cutting or transparency printing.


From <https://gemini.google.com/app/f94f4774f6847a7f>  Google Gemini (3.8 Flash)

Comments

Popular posts from this blog

#4297 ISS Astronaut Unable to Speak

#4188 Hybrid/GraphRAG

#3356 PHP and JavaScript to Retrieve and Set an API_Key