<div id="content">
<!-- PANEL (left side) -->
<div id="right-panel">
<div class="panel-tabs">
<button class="panel-tab active" onclick="switchTab('presets')">Start<span class="tab-badge"></span></button>
<button class="panel-tab" onclick="switchTab('settings')" id="tab-btn-settings">Dither<span class="tab-badge" id="badge-settings"></span></button>
<button class="panel-tab" onclick="switchTab('color')" id="tab-btn-color">Color<span class="tab-badge" id="badge-color"></span></button>
<button class="panel-tab" onclick="switchTab('channels')" id="tab-btn-channels">Channels<span class="tab-badge" id="badge-channels"></span></button>
<button class="panel-tab" onclick="switchTab('post')" id="tab-btn-post">Post<span class="tab-badge" id="badge-post"></span></button>
<button class="panel-tab" onclick="switchTab('export')">Export<span class="tab-badge"></span></button>
</div>
<!-- SETTINGS TAB -->
<div class="panel-content" id="tab-settings">
<div class="panel-section">
<div class="section-title">Algorithm</div>
<div class="tab-intro">Choose a dithering algorithm. Error diffusion algorithms spread quantization error to neighboring pixels.</div>
<div id="algo-grid"><!-- built by JS --></div>
<div id="algo-desc">Select an algorithm above.</div>
<div id="algo-diagram-wrap">
<button class="diagram-toggle" onclick="toggleAlgoDiagram()">▸ Show Pattern</button>
<div id="algo-diagram" class="hidden">
<canvas id="algo-diagram-canvas" width="180" height="120"></canvas>
<div id="algo-diagram-legend"></div>
</div>
</div>
</div>
<div class="panel-section">
<div class="section-title">Threshold & Tone</div>
<div class="histogram-row">
<div class="histogram-box">
<span class="hist-label">Input</span>
<canvas id="hist-canvas" title="Input luminance histogram — yellow line = threshold"></canvas>
</div>
<div class="histogram-box">
<span class="hist-label">Output</span>
<canvas id="hist-output-canvas" title="Output image histogram after dithering"></canvas>
</div>
</div>
<div class="control-row">
<label>Threshold <span id="threshold-val">128</span></label>
<input type="range" id="threshold" min="0" max="255" value="128" data-default="128" ondblclick="resetSlider(this)" oninput="updateVal('threshold-val',this.value);scheduleProcess();drawHistogram()">
</div>
<div class="control-row" id="error-amount-row">
<label>Error Diffusion <span id="error-amount-val">100%</span></label>
<input type="range" id="error-amount" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('error-amount-val',this.value+'%');scheduleProcess()">
</div>
<div class="control-row">
<label>Noise Mix <span id="noise-val">0%</span></label>
<input type="range" id="noise-mix" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('noise-val',this.value+'%');scheduleProcess()">
</div>
</div>
<details class="section-collapsible" id="preprocess-details">
<summary class="section-title collapsible">Pre-Processing <span class="collapse-arrow">▶</span></summary>
<div class="collapsible-content">
<div class="subsection-title">Tone</div>
<div class="control-row">
<label>Brightness <span id="brightness-val">0</span></label>
<input type="range" id="brightness" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('brightness-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Contrast <span id="contrast-val">0</span></label>
<input type="range" id="contrast" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('contrast-val',this.value);scheduleProcess()">
</div>
<div class="subsection-title">Levels</div>
<div class="control-row">
<label>Black Point <span id="black-point-val">0</span></label>
<input type="range" id="black-point" min="0" max="254" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('black-point-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Midpoint <span id="midpoint-val">128</span></label>
<input type="range" id="midpoint" min="1" max="254" value="128" data-default="128" ondblclick="resetSlider(this)" oninput="updateVal('midpoint-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>White Point <span id="white-point-val">255</span></label>
<input type="range" id="white-point" min="1" max="255" value="255" data-default="255" ondblclick="resetSlider(this)" oninput="updateVal('white-point-val',this.value);scheduleProcess()">
</div>
<div class="subsection-title">Sharpness</div>
<div class="control-row">
<label>Blur <span id="blur-val">0</span></label>
<input type="range" id="blur" min="0" max="10" step="0.5" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('blur-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Sharpen <span id="sharpen-val">0</span></label>
<input type="range" id="sharpen" min="0" max="5" step="0.5" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('sharpen-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Edge Boost <span id="edge-boost-val">0</span></label>
<input type="range" id="edge-boost" min="0" max="10" step="0.5" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('edge-boost-val',this.value);scheduleProcess()">
</div>
<div class="subsection-title">Transform</div>
<div class="toggle-row">
<span>Flip Horizontal</span>
<label class="toggle"><input type="checkbox" id="flip-h" onchange="scheduleProcess()"><span class="toggle-slider"></span></label>
</div>
<div class="toggle-row">
<span>Flip Vertical</span>
<label class="toggle"><input type="checkbox" id="flip-v" onchange="scheduleProcess()"><span class="toggle-slider"></span></label>
</div>
</div>
</details>
<div class="panel-section">
<div class="section-title">Process Resolution</div>
<div class="num-row">
<label>Width px</label>
<input type="number" id="proc-w" min="1" max="16384" placeholder="native" oninput="onProcDimChange('w')">
</div>
<div class="num-row">
<label>Height px</label>
<input type="number" id="proc-h" min="1" max="16384" placeholder="native" oninput="onProcDimChange('h')">
</div>
<div class="toggle-row">
<span>Lock aspect</span>
<label class="toggle"><input type="checkbox" id="proc-lock" checked=""><span class="toggle-slider"></span></label>
</div>
<div class="proc-dims-label" id="proc-dims-label">Native resolution</div>
<div class="preset-row">
<button class="preset-btn" onclick="setProcScale(0.25)">25%</button>
<button class="preset-btn" onclick="setProcScale(0.5)">50%</button>
<button class="preset-btn" onclick="setProcScale(1)">100%</button>
<button class="preset-btn" onclick="clearProcDims()">Native</button>
</div>
</div>
<div class="panel-section">
<div class="section-title">Pixel & Scan</div>
<div class="control-row">
<label>Pixel Block <span id="pixel-block-val">1px</span></label>
<input type="range" id="pixel-block" min="1" max="32" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('pixel-block-val',this.value+'px');scheduleProcess()">
</div>
<div class="toggle-row">
<span>Serpentine Scan</span>
<label class="toggle"><input type="checkbox" id="serpentine" onchange="scheduleProcess()"><span class="toggle-slider"></span></label>
</div>
<div class="toggle-row">
<span title="Dither R, G, B channels independently — produces chromatic output">Per-Channel Dither</span>
<label class="toggle"><input type="checkbox" id="per-channel" onchange="onPerChannelChange()"><span class="toggle-slider"></span></label>
</div>
<div class="toggle-row">
<span title="Stable per-image tile offset on ordered matrices — removes regular grid artefacts">Tile Offset</span>
<label class="toggle"><input type="checkbox" id="tile-offset" onchange="scheduleProcess()"><span class="toggle-slider"></span></label>
</div>
</div>
</div>
<!-- POST TAB -->
<div class="panel-content" id="tab-post">
<div class="panel-section post-overview-card">
<div class="section-title">Post Processing</div>
<div class="tab-intro">Shape the dithered result after the core algorithm stage. Use masks, blend modes, pattern motion, and tone remapping here.</div>
<div class="settings-summary" id="post-summary">
<span class="summary-tag"><strong>Clean Output</strong></span>
</div>
<div class="post-nav-row">
<button class="preset-btn post-nav-chip" onclick="openPostPanelSection('motion-preset-details')"><strong>Presets</strong> Build stack</button>
<button class="preset-btn post-nav-chip" onclick="openPostPanelSection('mask-details')"><strong>Finish</strong> Mask + blend</button>
<button class="preset-btn post-nav-chip" onclick="openPostPanelSection('pattern-animation-details')"><strong>Motion</strong> Tune systems</button>
<button class="preset-btn post-nav-chip" onclick="openPostPanelSection('still-frame-details')"><strong>Frames</strong> Generate sequence</button>
<button class="preset-btn post-nav-chip" onclick="openPostPanelSection('tone-curve-details')"><strong>Tone</strong> Final remap</button>
</div>
<div class="post-inline-summary">Start with a preset if you want a full motion stack, then open only the finish and motion sections you actually need. This keeps the Post tab closer to a workflow and reduces control hunting.</div>
<div class="pipeline-builder">
<div class="pipeline-toolbar">
<button class="preset-btn small" id="post-motion-earlier-btn" type="button" onclick="movePostMotionStage('earlier')">Move Motion Earlier</button>
<button class="preset-btn small" id="post-motion-later-btn" type="button" onclick="movePostMotionStage('later')">Move Motion Later</button>
<span class="pipeline-mode-copy" id="post-pipeline-mode">Motion runs after the finish stack.</span>
</div>
<div class="pipeline-stack" id="post-pipeline-stack"></div>
<div class="pipeline-technical-select">
<select id="post-motion-order" onchange="syncPostPipelineEditor();scheduleProcess()">
<option value="finish-first">Dither → Finish → Motion</option>
<option value="motion-first">Motion → Dither → Finish</option>
</select>
</div>
</div>
<div class="control-note">Motion First runs the motion systems on the source frame before preprocessing and dithering. Pattern Animation stays tied to the dither stage, and the painted mask still runs last.</div>
</div>
<div class="panel-section">
<div class="section-title">Motion Presets</div>
<details class="section-collapsible" id="motion-preset-details" open="">
<summary class="section-title collapsible">Preset Browser <span class="collapse-arrow">▶</span></summary>
<div class="collapsible-content">
<div class="motion-filter-row">
<button class="preset-btn small motion-filter-chip active" data-motion-filter="all" onclick="filterMotionPresets('all')">All</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="pattern" onclick="filterMotionPresets('pattern')">Pattern</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="melt" onclick="filterMotionPresets('melt')">Melt</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="liquid" onclick="filterMotionPresets('liquid')">Liquid</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="displace" onclick="filterMotionPresets('displace')">Displace</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="dissolve" onclick="filterMotionPresets('dissolve')">Dissolve</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="palette" onclick="filterMotionPresets('palette')">Palette</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="glitch" onclick="filterMotionPresets('glitch')">Glitch</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="sweep" onclick="filterMotionPresets('sweep')">Sweep</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="chroma" onclick="filterMotionPresets('chroma')">Chroma</button>
<button class="preset-btn small motion-filter-chip" data-motion-filter="multi" onclick="filterMotionPresets('multi')">Multi-Effect</button>
</div>
<div class="motion-preset-groups">
<div class="motion-preset-group" data-motion-group="pattern">
<div class="motion-preset-group-title">Pattern</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="matrix-drift" data-motion-tags="pattern" onclick="applyMotionPreset('matrix-drift')">
<span class="motion-preset-thumb matrix-drift"></span>
<span class="motion-preset-title">Matrix Drift</span>
<span class="motion-preset-best-for">Ordered dither only</span>
<span class="motion-preset-desc">Ordered dither drift that keeps the image stable while the matrix slides underneath.</span>
<span class="motion-system-badges"><span class="motion-system-badge pattern">Pattern</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="lattice-orbit" data-motion-tags="pattern" onclick="applyMotionPreset('lattice-orbit')">
<span class="motion-preset-thumb lattice-orbit"></span>
<span class="motion-preset-title">Lattice Orbit</span>
<span class="motion-preset-best-for">Ordered dither only</span>
<span class="motion-preset-desc">Pattern orbit preset for ordered screens that loops in a smooth circular lattice.</span>
<span class="motion-system-badges"><span class="motion-system-badge pattern">Pattern</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="melt">
<div class="motion-preset-group-title">Melt</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="molten-rise" data-motion-tags="melt" onclick="applyMotionPreset('molten-rise')">
<span class="motion-preset-thumb molten-rise"></span>
<span class="motion-preset-title">Molten Rise</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Vertical full-line melt that builds from clean to flowing highlights.</span>
<span class="motion-system-badges"><span class="motion-system-badge melt">Pixel Sort</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="melt-pulse" data-motion-tags="melt" onclick="applyMotionPreset('melt-pulse')">
<span class="motion-preset-thumb melt-pulse"></span>
<span class="motion-preset-title">Melt Pulse</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Whole-image pixel melt that swells up and drops back each cycle.</span>
<span class="motion-system-badges"><span class="motion-system-badge melt">Pixel Sort</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="ember-fall" data-motion-tags="pattern melt multi" onclick="applyMotionPreset('ember-fall')">
<span class="motion-preset-thumb ember-fall"></span>
<span class="motion-preset-title">Ember Fall</span>
<span class="motion-preset-best-for">Best with ordered dithers</span>
<span class="motion-preset-desc">Pattern shimmer plus a descending melt for glowing falloff and ember rain.</span>
<span class="motion-system-badges"><span class="motion-system-badge pattern">Pattern</span><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="sweep">
<div class="motion-preset-group-title">Sweep</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="scan-sweep" data-motion-tags="sweep" onclick="applyMotionPreset('scan-sweep')">
<span class="motion-preset-thumb scan-sweep"></span>
<span class="motion-preset-title">Scan Sweep</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Smooth row displacement that reads like a horizontal scan pass.</span>
<span class="motion-system-badges"><span class="motion-system-badge slice">Slice</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="stair-drop" data-motion-tags="sweep glitch" onclick="applyMotionPreset('stair-drop')">
<span class="motion-preset-thumb stair-drop"></span>
<span class="motion-preset-title">Stair Drop</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Chunked column offsets that hold, then collapse downward.</span>
<span class="motion-system-badges"><span class="motion-system-badge slice">Slice</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="heat-haze" data-motion-tags="sweep" onclick="applyMotionPreset('heat-haze')">
<span class="motion-preset-thumb heat-haze"></span>
<span class="motion-preset-title">Heat Haze</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Low-amplitude wave distortion for shimmering air and mirage-like motion.</span>
<span class="motion-system-badges"><span class="motion-system-badge slice">Slice</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="zoom-warp">
<div class="motion-preset-group-title">Zoom Warp</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="zoom-tunnel" data-motion-tags="sweep" onclick="applyMotionPreset('zoom-tunnel')">
<span class="motion-preset-thumb zoom-tunnel"></span>
<span class="motion-preset-title">Zoom Tunnel</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Center-pull tunnel motion that breathes through the frame without tearing edges apart.</span>
<span class="motion-system-badges"><span class="motion-system-badge zoom">Zoom Warp</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="iris-snap" data-motion-tags="sweep glitch" onclick="applyMotionPreset('iris-snap')">
<span class="motion-preset-thumb iris-snap"></span>
<span class="motion-preset-title">Iris Snap</span>
<span class="motion-preset-best-for">Works on bold high-contrast stills</span>
<span class="motion-preset-desc">A sharper push-pull zoom preset that pops like a shutter or impact hit.</span>
<span class="motion-system-badges"><span class="motion-system-badge zoom">Zoom Warp</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="block-shift">
<div class="motion-preset-group-title">Block Shift</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="tile-drift" data-motion-tags="sweep" onclick="applyMotionPreset('tile-drift')">
<span class="motion-preset-thumb tile-drift"></span>
<span class="motion-preset-title">Tile Drift</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Chunky pixel tiles slide in offset waves for a readable low-res drift.</span>
<span class="motion-system-badges"><span class="motion-system-badge block">Block Shift</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="mosaic-stairs" data-motion-tags="sweep glitch" onclick="applyMotionPreset('mosaic-stairs')">
<span class="motion-preset-thumb mosaic-stairs"></span>
<span class="motion-preset-title">Mosaic Stairs</span>
<span class="motion-preset-best-for">Works on graphic stills and sprites</span>
<span class="motion-preset-desc">Tile blocks step and lock in a crunchy staircase motion with visible phase holds.</span>
<span class="motion-system-badges"><span class="motion-system-badge block">Block Shift</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="liquid">
<div class="motion-preset-group-title">Liquid</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="liquid-glass" data-motion-tags="liquid sweep" onclick="applyMotionPreset('liquid-glass')">
<span class="motion-preset-thumb liquid-glass"></span>
<span class="motion-preset-title">Liquid Glass</span>
<span class="motion-preset-best-for">Works on portraits and soft gradients</span>
<span class="motion-preset-desc">Smooth refractive ripples that feel like water over glass instead of hard displacement.</span>
<span class="motion-system-badges"><span class="motion-system-badge liquid">Ripple Flow</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="undertow" data-motion-tags="liquid sweep" onclick="applyMotionPreset('undertow')">
<span class="motion-preset-thumb undertow"></span>
<span class="motion-preset-title">Undertow</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">A deeper radial pull that ripples the frame inward like a liquid undertow.</span>
<span class="motion-system-badges"><span class="motion-system-badge liquid">Ripple Flow</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="displacement">
<div class="motion-preset-group-title">Displacement Map</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="contour-drift" data-motion-tags="displace sweep liquid" onclick="applyMotionPreset('contour-drift')">
<span class="motion-preset-thumb contour-drift"></span>
<span class="motion-preset-title">Contour Drift</span>
<span class="motion-preset-best-for">Works on portraits and edge-rich stills</span>
<span class="motion-preset-desc">Luminance contours push the frame into a smooth topographic flow instead of a generic wave.</span>
<span class="motion-system-badges"><span class="motion-system-badge displace">Displace</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="channel-push" data-motion-tags="displace sweep glitch" onclick="applyMotionPreset('channel-push')">
<span class="motion-preset-thumb channel-push"></span>
<span class="motion-preset-title">Channel Push</span>
<span class="motion-preset-best-for">Works on graphic color stills</span>
<span class="motion-preset-desc">Uses a single channel as the map source for a harder directional shove with a broken-signal feel.</span>
<span class="motion-system-badges"><span class="motion-system-badge displace">Displace</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="dissolve">
<div class="motion-preset-group-title">Dissolve / Reassemble</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="pixel-fallout" data-motion-tags="dissolve glitch" onclick="applyMotionPreset('pixel-fallout')">
<span class="motion-preset-thumb pixel-fallout"></span>
<span class="motion-preset-title">Pixel Fallout</span>
<span class="motion-preset-best-for">Works on sprites, icons, and hard edges</span>
<span class="motion-preset-desc">Blocks scatter outward and drop apart like a controlled digital breakup.</span>
<span class="motion-system-badges"><span class="motion-system-badge dissolve">Dissolve</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="reassemble-pull" data-motion-tags="dissolve sweep glitch" onclick="applyMotionPreset('reassemble-pull')">
<span class="motion-preset-thumb reassemble-pull"></span>
<span class="motion-preset-title">Reassemble Pull</span>
<span class="motion-preset-best-for">Works on centered compositions and logos</span>
<span class="motion-preset-desc">A restrained implode preset that pulls broken pixel clusters back toward the source image.</span>
<span class="motion-system-badges"><span class="motion-system-badge dissolve">Dissolve</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="palette-cycle">
<div class="motion-preset-group-title">Palette Phase</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="neon-phase" data-motion-tags="palette chroma" onclick="applyMotionPreset('neon-phase')">
<span class="motion-preset-thumb neon-phase"></span>
<span class="motion-preset-title">Neon Phase</span>
<span class="motion-preset-best-for">Best on small bold palettes</span>
<span class="motion-preset-desc">Rotates palette indices for a bright arcade color-cycle loop with no extra spatial distortion.</span>
<span class="motion-system-badges"><span class="motion-system-badge palette">Palette</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="luma-tide" data-motion-tags="palette chroma sweep" onclick="applyMotionPreset('luma-tide')">
<span class="motion-preset-thumb luma-tide"></span>
<span class="motion-preset-title">Luma Tide</span>
<span class="motion-preset-best-for">Works on posters and graphic shading</span>
<span class="motion-preset-desc">Cycles tones by luma bands so palette motion reads like shifting light instead of hue swapping.</span>
<span class="motion-system-badges"><span class="motion-system-badge palette">Palette</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="trail-field">
<div class="motion-preset-group-title">Echo, Smear & Swirl</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="ghost-lag" data-motion-tags="glitch multi" onclick="applyMotionPreset('ghost-lag')">
<span class="motion-preset-thumb ghost-lag"></span>
<span class="motion-preset-title">Ghost Lag</span>
<span class="motion-preset-best-for">Best on GIFs and video clips</span>
<span class="motion-preset-desc">Longer buffered feedback trails that drag motion history behind the current frame.</span>
<span class="motion-system-badges"><span class="motion-system-badge hybrid">Feedback Echo</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="decay-drag" data-motion-tags="glitch multi" onclick="applyMotionPreset('decay-drag')">
<span class="motion-preset-thumb decay-drag"></span>
<span class="motion-preset-title">Decay Drag</span>
<span class="motion-preset-best-for">Best on silhouettes and contrasty motion</span>
<span class="motion-preset-desc">A heavier echo decay preset that feels like analog afterimage and dirty sensor drag.</span>
<span class="motion-system-badges"><span class="motion-system-badge hybrid">Feedback Echo</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="ink-drag" data-motion-tags="sweep multi" onclick="applyMotionPreset('ink-drag')">
<span class="motion-preset-thumb ink-drag"></span>
<span class="motion-preset-title">Ink Drag</span>
<span class="motion-preset-best-for">Works on portraits and graphic stills</span>
<span class="motion-preset-desc">Edge-weighted smear that pulls dark contours like wet ink on coarse paper.</span>
<span class="motion-system-badges"><span class="motion-system-badge hybrid">Smear Trail</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="racer-smear" data-motion-tags="sweep glitch multi" onclick="applyMotionPreset('racer-smear')">
<span class="motion-preset-thumb racer-smear"></span>
<span class="motion-preset-title">Racer Smear</span>
<span class="motion-preset-best-for">Works well on vehicles, sprites, and bold edges</span>
<span class="motion-preset-desc">A sharper diagonal smear with lighter edge focus for speed-line energy.</span>
<span class="motion-system-badges"><span class="motion-system-badge hybrid">Smear Trail</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="pinwheel-surge" data-motion-tags="sweep multi" onclick="applyMotionPreset('pinwheel-surge')">
<span class="motion-preset-thumb pinwheel-surge"></span>
<span class="motion-preset-title">Pinwheel Surge</span>
<span class="motion-preset-best-for">Works on logos, symbols, and centered compositions</span>
<span class="motion-preset-desc">A broader field twist that spins outward like a kinetic poster spiral.</span>
<span class="motion-system-badges"><span class="motion-system-badge hybrid">Swirl Field</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="vortex-lens" data-motion-tags="liquid sweep multi" onclick="applyMotionPreset('vortex-lens')">
<span class="motion-preset-thumb vortex-lens"></span>
<span class="motion-preset-title">Vortex Lens</span>
<span class="motion-preset-best-for">Best on portraits and radial compositions</span>
<span class="motion-preset-desc">Twirl and lens-like wobble combine into a restrained refractive vortex.</span>
<span class="motion-system-badges"><span class="motion-system-badge hybrid">Swirl Field</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="glitch-chroma">
<div class="motion-preset-group-title">Glitch & Chroma</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="glitch-burst" data-motion-tags="glitch chroma" onclick="applyMotionPreset('glitch-burst')">
<span class="motion-preset-thumb glitch-burst"></span>
<span class="motion-preset-title">Glitch Burst</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Short diagonal RGB split that fires hard near the start of the loop.</span>
<span class="motion-system-badges"><span class="motion-system-badge rgb">RGB Drift</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="chroma-orbit" data-motion-tags="chroma" onclick="applyMotionPreset('chroma-orbit')">
<span class="motion-preset-thumb chroma-orbit"></span>
<span class="motion-preset-title">Chroma Orbit</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Color channels drift in a smoother orbital pass with staggered timing.</span>
<span class="motion-system-badges"><span class="motion-system-badge rgb">RGB Drift</span></span>
</button>
</div>
</div>
<div class="motion-preset-group" data-motion-group="hybrid">
<div class="motion-preset-group-title">Hybrid</div>
<div class="motion-preset-grid">
<button class="preset-btn motion-preset-card" data-motion-preset="prism-flare" data-motion-tags="pattern chroma multi" onclick="applyMotionPreset('prism-flare')">
<span class="motion-preset-thumb prism-flare"></span>
<span class="motion-preset-title">Prism Flare</span>
<span class="motion-preset-best-for">Best with ordered dithers</span>
<span class="motion-preset-desc">Pattern motion and chroma split combine into a bright refracted flare loop.</span>
<span class="motion-system-badges"><span class="motion-system-badge pattern">Pattern</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="vhs-roll" data-motion-tags="glitch sweep chroma multi" onclick="applyMotionPreset('vhs-roll')">
<span class="motion-preset-thumb vhs-roll"></span>
<span class="motion-preset-title">VHS Roll</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Jittered row drift plus light channel split for tape-roll energy.</span>
<span class="motion-system-badges"><span class="motion-system-badge slice">Slice</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="shutter-tear" data-motion-tags="glitch sweep multi" onclick="applyMotionPreset('shutter-tear')">
<span class="motion-preset-thumb shutter-tear"></span>
<span class="motion-preset-title">Shutter Tear</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Hard stepped slice tears with offset chroma for abrupt shutter-slip motion.</span>
<span class="motion-system-badges"><span class="motion-system-badge slice">Slice</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="cascade" data-motion-tags="melt sweep chroma multi" onclick="applyMotionPreset('cascade')">
<span class="motion-preset-thumb cascade"></span>
<span class="motion-preset-title">Cascade</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Layered melt, row warp, and chroma burst offset across one cycle.</span>
<span class="motion-system-badges"><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge slice">Slice</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="echo-smear" data-motion-tags="melt chroma glitch multi" onclick="applyMotionPreset('echo-smear')">
<span class="motion-preset-thumb echo-smear"></span>
<span class="motion-preset-title">Echo Smear</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Segmented shadow sorting with stepped rhythm and vertical drift echo.</span>
<span class="motion-system-badges"><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="warp-melt-prism" data-motion-tags="melt chroma sweep multi" onclick="applyMotionPreset('warp-melt-prism')">
<span class="motion-preset-thumb warp-melt-prism"></span>
<span class="motion-preset-title">Warp Melt Prism</span>
<span class="motion-preset-best-for">Best on bold silhouettes and portraits</span>
<span class="motion-preset-desc">Zoom Warp stacks with Pixel Sort and RGB Drift for a refracted collapsing pulse.</span>
<span class="motion-system-badges"><span class="motion-system-badge zoom">Zoom Warp</span><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="tile-surge" data-motion-tags="melt chroma sweep multi" onclick="applyMotionPreset('tile-surge')">
<span class="motion-preset-thumb tile-surge"></span>
<span class="motion-preset-title">Tile Surge</span>
<span class="motion-preset-best-for">Works well on sprites and graphic textures</span>
<span class="motion-preset-desc">Block Shift, Pixel Sort, and RGB Drift hit in staggered passes for chunky surge motion.</span>
<span class="motion-system-badges"><span class="motion-system-badge block">Block Shift</span><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="liquid-tear" data-motion-tags="liquid melt chroma glitch multi" onclick="applyMotionPreset('liquid-tear')">
<span class="motion-preset-thumb liquid-tear"></span>
<span class="motion-preset-title">Liquid Tear</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Ripple Flow stacks with Pixel Sort and RGB Drift for a wet glitch smear with a strong trailing edge.</span>
<span class="motion-system-badges"><span class="motion-system-badge liquid">Ripple Flow</span><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="prism-echo" data-motion-tags="chroma glitch multi" onclick="applyMotionPreset('prism-echo')">
<span class="motion-preset-thumb prism-echo"></span>
<span class="motion-preset-title">Prism Echo</span>
<span class="motion-preset-best-for">Best on animated clips and high-contrast stills</span>
<span class="motion-preset-desc">Buffered feedback trails stack with RGB Drift for refracted temporal splitting.</span>
<
span class="motion-system-badges"><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Feedback Echo</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="smear-collapse" data-motion-tags="melt glitch multi" onclick="applyMotionPreset('smear-collapse')">
<span class="motion-preset-thumb smear-collapse"></span>
<span class="motion-preset-title">Smear Collapse</span>
<span class="motion-preset-best-for">Works on portraits, textures, and shadow-heavy stills</span>
<span class="motion-preset-desc">Smear Trail and Pixel Sort collapse together into a dragged tonal spill.</span>
<span class="motion-system-badges"><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge hybrid">Smear Trail</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="twirl-prism" data-motion-tags="chroma sweep multi" onclick="applyMotionPreset('twirl-prism')">
<span class="motion-preset-thumb twirl-prism"></span>
<span class="motion-preset-title">Twirl Prism</span>
<span class="motion-preset-best-for">Best on centered art, symbols, and portraits</span>
<span class="motion-preset-desc">Swirl Field and RGB Drift combine into a chromatic vortex with cleaner edges.</span>
<span class="motion-system-badges"><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Swirl Field</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="contour-collapse" data-motion-tags="displace melt sweep multi" onclick="applyMotionPreset('contour-collapse')">
<span class="motion-preset-thumb contour-collapse"></span>
<span class="motion-preset-title">Contour Collapse</span>
<span class="motion-preset-best-for">Best on portraits and shadow-rich stills</span>
<span class="motion-preset-desc">Displacement Map and Pixel Sort collapse together into a flowing terrain-like melt.</span>
<span class="motion-system-badges"><span class="motion-system-badge displace">Displace</span><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="dissolve-drift" data-motion-tags="dissolve glitch chroma multi" onclick="applyMotionPreset('dissolve-drift')">
<span class="motion-preset-thumb dissolve-drift"></span>
<span class="motion-preset-title">Dissolve Drift</span>
<span class="motion-preset-best-for">Works on sprites, UI art, and graphic motion</span>
<span class="motion-preset-desc">Pixel breakup and RGB Drift offset in sequence for a disintegrating broadcast-style sweep.</span>
<span class="motion-system-badges"><span class="motion-system-badge dissolve">Dissolve</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="palette-prism" data-motion-tags="palette chroma multi" onclick="applyMotionPreset('palette-prism')">
<span class="motion-preset-thumb palette-prism"></span>
<span class="motion-preset-title">Palette Prism</span>
<span class="motion-preset-best-for">Best on reduced palettes and posterized stills</span>
<span class="motion-preset-desc">Palette cycling and RGB Drift combine into a cleaner chromatic shimmer with indexed color movement.</span>
<span class="motion-system-badges"><span class="motion-system-badge palette">Palette</span><span class="motion-system-badge rgb">RGB Drift</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="acid-melt" data-motion-tags="palette melt chroma multi" onclick="applyMotionPreset('acid-melt')">
<span class="motion-preset-thumb acid-melt"></span>
<span class="motion-preset-title">Acid Melt</span>
<span class="motion-preset-best-for">Best on limited palettes and bright illustrations</span>
<span class="motion-preset-desc">Palette Phase Cycling rides on top of Pixel Sort for a toxic color-shifting melt.</span>
<span class="motion-system-badges"><span class="motion-system-badge palette">Palette</span><span class="motion-system-badge melt">Pixel Sort</span><span class="motion-system-badge hybrid">Hybrid</span></span>
</button>
<button class="preset-btn motion-preset-card" data-motion-preset="clear-motion" data-motion-tags="all" onclick="applyMotionPreset('clear-motion')">
<span class="motion-preset-thumb clear-motion"></span>
<span class="motion-preset-title">Clear Motion</span>
<span class="motion-preset-best-for">Works on any still image</span>
<span class="motion-preset-desc">Turn off the preset stack and return to a neutral post-animation state.</span>
<span class="motion-system-badges"><span class="motion-system-badge hybrid">Reset</span></span>
</button>
</div>
</div>
</div>
<div class="control-note" id="motion-preset-note">Apply a ready-made motion stack for still-image post animation, then fine-tune curves, offsets, and frame counts.</div>
</div>
</details>
</div>
<div class="panel-section">
<div class="section-title">Finish: Masking</div>
<div class="post-mini-note">Use this to bias where the post stack hits strongest before you tune the animated systems.</div>
<details class="section-collapsible" id="mask-details">
<summary class="section-title collapsible">Gradient Mask <span class="collapse-arrow">▶</span></summary>
<div class="collapsible-content">
<div class="control-row">
<label>Type</label>
<select id="mask-type" onchange="scheduleProcess()">
<option value="none">None</option>
<option value="radial">Radial — bright center</option>
<option value="radial-inv">Radial — bright edge</option>
<option value="linear-h">Linear Horizontal</option>
<option value="linear-v">Linear Vertical</option>
<option value="vignette">Vignette</option>
</select>
</div>
<div class="control-row">
<label>Strength <span id="mask-strength-val">0</span></label>
<input type="range" id="mask-strength" min="-128" max="128" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('mask-strength-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Position <span id="mask-pos-val">50%</span></label>
<input type="range" id="mask-pos" min="0" max="100" value="50" data-default="50" ondblclick="resetSlider(this)" oninput="updateVal('mask-pos-val',this.value+'%');scheduleProcess()">
</div>
<div class="control-row">
<label>Feather <span id="mask-feather-val">100%</span></label>
<input type="range" id="mask-feather" min="1" max="200" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('mask-feather-val',this.value+'%');scheduleProcess()">
</div>
</div>
</details>
</div>
<div class="panel-section">
<div class="section-title">Finish: Blend & Cleanup</div>
<div class="post-mini-note">These are your last-mile output controls for recombining the source, compressing tone, or flipping the result.</div>
<details class="section-collapsible" id="effects-details" open="">
<summary class="section-title collapsible">Blend & Finish <span class="collapse-arrow">▶</span></summary>
<div class="collapsible-content">
<div class="control-row">
<label>Blend Mode</label>
<select id="blend-mode" onchange="scheduleProcess()">
<option value="none">None (Replace)</option>
<option value="multiply">Multiply</option>
<option value="screen">Screen</option>
<option value="overlay">Overlay</option>
<option value="soft-light">Soft Light</option>
<option value="hard-light">Hard Light</option>
</select>
</div>
<div class="control-row">
<label>Blend Opacity <span id="blend-opacity-val">100%</span></label>
<input type="range" id="blend-opacity" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('blend-opacity-val',this.value+'%');scheduleProcess()">
</div>
<div class="control-row">
<label>Posterize <span id="posterize-val">Off</span></label>
<input type="range" id="posterize" min="0" max="16" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('posterize-val',this.value>0?this.value+' lvl':'Off');scheduleProcess()">
</div>
<div class="toggle-row">
<span>Invert</span>
<label class="toggle"><input type="checkbox" id="invert" onchange="scheduleProcess()"><span class="toggle-slider"></span></label>
</div>
</div>
</details>
</div>
<div class="panel-section">
<div class="section-title">Motion Systems</div>
<div class="post-inline-summary">Pattern animation only affects ordered dithers. The post systems below work on any still image, so you can build from subtle movement to full distortion stacks without changing the core dither setup.</div>
<div class="post-effect-jump-row">
<button class="preset-btn small" onclick="openPostPanelSection('pattern-animation-details')">Pattern</button>
<button class="preset-btn small" onclick="openPostPanelSection('pixel-sort-details')">Pixel Sort</button>
<button class="preset-btn small" onclick="openPostPanelSection('slice-warp-details')">Slice</button>
<button class="preset-btn small" onclick="openPostPanelSection('channel-drift-details')">RGB Drift</button>
<button class="preset-btn small" onclick="openPostPanelSection('zoom-warp-details')">Zoom Warp</button>
<button class="preset-btn small" onclick="openPostPanelSection('block-shift-details')">Block Shift</button>
<button class="preset-btn small" onclick="openPostPanelSection('ripple-flow-details')">Ripple Flow</button>
<button class="preset-btn small" onclick="openPostPanelSection('feedback-echo-details')">Echo</button>
<button class="preset-btn small" onclick="openPostPanelSection('smear-trail-details')">Smear</button>
<button class="preset-btn small" onclick="openPostPanelSection('swirl-field-details')">Swirl</button>
<button class="preset-btn small" onclick="openPostPanelSection('displacement-map-details')">Displace</button>
<button class="preset-btn small" onclick="openPostPanelSection('pixel-dissolve-details')">Dissolve</button>
<button class="preset-btn small" onclick="openPostPanelSection('palette-cycle-details')">Palette</button>
</div>
<div class="post-divider-label">Foundation</div>
<details class="section-collapsible motion-effect-card" id="pattern-animation-details" open="">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Pattern Animation</span>
<span class="motion-effect-subtitle">Ordered matrix phase motion for supported dither routes.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="pattern-anim-status-pill">Off</span>
<span class="motion-status-pill is-detail" id="pattern-anim-detail-pill">Ordered only</span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span title="Animate ordered dither pattern phase for static images">Enable Pattern Animation</span>
<label class="toggle"><input type="checkbox" id="pattern-anim" onchange="togglePatternAnim()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-motion-row">
<label>Motion Mode</label>
<select id="pattern-anim-motion" onchange="onPatternAnimSettingsChange()">
<option value="drift">Drift</option>
<option value="horizontal">Horizontal</option>
<option value="vertical">Vertical</option>
<option value="diagonal">Diagonal</option>
<option value="orbit">Orbit</option>
<option value="lissajous">Lissajous</option>
<option value="spiral">Spiral</option>
</select>
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-speed-row">
<label>Phase Step <span id="pattern-anim-speed-val">1.00×</span></label>
<input type="range" id="pattern-anim-speed" min="0.25" max="6" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('pattern-anim-speed-val',Number(this.value).toFixed(2)+'×');onPatternAnimSettingsChange()">
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-amplitude-row">
<label>Amplitude <span id="pattern-anim-amplitude-val">6</span></label>
<input type="range" id="pattern-anim-amplitude" min="0" max="24" step="1" value="6" data-default="6" ondblclick="resetSlider(this)" oninput="updateVal('pattern-anim-amplitude-val',this.value);onPatternAnimSettingsChange()">
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-frames-row">
<label>Cycle Frames <span id="pattern-anim-frames-val">12</span></label>
<input type="range" id="pattern-anim-frames" min="2" max="60" step="1" value="12" data-default="12" ondblclick="resetSlider(this)" oninput="updateVal('pattern-anim-frames-val',this.value);onPatternAnimSettingsChange()">
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-preview-fps-row">
<label>Preview FPS <span id="pattern-anim-preview-fps-val">12</span></label>
<input type="range" id="pattern-anim-preview-fps" min="1" max="30" step="1" value="12" data-default="12" ondblclick="resetSlider(this)" oninput="updateVal('pattern-anim-preview-fps-val',this.value);onPatternAnimSettingsChange(true)">
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-drift-x-row">
<label>X Drift <span id="pattern-anim-drift-x-val">1.00</span></label>
<input type="range" id="pattern-anim-drift-x" min="-4" max="4" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('pattern-anim-drift-x-val',Number(this.value).toFixed(2));onPatternAnimSettingsChange()">
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-drift-y-row">
<label>Y Drift <span id="pattern-anim-drift-y-val">0.70</span></label>
<input type="range" id="pattern-anim-drift-y" min="-4" max="4" step="0.25" value="0.7" data-default="0.7" ondblclick="resetSlider(this)" oninput="updateVal('pattern-anim-drift-y-val',Number(this.value).toFixed(2));onPatternAnimSettingsChange()">
</div>
<div class="control-row pattern-anim-control hidden-row" id="pattern-anim-loop-row">
<label>Loop Mode</label>
<select id="pattern-anim-loop" onchange="onPatternAnimSettingsChange()">
<option value="loop">Loop</option>
<option value="pingpong">Ping-Pong</option>
</select>
</div>
<div class="control-note pattern-anim-control" id="pattern-anim-note">Turn on Pattern Animation to preview and export animated ordered-matrix dithers.</div>
</div>
</details>
<details class="section-collapsible motion-effect-card" id="pixel-sort-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Pixel Sort Melt</span>
<span class="motion-effect-subtitle">Directional smear with gated highlight, midtone, or shadow runs.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="pixel-sort-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="pixel-sort-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Pixel Sort Melt</span>
<label class="toggle"><input type="checkbox" id="pixel-sort-enable" onchange="onPixelSortSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Direction</label>
<select id="pixel-sort-direction" onchange="onPixelSortSettingsChange()">
<option value="vertical">Vertical Melt</option>
<option value="horizontal">Horizontal Melt</option>
</select>
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Gate</label>
<select id="pixel-sort-gate" onchange="onPixelSortSettingsChange()">
<option value="highlights">Highlights</option>
<option value="midtones">Midtones</option>
<option value="shadows">Shadows</option>
<option value="all">All Tones</option>
</select>
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Coverage</label>
<select id="pixel-sort-coverage" onchange="onPixelSortSettingsChange()">
<option value="segments">Segmented Runs</option>
<option value="full-line">Full Line / Entire Image</option>
</select>
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Threshold <span id="pixel-sort-threshold-val">160</span></label>
<input type="range" id="pixel-sort-threshold" min="0" max="255" value="160" data-default="160" ondblclick="resetSlider(this)" oninput="updateVal('pixel-sort-threshold-val',this.value);onPixelSortSettingsChange()">
</div>
<div class="control-row pixel-sort-control hidden-row" id="pixel-sort-span-row">
<label>Segment Span <span id="pixel-sort-span-val">28 px</span></label>
<input type="range" id="pixel-sort-span" min="4" max="160" value="28" data-default="28" ondblclick="resetSlider(this)" oninput="updateVal('pixel-sort-span-val',this.value+' px');onPixelSortSettingsChange()">
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Mix <span id="pixel-sort-mix-val">100%</span></label>
<input type="range" id="pixel-sort-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('pixel-sort-mix-val',this.value+'%');onPixelSortSettingsChange()">
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Motion Mode</label>
<select id="pixel-sort-motion" onchange="onPixelSortSettingsChange(true)">
<option value="ooze">Ooze</option>
<option value="collapse">Collapse</option>
<option value="tear">Tear</option>
</select>
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Depth <span id="pixel-sort-depth-val">60%</span></label>
<input type="range" id="pixel-sort-depth" min="0" max="100" value="60" data-default="60" ondblclick="resetSlider(this)" oninput="updateVal('pixel-sort-depth-val',this.value+'%');onPixelSortSettingsChange(true)">
</div>
<div class="control-row pixel-sort-control hidden-row">
<label>Order</label>
<select id="pixel-sort-order" onchange="onPixelSortSettingsChange()">
<option value="bright-desc">Bright → Dark</option>
<option value="dark-asc">Dark → Bright</option>
</select>
</div>
<div class="toggle-row pixel-sort-control hidden-row">
<span>Animate Melt</span>
<label class="toggle"><input type="checkbox" id="pixel-sort-animate" onchange="onPixelSortSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row pixel-sort-control hidden-row" id="pixel-sort-apply-row">
<label>Apply Curve</label>
<select id="pixel-sort-apply" onchange="onPixelSortSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row pixel-sort-control hidden-row" id="pixel-sort-offset-row">
<label>Start Offset <span id="pixel-sort-offset-val">0%</span></label>
<input type="range" id="pixel-sort-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('pixel-sort-offset-val',this.value+'%');onPixelSortSettingsChange(true)">
</div>
<div class="control-row pixel-sort-control hidden-row" id="pixel-sort-frames-row">
<label>Cycle Frames <span id="pixel-sort-frames-val">16</span></label>
<input type="range" id="pixel-sort-frames" min="2" max="60" value="16" data-default="16" ondblclick="resetSlider(this)" oninput="updateVal('pixel-sort-frames-val',this.value);onPixelSortSettingsChange(true)">
</div>
<div class="control-row pixel-sort-control hidden-row" id="pixel-sort-step-row">
<label>Frame Shift <span id="pixel-sort-step-val">1.00×</span></label>
<input type="range" id="pixel-sort-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('pixel-sort-step-val',Number(this.value).toFixed(2)+'×');onPixelSortSettingsChange(true)">
</div>
<div class="control-note pixel-sort-control" id="pixel-sort-note">Enable Pixel Sort Melt to smear highlight or shadow runs into a static or animated melt effect for still images.</div>
</div>
</details>
<details class="section-collapsible motion-effect-card" id="slice-warp-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Slice Offset</span>
<span class="motion-effect-subtitle">Row or column displacement for wave, stairs, and jitter motion.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="slice-warp-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="slice-warp-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Slice Offset</span>
<label class="toggle"><input type="checkbox" id="slice-warp-enable" onchange="onSliceWarpSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row slice-warp-control hidden-row">
<label>Direction</label>
<select id="slice-warp-direction" onchange="onSliceWarpSettingsChange()">
<option value="horizontal">Shift Rows</option>
<option value="vertical">Shift Columns</option>
</select>
</div>
<div class="control-row slice-warp-control hidden-row">
<label>Mode</label>
<select id="slice-warp-mode" onchange="onSliceWarpSettingsChange()">
<option value="wave">Wave</option>
<option value="stairs">Stairs</option>
<option value="jitter">Jitter</option>
</select>
</div>
<div class="control-row slice-warp-control hidden-row">
<label>Amplitude <span id="slice-warp-amplitude-val">18 px</span></label>
<input type="range" id="slice-warp-amplitude" min="0" max="80" value="18" data-default="18" ondblclick="resetSlider(this)" oninput="updateVal('slice-warp-amplitude-val',this.value+' px');onSliceWarpSettingsChange()">
</div>
<div class="control-row slice-warp-control hidden-row">
<label>Frequency <span id="slice-warp-frequency-val">6</span></label>
<input type="range" id="slice-warp-frequency" min="1" max="24" value="6" data-default="6" ondblclick="resetSlider(this)" oninput="updateVal('slice-warp-frequency-val',this.value);onSliceWarpSettingsChange()">
</div>
<div class="control-row slice-warp-control hidden-row">
<label>Mix <span id="slice-warp-mix-val">100%</span></label>
<input type="range" id="slice-warp-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('slice-warp-mix-val',this.value+'%');onSliceWarpSettingsChange()">
</div>
<div class="toggle-row slice-warp-control hidden-row">
<span>Animate Offset</span>
<label class="toggle"><input type="checkbox" id="slice-warp-animate" onchange="onSliceWarpSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row slice-warp-control hidden-row" id="slice-warp-apply-row">
<label>Apply Curve</label>
<select id="slice-warp-apply" onchange="onSliceWarpSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row slice-warp-control hidden-row" id="slice-warp-offset-row">
<label>Start Offset <span id="slice-warp-offset-val">0%</span></label>
<input type="range" id="slice-warp-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('slice-warp-offset-val',this.value+'%');onSliceWarpSettingsChange(true)">
</div>
<div class="control-row slice-warp-control hidden-row">
<label>Cycle Frames <span id="slice-warp-frames-val">20</span></label>
<input type="range" id="slice-warp-frames" min="2" max="60" value="20" data-default="20" ondblclick="resetSlider(this)" oninput="updateVal('slice-warp-frames-val',this.value);onSliceWarpSettingsChange(true)">
</div>
<div class="control-row slice-warp-control hidden-row">
<label>Phase Speed <span id="slice-warp-step-val">1.00×</span></label>
<input type="range" id="slice-warp-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('slice-warp-step-val',Number(this.value).toFixed(2)+'×');onSliceWarpSettingsChange(true)">
</div>
<div class="control-note slice-warp-control" id="slice-warp-note">Slice Offset shifts rows or columns into wave, staircase, or jitter distortions for still-image motion design.</div>
</div>
</details>
<details class="section-collapsible motion-effect-card" id="channel-drift-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">RGB Drift</span>
<span class="motion-effect-subtitle">Chromatic channel separation with sweep, split, or orbit motion.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="channel-drift-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="channel-drift-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable RGB Drift</span>
<label class="toggle"><input type="checkbox" id="channel-drift-enable" onchange="onChannelDriftSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row channel-drift-control hidden-row">
<label>Motion</label>
<select id="channel-drift-motion" onchange="onChannelDriftSettingsChange()">
<option value="horizontal">Horizontal Split</option>
<option value="vertical">Vertical Split</option>
<option value="diagonal">Diagonal Split</option>
<option value="orbit">Orbit</option>
</select>
</div>
<div class="control-row channel-drift-control hidden-row">
<label>X Shift <span id="channel-drift-x-val">8 px</span></label>
<input type="range" id="channel-drift-x" min="0" max="40" value="8" data-default="8" ondblclick="resetSlider(this)" oninput="updateVal('channel-drift-x-val',this.value+' px');onChannelDriftSettingsChange()">
</div>
<div class="control-row channel-drift-control hidden-row">
<label>Y Shift <span id="channel-drift-y-val">2 px</span></label>
<input type="range" id="channel-drift-y" min="0" max="40" value="2" data-default="2" ondblclick="resetSlider(this)" oninput="updateVal('channel-drift-y-val',this.value+' px');onChannelDriftSettingsChange()">
</div>
<div class="control-row channel-drift-control hidden-row">
<label>Mix <span id="channel-drift-mix-val">100%</span></label>
<input type="range" id="channel-drift-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('channel-drift-mix-val',this.value+'%');onChannelDriftSettingsChange()">
</div>
<div class="toggle-row channel-drift-control hidden-row">
<span>Animate Drift</span>
<label class="toggle"><input type="checkbox" id="channel-drift-animate" onchange="onChannelDriftSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row channel-drift-control hidden-row" id="channel-drift-apply-row">
<label>Apply Curve</label>
<select id="channel-drift-apply" onchange="onChannelDriftSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row channel-drift-control hidden-row" id="channel-drift-offset-row">
<label>Start Offset <span id="channel-drift-offset-val">0%</span></label>
<input type="range" id="channel-drift-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('channel-drift-offset-val',this.value+'%');onChannelDriftSettingsChange(true)">
</div>
<div class="control-row channel-drift-control hidden-row">
<label>Cycle Frames <span id="channel-drift-frames-val">18</span></label>
<input type="range" id="channel-drift-frames" min="2" max="60" value="18" data-default="18" ondblclick="resetSlider(this)" oninput="updateVal('channel-drift-frames-val',this.value);onChannelDriftSettingsChange(true)">
</div>
<div class="control-row channel-drift-control hidden-row">
<label>Phase Speed <span id="channel-drift-step-val">1.00×</span></label>
<input type="range" id="channel-drift-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('channel-drift-step-val',Number(this.value).toFixed(2)+'×');onChannelDriftSettingsChange(true)">
</div>
<div class="control-note channel-drift-control" id="channel-drift-note">RGB Drift splits color channels into chromatic smear, glitch, and orbit-style animation from a still image.</div>
</div>
</details>
<div class="post-divider-label">Distortion</div>
<details class="section-collapsible motion-effect-card" id="zoom-warp-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Zoom Warp</span>
<span class="motion-effect-subtitle">Center-weighted pulse, tunnel, or snap distortion.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="zoom-warp-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="zoom-warp-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Zoom Warp</span>
<label class="toggle"><input type="checkbox" id="zoom-warp-enable" onchange="onZoomWarpSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Mode</label>
<select id="zoom-warp-mode" onchange="onZoomWarpSettingsChange()">
<option value="pulse">Pulse</option>
<option value="tunnel">Tunnel</option>
<option value="snap">Snap</option>
</select>
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Amount <span id="zoom-warp-amount-val">14 px</span></label>
<input type="range" id="zoom-warp-amount" min="0" max="80" value="14" data-default="14" ondblclick="resetSlider(this)" oninput="updateVal('zoom-warp-amount-val',this.value+' px');onZoomWarpSettingsChange()">
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Falloff <span id="zoom-warp-falloff-val">60%</span></label>
<input type="range" id="zoom-warp-falloff" min="5" max="100" value="60" data-default="60" ondblclick="resetSlider(this)" oninput="updateVal('zoom-warp-falloff-val',this.value+'%');onZoomWarpSettingsChange()">
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Mix <span id="zoom-warp-mix-val">100%</span></label>
<input type="range" id="zoom-warp-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('zoom-warp-mix-val',this.value+'%');onZoomWarpSettingsChange()">
</div>
<div class="toggle-row zoom-warp-control hidden-row">
<span>Animate Zoom</span>
<label class="toggle"><input type="checkbox" id="zoom-warp-animate" onchange="onZoomWarpSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Apply Curve</label>
<select id="zoom-warp-apply" onchange="onZoomWarpSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Start Offset <span id="zoom-warp-offset-val">0%</span></label>
<input type="range" id="zoom-warp-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('zoom-warp-offset-val',this.value+'%');onZoomWarpSettingsChange(true)">
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Cycle Frames <span id="zoom-warp-frames-val">18</span></label>
<input type="range" id="zoom-warp-frames" min="2" max="60" value="18" data-default="18" ondblclick="resetSlider(this)" oninput="updateVal('zoom-warp-frames-val',this.value);onZoomWarpSettingsChange(true)">
</div>
<div class="control-row zoom-warp-control hidden-row">
<label>Phase Speed <span id="zoom-warp-step-val">1.00×</span></label>
<input type="range" id="zoom-warp-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('zoom-warp-step-val',Number(this.value).toFixed(2)+'×');onZoomWarpSettingsChange(true)">
</div>
<div class="control-note zoom-warp-control" id="zoom-warp-note">Zoom Warp pushes the image in and out from the center for pulse, tunnel, or snap-style still-image motion.</div>
</div>
</details>
<details class="section-collapsible motion-effect-card" id="block-shift-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Block Shift</span>
<span class="motion-effect-subtitle">Tile-based drift, jitter, or stair-step chunk motion.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="block-shift-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="block-shift-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Block Shift</span>
<label class="toggle"><input type="checkbox" id="block-shift-enable" onchange="onBlockShiftSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row block-shift-control hidden-row">
<label>Mode</label>
<select id="block-shift-mode" onchange="onBlockShiftSettingsChange()">
<option value="drift">Drift</option>
<option value="jitter">Jitter</option>
<option value="stairs">Stairs</option>
</select>
</div>
<div class="control-row block-shift-control hidden-row">
<label>Block Size <span id="block-shift-size-val">20 px</span></label>
<input type="range" id="block-shift-size" min="4" max="80" value="20" data-default="20" ondblclick="resetSlider(this)" oninput="updateVal('block-shift-size-val',this.value+' px');onBlockShiftSettingsChange()">
</div>
<div class="control-row block-shift-control hidden-row">
<label>Amplitude <span id="block-shift-amplitude-val">12 px</span></label>
<input type="range" id="block-shift-amplitude" min="0" max="80" value="12" data-default="12" ondblclick="resetSlider(this)" oninput="updateVal('block-shift-amplitude-val',this.value+' px');onBlockShiftSettingsChange()">
</div>
<div class="control-row block-shift-control hidden-row">
<label>Mix <span id="block-shift-mix-val">100%</span></label>
<input type="range" id="block-shift-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('block-shift-mix-val',this.value+'%');onBlockShiftSettingsChange()">
</div>
<div class="toggle-row block-shift-control hidden-row">
<span>Animate Blocks</span>
<label class="toggle"><input type="checkbox" id="block-shift-animate" onchange="onBlockShiftSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row block-shift-control hidden-row">
<label>Apply Curve</label>
<select id="block-shift-apply" onchange="onBlockShiftSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row block-shift-control hidden-row">
<label>Start Offset <span id="block-shift-offset-val">0%</span></label>
<input type="range" id="block-shift-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('block-shift-offset-val',this.value+'%');onBlockShiftSettingsChange(true)">
</div>
<div class="control-row block-shift-control hidden-row">
<label>Cycle Frames <span id="block-shift-frames-val">20</span></label>
<input type="range" id="block-shift-frames" min="2" max="60" value="20" data-default="20" ondblclick="resetSlider(this)" oninput="updateVal('block-shift-frames-val',this.value);onBlockShiftSettingsChange(true)">
</div>
<div class="control-row block-shift-control hidden-row">
<label>Phase Speed <span id="block-shift-step-val">1.00×</span></label>
<input type="range" id="block-shift-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('block-shift-step-val',Number(this.value).toFixed(2)+'×');onBlockShiftSettingsChange(true)">
</div>
<div class="control-note block-shift-control" id="block-shift-note">Block Shift moves coarse pixel tiles in drift, jitter, or stair-step motion for chunkier still-image animation.</div>
</div>
</details>
<details class="section-collapsible motion-effect-card" id="ripple-flow-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Ripple Flow</span>
<span class="motion-effect-subtitle">Liquid-style wave, refractive, or vortex displacement.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="ripple-flow-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="ripple-flow-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Ripple Flow</span>
<label class="toggle"><input type="checkbox" id="ripple-flow-enable" onchange="onRippleFlowSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Mode</label>
<select id="ripple-flow-mode" onchange="onRippleFlowSettingsChange()">
<option value="ripple">Ripple</option>
<option value="liquid">Liquid</option>
<option value="vortex">Vortex</option>
</select>
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Amount <span id="ripple-flow-amount-val">10 px</span></label>
<input type="range" id="ripple-flow-amount" min="0" max="60" value="10" data-default="10" ondblclick="resetSlider(this)" oninput="updateVal('ripple-flow-amount-val',this.value+' px');onRippleFlowSettingsChange()">
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Frequency <span id="ripple-flow-frequency-val">4.0</span></label>
<input type="range" id="ripple-flow-frequency" min="1" max="12" step="0.5" value="4" data-default="4" ondblclick="resetSlider(this)" oninput="updateVal('ripple-flow-frequency-val',Number(this.value).toFixed(1));onRippleFlowSettingsChange()">
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Mix <span id="ripple-flow-mix-val">100%</span></label>
<input type="range" id="ripple-flow-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('ripple-flow-mix-val',this.value+'%');onRippleFlowSettingsChange()">
</div>
<div class="toggle-row ripple-flow-control hidden-row">
<span>Animate Ripple</span>
<label class="toggle"><input type="checkbox" id="ripple-flow-animate" onchange="onRippleFlowSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Apply Curve</label>
<select id="ripple-flow-apply" onchange="onRippleFlowSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Start Offset <span id="ripple-flow-offset-val">0%</span></label>
<input type="range" id="ripple-flow-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('ripple-flow-offset-val',this.value+'%');onRippleFlowSettingsChange(true)">
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Cycle Frames <span id="ripple-flow-frames-val">20</span></label>
<input type="range" id="ripple-flow-frames" min="2" max="60" value="20" data-default="20" ondblclick="resetSlider(this)" oninput="updateVal('ripple-flow-frames-val',this.value);onRippleFlowSettingsChange(true)">
</div>
<div class="control-row ripple-flow-control hidden-row">
<label>Phase Speed <span id="ripple-flow-step-val">1.00×</span></label>
<input type="range" id="ripple-flow-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('ripple-flow-step-val',Number(this.value).toFixed(2)+'×');onRippleFlowSettingsChange(true)">
</div>
<div class="control-note ripple-flow-control" id="ripple-flow-note">Ripple Flow adds liquid-style displacement that can read as refractive water, soft waves, or a vortex pull.</div>
</div>
</details>
<div class="post-divider-label">Trails</div>
<details class="section-collapsible motion-effect-card" id="feedback-echo-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Feedback Echo</span>
<span class="motion-effect-subtitle">Temporal ghosting that blends delayed frames into trails, strobes, and decays.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="feedback-echo-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="feedback-echo-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Feedback Echo</span>
<label class="toggle"><input type="checkbox" id="feedback-echo-enable" onchange="onFeedbackEchoSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Mode</label>
<select id="feedback-echo-mode" onchange="onFeedbackEchoSettingsChange()">
<option value="trail">Trail</option>
<option value="decay">Decay</option>
<option value="strobe">Strobe</option>
</select>
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>History Source</label>
<select id="feedback-echo-frame-source" onchange="onFeedbackEchoSettingsChange()">
<option value="source">Source Frames</option>
<option value="processed">Processed Frames</option>
</select>
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Lag Frames <span id="feedback-echo-lag-val">1</span></label>
<input type="range" id="feedback-echo-lag" min="1" max="12" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-lag-val',this.value);onFeedbackEchoSettingsChange()">
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>History Frames <span id="feedback-echo-history-val">4</span></label>
<input type="range" id="feedback-echo-history" min="1" max="12" value="4" data-default="4" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-history-val',this.value);onFeedbackEchoSettingsChange()">
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Decay <span id="feedback-echo-decay-val">55%</span></label>
<input type="range" id="feedback-echo-decay" min="0" max="100" value="55" data-default="55" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-decay-val',this.value+'%');onFeedbackEchoSettingsChange()">
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>X Drift <span id="feedback-echo-drift-x-val">6 px</span></label>
<input type="range" id="feedback-echo-drift-x" min="-40" max="40" value="6" data-default="6" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-drift-x-val',this.value+' px');onFeedbackEchoSettingsChange()">
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Y Drift <span id="feedback-echo-drift-y-val">2 px</span></label>
<input type="range" id="feedback-echo-drift-y" min="-40" max="40" value="2" data-default="2" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-drift-y-val',this.value+' px');onFeedbackEchoSettingsChange()">
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Mix <span id="feedback-echo-mix-val">100%</span></label>
<input type="range" id="feedback-echo-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-mix-val',this.value+'%');onFeedbackEchoSettingsChange()">
</div>
<div class="toggle-row feedback-echo-control hidden-row">
<span>Animate Echo</span>
<label class="toggle"><input type="checkbox" id="feedback-echo-animate" onchange="onFeedbackEchoSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Apply Curve</label>
<select id="feedback-echo-apply" onchange="onFeedbackEchoSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Start Offset <span id="feedback-echo-offset-val">0%</span></label>
<input type="range" id="feedback-echo-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-offset-val',this.value+'%');onFeedbackEchoSettingsChange(true)">
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Cycle Frames <span id="feedback-echo-frames-val">20</span></label>
<input type="range" id="feedback-echo-frames" min="2" max="60" value="20" data-default="20" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-frames-val',this.value);onFeedbackEchoSettingsChange(true)">
</div>
<div class="control-row feedback-echo-control hidden-row">
<label>Phase Speed <span id="feedback-echo-step-val">1.00×</span></label>
<input type="range" id="feedback-echo-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('feedback-echo-step-val',Number(this.value).toFixed(2)+'×');onFeedbackEchoSettingsChange(true)">
</div>
<div class="control-note feedback-echo-control" id="feedback-echo-note">Feedback Echo blends delayed frames into ghost trails on GIF and video sources, and uses a directional fallback for still-image loops.</div>
</div>
</details>
<details class="section-collapsible motion-effect-card" id="smear-trail-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Smear Trail</span>
<span class="motion-effect-subtitle">Directional drag with edge-weighted streaks, speed lines, and paint-like pull.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="smear-trail-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="smear-trail-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Smear Trail</span>
<label class="toggle"><input type="checkbox" id="smear-trail-enable" onchange="onSmearTrailSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Mode</label>
<select id="smear-trail-mode" onchange="onSmearTrailSettingsChange()">
<option value="horizontal">Horizontal</option>
<option value="vertical">Vertical</option>
<option value="diagonal">Diagonal</option>
<option value="radial">Radial</option>
</select>
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Length <span id="smear-trail-length-val">18 px</span></label>
<input type="range" id="smear-trail-length" min="1" max="64" value="18" data-default="18" ondblclick="resetSlider(this)" oninput="updateVal('smear-trail-length-val',this.value+' px');onSmearTrailSettingsChange()">
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Samples <span id="smear-trail-samples-val">5</span></label>
<input type="range" id="smear-trail-samples" min="2" max="10" value="5" data-default="5" ondblclick="resetSlider(this)" oninput="updateVal('smear-trail-samples-val',this.value);onSmearTrailSettingsChange()">
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Edge Focus <span id="smear-trail-edge-val">60%</span></label>
<input type="range" id="smear-trail-edge" min="0" max="100" value="60" data-default="60" ondblclick="resetSlider(this)" oninput="updateVal('smear-trail-edge-val',this.value+'%');onSmearTrailSettingsChange()">
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Mix <span id="smear-trail-mix-val">100%</span></label>
<input type="range" id="smear-trail-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('smear-trail-mix-val',this.value+'%');onSmearTrailSettingsChange()">
</div>
<div class="toggle-row smear-trail-control hidden-row">
<span>Animate Smear</span>
<label class="toggle"><input type="checkbox" id="smear-trail-animate" onchange="onSmearTrailSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Apply Curve</label>
<select id="smear-trail-apply" onchange="onSmearTrailSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Start Offset <span id="smear-trail-offset-val">0%</span></label>
<input type="range" id="smear-trail-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('smear-trail-offset-val',this.value+'%');onSmearTrailSettingsChange(true)">
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Cycle Frames <span id="smear-trail-frames-val">20</span></label>
<input type="range" id="smear-trail-frames" min="2" max="60" value="20" data-default="20" ondblclick="resetSlider(this)" oninput="updateVal('smear-trail-frames-val',this.value);onSmearTrailSettingsChange(true)">
</div>
<div class="control-row smear-trail-control hidden-row">
<label>Phase Speed <span id="smear-trail-step-val">1.00×</span></label>
<input type="range" id="smear-trail-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('smear-trail-step-val',Number(this.value).toFixed(2)+'×');onSmearTrailSettingsChange(true)">
</div>
<div class="control-note smear-trail-control" id="smear-trail-note">Smear Trail drags edges into directional streaks and works well for painterly speed lines or analog drag.</div>
</div>
</details>
<div class="post-divider-label">Field Warp</div>
<details class="section-collapsible motion-effect-card" id="swirl-field-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Swirl / Twirl Field</span>
<span class="motion-effect-subtitle">Radial twist and orbit-style remapping for vortex and poster-warp motion.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="swirl-field-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="swirl-field-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Swirl / Twirl</span>
<label class="toggle"><input type="checkbox" id="swirl-field-enable" onchange="onSwirlFieldSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Mode</label>
<select id="swirl-field-mode" onchange="onSwirlFieldSettingsChange()">
<option value="swirl">Swirl</option>
<option value="pinwheel">Pinwheel</option>
<option value="wobble">Wobble</option>
</select>
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Amount <span id="swirl-field-amount-val">16 px</span></label>
<input type="range" id="swirl-field-amount" min="0" max="60" value="16" data-default="16" ondblclick="resetSlider(this)" oninput="updateVal('swirl-field-amount-val',this.value+' px');onSwirlFieldSettingsChange()">
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Radius <span id="swirl-field-radius-val">70%</span></label>
<input type="range" id="swirl-field-radius" min="10" max="100" value="70" data-default="70" ondblclick="resetSlider(this)" oninput="updateVal('swirl-field-radius-val',this.value+'%');onSwirlFieldSettingsChange()">
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Mix <span id="swirl-field-mix-val">100%</span></label>
<input type="range" id="swirl-field-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('swirl-field-mix-val',this.value+'%');onSwirlFieldSettingsChange()">
</div>
<div class="toggle-row swirl-field-control hidden-row">
<span>Animate Swirl</span>
<label class="toggle"><input type="checkbox" id="swirl-field-animate" onchange="onSwirlFieldSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Apply Curve</label>
<select id="swirl-field-apply" onchange="onSwirlFieldSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Start Offset <span id="swirl-field-offset-val">0%</span></label>
<input type="range" id="swirl-field-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('swirl-field-offset-val',this.value+'%');onSwirlFieldSettingsChange(true)">
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Cycle Frames <span id="swirl-field-frames-val">22</span></label>
<input type="range" id="swirl-field-frames" min="2" max="60" value="22" data-default="22" ondblclick="resetSlider(this)" oninput="updateVal('swirl-field-frames-val',this.value);onSwirlFieldSettingsChange(true)">
</div>
<div class="control-row swirl-field-control hidden-row">
<label>Phase Speed <span id="swirl-field-step-val">1.00×</span></label>
<input type="range" id="swirl-field-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('swirl-field-step-val',Number(this.value).toFixed(2)+'×');onSwirlFieldSettingsChange(true)">
</div>
<div class="control-note swirl-field-control" id="swirl-field-note">Swirl / Twirl Field twists the image around a center-weighted field for vortex, pinwheel, or lens-like motion.</div>
</div>
</details>
<div class="post-divider-label">Mapped Motion</div>
<details class="section-collapsible motion-effect-card" id="displacement-map-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Directional Displacement Map</span>
<span class="motion-effect-subtitle">Flow, contour, or push motion driven by image luminance or channel values.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="displacement-map-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="displacement-map-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Displacement Map</span>
<label class="toggle"><input type="checkbox" id="displacement-map-enable" onchange="onDisplacementMapSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Map Source</label>
<select id="displacement-map-source" onchange="onDisplacementMapSettingsChange()">
<option value="luminance">Luminance</option>
<option value="red">Red Channel</option>
<option value="green">Green Channel</option>
<option value="blue">Blue Channel</option>
</select>
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Mode</label>
<select id="displacement-map-mode" onchange="onDisplacementMapSettingsChange()">
<option value="flow">Flow</option>
<option value="contour">Contour</option>
<option value="push">Push</option>
</select>
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Amount <span id="displacement-map-amount-val">12 px</span></label>
<input type="range" id="displacement-map-amount" min="0" max="48" value="12" data-default="12" ondblclick="resetSlider(this)" oninput="updateVal('displacement-map-amount-val',this.value+' px');onDisplacementMapSettingsChange()">
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Sample Radius <span id="displacement-map-scale-val">4 px</span></label>
<input type="range" id="displacement-map-scale" min="1" max="16" value="4" data-default="4" ondblclick="resetSlider(this)" oninput="updateVal('displacement-map-scale-val',this.value+' px');onDisplacementMapSettingsChange()">
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Push Angle <span id="displacement-map-angle-val">90°</span></label>
<input type="range" id="displacement-map-angle" min="0" max="360" value="90" data-default="90" ondblclick="resetSlider(this)" oninput="updateVal('displacement-map-angle-val',this.value+'°');onDisplacementMapSettingsChange()">
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Mix <span id="displacement-map-mix-val">100%</span></label>
<input type="range" id="displacement-map-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('displacement-map-mix-val',this.value+'%');onDisplacementMapSettingsChange()">
</div>
<div class="toggle-row displacement-map-control hidden-row">
<span>Animate Map</span>
<label class="toggle"><input type="checkbox" id="displacement-map-animate" onchange="onDisplacementMapSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Apply Curve</label>
<select id="displacement-map-apply" onchange="onDisplacementMapSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Start Offset <span id="displacement-map-offset-val">0%</span></label>
<input type="range" id="displacement-map-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('displacement-map-offset-val',this.value+'%');onDisplacementMapSettingsChange(true)">
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Cycle Frames <span id="displacement-map-frames-val">24</span></label>
<input type="range" id="displacement-map-frames" min="2" max="60" value="24" data-default="24" ondblclick="resetSlider(this)" oninput="updateVal('displacement-map-frames-val',this.value);onDisplacementMapSettingsChange(true)">
</div>
<div class="control-row displacement-map-control hidden-row">
<label>Phase Speed <span id="displacement-map-step-val">1.00×</span></label>
<input type="range" id="displacement-map-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('displacement-map-step-val',Number(this.value).toFixed(2)+'×');onDisplacementMapSettingsChange(true)">
</div>
<div class="control-note displacement-map-control" id="displacement-map-note">Directional Displacement Map bends the frame along luminance or channel-derived flow lines for contour and field-driven motion.</div>
</div>
</details>
<details class="section-collapsible motion-effect-card" id="pixel-dissolve-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Pixel Dissolve / Reassemble</span>
<span class="motion-effect-subtitle">Blocky scatter, drift, or implode motion that breaks the image apart and pulls it back together.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="pixel-dissolve-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="pixel-dissolve-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Pixel Dissolve</span>
<label class="toggle"><input type="checkbox" id="pixel-dissolve-enable" onchange="onPixelDissolveSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Mode</label>
<select id="pixel-dissolve-mode" onchange="onPixelDissolveSettingsChange()">
<option value="scatter">Scatter</option>
<option value="drift">Drift</option>
<option value="implode">Implode</option>
</select>
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Block Size <span id="pixel-dissolve-size-val">16 px</span></label>
<input type="range" id="pixel-dissolve-size" min="2" max="48" value="16" data-default="16" ondblclick="resetSlider(this)" oninput="updateVal('pixel-dissolve-size-val',this.value+' px');onPixelDissolveSettingsChange()">
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Spread <span id="pixel-dissolve-spread-val">20 px</span></label>
<input type="range" id="pixel-dissolve-spread" min="0" max="80" value="20" data-default="20" ondblclick="resetSlider(this)" oninput="updateVal('pixel-dissolve-spread-val',this.value+' px');onPixelDissolveSettingsChange()">
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Coverage <span id="pixel-dissolve-coverage-val">65%</span></label>
<input type="range" id="pixel-dissolve-coverage" min="0" max="100" value="65" data-default="65" ondblclick="resetSlider(this)" oninput="updateVal('pixel-dissolve-coverage-val',this.value+'%');onPixelDissolveSettingsChange()">
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Mix <span id="pixel-dissolve-mix-val">100%</span></label>
<input type="range" id="pixel-dissolve-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('pixel-dissolve-mix-val',this.value+'%');onPixelDissolveSettingsChange()">
</div>
<div class="toggle-row pixel-dissolve-control hidden-row">
<span>Animate Dissolve</span>
<label class="toggle"><input type="checkbox" id="pixel-dissolve-animate" onchange="onPixelDissolveSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Apply Curve</label>
<select id="pixel-dissolve-apply" onchange="onPixelDissolveSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Start Offset <span id="pixel-dissolve-offset-val">0%</span></label>
<input type="range" id="pixel-dissolve-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('pixel-dissolve-offset-val',this.value+'%');onPixelDissolveSettingsChange(true)">
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Cycle Frames <span id="pixel-dissolve-frames-val">24</span></label>
<input type="range" id="pixel-dissolve-frames" min="2" max="60" value="24" data-default="24" ondblclick="resetSlider(this)" oninput="updateVal('pixel-dissolve-frames-val',this.value);onPixelDissolveSettingsChange(true)">
</div>
<div class="control-row pixel-dissolve-control hidden-row">
<label>Phase Speed <span id="pixel-dissolve-step-val">1.00×</span></label>
<input type="range" id="pixel-dissolve-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('pixel-dissolve-step-val',Number(this.value).toFixed(2)+'×');onPixelDissolveSettingsChange(true)">
</div>
<div class="control-note pixel-dissolve-control" id="pixel-dissolve-note">Pixel Dissolve / Reassemble breaks the image into drifting block clusters that can scatter outward or collapse back toward the center.</div>
</div>
</details>
<div class="post-divider-label">Index & Color</div>
<details class="section-collapsible motion-effect-card" id="palette-cycle-details">
<summary class="section-title collapsible motion-effect-summary">
<span class="motion-effect-heading">
<span class="motion-effect-name">Palette Phase Cycling</span>
<span class="motion-effect-subtitle">Re-maps the dithered palette through rotating, wave-driven, or luminance-biased index shifts.</span>
</span>
<span class="motion-effect-meta">
<span class="motion-status-pill is-off" id="palette-cycle-status-pill">Off</span>
<span class="motion-status-pill is-detail is-hidden" id="palette-cycle-detail-pill"></span>
<span class="collapse-arrow">▶</span>
</span>
</summary>
<div class="collapsible-content">
<div class="toggle-row">
<span>Enable Palette Cycling</span>
<label class="toggle"><input type="checkbox" id="palette-cycle-enable" onchange="onPaletteCycleSettingsChange()"><span class="toggle-slider"></span></label>
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Mode</label>
<select id="palette-cycle-mode" onchange="onPaletteCycleSettingsChange()">
<option value="rotate">Rotate</option>
<option value="wave">Wave</option>
<option value="luma">Luma Shift</option>
</select>
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Index Shift <span id="palette-cycle-steps-val">2</span></label>
<input type="range" id="palette-cycle-steps" min="1" max="12" value="2" data-default="2" ondblclick="resetSlider(this)" oninput="updateVal('palette-cycle-steps-val',this.value);onPaletteCycleSettingsChange()">
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Frequency <span id="palette-cycle-frequency-val">2.0</span></label>
<input type="range" id="palette-cycle-frequency" min="0.5" max="8" step="0.5" value="2" data-default="2" ondblclick="resetSlider(this)" oninput="updateVal('palette-cycle-frequency-val',Number(this.value).toFixed(1));onPaletteCycleSettingsChange()">
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Mix <span id="palette-cycle-mix-val">100%</span></label>
<input type="range" id="palette-cycle-mix" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('palette-cycle-mix-val',this.value+'%');onPaletteCycleSettingsChange()">
</div>
<div class="toggle-row palette-cycle-control hidden-row">
<span>Animate Cycling</span>
<label class="toggle"><input type="checkbox" id="palette-cycle-animate" onchange="onPaletteCycleSettingsChange(true)"><span class="toggle-slider"></span></label>
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Apply Curve</label>
<select id="palette-cycle-apply" onchange="onPaletteCycleSettingsChange(true)">
<option value="constant">Loop Full Strength</option>
<option value="build-in">Build In</option>
<option value="ease-in-out">Ease In-Out</option>
<option value="build-out">Build Out</option>
<option value="pulse">Pulse</option>
<option value="hold-drop">Hold Then Drop</option>
<option value="burst">Burst</option>
<option value="stepped">Stepped Ramp</option>
</select>
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Start Offset <span id="palette-cycle-offset-val">0%</span></label>
<input type="range" id="palette-cycle-offset" min="0" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('palette-cycle-offset-val',this.value+'%');onPaletteCycleSettingsChange(true)">
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Cycle Frames <span id="palette-cycle-frames-val">24</span></label>
<input type="range" id="palette-cycle-frames" min="2" max="60" value="24" data-default="24" ondblclick="resetSlider(this)" oninput="updateVal('palette-cycle-frames-val',this.value);onPaletteCycleSettingsChange(true)">
</div>
<div class="control-row palette-cycle-control hidden-row">
<label>Phase Speed <span id="palette-cycle-step-val">1.00×</span></label>
<input type="range" id="palette-cycle-step" min="0.25" max="8" step="0.25" value="1" data-default="1" ondblclick="resetSlider(this)" oninput="updateVal('palette-cycle-step-val',Number(this.value).toFixed(2)+'×');onPaletteCycleSettingsChange(true)">
</div>
<div class="control-note palette-cycle-control" id="palette-cycle-note">Palette Phase Cycling remaps the dithered result through palette index shifts for old-school fire, water, neon, and monitor-style color motion.</div>
</div>
</details>
<details class="section-collapsible" id="still-frame-details" open="">
<summary class="section-title collapsible">Frame Sequence <span class="collapse-arrow">▶</span></summary>
<div class="collapsible-content">
<div class="preset-row" id="anim-frame-actions">
<button class="preset-btn" id="generate-pattern-frames-btn" onclick="generatePatternFramesForStillImage()">Generate Frames</button>
<button class="preset-btn" id="clear-generated-frames-btn" onclick="clearGeneratedStillFrames()">Clear Generated</button>
</div>
<div class="control-note" id="still-frame-note">Generate a real frame sequence from your current still-image animation settings so you can scrub, preview, and export it.</div>
</div>
</details>
</div>
<div class="panel-section">
<div class="section-title">Tone</div>
<details class="section-collapsible" id="tone-curve-details" open="">
<summary class="section-title collapsible">Tone Curve <span class="collapse-arrow">▶</span></summary>
<div class="collapsible-content">
<div class="curve-canvas-wrap">
<canvas id="tone-curve-canvas" width="200" height="120"></canvas>
</div>
<div class="curve-presets">
<button class="preset-btn" onclick="setCurvePreset('linear')">Linear</button>
<button class="preset-btn" onclick="setCurvePreset('s-curve')">S-Curve</button>
<button class="preset-btn" onclick="setCurvePreset('high-contrast')">Hi-Con</button>
<button class="preset-btn" onclick="setCurvePreset('low-contrast')">Lo-Con</button>
<button class="preset-btn" onclick="setCurvePreset('shadows')">Shadows</button>
<button class="preset-btn" onclick="setCurvePreset('highlights')">Highlights</button>
</div>
</div>
</details>
</div>
</div>
<!-- PRESETS TAB -->
<div class="panel-content active" id="tab-presets">
<div class="panel-section workflow-guide">
<div class="section-title">Workflow</div>
<div class="workflow-steps">
<div class="workflow-step" onclick="switchTab('settings')">
<span class="step-icon">◐</span>
<span class="step-label">Dither</span>
<span class="step-desc">Algorithm</span>
</div>
<div class="workflow-step" onclick="switchTab('color')">
<span class="step-icon">◔</span>
<span class="step-label">Color</span>
<span class="step-desc">Palette</span>
</div>
<div class="workflow-step" onclick="switchTab('channels')">
<span class="step-icon">◨</span>
<span class="step-label">Channels</span>
<span class="step-desc">RGB</span>
</div>
<div class="workflow-step" onclick="switchTab('post')">
<span class="step-icon">◌</span>
<span class="step-label">Post</span>
<span class="step-desc">Effects</span>
</div>
<div class="workflow-step" onclick="switchTab('export')">
<span class="step-icon">⬇</span>
<span class="step-label">Export</span>
<span class="step-desc">Save</span>
</div>
</div>
</div>
<div class="panel-section">
<div class="section-title">Current Settings</div>
<div class="settings-summary" id="settings-summary">
<span class="summary-tag"><strong id="summary-algo">Floyd-Steinberg</strong></span>
<span class="summary-tag">
<span class="summary-swatches" id="summary-swatches"></span>
<strong id="summary-palette">2</strong> colors
</span>
<span class="summary-tag hidden" id="summary-channel"><strong>Per-Channel</strong></span>
<span class="summary-tag" id="summary-threshold">T: <strong>128</strong></span>
</div>
<div class="quick-actions">
<button class="quick-btn" onclick="swapPaletteColors()" title="Swap foreground/background">⇄ Swap</button>
<button class="quick-btn" onclick="randomizeSettings()" title="Random settings">✶ Random</button>
<button class="quick-btn" onclick="randomizeMotionOnly()" title="Randomize only the motion stack">↻ Motion</button>
<button class="quick-btn" onclick="autoThreshold()" title="Auto-detect threshold from histogram">◐ Auto T</button>
<button class="quick-btn" onclick="copySettingsLink()" title="Copy settings link">⧉ Share</button>
</div>
<div class="quick-seed-row">
<button class="quick-seed-readout" id="random-seed-readout" type="button" onclick="copyRandomSeed()" title="Copy last random seed">Seed: Ready</button>
<span class="quick-seed-meta" id="random-seed-meta">Full stack and motion rerolls stamp the latest seed here.</span>
</div>
</div>
<div class="panel-section">
<div class="section-title">
Quick Start
<button class="preset-btn reset-all-btn" onclick="resetAllSettings()" title="Reset all settings to defaults">Reset All</button>
</div>
<div class="preset-search-wrap">
<input type="text" id="preset-search" class="preset-search" placeholder="Search presets..." oninput="filterPresets(this.value)">
<span class="preset-search-icon">🔍</span>
</div>
<div class="recent-presets" id="recent-presets"></div>
<div id="preset-grid"><!-- built by JS --></div>
</div>
<div class="panel-section tips-section">
<div class="section-title">Shortcuts</div>
<div class="shortcut-grid">
<span class="shortcut-key">Space</span><span class="shortcut-desc">Toggle original/dithered</span>
<span class="shortcut-key">Z</span><span class="shortcut-desc">Undo</span>
<span class="shortcut-key">⇧Z</span><span class="shortcut-desc">Redo</span>
<span class="shortcut-key">F</span><span class="shortcut-desc">Fit to viewport</span>
<span class="shortcut-key">1-4</span><span class="shortcut-desc">View modes</span>
</div>
</div>
</div>
<!-- COLOR TAB -->
<div class="panel-content" id="tab-color">
<div class="panel-section">
<div class="section-title">
Custom Palette
<span id="pal-n-info" class="pal-n-info">2 colors</span>
</div>
<div class="tab-intro">Build your output palette. With 3+ colors, multi-color dithering becomes available.</div>
<div id="pal-editor"><!-- built by JS --></div>
<div class="palette-btn-row">
<button class="preset-btn" onclick="reversePalette()" title="Reverse palette order">⇆ Reverse</button>
<button class="preset-btn" id="eyedropper-btn" onclick="toggleEyedropper()" title="Pick color from canvas">◉ Pick</button>
</div>
<div class="section-title compact">Extract from Image</div>
<div class="extract-options">
<button class="extract-btn" onclick="extractPalette(2)" title="Extract 2 dominant colors">2 colors</button>
<button class="extract-btn" onclick="extractPalette(4)" title="Extract 4 dominant colors">4 colors</button>
<button class="extract-btn" onclick="extractPalette(6)" title="Extract 6 dominant colors">6 colors</button>
<button class="extract-btn" onclick="extractPalette(8)" title="Extract 8 dominant colors">8 colors</button>
<button class="extract-btn" onclick="extractPalette(16)" title="Extract 16 dominant colors">16 colors</button>
</div>
</div>
<div class="panel-section">
<div class="section-title">Gradient Generator</div>
<div class="gradient-gen">
<div class="gradient-colors">
<input type="color" id="grad-start" value="#000000" class="grad-color" title="Start color" oninput="updateGradientPreview()">
<div class="gradient-preview" id="gradient-preview"></div>
<input type="color" id="grad-end" value="#ffffff" class="grad-color" title="End color" oninput="updateGradientPreview()">
</div>
<div class="gradient-controls">
<span class="grad-steps-label">4 colors</span>
<input type="range" id="grad-steps" min="2" max="16" value="4" oninput="updateGradientPreview()">
<button class="preset-btn" onclick="applyGradientPalette()" title="Apply gradient as palette">→ Apply</button>
</div>
</div>
</div>
<div class="panel-section">
<div class="section-title">Duo-Tone Presets</div>
<div id="palette-grid"></div>
</div>
<div class="panel-section">
<div class="section-title">Multi-Color Palettes</div>
<div id="multi-palette-grid"></div>
</div>
<div class="panel-section">
<div class="section-title">Color Mapping</div>
<div class="control-row">
<label>Multi-Color Mode</label>
<select id="multicolor-mode" onchange="updateAdvancedColorUI();scheduleProcess()">
<option value="algorithm">Follow Algorithm</option>
<option value="diffusion">Error Diffusion</option>
<option value="ordered">Ordered Matrix</option>
<option value="nearest">Nearest Color (No Dither)</option>
</select>
</div>
<div class="control-row">
<label>Strength <span id="multicolor-strength-val">100%</span></label>
<input type="range" id="multicolor-strength" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('multicolor-strength-val',this.value+'%');scheduleProcess()">
</div>
<div class="control-row">
<label>Distance Metric</label>
<select id="color-distance" onchange="updateAdvancedColorUI();scheduleProcess()">
<option value="rgb">RGB Euclidean</option>
<option value="weighted">Perceptual (Weighted)</option>
</select>
</div>
<div class="subsection-note" id="multicolor-note"></div>
</div>
<div class="panel-section">
<div class="section-title">Source</div>
<div class="control-row">
<label>Luminance Channel</label>
<select id="channel-mode" onchange="scheduleProcess()">
<option value="luminance">Luminance (Rec. 709)</option>
<option value="red">Red Only</option>
<option value="green">Green Only</option>
<option value="blue">Blue Only</option>
<option value="average">Average RGB</option>
</select>
</div>
<div class="control-row">
<label>Gamma <span id="gamma-val">1.00</span></label>
<input type="range" id="gamma" min="0.1" max="3.0" step="0.05" value="1.0" data-default="1.0" ondblclick="resetSlider(this)" oninput="updateVal('gamma-val',parseFloat(this.value).toFixed(2));scheduleProcess()">
</div>
<div class="channel-link" onclick="switchTab('channels')">
<span>⚡</span> For independent R/G/B dithering, see <strong>Channels</strong> tab
</div>
<div class="control-note" id="color-controls-hint"></div>
</div>
</div>
<!-- CHANNELS TAB -->
<div class="panel-content" id="tab-channels">
<div class="panel-section">
<div class="section-title">Channel Mode</div>
<div class="tab-intro">Dither R, G, B channels independently for chromatic effects.</div>
<div class="toggle-row channel-enable-row">
<span title="Process R, G, B channels independently with separate dither algorithms">Enable Per-Channel</span>
<label class="toggle"><input type="checkbox" id="per-channel-tab" onchange="onPerChannelTabChange()"><span class="toggle-slider"></span></label>
</div>
<div class="channels-status" id="channels-status">
<span class="status-dot"></span>
<span id="channels-status-text">Per-channel mode is off</span>
</div>
<div class="subsection-title">Global</div>
<div class="control-row">
<label>Default Algorithm</label>
<select id="per-channel-kernel" onchange="updateAdvancedColorUI();scheduleProcess()">
<option value="match">Use Main Algorithm</option>
<option value="floyd">Floyd-Steinberg</option>
<option value="atkinson">Atkinson</option>
<option value="sierra">Sierra</option>
<option value="sierra2">Sierra 2-Row</option>
<option value="sierra-lite">Sierra Lite</option>
<option value="burkes">Burkes</option>
<option value="fan">Fan</option>
<option value="false-fs">False Floyd-Steinberg</option>
</select>
</div>
<div class="control-row">
<label>Error Diffusion <span id="per-channel-error-val">100%</span></label>
<input type="range" id="per-channel-error" min="0" max="100" value="100" data-default="100" ondblclick="resetSlider(this)" oninput="updateVal('per-channel-error-val',this.value+'%');scheduleProcess()">
</div>
<div class="toggle-row">
<span title="Map per-channel result to nearest palette color">Map to Palette</span>
<label class="toggle"><input type="checkbox" id="per-channel-map-palette" onchange="updateAdvancedColorUI();scheduleProcess()"><span class="toggle-slider"></span></label>
</div>
<div class="toggle-row">
<span title="Use independent brightness/contrast/levels per channel">Per-Channel Processing</span>
<label class="toggle"><input type="checkbox" id="per-channel-processing" onchange="updateAdvancedColorUI();scheduleProcess()"><span class="toggle-slider"></span></label>
</div>
</div>
<div class="panel-section">
<div class="section-title">Channels</div>
<div class="channel-grid">
<!-- Red Channel -->
<details class="channel-settings red" id="per-ch-r-details" open="">
<summary class="channel-header">
<span class="ch-icon red">R</span>
<span>Red Channel</span>
<span class="ch-arrow">▶</span>
</summary>
<div class="channel-content">
<div class="ch-subsection">Dither</div>
<div class="control-row">
<label>Algorithm</label>
<select id="per-channel-r-algo" onchange="updateAdvancedColorUI();scheduleProcess()"></select>
</div>
<div class="control-row">
<label>Output Color</label>
<div class="ch-color-select" id="per-ch-r-color-wrap"></div>
</div>
<div class="control-row">
<label>Threshold Bias <span id="ch-bias-r-val">0</span></label>
<input type="range" id="ch-bias-r" min="-128" max="128" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('ch-bias-r-val',this.value);scheduleProcess()">
</div>
<div class="ch-subsection">Processing</div>
<div class="control-row">
<label>Threshold <span id="per-ch-r-threshold-val">50%</span></label>
<input type="range" id="per-ch-r-threshold" min="0" max="100" value="50" data-default="50" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-r-threshold-val',this.value+'%');scheduleProcess()">
</div>
<div class="control-row">
<label>Brightness <span id="per-ch-r-brightness-val">0</span></label>
<input type="range" id="per-ch-r-brightness" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-r-brightness-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Contrast <span id="per-ch-r-contrast-val">0</span></label>
<input type="range" id="per-ch-r-contrast" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-r-contrast-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Black Point <span id="per-ch-r-black-val">0</span></label>
<input type="range" id="per-ch-r-black" min="0" max="255" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-r-black-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>White Point <span id="per-ch-r-white-val">255</span></label>
<input type="range" id="per-ch-r-white" min="0" max="255" value="255" data-default="255" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-r-white-val',this.value);scheduleProcess()">
</div>
</div>
</details>
<!-- Green Channel -->
<details class="channel-settings green" id="per-ch-g-details">
<summary class="channel-header">
<span class="ch-icon green">G</span>
<span>Green Channel</span>
<span class="ch-arrow">▶</span>
</summary>
<div class="channel-content">
<div class="ch-subsection">Dither</div>
<div class="control-row">
<label>Algorithm</label>
<select id="per-channel-g-algo" onchange="updateAdvancedColorUI();scheduleProcess()"></select>
</div>
<div class="control-row">
<label>Output Color</label>
<div class="ch-color-select" id="per-ch-g-color-wrap"></div>
</div>
<div class="control-row">
<label>Threshold Bias <span id="ch-bias-g-val">0</span></label>
<input type="range" id="ch-bias-g" min="-128" max="128" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('ch-bias-g-val',this.value);scheduleProcess()">
</div>
<div class="ch-subsection">Processing</div>
<div class="control-row">
<label>Threshold <span id="per-ch-g-threshold-val">50%</span></label>
<input type="range" id="per-ch-g-threshold" min="0" max="100" value="50" data-default="50" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-g-threshold-val',this.value+'%');scheduleProcess()">
</div>
<div class="control-row">
<label>Brightness <span id="per-ch-g-brightness-val">0</span></label>
<input type="range" id="per-ch-g-brightness" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-g-brightness-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Contrast <span id="per-ch-g-contrast-val">0</span></label>
<input type="range" id="per-ch-g-contrast" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-g-contrast-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Black Point <span id="per-ch-g-black-val">0</span></label>
<input type="range" id="per-ch-g-black" min="0" max="255" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-g-black-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>White Point <span id="per-ch-g-white-val">255</span></label>
<input type="range" id="per-ch-g-white" min="0" max="255" value="255" data-default="255" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-g-white-val',this.value);scheduleProcess()">
</div>
</div>
</details>
<!-- Blue Channel -->
<details class="channel-settings blue" id="per-ch-b-details">
<summary class="channel-header">
<span class="ch-icon blue">B</span>
<span>Blue Channel</span>
<span class="ch-arrow">▶</span>
</summary>
<div class="channel-content">
<div class="ch-subsection">Dither</div>
<div class="control-row">
<label>Algorithm</label>
<select id="per-channel-b-algo" onchange="updateAdvancedColorUI();scheduleProcess()"></select>
</div>
<div class="control-row">
<label>Output Color</label>
<div class="ch-color-select" id="per-ch-b-color-wrap"></div>
</div>
<div class="control-row">
<label>Threshold Bias <span id="ch-bias-b-val">0</span></label>
<input type="range" id="ch-bias-b" min="-128" max="128" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('ch-bias-b-val',this.value);scheduleProcess()">
</div>
<div class="ch-subsection">Processing</div>
<div class="control-row">
<label>Threshold <span id="per-ch-b-threshold-val">50%</span></label>
<input type="range" id="per-ch-b-threshold" min="0" max="100" value="50" data-default="50" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-b-threshold-val',this.value+'%');scheduleProcess()">
</div>
<div class="control-row">
<label>Brightness <span id="per-ch-b-brightness-val">0</span></label>
<input type="range" id="per-ch-b-brightness" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-b-brightness-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Contrast <span id="per-ch-b-contrast-val">0</span></label>
<input type="range" id="per-ch-b-contrast" min="-100" max="100" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-b-contrast-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>Black Point <span id="per-ch-b-black-val">0</span></label>
<input type="range" id="per-ch-b-black" min="0" max="255" value="0" data-default="0" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-b-black-val',this.value);scheduleProcess()">
</div>
<div class="control-row">
<label>White Point <span id="per-ch-b-white-val">255</span></label>
<input type="range" id="per-ch-b-white" min="0" max="255" value="255" data-default="255" ondblclick="resetSlider(this)" oninput="updateVal('per-ch-b-white-val',this.value);scheduleProcess()">
</div>
</div>
</details>
</div>
<div class="control-note" id="per-channel-summary"></div>
</div>
</div>
<!-- EXPORT TAB -->
<div class="panel-content" id="tab-export">
<div class="panel-section">
<div class="section-title">Source Info</div>
<div class="tab-intro">Your loaded image stats. Export options change based on image type.</div>
<div id="file-info-grid">
<div class="info-tag"><span>File</span><span id="info-name">—</span></div>
<div class="info-tag"><span>Type</span><span id="info-type">—</span></div>
<div class="info-tag"><span>Size</span><span id="info-size">—</span></div>
<div class="info-tag"><span>Dims</span><span id="info-dims">—</span></div>
<div class="info-tag"><span>Frames</span><span id="info-frames">—</span></div>
</div>
</div>
<div class="panel-section">
<div class="section-title">Output Stats</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Unique Colors</span>
<span class="stat-value" id="stat-unique-colors">—</span>
</div>
<div class="stat-item">
<span class="stat-label">Palette Usage</span>
<span class="stat-value" id="stat-palette-usage">—</span>
</div>
<div class="stat-item">
<span class="stat-label">Est. PNG Size</span>
<span class="stat-value" id="stat-png-size">—</span>
</div>
<div class="stat-item">
<span class="stat-label">Compression</span>
<span class="stat-value" id="stat-compression">—</span>
</div>
</div>
<button class="preset-btn full-width-btn" onclick="updateOutputStats()">↻ Refresh Stats</button>
</div>
<div class="panel-section">
<div class="section-title">View Options</div>
<div class="toggle-row">
<span>Pixel Grid (at zoom ≥200%)</span>
<label class="toggle"><input type="checkbox" id="pixel-grid-toggle" onchange="togglePixelGrid()"><span class="toggle-slider"></span></label>
</div>
</div>
<div class="panel-section export-section">
<div class="section-title">Image Export</div>
<div class="num-row">
<label>Width (px)</label>
<input type="number" id="exp-width" value="" min="1" max="16384" placeholder="auto" oninput="updateExpLabel()">
</div>
<div class="exp-dims-label" id="exp-dims-label">—</div>
<div class="preset-row">
<button class="preset-btn" onclick="setExpScale(0.25)">25%</button>
<button class="preset-btn" onclick="setExpScale(0.5)">50%</button>
<button class="preset-btn" onclick="setExpScale(1)">1×</button>
<button class="preset-btn" onclick="setExpScale(2)">2×</button>
<button class="preset-btn" onclick="setExpScale(4)">4×</button>
</div>
<div class="exp-btn-group">
<button class="exp-btn" onclick="exportPNG()">Export PNG <span class="ext">.png</span></button>
<button class="exp-btn" onclick="exportWebP()">Export WebP <span class="ext">.webp</span></button>
<button class="exp-btn" onclick="exportJPEG()">Export JPEG <span class="ext">.jpg</span></button>
<button class="exp-btn" onclick="exportSVG()">Export SVG <span class="ext">.svg</span></button>
<button class="exp-btn" onclick="copyToClipboard()">Copy to Clipboard <span class="ext">PNG</span></button>
</div>
</div>
<div class="panel-section export-section" id="anim-export-section">
<div class="section-title">Animation Export</div>
<div class="control-row">
<label>Preview & Export FPS <span id="gif-fps-val">12</span></label>
<input type="range" id="gif-fps" min="1" max="60" value="12" data-default="12" ondblclick="resetSlider(this)" oninput="updateVal('gif-fps-val',this.value);if(sourceType==='gif'&&isPlaying){clearGIFTimer();scheduleGIFFrame();}">
</div>
<div class="control-row">
<label>Max Frames <span id="gif-maxframes-val">60</span></label>
<input type="range" id="gif-maxframes" min="1" max="300" value="60" data-default="60" ondblclick="resetSlider(this)" oninput="updateVal('gif-maxframes-val',this.value);clearGeneratedStillFrames(false);updateAnimationExportState()">
</div>
<div class="control-row">
<label>Loop Mode</label>
<select id="gif-loop-mode" onchange="syncGifPlaybackControls()">
<option value="infinite">Infinite Loop</option>
<option value="once">Play Once</option>
<option value="count">Custom Plays</option>
</select>
</div>
<div class="control-row hidden-row" id="gif-loop-count-row">
<label>Total Plays</label>
<input type="number" id="gif-loop-count" min="1" max="99" value="3" data-default="3" oninput="syncGifPlaybackControls()">
</div>
<div class="control-row">
<label>Playback Direction</label>
<select id="gif-playback-direction" onchange="updateAnimationExportState()">
<option value="forward">Forward</option>
<option value="pingpong">Ping-Pong</option>
</select>
</div>
<div class="control-note" id="anim-export-note">Load a source to prepare animation export.</div>
<div class="exp-btn-group">
<button class="exp-btn" onclick="exportGIF()">Export GIF <span class="ext">.gif</span></button>
<button class="exp-btn" onclick="exportFrames()">Export Frames ZIP <span class="ext">.zip</span></button>
</div>
</div>
<div class="panel-section tips-section">
<div class="section-title">Tips</div>
<div class="tip-item"><strong>PNG</strong> — Best for web, lossless compression</div>
<div class="tip-item"><strong>SVG</strong> — Perfect scaling, ideal for print</div>
<div class="tip-item"><strong>2× / 4×</strong> — Scale up for crisp display on HiDPI</div>
</div>
</div>
</div>
<!-- TOOLBAR -->
<div id="left-toolbar">
<div class="tb-logo">DS</div>
<div class="tb-sep"></div>
<button class="tb-btn" id="open-btn" title="Open file">⊕<br>OPEN</button>
<input type="file" id="file-input" class="visually-hidden-input" accept="image/*,video/*,.gif">
<div class="tb-sep"></div>
<div class="tb-lbl">VIEW</div>
<button class="tb-btn active" id="view-split-btn" onclick="setView('split')" title="Split view">⊞</button>
<button class="tb-btn" id="view-output-btn" onclick="setView('output')" title="Output only">◪</button>
<button class="tb-btn" id="view-original-btn" onclick="setView('original')" title="Original only">◫</button>
<button class="tb-btn" id="view-compare-btn" onclick="setView('compare')" title="Compare slider">⊡</button>
<div class="tb-sep"></div>
<div class="tb-lbl">ZOOM</div>
<button class="tb-btn" onclick="fitToViewport()" title="Fit to viewport">FIT</button>
<button class="tb-btn" onclick="setZoom(0.5)" title="50%">½×</button>
<button class="tb-btn" onclick="setZoom(1)" title="100%">1×</button>
<button class="tb-btn" onclick="setZoom(2)" title="200%">2×</button>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="autoSettings()" title="Auto-detect best dither settings">AUTO</button>
<button class="tb-btn" id="crop-btn" onclick="toggleCropMode()" title="Crop image">CROP</button>
<button class="tb-btn" id="mask-btn" onclick="toggleMaskMode()" title="Paint mask to protect regions">MASK</button>
<button class="tb-btn" onclick="rotateImage(90)" title="Rotate 90° clockwise">↻ ROT</button>
<button class="tb-btn" id="theme-btn" onclick="toggleTheme()" title="Toggle light/dark theme">☀<br>LITE</button>
<button class="tb-btn" onclick="openInfo()" title="Help & features">?<br>INFO</button>
<div class="tb-sep"></div>
<button class="tb-btn primary" onclick="exportPNG()" title="Export PNG">PNG↓</button>
</div>
<!-- CANVAS AREA -->
<div id="canvas-area">
<div id="canvas-toolbar">
<span class="ct-label" id="file-label">No file loaded</span>
<div class="toolbar-spacer"></div>
<span class="ct-label" id="undo-label" title="Undo / Redo (Ctrl+Z / Ctrl+Shift+Z)" onclick="undo()"></span>
<span class="ct-label" id="dim-label"></span>
<span class="ct-label" id="zoom-label"></span>
<div id="hud-proc-res" onclick="toggleHudProcDropdown(event)" title="Process Resolution">
<span class="hud-proc-icon">⊞</span>
<span class="hud-proc-text" id="hud-proc-label">Native</span>
<div id="hud-proc-dropdown">
<button onclick="hudSetProcScale(1)" data-scale="1">100%</button>
<button onclick="hudSetProcScale(0.75)" data-scale="0.75">75%</button>
<button onclick="hudSetProcScale(0.5)" data-scale="0.5">50%</button>
<button onclick="hudSetProcScale(0.25)" data-scale="0.25">25%</button>
<button onclick="hudSetProcScale(0.125)" data-scale="0.125">12.5%</button>
<div class="dropdown-sep"></div>
<div class="dropdown-inputs">
<input type="number" id="hud-proc-w" placeholder="W" min="1" oninput="hudOnProcInput('w')">
<span>×</span>
<input type="number" id="hud-proc-h" placeholder="H" min="1" oninput="hudOnProcInput('h')">
</div>
<div class="dropdown-sep"></div>
<button onclick="hudClearProc()" data-scale="native">Native</button>
</div>
</div>
<button id="hud-proc-lock" onclick="toggleProcResLock(event)" title="Lock resolution (presets won't change it)">🔓</button>
<button id="hud-pause-thumbs" onclick="togglePauseThumbs(event)" title="Pause thumbnail updates">▶</button>
<span class="ct-label accent" id="process-label"></span>
</div>
<div id="canvas-wrap" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)">
<div id="drop-zone" onclick="document.getElementById('file-input').click()">
<div class="drop-icon">◈</div>
<div class="drop-text">Drop or click to open</div>
<div class="drop-sub">Supports images, videos, and animated GIFs</div>
<div class="drop-formats">
<span class="drop-format">PNG</span>
<span class="drop-format">JPG</span>
<span class="drop-format">GIF</span>
<span class="drop-format">MP4</span>
<span class="drop-format">WEBP</span>
</div>
</div>
<div id="canvases">
<div class="canvas-container" id="orig-container">
<div class="canvas-label">Original</div>
<canvas id="original-canvas"></canvas>
</div>
<div class="canvas-container" id="out-container">
<div class="canvas-label">Dithered</div>
<canvas id="output-canvas"></canvas>
<canvas id="pixel-grid-overlay"></canvas>
<canvas id="mask-canvas"></canvas>
<div id="mask-info">🖌️ Paint to protect • Right-click to erase • Esc to exit</div>
</div>
<div class="canvas-container" id="compare-container">
<div class="canvas-label">Compare</div>
<div id="compare-inner">
<canvas id="comp-canvas-orig"></canvas>
<div id="comp-clip">
<canvas id="comp-canvas-out"></canvas>
</div>
<div id="comp-handle">
<div class="comp-line"></div>
<div class="comp-circle">⇔</div>
</div>
<div class="comp-side-lbl" id="comp-lbl-orig">Original</div>
<div class="comp-side-lbl" id="comp-lbl-out">Dithered</div>
</div>
</div>
</div>
</div>
<!-- Video / GIF controls -->
<div id="video-controls">
<button id="play-btn" onclick="togglePlay()" title="Play / Pause">▶</button>
<button class="frame-btn" onclick="stepFrame(-1)" title="Step back">◀◀</button>
<button class="frame-btn" onclick="stepFrame(1)" title="Step forward">▶▶</button>
<div id="seek-wrap">
<input type="range" id="video-seek" min="0" max="1000" value="0" onpointerdown="startScrub()" onpointerup="endScrub(this.value)" oninput="onScrubInput(this.value)">
<canvas id="frame-ticks" height="4"></canvas>
</div>
<span id="video-time">0:00 / 0:00</span>
<label class="toggle loop-toggle" title="Loop">
<input type="checkbox" id="media-loop" checked="" onchange="onLoopChange()">
<span class="toggle-slider"></span>
</label>
<select id="speed-select" title="Playback speed" onchange="setPlaybackSpeed(+this.value)">
<option value="0.25">¼×</option>
<option value="0.5">½×</option>
<option value="1" selected="">1×</option>
<option value="2">2×</option>
<option value="4">4×</option>
</select>
</div>
<div id="progress-bar"><div id="progress-fill"></div></div>
</div>
</div>
Dither Studio v5
✕ Close
A browser-based dithering tool. Drop or open any image, video, or GIF to apply real-time dithering with 25+ algorithms, full pre-processing, advanced multi-color and per-channel control, creative effects, and multi-format export. All processing happens locally — no uploads.
✨ New in v5
Blend Modes Mix dithered output with original using Multiply, Screen, Overlay, Soft/Hard Light.
Tone Curve Interactive curve editor with S-Curve, High/Low Contrast, Brighten, Darken presets.
Pattern Animation Animate dither patterns with adjustable speed for live motion effects.
Pixel Grid Toggle overlay grid visible at 2× zoom and above.
Extract Palette Auto-extract dominant colors (2–16) from the current image.
Output Stats View unique colors, palette usage, estimated PNG size, and compression ratio.
Process Resolution HUD dropdown with quick presets (100%, 75%, 50%, 25%, 12.5%) plus custom input with lock.
Crop & Rotate Use the CROP button to select a region, or ROTATE to turn the image.
Eyedropper Click the eyedropper in the Color tab to pick colors from the canvas.
Drag Palette Drag palette swatches to reorder them.
◐ Algorithms
Error Diffusion — Floyd-Steinberg · Atkinson · Burkes · JJN · Stucki · Sierra · Riemersma
Ordered / Bayer — 2×–32× matrices · Clustered Dot · Blue Noise · Void-Cluster
Special — Scanline H/V · Diagonal · Cross · Threshold · Random
⚙ Pre-Processing
Brightness Uniform offset (±100)
Contrast Scales around mid-grey
Levels Black/white point clip
Gamma Power-law correction
Blur / Sharpen Soften or enhance edges
Edge Boost Sobel edge enhancement
Tone Curve Custom input→output mapping
▦ Dither Controls
Error % How much error spreads
Noise Mix White noise blend
Pixel Block N×N coarse dithering
Serpentine Alternating row scan
Source Chan Luma derivation mode
Process Res Downscale before dither
◑ Effects
Blend Mode Multiply, Screen, Overlay, etc.
Blend Opacity 0–100% blend strength
Pattern Anim Animated dither patterns
Anim Speed 0.5× – 3× phase shift rate
🎨 Color Workflow
Quick Setup Standard / Multi / Per-Chan modes
Multi-Color 3+ color palette dithering
Per-Channel Independent R/G/B dithering
Extract Sample colors from image
Chan Bias Per-channel threshold offset
⌨ Keyboard Shortcuts
Space Play / Pause media
← → Step frame
F Fit to viewport
1 2 Set zoom 1× / 2×
G Toggle pixel grid
Scroll Zoom at cursor
Esc Close / Exit mode
📤 Export
Formats: PNG · WebP · JPEG · SVG · Clipboard
Animation: GIF · ZIP of PNG frames
Scaling: 25% – 4× with custom dimensions