diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 380811c..5f7938e 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,53 +1,53 @@ -{"id":"tooscut-0ip","title":"Implement spline/curve editor for keyframes","description":"Add a spline/curve editor UI for fine-tuning keyframe interpolation. This includes:\n\n- Bezier curve editor component (similar to After Effects graph editor)\n- Visual representation of property values over time\n- Draggable control points for bezier handles\n- Easing presets (ease-in, ease-out, ease-in-out, linear, custom)\n- Integration with keyframe system to edit interpolation curves\n- Zoom/pan controls for timeline navigation\n\nReference: subformer spline editor at /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T02:14:28.686002-08:00","created_by":"mohebifar","updated_at":"2026-02-07T22:16:06.811012-08:00","closed_at":"2026-02-07T22:16:06.811012-08:00","close_reason":"Closed","dependencies":[{"issue_id":"tooscut-0ip","depends_on_id":"tooscut-vev","type":"blocks","created_at":"2026-02-06T02:15:17.682583-08:00","created_by":"mohebifar"}]} -{"id":"tooscut-1fj","title":"Make menu bar functional","description":"The menu bar is currently fully decorative/non-functional. Wire up all menu items to their corresponding actions (File: new project, open, save, export; Edit: undo, redo, cut, copy, paste, delete; View: zoom controls, panel toggles; etc). Menu items without implementations should be disabled/grayed out.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:15.451904-08:00","created_by":"mohebifar","updated_at":"2026-02-22T16:48:23.082522-08:00","closed_at":"2026-02-22T16:48:23.082522-08:00","close_reason":"Closed","labels":["bug"]} -{"id":"tooscut-2ig","title":"Color Grading: Curves Editor node","description":"Implement RGB curves editor node for the color grading pipeline. Includes: master curve, per-channel R/G/B curves, and advanced hue-vs-sat/hue-vs-hue curves. Needs: interactive bezier curve UI component, 1D LUT texture generation from curves, shader integration for curve application. Types already defined in Rust (Curves, Curve1D) and TypeScript.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-06T13:08:29.099134-07:00","created_by":"mohebifar","updated_at":"2026-04-06T13:19:07.156573-07:00","closed_at":"2026-04-06T13:19:07.156573-07:00","close_reason":"Closed"} -{"id":"tooscut-38j","title":"Add audio effects (EQ, reverb, compression, noise gate)","description":"Add audio processing effects beyond volume and speed. Potential effects to implement:\n\n- **EQ (Equalizer)**: Low/mid/high band adjustment for tonal control\n- **Reverb**: Room simulation for spatial depth\n- **Compression**: Dynamic range compression to even out volume levels\n- **Noise gate**: Suppress audio below a threshold to remove background noise\n- **Low-pass / High-pass filters**: Frequency filtering\n\nEach effect needs:\n1. Rust implementation in crates/audio-engine (DSP processing in the mixer pipeline)\n2. UI controls in the audio properties panel (apps/ui/src/components/editor/audio-properties.tsx)\n3. Keyframeable parameters via the existing KeyframeInput system\n4. Timeline state serialization (AudioClipState in packages/render-engine/src/audio-engine.ts)\n5. Support in both playback (AudioWorklet) and export (offline WASM rendering) paths","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-23T01:25:37.868493-08:00","created_by":"mohebifar","updated_at":"2026-03-31T13:01:28.876263-07:00","closed_at":"2026-03-31T13:01:28.876263-07:00","close_reason":"Fully implemented: EQ with visual frequency response editor, compressor, noise gate, and reverb all have WASM DSP + UI controls + keyframe support"} -{"id":"tooscut-38z","title":"Transition system: in, out, and cross transitions","description":"Add ability to apply transitions (fade, dissolve, wipe, slide, etc.) to clips. Transitions should be droppable from the left panel onto clip edges (in/out) or between adjacent clips (cross transitions). The left panel should show a visual preview of each transition type. Leverage the existing transition types already defined in the render engine.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:28.62805-08:00","created_by":"mohebifar","updated_at":"2026-02-22T01:43:24.526507-08:00","closed_at":"2026-02-22T01:43:24.526507-08:00","close_reason":"Closed"} -{"id":"tooscut-3ck","title":"Add project settings (dimensions, resolution, orientation, frame rate)","description":"Add a project settings UI that allows users to configure: video dimensions (width/height), resolution presets (1080p, 4K, etc), orientation (landscape/portrait), and frame rate (24, 30, 60 fps, custom). Settings should be stored in the project record in DexieJS and applied to the compositor/export pipeline.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:16.677159-08:00","created_by":"mohebifar","updated_at":"2026-02-22T11:48:45.255249-08:00","closed_at":"2026-02-22T11:48:45.255249-08:00","close_reason":"Closed","labels":["feature"]} -{"id":"tooscut-40n","title":"Timeline markers with in/out points","description":"## Summary\nAdd named timeline markers and in/out point system for marking regions of interest.\n\n## UX Design\n\n### Marker creation\n- M: Drop a marker at the current playhead position\n- Marker appears as a small colored triangle on the ruler area (above tracks, below timecode)\n- Default color: cyan. Can be changed per marker.\n- Double-click marker triangle to rename (inline text edit on ruler)\n- Right-click marker: context menu with Rename, Change Color, Delete\n\n### Marker navigation\n- Shift+M: Go to next marker\n- Alt+M (or Ctrl+Shift+M): Go to previous marker\n- Markers listed in a dropdown/popover accessible from the toolbar (optional, lower priority)\n\n### In/Out points\n- I: Set in-point at playhead\n- O: Set out-point at playhead\n- In/out displayed as a highlighted region on the ruler (semi-transparent overlay between in and out)\n- Alt+I / Alt+O: Clear in/out point\n- In/out points constrain export range when set (pass to export dialog)\n- In/out points constrain playback loop when loop mode is enabled\n\n### Visual design\n- Markers: small downward-pointing triangles (6px) on the ruler, same row as timecode\n- Marker label: tiny text above the triangle, visible on hover or always if space permits\n- In/out region: subtle colored overlay on the ruler between the two points\n- In point: [ bracket icon. Out point: ] bracket icon.\n\n### Store changes\n- Add markers: Array\u003c{ id, time, name, color }\u003e to project state\n- Add inPoint: number | null, outPoint: number | null to store\n- Actions: addMarker, removeMarker, updateMarker, setInPoint, setOutPoint, clearInOutPoints\n- Markers saved with project (persisted to IndexedDB)\n- In/out points are session-only (not persisted)\n\n### Integration with export\n- If in/out points are set, export dialog should show option to export only the marked region\n- Default: export full timeline. Checkbox: \"Export in/out range only\"","status":"open","priority":2,"issue_type":"feature","created_at":"2026-03-30T01:10:44.409844-07:00","created_by":"mohebifar","updated_at":"2026-03-30T01:10:44.409844-07:00"} -{"id":"tooscut-42i","title":"Gesture to move clips between tracks","description":"Add ability to use drag gestures to move a clip/text/shape from one track to another. Should work with linked clips (video+audio moving together to their respective track types).","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.874235-08:00","created_by":"mohebifar","updated_at":"2026-02-05T00:14:51.901763-08:00","closed_at":"2026-02-05T00:14:51.901763-08:00","close_reason":"Closed"} -{"id":"tooscut-4cn","title":"Fix undo/redo requiring multiple keypresses","description":"Bug: between every undo/redo action, the user needs to press Cmd+Z multiple times for it to take effect. The first undo works with a single Cmd+Z, but subsequent undos require pressing Cmd+Z twice (or more). This suggests the temporal store (zundo) is recording duplicate or no-op history entries, or the undo handler is consuming keystrokes without performing the undo.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-22T02:12:22.105794-08:00","created_by":"mohebifar","updated_at":"2026-02-22T11:02:30.122139-08:00","closed_at":"2026-02-22T11:02:30.122141-08:00","labels":["bug"]} -{"id":"tooscut-4f5","title":"Preview doesn't update when paused and making changes","description":"When paused, moving a clip or making any change doesn't update the preview. The preview must re-render when timeline state changes while paused.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-02-03T15:31:54.584644-08:00","created_by":"mohebifar","updated_at":"2026-02-03T15:36:39.074212-08:00","closed_at":"2026-02-03T15:36:39.074212-08:00","close_reason":"Closed"} -{"id":"tooscut-5du","title":"Linked clip doesn't move visually during drag gesture","description":"When moving a clip, the linked audio/video clip doesn't move with it during the move gesture. It only moves on mouse up. The UX doesn't feel good - linked clips should move together in real-time during the drag.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-02-03T15:31:54.907253-08:00","created_by":"mohebifar","updated_at":"2026-02-03T15:42:35.648624-08:00","closed_at":"2026-02-03T15:42:35.648624-08:00","close_reason":"Closed"} -{"id":"tooscut-61n","title":"Fix file import via menu bar (File \u003e Import Media)","description":"The File \u003e Import Media menu item opens the file picker dialog, but after selecting a file, nothing happens — the asset is not added to the assets panel. The file picker integration (importFilesWithPicker) is not completing the import flow when triggered from the menu bar.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-22T15:37:50.751826-08:00","created_by":"mohebifar","updated_at":"2026-02-22T15:40:20.276596-08:00","closed_at":"2026-02-22T15:40:20.276596-08:00","close_reason":"Closed"} -{"id":"tooscut-63m","title":"Color Grading: HSL Qualifier node","description":"Implement HSL Qualifier (secondary color correction) node. Allows isolating specific color ranges by hue/saturation/luminance with soft edges, then applying a correction (PrimaryCorrection) only within that mask. Includes: qualifier mask preview overlay, hue/sat/lum range picker UI with center/width/softness controls, invert toggle, qualifier mask visualization on the preview panel. Shader functions already written (HSL_QUALIFIER_FUNCTIONS). Types defined: HslQualifier.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-06T13:08:42.895501-07:00","created_by":"mohebifar","updated_at":"2026-04-06T13:19:07.183977-07:00","closed_at":"2026-04-06T13:19:07.183977-07:00","close_reason":"Closed"} -{"id":"tooscut-63o","title":"Drag and drop files from OS file explorer directly to timeline","description":"Allow dragging files from Finder/file explorer directly onto the timeline to import and place them in one action. If the dropped file matches an already-imported asset (by name/size/type), reuse the existing asset rather than duplicating it in the assets panel. If it's a new file, import it as a new asset first, then add the clip to the timeline at the drop position.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:46.528465-08:00","created_by":"mohebifar","updated_at":"2026-02-22T17:25:09.399064-08:00","closed_at":"2026-02-22T17:25:09.399064-08:00","close_reason":"Closed"} -{"id":"tooscut-731","title":"Timeline drag-to-select (rubber band selection)","description":"Add rubber-band / marquee drag selection to the timeline view. Currently multi-select only works by holding Shift/Cmd and clicking individual clips. Users should be able to click and drag on empty timeline space to draw a selection rectangle that selects all clips it intersects.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:13.781927-08:00","created_by":"mohebifar","updated_at":"2026-02-22T11:34:18.138855-08:00","closed_at":"2026-02-22T11:34:18.138855-08:00","close_reason":"Closed","labels":["feature"]} -{"id":"tooscut-8rt","title":"Color Grading: Power Window node","description":"Implement Power Window (regional mask) node. Allows applying corrections to specific regions of the frame using shapes (circle/ellipse, rectangle, polygon, linear gradient). Includes: interactive shape overlay on preview panel with drag handles for position/size/rotation/softness, inner/outer softness controls, invert toggle. Each window carries its own PrimaryCorrection. Types defined: PowerWindow, PowerWindowShape.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-04-06T13:08:49.169099-07:00","created_by":"mohebifar","updated_at":"2026-04-06T13:19:07.194997-07:00","closed_at":"2026-04-06T13:19:07.194997-07:00","close_reason":"Closed","dependencies":[{"issue_id":"tooscut-8rt","depends_on_id":"tooscut-63m","type":"blocks","created_at":"2026-04-06T13:08:55.133021-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-a6i","title":"Add ability to add and remove tracks","description":"Implement UI and functionality to:\n- Add new video/audio track pairs to the timeline\n- Remove existing tracks from the timeline","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.221806-08:00","created_by":"mohebifar","updated_at":"2026-02-03T22:40:45.317523-08:00","closed_at":"2026-02-03T22:40:45.317523-08:00","close_reason":"Closed"} -{"id":"tooscut-and","title":"Color Grading System","description":"## Summary\nImplement a professional-grade color grading system with feature parity to DaVinci Resolve and Final Cut Pro.\n\n## Core Features\n- **Primary Correction** — CDL-based (slope/offset/power) + exposure, temperature, tint, saturation\n- **Color Wheels** — Lift/Gamma/Gain with luminance controls\n- **Curves** — Master, RGB, plus advanced (Hue vs Sat, Lum vs Sat, etc.)\n- **3D LUT Support** — .cube file parsing with tetrahedral interpolation\n- **HSL Qualifier** — Secondary color correction keying\n- **Power Windows** — Shapes/masks for regional corrections\n- **Scopes** — Waveform, vectorscope, histogram, parade (GPU-computed)\n- **Node-Based Pipeline** — Ordered processing chain per clip\n- **Color Matching** — Auto-match between clips\n\n## Architecture\nAll rendering happens in Rust/WASM (per project rules):\n- Color space conversions (sRGB ↔ Linear ↔ ACEScg ↔ Log)\n- CDL/LGG operations in WGSL shaders\n- 3D LUT textures with tetrahedral interpolation\n- Compute shaders for scope generation\n\nTypeScript/React handles UI only:\n- Color wheel canvas components\n- Curves editor\n- Scope display (Canvas 2D rendering of WASM-computed data)\n\n## Key Files to Modify\n- `crates/types/src/` — Add `color_grading.rs`\n- `crates/compositor/src/` — Add shaders and pipeline integration\n- `packages/render-engine/src/types.ts` — TypeScript definitions\n- `apps/ui/src/components/editor/` — New color grading UI components\n- `apps/ui/src/state/video-editor-store.ts` — State management","status":"open","priority":2,"issue_type":"epic","created_at":"2026-04-04T14:02:16.802358-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:02:16.802358-07:00"} -{"id":"tooscut-and.1","title":"Color grading foundation types and color space shaders","description":"## Summary\nDefine core types and implement color space conversion shaders as the foundation for the color grading system.\n\n## Tasks\n\n### Rust Types (crates/types/src/color_grading.rs)\n- [ ] `ColorSpace` enum (sRGB, Linear, ACEScg, LogC, SLog3, CLog3, VLog)\n- [ ] `PrimaryCorrection` struct (CDL: slope, offset, power + saturation, exposure, temperature, tint)\n- [ ] `ColorWheelValue` struct (angle, distance)\n- [ ] `ColorWheels` struct (lift, gamma, gain with luminance controls)\n- [ ] `Curve1D` and `CurvePoint` structs\n- [ ] `ColorGradingState` struct (combines all grading data for a layer)\n- [ ] Add `color_grading: Option\u003cColorGradingState\u003e` to `MediaLayerData`\n\n### TypeScript Types (packages/render-engine/src/types.ts)\n- [ ] Mirror all Rust types in TypeScript\n- [ ] Use snake_case to match serde serialization\n\n### WGSL Shaders (crates/compositor/src/shaders/color_grading.wgsl)\n- [ ] `srgb_to_linear()` and `linear_to_srgb()`\n- [ ] `rgb_to_hsl()` and `hsl_to_rgb()`\n- [ ] LogC/SLog3/VLog transfer functions\n- [ ] ACEScg matrix conversions\n\n### Uniforms (crates/compositor/src/color_grading_uniforms.rs)\n- [ ] `ColorGradingUniforms` struct with proper alignment (repr(C), Pod, Zeroable)\n- [ ] All CDL parameters, wheel values, qualifier params, flags\n\n## Acceptance Criteria\n- Types compile in Rust and generate correct TypeScript bindings via tsify\n- Color space conversion shaders pass visual tests (known color values)\n- Uniforms struct has correct GPU alignment","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:02:29.647666-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:41:09.868939-07:00","closed_at":"2026-04-04T14:41:09.868939-07:00","close_reason":"Closed","dependencies":[{"issue_id":"tooscut-and.1","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:02:29.649247-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.10","title":"Color matching and grade management","description":"## Summary\nImplement advanced color grading workflow features: clip matching, grade presets, copy/paste, and stills.\n\n## Tasks\n\n### Color Matching\n- [ ] `ColorMatchNode` type with reference clip/frame\n- [ ] Histogram matching algorithm:\n - Compute histograms of source and target\n - Generate CDL values to match distributions\n- [ ] UI: Select reference frame, click \"Match\"\n- [ ] Manual refinement after auto-match\n\n### Copy/Paste Grades\n- [ ] Copy full grade (all nodes) from clip\n- [ ] Paste grade to one or multiple clips\n- [ ] Paste specific nodes only (cherry-pick)\n- [ ] Keyboard shortcuts: Cmd+Shift+C / Cmd+Shift+V\n\n### Grade Presets (Stills)\n- [ ] Save current grade as named preset\n- [ ] Preset browser with thumbnails\n- [ ] Apply preset to clip\n- [ ] Export/import presets (JSON file)\n- [ ] Built-in presets: Film looks, B\u0026W, Vintage, etc.\n\n### Gallery (Stills)\n- [ ] Capture still (frame + grade) for reference\n- [ ] Gallery panel showing captured stills\n- [ ] Click still to apply its grade\n- [ ] Compare current frame to still\n\n### Ripple Grade Changes\n- [ ] Option to apply grade changes to all clips with same source\n- [ ] Useful for multi-cam or repeated shots\n\n### State Management\n- [ ] Add `colorGradingPresets: Map\u003cstring, ClipColorGrading\u003e` to store\n- [ ] Add `capturedStills: Array\u003c{ frame, grade, thumbnail }\u003e` to store\n- [ ] Add actions: `savePreset`, `applyPreset`, `captureStill`, `copyGrade`, `pasteGrade`\n\n## Acceptance Criteria\n- Color match produces visually similar results between clips\n- Copy/paste works across clips in same or different projects\n- Presets persist and load correctly\n- Gallery stills show accurate previews\n- Can quickly match multiple clips to a reference","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-04T14:04:39.71225-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:04:39.71225-07:00","dependencies":[{"issue_id":"tooscut-and.10","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:04:39.714933-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.10","depends_on_id":"tooscut-and.9","type":"blocks","created_at":"2026-04-04T14:04:39.716245-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.2","title":"Primary color correction (CDL)","description":"## Summary\nImplement CDL (Color Decision List) based primary color correction with basic adjustment controls.\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `apply_cdl()` function: `out = pow(in * slope + offset, power)`\n- [ ] `apply_saturation()` function using luminance mix\n- [ ] `apply_exposure()` function (EV stops, -4 to +4)\n- [ ] `apply_temperature_tint()` function (Kelvin offset + green-magenta)\n- [ ] Highlight/shadow recovery functions\n\n### Compositor Integration\n- [ ] Add color grading render pass after media compositing\n- [ ] Create bind group for color grading uniforms\n- [ ] Wire uniforms from `MediaLayerData.color_grading` to shader\n\n### UI Components (apps/ui/src/components/editor/color-grading/)\n- [ ] `PrimaryCorrectionPanel` component with:\n - Exposure slider (-4 to +4 EV)\n - Temperature slider (2000K to 10000K)\n - Tint slider (green to magenta)\n - Contrast slider\n - Highlights/Shadows sliders\n - Saturation slider\n- [ ] CDL sliders (Slope R/G/B, Offset R/G/B, Power R/G/B) in collapsible \"Advanced\" section\n\n### State Management\n- [ ] Add `updateClipPrimaryCorrection(clipId, correction)` action\n- [ ] Integrate with undo/redo (temporal middleware)\n\n### Keyframe Support\n- [ ] Add primary correction properties to `COLOR_GRADING_ANIMATABLE_PROPERTIES`\n- [ ] Test keyframe interpolation for exposure, temperature, etc.\n\n## Acceptance Criteria\n- Adjusting exposure visibly changes clip brightness in preview\n- CDL values match ASC-CDL standard behavior\n- Changes are undoable\n- Keyframes animate smoothly","notes":"Completed: UI components (PrimaryCorrectionProperties, ColorGradingPanel), state management (updateClipColorGrading action), TypeScript types with keyframe support. Remaining: WGSL shader integration, bind group creation, visual testing.","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:02:42.611481-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:50:13.74347-07:00","dependencies":[{"issue_id":"tooscut-and.2","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:02:42.613067-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.2","depends_on_id":"tooscut-and.1","type":"blocks","created_at":"2026-04-04T14:02:42.615138-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.3","title":"Color wheels (Lift/Gamma/Gain)","description":"## Summary\nImplement Lift/Gamma/Gain color wheels for intuitive shadow/midtone/highlight color adjustment.\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `color_wheel_to_rgb()` - convert angle/distance to RGB offset\n- [ ] `apply_lift_gamma_gain()` function:\n - Lift affects shadows (adds color when pixel is dark)\n - Gamma affects midtones (power function)\n - Gain affects highlights (multiplies)\n- [ ] Each wheel has RGB color shift + luminance master\n\n### UI Components\n- [ ] `ColorWheel` component (Canvas-based):\n - Circular color picker with saturation gradient\n - Draggable position indicator\n - Double-click to reset to center\n - Luminance slider below wheel\n- [ ] `ColorWheelsPanel` with three wheels (Lift, Gamma, Gain)\n- [ ] \"Link wheels\" toggle for global adjustments\n- [ ] Reset button per wheel and global reset\n\n### Interaction Design\n- [ ] Mouse drag updates angle + distance from center\n- [ ] Shift+drag constrains to current angle (saturation only)\n- [ ] Ctrl+drag for fine adjustment (0.1x speed)\n- [ ] Keyboard: arrow keys for small adjustments when focused\n\n### State Management\n- [ ] Add `updateClipColorWheels(clipId, wheels)` action\n- [ ] Wheel values stored as `{ angle: number, distance: number }`\n\n## Acceptance Criteria\n- Dragging lift wheel adds color to dark areas only\n- Dragging gain wheel adds color to bright areas only\n- Gamma affects midtones without crushing blacks/whites\n- Luminance sliders work independently from color shift\n- All changes are undoable","notes":"Completed: Canvas-based ColorWheel component with drag/shift+drag/ctrl+drag interactions, ColorWheelsProperties panel (Lift/Gamma/Gain), and React Flow node graph visualization. The node graph shows the color grading pipeline with color-coded nodes, enable/disable toggles, and drag-to-reorder support.","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:02:55.013616-07:00","created_by":"mohebifar","updated_at":"2026-04-04T16:58:32.135196-07:00","dependencies":[{"issue_id":"tooscut-and.3","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:02:55.015382-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.3","depends_on_id":"tooscut-and.2","type":"blocks","created_at":"2026-04-04T14:02:55.018517-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.4","title":"Curves editor","description":"## Summary\nImplement RGB curves for precise tonal control, plus advanced curves (Hue vs Sat, Lum vs Sat, etc.).\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `evaluate_curve()` function using 1D LUT texture sampling\n- [ ] Support for smooth spline interpolation between control points\n- [ ] Apply curves in correct order: Master → R → G → B → Advanced\n\n### Curve LUT Generation (TypeScript)\n- [ ] `generateCurveLUT(points: CurvePoint[])` - outputs 256-entry Float32Array\n- [ ] Catmull-Rom or cubic Bezier interpolation between points\n- [ ] Handle edge cases (points at 0,0 and 1,1)\n\n### UI Components\n- [ ] `CurvesEditor` component (Canvas-based):\n - 256x256 grid with diagonal reference line\n - Click to add control point\n - Drag to move control point\n - Double-click or Delete key to remove point\n - Smooth curve rendering through points\n- [ ] Channel selector tabs: Master, Red, Green, Blue\n- [ ] Advanced curves dropdown: Hue vs Sat, Hue vs Lum, Sat vs Sat, Lum vs Sat\n- [ ] Preset curves: S-curve, fade, negative, etc.\n- [ ] Reset button\n\n### Advanced Curves\n- [ ] Hue vs Hue - rotate hues selectively\n- [ ] Hue vs Sat - saturate/desaturate specific hues\n- [ ] Hue vs Lum - brighten/darken specific hues\n- [ ] Lum vs Sat - saturate shadows/highlights differently\n- [ ] Sat vs Sat - compress or expand saturation range\n\n### State Management\n- [ ] Add `updateClipCurves(clipId, curves)` action\n- [ ] Curve presets stored separately for quick application\n\n### Performance\n- [ ] Regenerate LUT texture only when curve points change\n- [ ] Debounce during drag operations\n- [ ] Cache curve textures per clip\n\n## Acceptance Criteria\n- Dragging curve up brightens, down darkens\n- RGB curves affect only their respective channels\n- S-curve increases contrast visually\n- Smooth interpolation (no stair-stepping)\n- 60fps interaction during curve editing","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:09.958519-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:03:09.958519-07:00","dependencies":[{"issue_id":"tooscut-and.4","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:09.960905-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.4","depends_on_id":"tooscut-and.2","type":"blocks","created_at":"2026-04-04T14:03:09.964293-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.5","title":"3D LUT support (.cube files)","description":"## Summary\nImplement 3D LUT loading, GPU upload, and tetrahedral interpolation for professional color transforms.\n\n## Tasks\n\n### .cube File Parser (TypeScript)\n- [ ] `parseCubeFile(content: string): CubeLUT`\n- [ ] Parse header: TITLE, LUT_3D_SIZE, DOMAIN_MIN, DOMAIN_MAX\n- [ ] Parse RGB triplet data lines\n- [ ] Validate cube size (common: 17, 33, 65)\n- [ ] Handle comments and empty lines\n\n### LUT Manager (Rust/WASM)\n- [ ] `LutManager` struct with HashMap of loaded LUTs\n- [ ] `upload_lut(id, size, data)` - creates 3D texture on GPU\n- [ ] `remove_lut(id)` - frees GPU memory\n- [ ] `get_lut_texture(id)` - returns texture view for binding\n\n### WGSL Shader Implementation\n- [ ] `apply_lut_trilinear()` - basic trilinear interpolation\n- [ ] `apply_lut_tetrahedral()` - higher quality tetrahedral interpolation\n- [ ] Handle domain min/max scaling\n- [ ] LUT sampler with clamp-to-edge addressing\n\n### UI Components\n- [ ] `LutBrowserPanel` component:\n - Grid view of available LUTs with thumbnails\n - Search/filter by name\n - Preview on hover\n - Click to apply\n- [ ] LUT import button (file picker for .cube)\n- [ ] LUT intensity slider (mix with original)\n- [ ] Remove LUT button\n\n### LUT Thumbnail Generation\n- [ ] Generate preview by applying LUT to standard gradient image\n- [ ] Cache thumbnails in IndexedDB\n\n### State Management\n- [ ] Add `loadedLuts: Map\u003cstring, LoadedLut\u003e` to store\n- [ ] Add `loadLut(file)`, `removeLut(id)`, `setClipLut(clipId, lutId)` actions\n- [ ] LUT data persisted in project (or referenced by path)\n\n### Memory Management\n- [ ] Limit simultaneous loaded LUTs (e.g., 10 max)\n- [ ] LRU eviction for unused LUTs\n- [ ] 33³ × 16 bytes = ~575KB per LUT\n\n## Acceptance Criteria\n- .cube files from DaVinci/Resolve load correctly\n- LUT applied matches reference implementation\n- Tetrahedral interpolation has no visible banding\n- LUT browser shows accurate previews\n- Memory usage stays bounded with many LUTs","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:25.934398-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:41:14.358352-07:00","dependencies":[{"issue_id":"tooscut-and.5","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:25.936583-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.5","depends_on_id":"tooscut-and.1","type":"blocks","created_at":"2026-04-04T14:03:25.938607-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.6","title":"HSL Qualifier (secondary color correction)","description":"## Summary\nImplement HSL qualifier for isolating and correcting specific colors (secondary color correction / keying).\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `hsl_qualifier_mask()` function:\n - Hue center + width + softness (circular distance)\n - Saturation center + width + softness\n - Luminance center + width + softness\n - Combine masks multiplicatively\n - Invert option\n- [ ] Apply correction multiplied by qualifier mask\n- [ ] Edge softness using smoothstep\n\n### UI Components\n- [ ] `HslQualifierPanel` component:\n - Hue range selector (circular/strip visualization)\n - Saturation range selector (horizontal bar)\n - Luminance range selector (horizontal bar)\n - Softness controls for each dimension\n - Invert toggle\n- [ ] Eyedropper tool to pick qualifier center from preview\n- [ ] \"Show Mask\" toggle to visualize selection (B\u0026W mask view)\n- [ ] Correction controls (reuse PrimaryCorrectionPanel) applied to qualified region\n\n### Eyedropper Interaction\n- [ ] Click on preview to sample pixel HSL values\n- [ ] Shift+click to add to selection (expand range)\n- [ ] Alt+click to subtract from selection\n- [ ] Sample multiple pixels for average\n\n### Mask Visualization\n- [ ] Toggle between: Normal view, Mask overlay, Mask only\n- [ ] Mask overlay shows selection as highlight color\n- [ ] Mask only shows B\u0026W (white = selected)\n\n### State Management\n- [ ] Add `HslQualifierNode` to color grading node types\n- [ ] Qualifier stores center, width, softness for H/S/L\n- [ ] Correction stored within qualifier node\n\n## Acceptance Criteria\n- Can isolate skin tones and adjust without affecting background\n- Can select sky blue and increase saturation\n- Soft edges blend naturally (no harsh cutoffs)\n- Eyedropper accurately picks colors from preview\n- Mask visualization helps dial in selection","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:39.983433-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:03:39.983433-07:00","dependencies":[{"issue_id":"tooscut-and.6","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:39.985365-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.6","depends_on_id":"tooscut-and.2","type":"blocks","created_at":"2026-04-04T14:03:39.987334-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.7","title":"Power windows (masks for regional correction)","description":"## Summary\nImplement power windows (geometric masks) for regional color corrections - vignettes, sky gradients, face isolation, etc.\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] Shape mask generators:\n - Circle/Ellipse\n - Rectangle with corner radius\n - Linear gradient\n - Polygon (for complex shapes)\n- [ ] Transform: position, scale, rotation\n- [ ] Inner/outer softness (feathering)\n- [ ] Invert option\n- [ ] Combine with qualifier mask (AND operation)\n\n### UI Components\n- [ ] `PowerWindowPanel` component:\n - Shape selector (circle, rectangle, gradient, polygon)\n - Position X/Y sliders (% of frame)\n - Scale X/Y sliders\n - Rotation slider\n - Softness inner/outer sliders\n - Invert toggle\n- [ ] On-canvas shape overlay:\n - Draggable shape outline on preview\n - Corner handles for resize\n - Rotation handle\n - Center point for position\n\n### Shape Drawing\n- [ ] Circle: center + radius, draggable edge\n- [ ] Rectangle: corner handles, aspect ratio lock option\n- [ ] Gradient: start point + end point + angle\n- [ ] Polygon: click to add points, close path\n\n### Tracking (stretch goal)\n- [ ] Manual keyframe tracking (position/scale/rotation keyframes)\n- [ ] Data structure for tracked window transforms\n- [ ] Interpolation between tracking keyframes\n\n### Combination with Qualifier\n- [ ] Power window can be combined with HSL qualifier\n- [ ] \"Window AND Qualifier\" mode - both must match\n- [ ] Useful for: face in specific area, sky in upper region only\n\n## Acceptance Criteria\n- Can create vignette effect with soft circular window\n- Can isolate upper sky with gradient window\n- Can draw polygon around subject\n- On-canvas controls are intuitive (drag to move, handles to resize)\n- Soft edges blend naturally","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:54.558574-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:03:54.558574-07:00","dependencies":[{"issue_id":"tooscut-and.7","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:54.560807-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.7","depends_on_id":"tooscut-and.6","type":"blocks","created_at":"2026-04-04T14:03:54.563678-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.8","title":"Video scopes (waveform, vectorscope, histogram)","description":"## Summary\nImplement professional video scopes computed on GPU for real-time color analysis.\n\n## Tasks\n\n### Scope Types\n- [ ] **Histogram** - RGB + Luma distribution (256 bins each)\n- [ ] **Waveform** - Luma or RGB waveform (brightness by horizontal position)\n- [ ] **RGB Parade** - Separate R/G/B waveforms side by side\n- [ ] **Vectorscope** - Color distribution on color wheel (hue angle + saturation radius)\n\n### WGSL Compute Shaders\n- [ ] `compute_histogram.wgsl`:\n - Atomic histogram binning\n - Output: 4 × 256 buffer (R, G, B, Luma)\n- [ ] `compute_waveform.wgsl`:\n - For each column, accumulate pixel brightness into rows\n - Output: 2D texture (width × 256)\n- [ ] `compute_vectorscope.wgsl`:\n - Map each pixel to 2D position by hue/sat\n - Accumulate density\n - Output: 256×256 texture\n\n### Rust/WASM Implementation\n- [ ] `ScopeComputer` struct managing compute pipelines\n- [ ] `compute_histogram(texture_id) -\u003e Vec\u003cf32\u003e`\n- [ ] `compute_waveform(texture_id, width) -\u003e Vec\u003cu8\u003e`\n- [ ] `compute_vectorscope(texture_id, size) -\u003e Vec\u003cu8\u003e`\n\n### UI Components\n- [ ] `ScopesPanel` component:\n - Scope type selector tabs\n - Canvas for scope display\n - Graticule overlays (scale markers)\n- [ ] Histogram: stacked or separate R/G/B view option\n- [ ] Waveform: Luma / RGB / Parade mode selector\n- [ ] Vectorscope: skin tone line, color target boxes\n\n### Graticules and Reference Lines\n- [ ] Histogram: 0%, 50%, 100% markers\n- [ ] Waveform: IRE/percentage scale, legal range indicators (16-235)\n- [ ] Vectorscope: color boxes (R, Mg, B, Cy, G, Yl), skin tone line\n\n### Performance\n- [ ] Compute scopes on requestAnimationFrame, not every frame\n- [ ] Throttle during playback (every 2-3 frames)\n- [ ] Full rate when paused\n- [ ] Resolution option: 1x, 1/2, 1/4 for faster computation\n\n## Acceptance Criteria\n- Histogram matches reference scope software\n- Waveform shows correct brightness distribution\n- Vectorscope shows correct color positions\n- Scopes update in real-time during playback (\u003c16ms compute time)\n- Graticules help interpret scope readings","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:04:11.037853-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:41:14.406509-07:00","dependencies":[{"issue_id":"tooscut-and.8","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:04:11.040172-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.8","depends_on_id":"tooscut-and.1","type":"blocks","created_at":"2026-04-04T14:04:11.04237-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-and.9","title":"Node-based color grading pipeline","description":"## Summary\nImplement node-based color grading pipeline allowing multiple stacked corrections executed in order.\n\n## Tasks\n\n### Data Structures\n- [ ] `ColorGradingNode` union type (primary, wheels, curves, LUT, qualifier, window)\n- [ ] `ClipColorGrading` with ordered `nodes: ColorGradingNode[]` array\n- [ ] Each node has: id, type, enabled, mix, label\n\n### Node Execution Engine (Rust/WASM)\n- [ ] `execute_color_pipeline(texture, nodes) -\u003e texture`\n- [ ] Execute nodes in order, each reading previous output\n- [ ] Skip disabled nodes\n- [ ] Apply per-node mix (blend with input)\n- [ ] Intermediate render targets for multi-pass\n\n### UI Components\n- [ ] `NodeListPanel` component:\n - Vertical list of nodes (not visual graph, like Resolve's mini timeline)\n - Drag to reorder\n - Enable/disable toggle per node\n - Mix slider per node\n - Expand/collapse node settings\n - Add node button (dropdown of types)\n - Delete node button\n- [ ] Node type icons for quick identification\n- [ ] \"Solo\" button to preview single node effect\n\n### Node Operations\n- [ ] Add node (at end or after selected)\n- [ ] Remove node\n- [ ] Reorder nodes (drag \u0026 drop)\n- [ ] Duplicate node\n- [ ] Enable/disable node\n- [ ] Reset node to defaults\n- [ ] Copy/paste node between clips\n\n### Bypass and Preview\n- [ ] Global bypass toggle (show original)\n- [ ] \"Solo node\" mode - only selected node active\n- [ ] A/B comparison split view (future enhancement)\n\n### State Management\n- [ ] Add `addColorGradingNode(clipId, node, index?)` action\n- [ ] Add `removeColorGradingNode(clipId, nodeId)` action\n- [ ] Add `reorderColorGradingNodes(clipId, fromIndex, toIndex)` action\n- [ ] Add `updateColorGradingNode(clipId, nodeId, updates)` action\n\n## Acceptance Criteria\n- Can stack multiple corrections (e.g., Primary → Curves → LUT)\n- Node order affects output (e.g., LUT before vs after primary)\n- Disabling node removes its effect\n- Mix slider blends node effect with input\n- Drag reordering updates preview immediately","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:04:26.186835-07:00","created_by":"mohebifar","updated_at":"2026-04-04T14:04:26.186835-07:00","dependencies":[{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:04:26.187706-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.3","type":"blocks","created_at":"2026-04-04T14:04:26.188467-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.4","type":"blocks","created_at":"2026-04-04T14:04:26.189223-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.5","type":"blocks","created_at":"2026-04-04T14:04:26.189985-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.6","type":"blocks","created_at":"2026-04-04T14:04:26.190624-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.7","type":"blocks","created_at":"2026-04-04T14:04:26.191412-07:00","created_by":"mohebifar"}]} -{"id":"tooscut-bxi","title":"Show video clip thumbnails in timeline","description":"Display thumbnail previews for video clips in the timeline:\n- Performant rendering like subformer\n- Handle zoom in/out properly (show more/fewer thumbnails)\n- Reference: /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:57.360668-08:00","created_by":"mohebifar","updated_at":"2026-02-04T23:22:18.364728-08:00","closed_at":"2026-02-04T23:22:18.364728-08:00","close_reason":"Closed"} -{"id":"tooscut-d1w","title":"Create numeric input component with drag-to-adjust","description":"Create a numeric input component that:\n- Can increase/decrease value by click and drag left/right\n- Allows direct editing when clicked\n- Accepts a suffix prop for units (e.g., '%', 'px', '°')\n- Will be used in the properties panel for transform/effect controls","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:55.226838-08:00","created_by":"mohebifar","updated_at":"2026-02-03T15:44:26.731079-08:00","closed_at":"2026-02-03T15:44:26.731079-08:00","close_reason":"Closed"} -{"id":"tooscut-dz0","title":"Generate and display project thumbnails","description":"Projects should have a thumbnail that is shown on the project list page. Generate a thumbnail from the project content (e.g., render a frame from the timeline at a representative time) and store it as thumbnailDataUrl in the DexieJS projects table. Display these thumbnails in the project cards on the home/projects page.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:19.167347-08:00","created_by":"mohebifar","updated_at":"2026-02-22T17:08:56.282213-08:00","closed_at":"2026-02-22T17:08:56.282213-08:00","close_reason":"Closed","labels":["feature"]} -{"id":"tooscut-fbw","title":"Proper handling of clip trimming","description":"Implement proper clip trimming functionality:\n- Trim from left edge (adjusts start time and in-point)\n- Trim from right edge (adjusts duration)\n- Visual feedback during trim operation\n- Respect asset duration limits","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:57.195771-08:00","created_by":"mohebifar","updated_at":"2026-02-05T00:14:26.507498-08:00","closed_at":"2026-02-05T00:14:26.507498-08:00","close_reason":"Closed"} -{"id":"tooscut-i1k","title":"Snap clips in timeline (move and trim)","description":"When moving a clip close to another clip's edge, snap them together automatically. When trimming a clip, snap to edges of clips on other tracks. Show a visual snap line indicator when snapping occurs. Should work for both move and trim operations across all tracks.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:21.397626-08:00","created_by":"mohebifar","updated_at":"2026-02-07T00:32:00.23669-08:00","closed_at":"2026-02-07T00:32:00.23669-08:00","close_reason":"Closed"} -{"id":"tooscut-ixy","title":"J/K/L playback shortcuts and expanded keyboard navigation","description":"## Summary\nAdd industry-standard NLE keyboard shortcuts for playback control and navigation.\n\n## UX Design\n\n### J/K/L Playback (highest priority)\n- L: Play forward. Press again to increase speed (1x -\u003e 2x -\u003e 4x -\u003e 8x)\n- K: Pause. Hold K + tap L = step forward one frame. Hold K + tap J = step backward one frame.\n- J: Play reverse. Press again to increase reverse speed (1x -\u003e 2x -\u003e 4x -\u003e 8x)\n- Speed resets when switching direction or pressing K\n- Show current playback speed in the playback controls bar when not 1x (e.g. \"2x\" or \"-4x\")\n\n### Frame-accurate navigation\n- , (comma): Previous frame (already in menu, verify wired)\n- . (period): Next frame (already in menu, verify wired)\n- Shift+, : Jump back 1 second (10 frames at 30fps, or fps-based)\n- Shift+. : Jump forward 1 second\n- Home: Jump to start of timeline (verify working)\n- End: Jump to end of last clip (verify working)\n\n### Clip navigation\n- Up arrow: Select previous clip on same track (or move selection up a track)\n- Down arrow: Select next clip on same track (or move selection down a track)\n- Shift+Left/Right: Nudge selected clip by 1 frame\n- Alt+Left/Right: Nudge selected clip by 10 frames\n\n### Implementation notes\n- All shortcuts registered in the existing global keydown handler in canvas-timeline.tsx\n- Skip when focus is on INPUT/TEXTAREA (existing pattern)\n- Store needs: playbackSpeed state, setPlaybackSpeed action\n- Audio engine needs speed parameter adjustment during J/K/L playback\n- Playback controls bar should show speed indicator when not 1x","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-03-30T01:03:57.129655-07:00","created_by":"mohebifar","updated_at":"2026-03-30T18:31:20.948554-07:00","closed_at":"2026-03-30T18:31:20.948554-07:00","close_reason":"Closed"} -{"id":"tooscut-jt2","title":"Implement audio renderer WASM module","description":"Create an audio renderer WASM module for audio processing and playback. Use subformer's implementation at /Users/mohebifar/dev/other/kareem/subformer for inspiration on how audio rendering is handled.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:55.552314-08:00","created_by":"mohebifar","updated_at":"2026-02-03T16:17:41.966779-08:00","closed_at":"2026-02-03T16:17:41.966779-08:00","close_reason":"Closed"} -{"id":"tooscut-k4m","title":"Ripple editing mode","description":"## Summary\nAdd a ripple editing mode that shifts downstream clips when deleting, trimming, or moving clips.\n\n## UX Design\n\n### Toggle\n- Ripple toggle button in TimelineToolbar (next to tool selector)\n- Keyboard shortcut: R to toggle (consistent with V=Select, C=Razor pattern)\n- Visual indicator: button stays highlighted when active\n\n### Behavior when ripple is ON\n\n**Ripple Delete:**\n- Deleting a clip shifts all clips to the right on the same track left to fill the gap\n- Linked clips (audio+video pairs) ripple together\n- Other tracks are NOT affected\n\n**Ripple Trim:**\n- Trimming right edge: downstream clips shift to match\n- Trimming left edge: clip and all downstream clips shift together\n- Visual preview: ghost outlines show where downstream clips will land during drag\n\n### Behavior when ripple is OFF\n- Current behavior (no change)\n\n### Store Changes\n- Add rippleMode boolean to store state with toggleRippleMode() action\n- Add rippleDelete(clipId) that removes clip and shifts downstream\n- Modify trimLeft/trimRight to accept a ripple flag\n- Add utility: getDownstreamClips(trackId, afterTime)\n\n### Visual Feedback\n- Subtle indicator on timeline when ripple mode is active\n- During ripple drag/trim, show ghost positions of affected downstream clips","status":"open","priority":1,"issue_type":"feature","created_at":"2026-03-30T01:03:25.119552-07:00","created_by":"mohebifar","updated_at":"2026-03-30T01:03:25.119552-07:00"} -{"id":"tooscut-li4","title":"Audio clip fade handles on timeline","description":"## Summary\nAdd draggable fade-in/fade-out handles on audio clip edges in the timeline for quick volume fades.\n\n## UX Design\n\n### Visual design\n- Small triangular handles at the top corners of audio clips on the timeline\n- Fade-in handle: top-left corner. Fade-out handle: top-right corner\n- Handles 8x8px, visible on hover over audio clips\n- Fade region draws a curved line from 0 to 100% volume\n- Fade curve rendered as a semi-transparent overlay on the clip, above the waveform\n\n### Interaction\n- Drag fade-in handle rightward to increase fade-in duration\n- Drag fade-out handle leftward to increase fade-out duration\n- Minimum fade: 1 frame. Maximum fade: half the clip duration\n- Snapping: fade handles snap to grid lines (same as clip snap)\n- Double-click a fade handle to reset it to 0 (remove fade)\n\n### Data model\n- Uses dedicated fadeIn/fadeOut duration fields on the clip (NOT keyframes)\n- Fade durations are relative to clip edges so they survive trimming\n- The WASM audio mixer already supports per-clip fade_in and fade_out durations\n- Currently these are always 0 because there is no UI to set them\n\n### Store changes\n- Add fadeIn and fadeOut number fields to AudioClip type (duration in frames)\n- Add setClipFadeIn(clipId, frames) and setClipFadeOut(clipId, frames) actions\n- Pass fade values through useAudioEngine timeline sync with framesToSeconds conversion\n- These are undoable (temporal-tracked state)\n\n### Timeline rendering (Konva)\n- Detect hover near top corners of audio clips (8px hit zone)\n- Show ew-resize cursor on hover\n- During drag, render the fade curve preview in real-time\n- Fade region: filled area between the straight top edge and the curve","status":"open","priority":2,"issue_type":"feature","created_at":"2026-03-31T13:01:17.976325-07:00","created_by":"mohebifar","updated_at":"2026-03-31T13:01:17.976325-07:00"} -{"id":"tooscut-lso","title":"Implement video export with muxer and web worker parallelization","description":"Add ability to export videos using the same muxer method and web worker parallelization used in subformer. Reference: /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:55.879274-08:00","created_by":"mohebifar","updated_at":"2026-02-06T02:12:38.306274-08:00","closed_at":"2026-02-06T02:12:38.306274-08:00","close_reason":"Closed"} -{"id":"tooscut-mgr","title":"Project persistence with IndexedDB and project chooser page","description":"Store projects in IndexedDB using DexieJS. Add a project list page (home/index route) where users can see saved projects, create new ones, and open existing ones. Projects should auto-save on changes (debounced). Store project name, settings, timeline content, and thumbnail.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:26.053163-08:00","created_by":"mohebifar","updated_at":"2026-02-07T22:16:17.087971-08:00","closed_at":"2026-02-07T22:16:17.087971-08:00","close_reason":"Closed"} -{"id":"tooscut-n75","title":"Color Grading: LUT Support node","description":"Implement 3D LUT loading and application node. Includes: .cube file parser (standard 3D LUT format), LUT preview thumbnail, 3D texture upload to GPU, trilinear and tetrahedral interpolation in shader (shader functions already written in color_grading_shader.rs), LUT browser UI. Types already defined: LutReference, LutInterpolation.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-06T13:08:35.744189-07:00","created_by":"mohebifar","updated_at":"2026-04-06T13:19:07.170923-07:00","closed_at":"2026-04-06T13:19:07.170923-07:00","close_reason":"Closed"} -{"id":"tooscut-nse","title":"Add shapes, texts, images, clips and audios to timeline","description":"Implement ability to add various layer types to the timeline:\n- Shape layers (rectangles, circles, polygons, lines)\n- Text layers\n- Image layers\n- Video clips\n- Audio clips","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.702715-08:00","created_by":"mohebifar","updated_at":"2026-02-06T01:14:51.651987-08:00","closed_at":"2026-02-06T01:14:51.651987-08:00","close_reason":"Already implemented: Asset panel, text panel, and shape panel all support drag-and-drop to timeline. Canvas timeline has drop handlers for all clip types. Store has addClipToTrack action supporting video, audio, image, text, and shape clips."} -{"id":"tooscut-p7h","title":"Auto-place first clip at timeline start","description":"When adding the very first clip to an empty timeline, automatically place it at time 0 (the beginning). This only applies when the timeline has no existing clips. Subsequent clips should still be placed at the drop position or current time.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-02-06T21:58:30.572339-08:00","created_by":"mohebifar","updated_at":"2026-02-06T22:20:58.705414-08:00","closed_at":"2026-02-06T22:20:58.705414-08:00","close_reason":"Closed"} -{"id":"tooscut-pvp","title":"Add transform mode and view mode toggle for preview panel","description":"Introduce two modes for the preview panel: View mode and Transform mode, with toggle buttons below the preview. Currently, clicking on clip boundaries in the preview or selecting any clip from the timeline shows transform handles (resize/move). This should only happen in Transform mode. In View mode, transform handles should be hidden and clips should not be movable/resizable in the preview. Add toggle buttons (e.g. icons) below the preview panel to switch between modes.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:43.765143-08:00","created_by":"mohebifar","updated_at":"2026-02-22T19:41:28.491218-08:00","closed_at":"2026-02-22T19:41:28.491218-08:00","close_reason":"Closed"} -{"id":"tooscut-qpn","title":"Add mute/unmute and lock/unlock for tracks","description":"Implement track controls:\n- Mute/unmute: For video tracks, this hides the track's clips from rendering. For audio tracks, this mutes the audio.\n- Lock/unlock: Prevents editing of clips on locked tracks","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.544018-08:00","created_by":"mohebifar","updated_at":"2026-02-04T02:09:25.954736-08:00","closed_at":"2026-02-04T02:09:25.954736-08:00","close_reason":"Closed"} -{"id":"tooscut-sg3","title":"Show audio waveform on audio clips","description":"Display audio waveform visualization on audio clips in the timeline:\n- Performant rendering like subformer\n- Handle zoom in/out properly\n- Reference: /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:57.521544-08:00","created_by":"mohebifar","updated_at":"2026-02-05T00:12:30.453041-08:00","closed_at":"2026-02-05T00:12:30.453041-08:00","close_reason":"Closed"} -{"id":"tooscut-shs","title":"Multi-select clips: batch move and trim","description":"Allow selecting multiple clips in the timeline (click + shift/ctrl, or drag-select). Moving one selected clip moves all selected clips. Trimming/extending one selected clip applies the same delta to all selected clips.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:23.231693-08:00","created_by":"mohebifar","updated_at":"2026-02-22T19:42:40.147061-08:00","closed_at":"2026-02-22T19:42:40.147061-08:00","close_reason":"Closed"} -{"id":"tooscut-sp0","title":"Fix overlap handling to trim instead of delete","description":"Fix the behavior of overlap handling. When a clip overlaps another clip:\n- Instead of deleting the overlapped clip, it should trim it to avoid the overlap\n- Only delete if the clip is fully overlapped (100% covered)\n- Partial overlaps should result in trimming the overlapped portion","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-02-03T15:31:57.038584-08:00","created_by":"mohebifar","updated_at":"2026-02-04T01:52:40.207676-08:00","closed_at":"2026-02-04T01:52:40.207676-08:00","close_reason":"Closed"} -{"id":"tooscut-t6f","title":"Drag and drop assets from assets panel to preview","description":"Allow dragging assets from the assets panel directly into the preview panel. The asset should be added at the current playhead timestamp on the highest (frontmost) video track. If the highest track is already occupied at the playhead position and/or adding the asset would overlap existing clips, automatically create a new track pair and place the asset there instead.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:39.905379-08:00","created_by":"mohebifar","updated_at":"2026-02-22T17:20:28.702887-08:00","closed_at":"2026-02-22T17:20:28.702887-08:00","close_reason":"Closed"} -{"id":"tooscut-vev","title":"Implement keyframe animation system","description":"Add keyframe animation support for clip properties (position, scale, rotation, opacity, etc.). This includes:\n\n- Keyframe data model in clip state (KeyframeTracks with property -\u003e keyframe[] mapping)\n- Keyframe interpolation using the existing EvaluatorManager from render-engine\n- UI for adding/removing keyframes at current playhead position\n- Visual keyframe indicators on timeline clips\n- Property panel integration to show animated values\n\nReference: subformer implementation at /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T02:14:25.299746-08:00","created_by":"mohebifar","updated_at":"2026-02-06T21:58:14.325586-08:00","closed_at":"2026-02-06T21:58:14.325586-08:00","close_reason":"Closed"} -{"id":"tooscut-wio","title":"Playhead is not movable during playback","description":"While playing, the user cannot move the playhead to scrub/seek. It just keeps playing and ignores playhead drag interaction. Expected: dragging the playhead during playback should pause and seek (or seek while playing).","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-06T21:58:19.344465-08:00","created_by":"mohebifar","updated_at":"2026-02-06T22:18:49.143947-08:00","closed_at":"2026-02-06T22:18:49.143947-08:00","close_reason":"Closed"} -{"id":"tooscut-wlg","title":"Drag ghost should reflect actual clip duration","description":"When dragging a clip from the assets panel onto the timeline, the preview ghost/shadow should be representative of the actual duration of the video/audio asset, not a fixed size. The ghost width should match what the clip will look like at the current zoom level.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-02-06T21:58:32.635988-08:00","created_by":"mohebifar","updated_at":"2026-02-06T22:23:14.006719-08:00","closed_at":"2026-02-06T22:23:14.006719-08:00","close_reason":"Closed"} -{"id":"tooscut-yq4","title":"Cut (Cmd+X) and Duplicate clip operations","description":"## Summary\nImplement cut (copy + delete) and duplicate operations. Cut menu item exists but is disabled.\n\n## UX Design\n\n### Cut (Cmd+X)\n- Copy selected clips to clipboard, then delete them\n- If ripple mode is on (see tooscut-k4m), ripple-delete after copy\n- If ripple mode is off, just delete (leave gap)\n- Linked clips: cut both video and audio together\n- Menu item in Edit menu already exists — enable it and wire handler\n\n### Duplicate (Cmd+D)\n- Duplicate selected clips and place them immediately after the originals\n- New clips start at the end of the rightmost selected clip\n- Linked clips: duplicate both video and audio\n- Select the new duplicates (deselect originals)\n- Useful for repeating patterns, creating variations\n\n### Implementation\n- Store actions needed: cutSelectedClips() — calls copySelectedClips() then removeClip() for each\n- Store actions needed: duplicateSelectedClips() — deep-clone clips, assign new IDs, offset startTime\n- Keyboard handler: add Cmd+X and Cmd+D to global keydown in canvas-timeline.tsx\n- Menu: enable existing Cut item, add Duplicate item to Edit menu\n- Duplicate should generate new clip IDs and new linkedClipIds for pairs","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-03-30T01:11:31.108201-07:00","created_by":"mohebifar","updated_at":"2026-04-01T22:15:34.121127-07:00","closed_at":"2026-04-01T22:15:34.121127-07:00","close_reason":"Closed"} -{"id":"tooscut-yqc","title":"Make entire assets panel a dropzone for file imports","description":"Make the entire assets panel act as a dropzone so users can drag and drop files from Finder/file explorer anywhere on the assets panel to import them. Currently only specific areas may accept drops.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:48.50237-08:00","created_by":"mohebifar","updated_at":"2026-02-22T16:50:04.215085-08:00","closed_at":"2026-02-22T16:50:04.215085-08:00","close_reason":"Closed"} +{"id":"tooscut-0ip","title":"Implement spline/curve editor for keyframes","description":"Add a spline/curve editor UI for fine-tuning keyframe interpolation. This includes:\n\n- Bezier curve editor component (similar to After Effects graph editor)\n- Visual representation of property values over time\n- Draggable control points for bezier handles\n- Easing presets (ease-in, ease-out, ease-in-out, linear, custom)\n- Integration with keyframe system to edit interpolation curves\n- Zoom/pan controls for timeline navigation\n\nReference: subformer spline editor at /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T02:14:28.686002-08:00","updated_at":"2026-02-07T22:16:06.811012-08:00","closed_at":"2026-02-07T22:16:06.811012-08:00","close_reason":"Closed","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-0ip","depends_on_id":"tooscut-vev","type":"blocks","created_at":"2026-02-06T02:15:17.682583-08:00","created_by":"mohebifar"}]} +{"id":"tooscut-1fj","title":"Make menu bar functional","description":"The menu bar is currently fully decorative/non-functional. Wire up all menu items to their corresponding actions (File: new project, open, save, export; Edit: undo, redo, cut, copy, paste, delete; View: zoom controls, panel toggles; etc). Menu items without implementations should be disabled/grayed out.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:15.451904-08:00","updated_at":"2026-02-22T16:48:23.082522-08:00","closed_at":"2026-02-22T16:48:23.082522-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-2ig","title":"Color Grading: Curves Editor node","description":"Implement RGB curves editor node for the color grading pipeline. Includes: master curve, per-channel R/G/B curves, and advanced hue-vs-sat/hue-vs-hue curves. Needs: interactive bezier curve UI component, 1D LUT texture generation from curves, shader integration for curve application. Types already defined in Rust (Curves, Curve1D) and TypeScript.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-06T13:08:29.099134-07:00","updated_at":"2026-04-06T13:19:07.156573-07:00","closed_at":"2026-04-06T13:19:07.156573-07:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-38j","title":"Add audio effects (EQ, reverb, compression, noise gate)","description":"Add audio processing effects beyond volume and speed. Potential effects to implement:\n\n- **EQ (Equalizer)**: Low/mid/high band adjustment for tonal control\n- **Reverb**: Room simulation for spatial depth\n- **Compression**: Dynamic range compression to even out volume levels\n- **Noise gate**: Suppress audio below a threshold to remove background noise\n- **Low-pass / High-pass filters**: Frequency filtering\n\nEach effect needs:\n1. Rust implementation in crates/audio-engine (DSP processing in the mixer pipeline)\n2. UI controls in the audio properties panel (apps/ui/src/components/editor/audio-properties.tsx)\n3. Keyframeable parameters via the existing KeyframeInput system\n4. Timeline state serialization (AudioClipState in packages/render-engine/src/audio-engine.ts)\n5. Support in both playback (AudioWorklet) and export (offline WASM rendering) paths","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-23T01:25:37.868493-08:00","updated_at":"2026-03-31T13:01:28.876263-07:00","closed_at":"2026-03-31T13:01:28.876263-07:00","close_reason":"Fully implemented: EQ with visual frequency response editor, compressor, noise gate, and reverb all have WASM DSP + UI controls + keyframe support","created_by":"mohebifar"} +{"id":"tooscut-38z","title":"Transition system: in, out, and cross transitions","description":"Add ability to apply transitions (fade, dissolve, wipe, slide, etc.) to clips. Transitions should be droppable from the left panel onto clip edges (in/out) or between adjacent clips (cross transitions). The left panel should show a visual preview of each transition type. Leverage the existing transition types already defined in the render engine.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:28.62805-08:00","updated_at":"2026-02-22T01:43:24.526507-08:00","closed_at":"2026-02-22T01:43:24.526507-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-3ck","title":"Add project settings (dimensions, resolution, orientation, frame rate)","description":"Add a project settings UI that allows users to configure: video dimensions (width/height), resolution presets (1080p, 4K, etc), orientation (landscape/portrait), and frame rate (24, 30, 60 fps, custom). Settings should be stored in the project record in DexieJS and applied to the compositor/export pipeline.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:16.677159-08:00","updated_at":"2026-02-22T11:48:45.255249-08:00","closed_at":"2026-02-22T11:48:45.255249-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-40n","title":"Timeline markers with in/out points","description":"## Summary\nAdd named timeline markers and in/out point system for marking regions of interest.\n\n## UX Design\n\n### Marker creation\n- M: Drop a marker at the current playhead position\n- Marker appears as a small colored triangle on the ruler area (above tracks, below timecode)\n- Default color: cyan. Can be changed per marker.\n- Double-click marker triangle to rename (inline text edit on ruler)\n- Right-click marker: context menu with Rename, Change Color, Delete\n\n### Marker navigation\n- Shift+M: Go to next marker\n- Alt+M (or Ctrl+Shift+M): Go to previous marker\n- Markers listed in a dropdown/popover accessible from the toolbar (optional, lower priority)\n\n### In/Out points\n- I: Set in-point at playhead\n- O: Set out-point at playhead\n- In/out displayed as a highlighted region on the ruler (semi-transparent overlay between in and out)\n- Alt+I / Alt+O: Clear in/out point\n- In/out points constrain export range when set (pass to export dialog)\n- In/out points constrain playback loop when loop mode is enabled\n\n### Visual design\n- Markers: small downward-pointing triangles (6px) on the ruler, same row as timecode\n- Marker label: tiny text above the triangle, visible on hover or always if space permits\n- In/out region: subtle colored overlay on the ruler between the two points\n- In point: [ bracket icon. Out point: ] bracket icon.\n\n### Store changes\n- Add markers: Array\u003c{ id, time, name, color }\u003e to project state\n- Add inPoint: number | null, outPoint: number | null to store\n- Actions: addMarker, removeMarker, updateMarker, setInPoint, setOutPoint, clearInOutPoints\n- Markers saved with project (persisted to IndexedDB)\n- In/out points are session-only (not persisted)\n\n### Integration with export\n- If in/out points are set, export dialog should show option to export only the marked region\n- Default: export full timeline. Checkbox: \"Export in/out range only\"","status":"open","priority":2,"issue_type":"feature","created_at":"2026-03-30T01:10:44.409844-07:00","updated_at":"2026-03-30T01:10:44.409844-07:00","created_by":"mohebifar"} +{"id":"tooscut-42i","title":"Gesture to move clips between tracks","description":"Add ability to use drag gestures to move a clip/text/shape from one track to another. Should work with linked clips (video+audio moving together to their respective track types).","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.874235-08:00","updated_at":"2026-02-05T00:14:51.901763-08:00","closed_at":"2026-02-05T00:14:51.901763-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-4cn","title":"Fix undo/redo requiring multiple keypresses","description":"Bug: between every undo/redo action, the user needs to press Cmd+Z multiple times for it to take effect. The first undo works with a single Cmd+Z, but subsequent undos require pressing Cmd+Z twice (or more). This suggests the temporal store (zundo) is recording duplicate or no-op history entries, or the undo handler is consuming keystrokes without performing the undo.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-22T02:12:22.105794-08:00","updated_at":"2026-02-22T11:02:30.122139-08:00","closed_at":"2026-02-22T11:02:30.122141-08:00","created_by":"mohebifar"} +{"id":"tooscut-4f5","title":"Preview doesn't update when paused and making changes","description":"When paused, moving a clip or making any change doesn't update the preview. The preview must re-render when timeline state changes while paused.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-02-03T15:31:54.584644-08:00","updated_at":"2026-02-03T15:36:39.074212-08:00","closed_at":"2026-02-03T15:36:39.074212-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-5du","title":"Linked clip doesn't move visually during drag gesture","description":"When moving a clip, the linked audio/video clip doesn't move with it during the move gesture. It only moves on mouse up. The UX doesn't feel good - linked clips should move together in real-time during the drag.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-02-03T15:31:54.907253-08:00","updated_at":"2026-02-03T15:42:35.648624-08:00","closed_at":"2026-02-03T15:42:35.648624-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-61n","title":"Fix file import via menu bar (File \u003e Import Media)","description":"The File \u003e Import Media menu item opens the file picker dialog, but after selecting a file, nothing happens — the asset is not added to the assets panel. The file picker integration (importFilesWithPicker) is not completing the import flow when triggered from the menu bar.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-22T15:37:50.751826-08:00","updated_at":"2026-02-22T15:40:20.276596-08:00","closed_at":"2026-02-22T15:40:20.276596-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-63m","title":"Color Grading: HSL Qualifier node","description":"Implement HSL Qualifier (secondary color correction) node. Allows isolating specific color ranges by hue/saturation/luminance with soft edges, then applying a correction (PrimaryCorrection) only within that mask. Includes: qualifier mask preview overlay, hue/sat/lum range picker UI with center/width/softness controls, invert toggle, qualifier mask visualization on the preview panel. Shader functions already written (HSL_QUALIFIER_FUNCTIONS). Types defined: HslQualifier.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-06T13:08:42.895501-07:00","updated_at":"2026-04-06T13:19:07.183977-07:00","closed_at":"2026-04-06T13:19:07.183977-07:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-63o","title":"Drag and drop files from OS file explorer directly to timeline","description":"Allow dragging files from Finder/file explorer directly onto the timeline to import and place them in one action. If the dropped file matches an already-imported asset (by name/size/type), reuse the existing asset rather than duplicating it in the assets panel. If it's a new file, import it as a new asset first, then add the clip to the timeline at the drop position.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:46.528465-08:00","updated_at":"2026-02-22T17:25:09.399064-08:00","closed_at":"2026-02-22T17:25:09.399064-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-731","title":"Timeline drag-to-select (rubber band selection)","description":"Add rubber-band / marquee drag selection to the timeline view. Currently multi-select only works by holding Shift/Cmd and clicking individual clips. Users should be able to click and drag on empty timeline space to draw a selection rectangle that selects all clips it intersects.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:13.781927-08:00","updated_at":"2026-02-22T11:34:18.138855-08:00","closed_at":"2026-02-22T11:34:18.138855-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-8rt","title":"Color Grading: Power Window node","description":"Implement Power Window (regional mask) node. Allows applying corrections to specific regions of the frame using shapes (circle/ellipse, rectangle, polygon, linear gradient). Includes: interactive shape overlay on preview panel with drag handles for position/size/rotation/softness, inner/outer softness controls, invert toggle. Each window carries its own PrimaryCorrection. Types defined: PowerWindow, PowerWindowShape.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-04-06T13:08:49.169099-07:00","updated_at":"2026-04-06T13:19:07.194997-07:00","closed_at":"2026-04-06T13:19:07.194997-07:00","close_reason":"Closed","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-8rt","depends_on_id":"tooscut-63m","type":"blocks","created_at":"2026-04-06T13:08:55.133021-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-a6i","title":"Add ability to add and remove tracks","description":"Implement UI and functionality to:\n- Add new video/audio track pairs to the timeline\n- Remove existing tracks from the timeline","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.221806-08:00","updated_at":"2026-02-03T22:40:45.317523-08:00","closed_at":"2026-02-03T22:40:45.317523-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-and","title":"Color Grading System","description":"## Summary\nImplement a professional-grade color grading system with feature parity to DaVinci Resolve and Final Cut Pro.\n\n## Core Features\n- **Primary Correction** — CDL-based (slope/offset/power) + exposure, temperature, tint, saturation\n- **Color Wheels** — Lift/Gamma/Gain with luminance controls\n- **Curves** — Master, RGB, plus advanced (Hue vs Sat, Lum vs Sat, etc.)\n- **3D LUT Support** — .cube file parsing with tetrahedral interpolation\n- **HSL Qualifier** — Secondary color correction keying\n- **Power Windows** — Shapes/masks for regional corrections\n- **Scopes** — Waveform, vectorscope, histogram, parade (GPU-computed)\n- **Node-Based Pipeline** — Ordered processing chain per clip\n- **Color Matching** — Auto-match between clips\n\n## Architecture\nAll rendering happens in Rust/WASM (per project rules):\n- Color space conversions (sRGB ↔ Linear ↔ ACEScg ↔ Log)\n- CDL/LGG operations in WGSL shaders\n- 3D LUT textures with tetrahedral interpolation\n- Compute shaders for scope generation\n\nTypeScript/React handles UI only:\n- Color wheel canvas components\n- Curves editor\n- Scope display (Canvas 2D rendering of WASM-computed data)\n\n## Key Files to Modify\n- `crates/types/src/` — Add `color_grading.rs`\n- `crates/compositor/src/` — Add shaders and pipeline integration\n- `packages/render-engine/src/types.ts` — TypeScript definitions\n- `apps/ui/src/components/editor/` — New color grading UI components\n- `apps/ui/src/state/video-editor-store.ts` — State management","status":"open","priority":2,"issue_type":"epic","created_at":"2026-04-04T14:02:16.802358-07:00","updated_at":"2026-04-04T14:02:16.802358-07:00","created_by":"mohebifar"} +{"id":"tooscut-and.1","title":"Color grading foundation types and color space shaders","description":"## Summary\nDefine core types and implement color space conversion shaders as the foundation for the color grading system.\n\n## Tasks\n\n### Rust Types (crates/types/src/color_grading.rs)\n- [ ] `ColorSpace` enum (sRGB, Linear, ACEScg, LogC, SLog3, CLog3, VLog)\n- [ ] `PrimaryCorrection` struct (CDL: slope, offset, power + saturation, exposure, temperature, tint)\n- [ ] `ColorWheelValue` struct (angle, distance)\n- [ ] `ColorWheels` struct (lift, gamma, gain with luminance controls)\n- [ ] `Curve1D` and `CurvePoint` structs\n- [ ] `ColorGradingState` struct (combines all grading data for a layer)\n- [ ] Add `color_grading: Option\u003cColorGradingState\u003e` to `MediaLayerData`\n\n### TypeScript Types (packages/render-engine/src/types.ts)\n- [ ] Mirror all Rust types in TypeScript\n- [ ] Use snake_case to match serde serialization\n\n### WGSL Shaders (crates/compositor/src/shaders/color_grading.wgsl)\n- [ ] `srgb_to_linear()` and `linear_to_srgb()`\n- [ ] `rgb_to_hsl()` and `hsl_to_rgb()`\n- [ ] LogC/SLog3/VLog transfer functions\n- [ ] ACEScg matrix conversions\n\n### Uniforms (crates/compositor/src/color_grading_uniforms.rs)\n- [ ] `ColorGradingUniforms` struct with proper alignment (repr(C), Pod, Zeroable)\n- [ ] All CDL parameters, wheel values, qualifier params, flags\n\n## Acceptance Criteria\n- Types compile in Rust and generate correct TypeScript bindings via tsify\n- Color space conversion shaders pass visual tests (known color values)\n- Uniforms struct has correct GPU alignment","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:02:29.647666-07:00","updated_at":"2026-04-04T14:41:09.868939-07:00","closed_at":"2026-04-04T14:41:09.868939-07:00","close_reason":"Closed","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.1","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:02:29.649247-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.10","title":"Color matching and grade management","description":"## Summary\nImplement advanced color grading workflow features: clip matching, grade presets, copy/paste, and stills.\n\n## Tasks\n\n### Color Matching\n- [ ] `ColorMatchNode` type with reference clip/frame\n- [ ] Histogram matching algorithm:\n - Compute histograms of source and target\n - Generate CDL values to match distributions\n- [ ] UI: Select reference frame, click \"Match\"\n- [ ] Manual refinement after auto-match\n\n### Copy/Paste Grades\n- [ ] Copy full grade (all nodes) from clip\n- [ ] Paste grade to one or multiple clips\n- [ ] Paste specific nodes only (cherry-pick)\n- [ ] Keyboard shortcuts: Cmd+Shift+C / Cmd+Shift+V\n\n### Grade Presets (Stills)\n- [ ] Save current grade as named preset\n- [ ] Preset browser with thumbnails\n- [ ] Apply preset to clip\n- [ ] Export/import presets (JSON file)\n- [ ] Built-in presets: Film looks, B\u0026W, Vintage, etc.\n\n### Gallery (Stills)\n- [ ] Capture still (frame + grade) for reference\n- [ ] Gallery panel showing captured stills\n- [ ] Click still to apply its grade\n- [ ] Compare current frame to still\n\n### Ripple Grade Changes\n- [ ] Option to apply grade changes to all clips with same source\n- [ ] Useful for multi-cam or repeated shots\n\n### State Management\n- [ ] Add `colorGradingPresets: Map\u003cstring, ClipColorGrading\u003e` to store\n- [ ] Add `capturedStills: Array\u003c{ frame, grade, thumbnail }\u003e` to store\n- [ ] Add actions: `savePreset`, `applyPreset`, `captureStill`, `copyGrade`, `pasteGrade`\n\n## Acceptance Criteria\n- Color match produces visually similar results between clips\n- Copy/paste works across clips in same or different projects\n- Presets persist and load correctly\n- Gallery stills show accurate previews\n- Can quickly match multiple clips to a reference","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-04T14:04:39.71225-07:00","updated_at":"2026-04-04T14:04:39.71225-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.10","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:04:39.714933-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.10","depends_on_id":"tooscut-and.9","type":"blocks","created_at":"2026-04-04T14:04:39.716245-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.2","title":"Primary color correction (CDL)","description":"## Summary\nImplement CDL (Color Decision List) based primary color correction with basic adjustment controls.\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `apply_cdl()` function: `out = pow(in * slope + offset, power)`\n- [ ] `apply_saturation()` function using luminance mix\n- [ ] `apply_exposure()` function (EV stops, -4 to +4)\n- [ ] `apply_temperature_tint()` function (Kelvin offset + green-magenta)\n- [ ] Highlight/shadow recovery functions\n\n### Compositor Integration\n- [ ] Add color grading render pass after media compositing\n- [ ] Create bind group for color grading uniforms\n- [ ] Wire uniforms from `MediaLayerData.color_grading` to shader\n\n### UI Components (apps/ui/src/components/editor/color-grading/)\n- [ ] `PrimaryCorrectionPanel` component with:\n - Exposure slider (-4 to +4 EV)\n - Temperature slider (2000K to 10000K)\n - Tint slider (green to magenta)\n - Contrast slider\n - Highlights/Shadows sliders\n - Saturation slider\n- [ ] CDL sliders (Slope R/G/B, Offset R/G/B, Power R/G/B) in collapsible \"Advanced\" section\n\n### State Management\n- [ ] Add `updateClipPrimaryCorrection(clipId, correction)` action\n- [ ] Integrate with undo/redo (temporal middleware)\n\n### Keyframe Support\n- [ ] Add primary correction properties to `COLOR_GRADING_ANIMATABLE_PROPERTIES`\n- [ ] Test keyframe interpolation for exposure, temperature, etc.\n\n## Acceptance Criteria\n- Adjusting exposure visibly changes clip brightness in preview\n- CDL values match ASC-CDL standard behavior\n- Changes are undoable\n- Keyframes animate smoothly","notes":"Completed: UI components (PrimaryCorrectionProperties, ColorGradingPanel), state management (updateClipColorGrading action), TypeScript types with keyframe support. Remaining: WGSL shader integration, bind group creation, visual testing.","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:02:42.611481-07:00","updated_at":"2026-04-04T14:50:13.74347-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.2","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:02:42.613067-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.2","depends_on_id":"tooscut-and.1","type":"blocks","created_at":"2026-04-04T14:02:42.615138-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.3","title":"Color wheels (Lift/Gamma/Gain)","description":"## Summary\nImplement Lift/Gamma/Gain color wheels for intuitive shadow/midtone/highlight color adjustment.\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `color_wheel_to_rgb()` - convert angle/distance to RGB offset\n- [ ] `apply_lift_gamma_gain()` function:\n - Lift affects shadows (adds color when pixel is dark)\n - Gamma affects midtones (power function)\n - Gain affects highlights (multiplies)\n- [ ] Each wheel has RGB color shift + luminance master\n\n### UI Components\n- [ ] `ColorWheel` component (Canvas-based):\n - Circular color picker with saturation gradient\n - Draggable position indicator\n - Double-click to reset to center\n - Luminance slider below wheel\n- [ ] `ColorWheelsPanel` with three wheels (Lift, Gamma, Gain)\n- [ ] \"Link wheels\" toggle for global adjustments\n- [ ] Reset button per wheel and global reset\n\n### Interaction Design\n- [ ] Mouse drag updates angle + distance from center\n- [ ] Shift+drag constrains to current angle (saturation only)\n- [ ] Ctrl+drag for fine adjustment (0.1x speed)\n- [ ] Keyboard: arrow keys for small adjustments when focused\n\n### State Management\n- [ ] Add `updateClipColorWheels(clipId, wheels)` action\n- [ ] Wheel values stored as `{ angle: number, distance: number }`\n\n## Acceptance Criteria\n- Dragging lift wheel adds color to dark areas only\n- Dragging gain wheel adds color to bright areas only\n- Gamma affects midtones without crushing blacks/whites\n- Luminance sliders work independently from color shift\n- All changes are undoable","notes":"Completed: Canvas-based ColorWheel component with drag/shift+drag/ctrl+drag interactions, ColorWheelsProperties panel (Lift/Gamma/Gain), and React Flow node graph visualization. The node graph shows the color grading pipeline with color-coded nodes, enable/disable toggles, and drag-to-reorder support.","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:02:55.013616-07:00","updated_at":"2026-04-04T16:58:32.135196-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.3","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:02:55.015382-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.3","depends_on_id":"tooscut-and.2","type":"blocks","created_at":"2026-04-04T14:02:55.018517-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.4","title":"Curves editor","description":"## Summary\nImplement RGB curves for precise tonal control, plus advanced curves (Hue vs Sat, Lum vs Sat, etc.).\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `evaluate_curve()` function using 1D LUT texture sampling\n- [ ] Support for smooth spline interpolation between control points\n- [ ] Apply curves in correct order: Master → R → G → B → Advanced\n\n### Curve LUT Generation (TypeScript)\n- [ ] `generateCurveLUT(points: CurvePoint[])` - outputs 256-entry Float32Array\n- [ ] Catmull-Rom or cubic Bezier interpolation between points\n- [ ] Handle edge cases (points at 0,0 and 1,1)\n\n### UI Components\n- [ ] `CurvesEditor` component (Canvas-based):\n - 256x256 grid with diagonal reference line\n - Click to add control point\n - Drag to move control point\n - Double-click or Delete key to remove point\n - Smooth curve rendering through points\n- [ ] Channel selector tabs: Master, Red, Green, Blue\n- [ ] Advanced curves dropdown: Hue vs Sat, Hue vs Lum, Sat vs Sat, Lum vs Sat\n- [ ] Preset curves: S-curve, fade, negative, etc.\n- [ ] Reset button\n\n### Advanced Curves\n- [ ] Hue vs Hue - rotate hues selectively\n- [ ] Hue vs Sat - saturate/desaturate specific hues\n- [ ] Hue vs Lum - brighten/darken specific hues\n- [ ] Lum vs Sat - saturate shadows/highlights differently\n- [ ] Sat vs Sat - compress or expand saturation range\n\n### State Management\n- [ ] Add `updateClipCurves(clipId, curves)` action\n- [ ] Curve presets stored separately for quick application\n\n### Performance\n- [ ] Regenerate LUT texture only when curve points change\n- [ ] Debounce during drag operations\n- [ ] Cache curve textures per clip\n\n## Acceptance Criteria\n- Dragging curve up brightens, down darkens\n- RGB curves affect only their respective channels\n- S-curve increases contrast visually\n- Smooth interpolation (no stair-stepping)\n- 60fps interaction during curve editing","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:09.958519-07:00","updated_at":"2026-04-04T14:03:09.958519-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.4","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:09.960905-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.4","depends_on_id":"tooscut-and.2","type":"blocks","created_at":"2026-04-04T14:03:09.964293-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.5","title":"3D LUT support (.cube files)","description":"## Summary\nImplement 3D LUT loading, GPU upload, and tetrahedral interpolation for professional color transforms.\n\n## Tasks\n\n### .cube File Parser (TypeScript)\n- [ ] `parseCubeFile(content: string): CubeLUT`\n- [ ] Parse header: TITLE, LUT_3D_SIZE, DOMAIN_MIN, DOMAIN_MAX\n- [ ] Parse RGB triplet data lines\n- [ ] Validate cube size (common: 17, 33, 65)\n- [ ] Handle comments and empty lines\n\n### LUT Manager (Rust/WASM)\n- [ ] `LutManager` struct with HashMap of loaded LUTs\n- [ ] `upload_lut(id, size, data)` - creates 3D texture on GPU\n- [ ] `remove_lut(id)` - frees GPU memory\n- [ ] `get_lut_texture(id)` - returns texture view for binding\n\n### WGSL Shader Implementation\n- [ ] `apply_lut_trilinear()` - basic trilinear interpolation\n- [ ] `apply_lut_tetrahedral()` - higher quality tetrahedral interpolation\n- [ ] Handle domain min/max scaling\n- [ ] LUT sampler with clamp-to-edge addressing\n\n### UI Components\n- [ ] `LutBrowserPanel` component:\n - Grid view of available LUTs with thumbnails\n - Search/filter by name\n - Preview on hover\n - Click to apply\n- [ ] LUT import button (file picker for .cube)\n- [ ] LUT intensity slider (mix with original)\n- [ ] Remove LUT button\n\n### LUT Thumbnail Generation\n- [ ] Generate preview by applying LUT to standard gradient image\n- [ ] Cache thumbnails in IndexedDB\n\n### State Management\n- [ ] Add `loadedLuts: Map\u003cstring, LoadedLut\u003e` to store\n- [ ] Add `loadLut(file)`, `removeLut(id)`, `setClipLut(clipId, lutId)` actions\n- [ ] LUT data persisted in project (or referenced by path)\n\n### Memory Management\n- [ ] Limit simultaneous loaded LUTs (e.g., 10 max)\n- [ ] LRU eviction for unused LUTs\n- [ ] 33³ × 16 bytes = ~575KB per LUT\n\n## Acceptance Criteria\n- .cube files from DaVinci/Resolve load correctly\n- LUT applied matches reference implementation\n- Tetrahedral interpolation has no visible banding\n- LUT browser shows accurate previews\n- Memory usage stays bounded with many LUTs","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:25.934398-07:00","updated_at":"2026-04-04T14:41:14.358352-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.5","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:25.936583-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.5","depends_on_id":"tooscut-and.1","type":"blocks","created_at":"2026-04-04T14:03:25.938607-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.6","title":"HSL Qualifier (secondary color correction)","description":"## Summary\nImplement HSL qualifier for isolating and correcting specific colors (secondary color correction / keying).\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] `hsl_qualifier_mask()` function:\n - Hue center + width + softness (circular distance)\n - Saturation center + width + softness\n - Luminance center + width + softness\n - Combine masks multiplicatively\n - Invert option\n- [ ] Apply correction multiplied by qualifier mask\n- [ ] Edge softness using smoothstep\n\n### UI Components\n- [ ] `HslQualifierPanel` component:\n - Hue range selector (circular/strip visualization)\n - Saturation range selector (horizontal bar)\n - Luminance range selector (horizontal bar)\n - Softness controls for each dimension\n - Invert toggle\n- [ ] Eyedropper tool to pick qualifier center from preview\n- [ ] \"Show Mask\" toggle to visualize selection (B\u0026W mask view)\n- [ ] Correction controls (reuse PrimaryCorrectionPanel) applied to qualified region\n\n### Eyedropper Interaction\n- [ ] Click on preview to sample pixel HSL values\n- [ ] Shift+click to add to selection (expand range)\n- [ ] Alt+click to subtract from selection\n- [ ] Sample multiple pixels for average\n\n### Mask Visualization\n- [ ] Toggle between: Normal view, Mask overlay, Mask only\n- [ ] Mask overlay shows selection as highlight color\n- [ ] Mask only shows B\u0026W (white = selected)\n\n### State Management\n- [ ] Add `HslQualifierNode` to color grading node types\n- [ ] Qualifier stores center, width, softness for H/S/L\n- [ ] Correction stored within qualifier node\n\n## Acceptance Criteria\n- Can isolate skin tones and adjust without affecting background\n- Can select sky blue and increase saturation\n- Soft edges blend naturally (no harsh cutoffs)\n- Eyedropper accurately picks colors from preview\n- Mask visualization helps dial in selection","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:39.983433-07:00","updated_at":"2026-04-04T14:03:39.983433-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.6","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:39.985365-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.6","depends_on_id":"tooscut-and.2","type":"blocks","created_at":"2026-04-04T14:03:39.987334-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.7","title":"Power windows (masks for regional correction)","description":"## Summary\nImplement power windows (geometric masks) for regional color corrections - vignettes, sky gradients, face isolation, etc.\n\n## Tasks\n\n### WGSL Shader Implementation\n- [ ] Shape mask generators:\n - Circle/Ellipse\n - Rectangle with corner radius\n - Linear gradient\n - Polygon (for complex shapes)\n- [ ] Transform: position, scale, rotation\n- [ ] Inner/outer softness (feathering)\n- [ ] Invert option\n- [ ] Combine with qualifier mask (AND operation)\n\n### UI Components\n- [ ] `PowerWindowPanel` component:\n - Shape selector (circle, rectangle, gradient, polygon)\n - Position X/Y sliders (% of frame)\n - Scale X/Y sliders\n - Rotation slider\n - Softness inner/outer sliders\n - Invert toggle\n- [ ] On-canvas shape overlay:\n - Draggable shape outline on preview\n - Corner handles for resize\n - Rotation handle\n - Center point for position\n\n### Shape Drawing\n- [ ] Circle: center + radius, draggable edge\n- [ ] Rectangle: corner handles, aspect ratio lock option\n- [ ] Gradient: start point + end point + angle\n- [ ] Polygon: click to add points, close path\n\n### Tracking (stretch goal)\n- [ ] Manual keyframe tracking (position/scale/rotation keyframes)\n- [ ] Data structure for tracked window transforms\n- [ ] Interpolation between tracking keyframes\n\n### Combination with Qualifier\n- [ ] Power window can be combined with HSL qualifier\n- [ ] \"Window AND Qualifier\" mode - both must match\n- [ ] Useful for: face in specific area, sky in upper region only\n\n## Acceptance Criteria\n- Can create vignette effect with soft circular window\n- Can isolate upper sky with gradient window\n- Can draw polygon around subject\n- On-canvas controls are intuitive (drag to move, handles to resize)\n- Soft edges blend naturally","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:03:54.558574-07:00","updated_at":"2026-04-04T14:03:54.558574-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.7","depends_on_id":"tooscut-and.6","type":"blocks","created_at":"2026-04-04T14:03:54.563678-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.7","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:03:54.560807-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.8","title":"Video scopes (waveform, vectorscope, histogram)","description":"## Summary\nImplement professional video scopes computed on GPU for real-time color analysis.\n\n## Tasks\n\n### Scope Types\n- [ ] **Histogram** - RGB + Luma distribution (256 bins each)\n- [ ] **Waveform** - Luma or RGB waveform (brightness by horizontal position)\n- [ ] **RGB Parade** - Separate R/G/B waveforms side by side\n- [ ] **Vectorscope** - Color distribution on color wheel (hue angle + saturation radius)\n\n### WGSL Compute Shaders\n- [ ] `compute_histogram.wgsl`:\n - Atomic histogram binning\n - Output: 4 × 256 buffer (R, G, B, Luma)\n- [ ] `compute_waveform.wgsl`:\n - For each column, accumulate pixel brightness into rows\n - Output: 2D texture (width × 256)\n- [ ] `compute_vectorscope.wgsl`:\n - Map each pixel to 2D position by hue/sat\n - Accumulate density\n - Output: 256×256 texture\n\n### Rust/WASM Implementation\n- [ ] `ScopeComputer` struct managing compute pipelines\n- [ ] `compute_histogram(texture_id) -\u003e Vec\u003cf32\u003e`\n- [ ] `compute_waveform(texture_id, width) -\u003e Vec\u003cu8\u003e`\n- [ ] `compute_vectorscope(texture_id, size) -\u003e Vec\u003cu8\u003e`\n\n### UI Components\n- [ ] `ScopesPanel` component:\n - Scope type selector tabs\n - Canvas for scope display\n - Graticule overlays (scale markers)\n- [ ] Histogram: stacked or separate R/G/B view option\n- [ ] Waveform: Luma / RGB / Parade mode selector\n- [ ] Vectorscope: skin tone line, color target boxes\n\n### Graticules and Reference Lines\n- [ ] Histogram: 0%, 50%, 100% markers\n- [ ] Waveform: IRE/percentage scale, legal range indicators (16-235)\n- [ ] Vectorscope: color boxes (R, Mg, B, Cy, G, Yl), skin tone line\n\n### Performance\n- [ ] Compute scopes on requestAnimationFrame, not every frame\n- [ ] Throttle during playback (every 2-3 frames)\n- [ ] Full rate when paused\n- [ ] Resolution option: 1x, 1/2, 1/4 for faster computation\n\n## Acceptance Criteria\n- Histogram matches reference scope software\n- Waveform shows correct brightness distribution\n- Vectorscope shows correct color positions\n- Scopes update in real-time during playback (\u003c16ms compute time)\n- Graticules help interpret scope readings","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:04:11.037853-07:00","updated_at":"2026-04-04T14:41:14.406509-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.8","depends_on_id":"tooscut-and.1","type":"blocks","created_at":"2026-04-04T14:04:11.04237-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.8","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:04:11.040172-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-and.9","title":"Node-based color grading pipeline","description":"## Summary\nImplement node-based color grading pipeline allowing multiple stacked corrections executed in order.\n\n## Tasks\n\n### Data Structures\n- [ ] `ColorGradingNode` union type (primary, wheels, curves, LUT, qualifier, window)\n- [ ] `ClipColorGrading` with ordered `nodes: ColorGradingNode[]` array\n- [ ] Each node has: id, type, enabled, mix, label\n\n### Node Execution Engine (Rust/WASM)\n- [ ] `execute_color_pipeline(texture, nodes) -\u003e texture`\n- [ ] Execute nodes in order, each reading previous output\n- [ ] Skip disabled nodes\n- [ ] Apply per-node mix (blend with input)\n- [ ] Intermediate render targets for multi-pass\n\n### UI Components\n- [ ] `NodeListPanel` component:\n - Vertical list of nodes (not visual graph, like Resolve's mini timeline)\n - Drag to reorder\n - Enable/disable toggle per node\n - Mix slider per node\n - Expand/collapse node settings\n - Add node button (dropdown of types)\n - Delete node button\n- [ ] Node type icons for quick identification\n- [ ] \"Solo\" button to preview single node effect\n\n### Node Operations\n- [ ] Add node (at end or after selected)\n- [ ] Remove node\n- [ ] Reorder nodes (drag \u0026 drop)\n- [ ] Duplicate node\n- [ ] Enable/disable node\n- [ ] Reset node to defaults\n- [ ] Copy/paste node between clips\n\n### Bypass and Preview\n- [ ] Global bypass toggle (show original)\n- [ ] \"Solo node\" mode - only selected node active\n- [ ] A/B comparison split view (future enhancement)\n\n### State Management\n- [ ] Add `addColorGradingNode(clipId, node, index?)` action\n- [ ] Add `removeColorGradingNode(clipId, nodeId)` action\n- [ ] Add `reorderColorGradingNodes(clipId, fromIndex, toIndex)` action\n- [ ] Add `updateColorGradingNode(clipId, nodeId, updates)` action\n\n## Acceptance Criteria\n- Can stack multiple corrections (e.g., Primary → Curves → LUT)\n- Node order affects output (e.g., LUT before vs after primary)\n- Disabling node removes its effect\n- Mix slider blends node effect with input\n- Drag reordering updates preview immediately","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-04T14:04:26.186835-07:00","updated_at":"2026-04-04T14:04:26.186835-07:00","created_by":"mohebifar","dependencies":[{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.6","type":"blocks","created_at":"2026-04-04T14:04:26.190624-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.7","type":"blocks","created_at":"2026-04-04T14:04:26.191412-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and","type":"parent-child","created_at":"2026-04-04T14:04:26.187706-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.3","type":"blocks","created_at":"2026-04-04T14:04:26.188467-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.4","type":"blocks","created_at":"2026-04-04T14:04:26.189223-07:00","created_by":"mohebifar"},{"issue_id":"tooscut-and.9","depends_on_id":"tooscut-and.5","type":"blocks","created_at":"2026-04-04T14:04:26.189985-07:00","created_by":"mohebifar"}]} +{"id":"tooscut-bxi","title":"Show video clip thumbnails in timeline","description":"Display thumbnail previews for video clips in the timeline:\n- Performant rendering like subformer\n- Handle zoom in/out properly (show more/fewer thumbnails)\n- Reference: /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:57.360668-08:00","updated_at":"2026-02-04T23:22:18.364728-08:00","closed_at":"2026-02-04T23:22:18.364728-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-d1w","title":"Create numeric input component with drag-to-adjust","description":"Create a numeric input component that:\n- Can increase/decrease value by click and drag left/right\n- Allows direct editing when clicked\n- Accepts a suffix prop for units (e.g., '%', 'px', '°')\n- Will be used in the properties panel for transform/effect controls","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:55.226838-08:00","updated_at":"2026-02-03T15:44:26.731079-08:00","closed_at":"2026-02-03T15:44:26.731079-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-dz0","title":"Generate and display project thumbnails","description":"Projects should have a thumbnail that is shown on the project list page. Generate a thumbnail from the project content (e.g., render a frame from the timeline at a representative time) and store it as thumbnailDataUrl in the DexieJS projects table. Display these thumbnails in the project cards on the home/projects page.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-22T02:12:19.167347-08:00","updated_at":"2026-02-22T17:08:56.282213-08:00","closed_at":"2026-02-22T17:08:56.282213-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-fbw","title":"Proper handling of clip trimming","description":"Implement proper clip trimming functionality:\n- Trim from left edge (adjusts start time and in-point)\n- Trim from right edge (adjusts duration)\n- Visual feedback during trim operation\n- Respect asset duration limits","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:57.195771-08:00","updated_at":"2026-02-05T00:14:26.507498-08:00","closed_at":"2026-02-05T00:14:26.507498-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-i1k","title":"Snap clips in timeline (move and trim)","description":"When moving a clip close to another clip's edge, snap them together automatically. When trimming a clip, snap to edges of clips on other tracks. Show a visual snap line indicator when snapping occurs. Should work for both move and trim operations across all tracks.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:21.397626-08:00","updated_at":"2026-02-07T00:32:00.23669-08:00","closed_at":"2026-02-07T00:32:00.23669-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-ixy","title":"J/K/L playback shortcuts and expanded keyboard navigation","description":"## Summary\nAdd industry-standard NLE keyboard shortcuts for playback control and navigation.\n\n## UX Design\n\n### J/K/L Playback (highest priority)\n- L: Play forward. Press again to increase speed (1x -\u003e 2x -\u003e 4x -\u003e 8x)\n- K: Pause. Hold K + tap L = step forward one frame. Hold K + tap J = step backward one frame.\n- J: Play reverse. Press again to increase reverse speed (1x -\u003e 2x -\u003e 4x -\u003e 8x)\n- Speed resets when switching direction or pressing K\n- Show current playback speed in the playback controls bar when not 1x (e.g. \"2x\" or \"-4x\")\n\n### Frame-accurate navigation\n- , (comma): Previous frame (already in menu, verify wired)\n- . (period): Next frame (already in menu, verify wired)\n- Shift+, : Jump back 1 second (10 frames at 30fps, or fps-based)\n- Shift+. : Jump forward 1 second\n- Home: Jump to start of timeline (verify working)\n- End: Jump to end of last clip (verify working)\n\n### Clip navigation\n- Up arrow: Select previous clip on same track (or move selection up a track)\n- Down arrow: Select next clip on same track (or move selection down a track)\n- Shift+Left/Right: Nudge selected clip by 1 frame\n- Alt+Left/Right: Nudge selected clip by 10 frames\n\n### Implementation notes\n- All shortcuts registered in the existing global keydown handler in canvas-timeline.tsx\n- Skip when focus is on INPUT/TEXTAREA (existing pattern)\n- Store needs: playbackSpeed state, setPlaybackSpeed action\n- Audio engine needs speed parameter adjustment during J/K/L playback\n- Playback controls bar should show speed indicator when not 1x","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-03-30T01:03:57.129655-07:00","updated_at":"2026-03-30T18:31:20.948554-07:00","closed_at":"2026-03-30T18:31:20.948554-07:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-jt2","title":"Implement audio renderer WASM module","description":"Create an audio renderer WASM module for audio processing and playback. Use subformer's implementation at /Users/mohebifar/dev/other/kareem/subformer for inspiration on how audio rendering is handled.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:55.552314-08:00","updated_at":"2026-02-03T16:17:41.966779-08:00","closed_at":"2026-02-03T16:17:41.966779-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-k4m","title":"Ripple editing mode","description":"## Summary\nAdd a ripple editing mode that shifts downstream clips when deleting, trimming, or moving clips.\n\n## UX Design\n\n### Toggle\n- Ripple toggle button in TimelineToolbar (next to tool selector)\n- Keyboard shortcut: R to toggle (consistent with V=Select, C=Razor pattern)\n- Visual indicator: button stays highlighted when active\n\n### Behavior when ripple is ON\n\n**Ripple Delete:**\n- Deleting a clip shifts all clips to the right on the same track left to fill the gap\n- Linked clips (audio+video pairs) ripple together\n- Other tracks are NOT affected\n\n**Ripple Trim:**\n- Trimming right edge: downstream clips shift to match\n- Trimming left edge: clip and all downstream clips shift together\n- Visual preview: ghost outlines show where downstream clips will land during drag\n\n### Behavior when ripple is OFF\n- Current behavior (no change)\n\n### Store Changes\n- Add rippleMode boolean to store state with toggleRippleMode() action\n- Add rippleDelete(clipId) that removes clip and shifts downstream\n- Modify trimLeft/trimRight to accept a ripple flag\n- Add utility: getDownstreamClips(trackId, afterTime)\n\n### Visual Feedback\n- Subtle indicator on timeline when ripple mode is active\n- During ripple drag/trim, show ghost positions of affected downstream clips","status":"open","priority":1,"issue_type":"feature","created_at":"2026-03-30T01:03:25.119552-07:00","updated_at":"2026-03-30T01:03:25.119552-07:00","created_by":"mohebifar"} +{"id":"tooscut-li4","title":"Audio clip fade handles on timeline","description":"## Summary\nAdd draggable fade-in/fade-out handles on audio clip edges in the timeline for quick volume fades.\n\n## UX Design\n\n### Visual design\n- Small triangular handles at the top corners of audio clips on the timeline\n- Fade-in handle: top-left corner. Fade-out handle: top-right corner\n- Handles 8x8px, visible on hover over audio clips\n- Fade region draws a curved line from 0 to 100% volume\n- Fade curve rendered as a semi-transparent overlay on the clip, above the waveform\n\n### Interaction\n- Drag fade-in handle rightward to increase fade-in duration\n- Drag fade-out handle leftward to increase fade-out duration\n- Minimum fade: 1 frame. Maximum fade: half the clip duration\n- Snapping: fade handles snap to grid lines (same as clip snap)\n- Double-click a fade handle to reset it to 0 (remove fade)\n\n### Data model\n- Uses dedicated fadeIn/fadeOut duration fields on the clip (NOT keyframes)\n- Fade durations are relative to clip edges so they survive trimming\n- The WASM audio mixer already supports per-clip fade_in and fade_out durations\n- Currently these are always 0 because there is no UI to set them\n\n### Store changes\n- Add fadeIn and fadeOut number fields to AudioClip type (duration in frames)\n- Add setClipFadeIn(clipId, frames) and setClipFadeOut(clipId, frames) actions\n- Pass fade values through useAudioEngine timeline sync with framesToSeconds conversion\n- These are undoable (temporal-tracked state)\n\n### Timeline rendering (Konva)\n- Detect hover near top corners of audio clips (8px hit zone)\n- Show ew-resize cursor on hover\n- During drag, render the fade curve preview in real-time\n- Fade region: filled area between the straight top edge and the curve","status":"open","priority":2,"issue_type":"feature","created_at":"2026-03-31T13:01:17.976325-07:00","updated_at":"2026-03-31T13:01:17.976325-07:00","created_by":"mohebifar"} +{"id":"tooscut-lso","title":"Implement video export with muxer and web worker parallelization","description":"Add ability to export videos using the same muxer method and web worker parallelization used in subformer. Reference: /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:55.879274-08:00","updated_at":"2026-02-06T02:12:38.306274-08:00","closed_at":"2026-02-06T02:12:38.306274-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-mgr","title":"Project persistence with IndexedDB and project chooser page","description":"Store projects in IndexedDB using DexieJS. Add a project list page (home/index route) where users can see saved projects, create new ones, and open existing ones. Projects should auto-save on changes (debounced). Store project name, settings, timeline content, and thumbnail.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:26.053163-08:00","updated_at":"2026-02-07T22:16:17.087971-08:00","closed_at":"2026-02-07T22:16:17.087971-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-n75","title":"Color Grading: LUT Support node","description":"Implement 3D LUT loading and application node. Includes: .cube file parser (standard 3D LUT format), LUT preview thumbnail, 3D texture upload to GPU, trilinear and tetrahedral interpolation in shader (shader functions already written in color_grading_shader.rs), LUT browser UI. Types already defined: LutReference, LutInterpolation.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-06T13:08:35.744189-07:00","updated_at":"2026-04-06T13:19:07.170923-07:00","closed_at":"2026-04-06T13:19:07.170923-07:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-nse","title":"Add shapes, texts, images, clips and audios to timeline","description":"Implement ability to add various layer types to the timeline:\n- Shape layers (rectangles, circles, polygons, lines)\n- Text layers\n- Image layers\n- Video clips\n- Audio clips","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.702715-08:00","updated_at":"2026-02-06T01:14:51.651987-08:00","closed_at":"2026-02-06T01:14:51.651987-08:00","close_reason":"Already implemented: Asset panel, text panel, and shape panel all support drag-and-drop to timeline. Canvas timeline has drop handlers for all clip types. Store has addClipToTrack action supporting video, audio, image, text, and shape clips.","created_by":"mohebifar"} +{"id":"tooscut-p7h","title":"Auto-place first clip at timeline start","description":"When adding the very first clip to an empty timeline, automatically place it at time 0 (the beginning). This only applies when the timeline has no existing clips. Subsequent clips should still be placed at the drop position or current time.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-02-06T21:58:30.572339-08:00","updated_at":"2026-02-06T22:20:58.705414-08:00","closed_at":"2026-02-06T22:20:58.705414-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-pvp","title":"Add transform mode and view mode toggle for preview panel","description":"Introduce two modes for the preview panel: View mode and Transform mode, with toggle buttons below the preview. Currently, clicking on clip boundaries in the preview or selecting any clip from the timeline shows transform handles (resize/move). This should only happen in Transform mode. In View mode, transform handles should be hidden and clips should not be movable/resizable in the preview. Add toggle buttons (e.g. icons) below the preview panel to switch between modes.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:43.765143-08:00","updated_at":"2026-02-22T19:41:28.491218-08:00","closed_at":"2026-02-22T19:41:28.491218-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-qpn","title":"Add mute/unmute and lock/unlock for tracks","description":"Implement track controls:\n- Mute/unmute: For video tracks, this hides the track's clips from rendering. For audio tracks, this mutes the audio.\n- Lock/unlock: Prevents editing of clips on locked tracks","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:56.544018-08:00","updated_at":"2026-02-04T02:09:25.954736-08:00","closed_at":"2026-02-04T02:09:25.954736-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-sg3","title":"Show audio waveform on audio clips","description":"Display audio waveform visualization on audio clips in the timeline:\n- Performant rendering like subformer\n- Handle zoom in/out properly\n- Reference: /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-03T15:31:57.521544-08:00","updated_at":"2026-02-05T00:12:30.453041-08:00","closed_at":"2026-02-05T00:12:30.453041-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-shs","title":"Multi-select clips: batch move and trim","description":"Allow selecting multiple clips in the timeline (click + shift/ctrl, or drag-select). Moving one selected clip moves all selected clips. Trimming/extending one selected clip applies the same delta to all selected clips.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T21:58:23.231693-08:00","updated_at":"2026-02-22T19:42:40.147061-08:00","closed_at":"2026-02-22T19:42:40.147061-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-sp0","title":"Fix overlap handling to trim instead of delete","description":"Fix the behavior of overlap handling. When a clip overlaps another clip:\n- Instead of deleting the overlapped clip, it should trim it to avoid the overlap\n- Only delete if the clip is fully overlapped (100% covered)\n- Partial overlaps should result in trimming the overlapped portion","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-02-03T15:31:57.038584-08:00","updated_at":"2026-02-04T01:52:40.207676-08:00","closed_at":"2026-02-04T01:52:40.207676-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-t6f","title":"Drag and drop assets from assets panel to preview","description":"Allow dragging assets from the assets panel directly into the preview panel. The asset should be added at the current playhead timestamp on the highest (frontmost) video track. If the highest track is already occupied at the playhead position and/or adding the asset would overlap existing clips, automatically create a new track pair and place the asset there instead.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:39.905379-08:00","updated_at":"2026-02-22T17:20:28.702887-08:00","closed_at":"2026-02-22T17:20:28.702887-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-vev","title":"Implement keyframe animation system","description":"Add keyframe animation support for clip properties (position, scale, rotation, opacity, etc.). This includes:\n\n- Keyframe data model in clip state (KeyframeTracks with property -\u003e keyframe[] mapping)\n- Keyframe interpolation using the existing EvaluatorManager from render-engine\n- UI for adding/removing keyframes at current playhead position\n- Visual keyframe indicators on timeline clips\n- Property panel integration to show animated values\n\nReference: subformer implementation at /Users/mohebifar/dev/other/kareem/subformer","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-06T02:14:25.299746-08:00","updated_at":"2026-02-06T21:58:14.325586-08:00","closed_at":"2026-02-06T21:58:14.325586-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-wio","title":"Playhead is not movable during playback","description":"While playing, the user cannot move the playhead to scrub/seek. It just keeps playing and ignores playhead drag interaction. Expected: dragging the playhead during playback should pause and seek (or seek while playing).","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-06T21:58:19.344465-08:00","updated_at":"2026-02-06T22:18:49.143947-08:00","closed_at":"2026-02-06T22:18:49.143947-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-wlg","title":"Drag ghost should reflect actual clip duration","description":"When dragging a clip from the assets panel onto the timeline, the preview ghost/shadow should be representative of the actual duration of the video/audio asset, not a fixed size. The ghost width should match what the clip will look like at the current zoom level.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-02-06T21:58:32.635988-08:00","updated_at":"2026-02-06T22:23:14.006719-08:00","closed_at":"2026-02-06T22:23:14.006719-08:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-yq4","title":"Cut (Cmd+X) and Duplicate clip operations","description":"## Summary\nImplement cut (copy + delete) and duplicate operations. Cut menu item exists but is disabled.\n\n## UX Design\n\n### Cut (Cmd+X)\n- Copy selected clips to clipboard, then delete them\n- If ripple mode is on (see tooscut-k4m), ripple-delete after copy\n- If ripple mode is off, just delete (leave gap)\n- Linked clips: cut both video and audio together\n- Menu item in Edit menu already exists — enable it and wire handler\n\n### Duplicate (Cmd+D)\n- Duplicate selected clips and place them immediately after the originals\n- New clips start at the end of the rightmost selected clip\n- Linked clips: duplicate both video and audio\n- Select the new duplicates (deselect originals)\n- Useful for repeating patterns, creating variations\n\n### Implementation\n- Store actions needed: cutSelectedClips() — calls copySelectedClips() then removeClip() for each\n- Store actions needed: duplicateSelectedClips() — deep-clone clips, assign new IDs, offset startTime\n- Keyboard handler: add Cmd+X and Cmd+D to global keydown in canvas-timeline.tsx\n- Menu: enable existing Cut item, add Duplicate item to Edit menu\n- Duplicate should generate new clip IDs and new linkedClipIds for pairs","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-03-30T01:11:31.108201-07:00","updated_at":"2026-04-01T22:15:34.121127-07:00","closed_at":"2026-04-01T22:15:34.121127-07:00","close_reason":"Closed","created_by":"mohebifar"} +{"id":"tooscut-yqc","title":"Make entire assets panel a dropzone for file imports","description":"Make the entire assets panel act as a dropzone so users can drag and drop files from Finder/file explorer anywhere on the assets panel to import them. Currently only specific areas may accept drops.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-22T15:37:48.50237-08:00","updated_at":"2026-02-22T16:50:04.215085-08:00","closed_at":"2026-02-22T16:50:04.215085-08:00","close_reason":"Closed","created_by":"mohebifar"} diff --git a/apps/ui/src/components/editor/color-grading/color-grading-panel.tsx b/apps/ui/src/components/editor/color-grading/color-grading-panel.tsx index 7604bbe..c656816 100644 --- a/apps/ui/src/components/editor/color-grading/color-grading-panel.tsx +++ b/apps/ui/src/components/editor/color-grading/color-grading-panel.tsx @@ -11,6 +11,8 @@ import type { ColorGrading, ColorSpace, + Gamut, + ToneMapping, PrimaryCorrection, ColorWheels, Curves, @@ -145,7 +147,13 @@ export function ColorGradingPanel({ // Update a CST node const handleUpdateCstNode = useCallback( - (updates: { from_space?: ColorSpace; to_space?: ColorSpace }) => { + (updates: { + from_space?: ColorSpace; + to_space?: ColorSpace; + from_gamut?: Gamut; + to_gamut?: Gamut; + tone_mapping?: ToneMapping; + }) => { if (!selectedNode || selectedNode.type !== "ColorSpaceTransform") return; const newNodes = grading.nodes.map((node) => { @@ -227,6 +235,9 @@ export function ColorGradingPanel({ mix: 1, from_space: "SLog3", to_space: "Srgb", + from_gamut: "Rec709", + to_gamut: "Rec709", + tone_mapping: "Simple", }; break; default: @@ -545,7 +556,13 @@ interface NodeParameterEditorProps { onUpdatePrimary: (key: keyof PrimaryCorrection, value: number | [number, number, number]) => void; onUpdateColorWheels: (updates: Partial) => void; onUpdateCurves: (curves: Curves) => void; - onUpdateCst: (updates: { from_space?: ColorSpace; to_space?: ColorSpace }) => void; + onUpdateCst: (updates: { + from_space?: ColorSpace; + to_space?: ColorSpace; + from_gamut?: Gamut; + to_gamut?: Gamut; + tone_mapping?: ToneMapping; + }) => void; onUpdateLut: (updates: Partial) => void; onUpdateQualifier: (key: keyof HslQualifier, value: number | boolean) => void; onUpdateQualifierCorrection: ( @@ -645,6 +662,9 @@ function NodeParameterEditor({ )} diff --git a/apps/ui/src/components/editor/color-grading/cst-properties.tsx b/apps/ui/src/components/editor/color-grading/cst-properties.tsx index 7e9556b..07b3e56 100644 --- a/apps/ui/src/components/editor/color-grading/cst-properties.tsx +++ b/apps/ui/src/components/editor/color-grading/cst-properties.tsx @@ -1,8 +1,11 @@ /** * Color Space Transform node properties editor. + * + * Allows separate selection of transfer function (gamma curve) + * and gamut (color primaries) for source and target. */ -import type { ColorSpace } from "@tooscut/render-engine"; +import type { ColorSpace, Gamut, ToneMapping } from "@tooscut/render-engine"; import { Select, @@ -14,6 +17,7 @@ import { SelectValue, } from "../../ui/select"; +// Transfer function options (gamma curves) const STANDARD_OPTIONS: { value: ColorSpace; label: string }[] = [ { value: "Srgb", label: "sRGB" }, { value: "Linear", label: "Linear" }, @@ -33,23 +37,98 @@ const LOG_OPTIONS: { value: ColorSpace; label: string }[] = [ { value: "RedLog3G10", label: "RED Log3G10" }, ]; +// Gamut (color primaries) options +const STANDARD_GAMUT_OPTIONS: { value: Gamut; label: string }[] = [ + { value: "Rec709", label: "Rec.709 / sRGB" }, + { value: "DciP3", label: "DCI-P3 (D65)" }, + { value: "Rec2020", label: "Rec.2020" }, +]; + +const CAMERA_GAMUT_OPTIONS: { value: Gamut; label: string }[] = [ + { value: "SGamut", label: "Sony S-Gamut" }, + { value: "SGamut3", label: "Sony S-Gamut3" }, + { value: "SGamut3Cine", label: "Sony S-Gamut3.Cine" }, + { value: "ArriWideGamut", label: "ARRI Wide Gamut" }, + { value: "VGamut", label: "Panasonic V-Gamut" }, + { value: "BmdWideGamut", label: "BMD Wide Gamut" }, + { value: "RedWideGamut", label: "RED Wide Gamut" }, +]; + +const ACES_GAMUT_OPTIONS: { value: Gamut; label: string }[] = [ + { value: "AcesCgAp1", label: "ACES AP1 (ACEScg)" }, +]; + +const TONE_MAPPING_OPTIONS: { value: ToneMapping; label: string }[] = [ + { value: "None", label: "None" }, + { value: "Simple", label: "Simple" }, +]; + interface CstPropertiesProps { fromSpace: ColorSpace; toSpace: ColorSpace; - onChange: (updates: { from_space?: ColorSpace; to_space?: ColorSpace }) => void; + fromGamut: Gamut; + toGamut: Gamut; + toneMapping: ToneMapping; + onChange: (updates: { + from_space?: ColorSpace; + to_space?: ColorSpace; + from_gamut?: Gamut; + to_gamut?: Gamut; + tone_mapping?: ToneMapping; + }) => void; } -export function CstProperties({ fromSpace, toSpace, onChange }: CstPropertiesProps) { +export function CstProperties({ + fromSpace, + toSpace, + fromGamut, + toGamut, + toneMapping, + onChange, +}: CstPropertiesProps) { return (
-
- - onChange({ from_space: v })} /> +
Transfer Function
+
+
+ + onChange({ from_space: v })} /> +
+
+ + onChange({ to_space: v })} /> +
-
- - onChange({ to_space: v })} /> +
Color Gamut
+
+
+ + onChange({ from_gamut: v })} /> +
+
+ + onChange({ to_gamut: v })} /> +
+
Tone Mapping
+
); } @@ -101,3 +180,51 @@ function ColorSpaceSelect({ ); } + +function GamutSelect({ + value, + onValueChange, +}: { + value: Gamut; + onValueChange: (value: Gamut) => void; +}) { + return ( + + ); +} diff --git a/crates/compositor/src/color_grading_uniforms.rs b/crates/compositor/src/color_grading_uniforms.rs index 621e91b..c965695 100644 --- a/crates/compositor/src/color_grading_uniforms.rs +++ b/crates/compositor/src/color_grading_uniforms.rs @@ -306,8 +306,10 @@ pub struct ColorGradingUniforms { pub input_cst: u32, /// Output color space transform (ColorSpace enum as u32, 0 = sRGB). pub output_cst: u32, - /// Padding to maintain vec4 alignment. - pub _pad_align: [f32; 2], + /// Input gamut (Gamut enum as u32, 0 = Rec709). + pub input_gamut: u32, + /// Output gamut (Gamut enum as u32, 0 = Rec709). + pub output_gamut: u32, // === Qualifier correction CDL (applied within qualified region) === /// Qualifier correction slope. @@ -337,8 +339,16 @@ pub struct ColorGradingUniforms { /// Window mix + pad. pub window_mix: [f32; 4], // 16 bytes - // Padding to 512 bytes (400 used, 112 remaining = 28 floats) - pub _pad: [f32; 28], + /// Tone mapping method (0 = none, 1 = simple). + pub tone_mapping_method: u32, + /// Tone mapping 'a' parameter (rolloff scale). + pub tone_mapping_a: f32, + /// Tone mapping 'b' parameter (rolloff offset). + pub tone_mapping_b: f32, + pub _pad_tone: f32, + + // Padding to 512 bytes (416 used, 96 remaining = 24 floats) + pub _pad: [f32; 24], } impl Default for ColorGradingUniforms { @@ -364,7 +374,8 @@ impl Default for ColorGradingUniforms { lut_size: 33.0, input_cst: 0, output_cst: 0, - _pad_align: [0.0; 2], + input_gamut: 0, + output_gamut: 0, // Qualifier correction q_slope: [1.0, 1.0, 1.0, 1.0], q_offset: [0.0, 0.0, 0.0, 0.0], @@ -379,7 +390,11 @@ impl Default for ColorGradingUniforms { w_power: [1.0, 1.0, 1.0, 1.0], w_adjustments: [1.0, 0.0, 0.0, 0.0], window_mix: [1.0, 0.0, 0.0, 0.0], - _pad: [0.0; 28], + tone_mapping_method: 0, + tone_mapping_a: 1.0, + tone_mapping_b: 1.0, + _pad_tone: 0.0, + _pad: [0.0; 24], } } } @@ -394,6 +409,59 @@ pub const FLAG_QUALIFIER_ENABLED: u32 = 1 << 5; pub const FLAG_WINDOW_ENABLED: u32 = 1 << 6; pub const FLAG_INPUT_CST: u32 = 1 << 7; pub const FLAG_OUTPUT_CST: u32 = 1 << 8; +pub const FLAG_INPUT_GAMUT: u32 = 1 << 9; +pub const FLAG_OUTPUT_GAMUT: u32 = 1 << 10; + +/// Get peak scene-linear luminance (in nits) for a source transfer function. +/// Used to compute tone mapping parameters. +fn source_peak_nits(cs: &tooscut_types::ColorSpace) -> f32 { + use tooscut_types::ColorSpace; + // Peak nits = peak_scene_linear * 100 (where 1.0 scene-linear = 100 nits SDR) + match cs { + ColorSpace::SLog2 => 1376.0, // 13.76 scene-linear + ColorSpace::SLog3 => 3842.0, // 38.42 scene-linear + ColorSpace::LogC => 5508.0, // 55.08 scene-linear (EI800) + ColorSpace::VLog => 4609.0, // 46.09 scene-linear + ColorSpace::CLog3 => 2500.0, // ~25 scene-linear + ColorSpace::BmFilm => 5508.0, // approximate as LogC + ColorSpace::RedLog3G10 => 3842.0, // approximate as S-Log3 + ColorSpace::AcesCg => 6500.0, // ACES scene-referred, ~65 peak + ColorSpace::Linear => 1000.0, // arbitrary + ColorSpace::Srgb => 100.0, // already display-referred + } +} + +/// Compute tone mapping a/b parameters from input/output peak luminance. +/// Formula: f(x) = a * x / (x + b) +/// Derived from constraints: f(input_white) = output_white, plus adaptation. +fn compute_tone_mapping_ab(input_nits: f32, output_nits: f32, adaptation: f32) -> (f32, f32) { + let iw = input_nits / output_nits; + let ow = 1.0; + if (iw - ow).abs() < 0.001 { + return (1.0, 1.0); // No tone mapping needed + } + let b = (iw - (adaptation / 100.0) * (iw / ow)) / ((iw / ow) - 1.0); + let a = ow / (iw / (iw + b)); + (a, b) +} + +/// Convert Gamut enum to u32 for shader. +fn gamut_to_u32(g: &tooscut_types::Gamut) -> u32 { + use tooscut_types::Gamut; + match g { + Gamut::Rec709 => 0, + Gamut::SGamut => 1, + Gamut::SGamut3 => 2, + Gamut::SGamut3Cine => 3, + Gamut::ArriWideGamut => 4, + Gamut::AcesCgAp1 => 5, + Gamut::RedWideGamut => 6, + Gamut::DciP3 => 7, + Gamut::Rec2020 => 8, + Gamut::VGamut => 9, + Gamut::BmdWideGamut => 10, + } +} /// Convert ColorSpace enum to u32 for shader. fn color_space_to_u32(cs: &tooscut_types::ColorSpace) -> u32 { @@ -426,35 +494,64 @@ impl ColorGradingUniforms { } // Scan for CST nodes: first enabled CST → input, last enabled CST → output - let mut first_cst: Option<(tooscut_types::ColorSpace, tooscut_types::ColorSpace)> = None; - let mut last_cst: Option<(tooscut_types::ColorSpace, tooscut_types::ColorSpace)> = None; + let mut first_cst: Option<&ColorGradingNode> = None; + let mut last_cst: Option<&ColorGradingNode> = None; for node in &grading.nodes { - if let ColorGradingNode::ColorSpaceTransform { - enabled: true, - from_space, - to_space, - .. - } = node - { + if let ColorGradingNode::ColorSpaceTransform { enabled: true, .. } = node { if first_cst.is_none() { - first_cst = Some((from_space.clone(), to_space.clone())); + first_cst = Some(node); } - last_cst = Some((from_space.clone(), to_space.clone())); + last_cst = Some(node); } } - // First CST node: use from_space as input CST (convert from source to linear) - if let Some((from_space, _)) = &first_cst { - if *from_space != tooscut_types::ColorSpace::Srgb { - uniforms.flags |= FLAG_INPUT_CST; - uniforms.input_cst = color_space_to_u32(from_space); + // First CST node: use from_space/from_gamut/tone_mapping as input transforms + if let Some(ColorGradingNode::ColorSpaceTransform { + from_space, + from_gamut, + tone_mapping, + .. + }) = first_cst + { + // Tone mapping method + parameters + if let Some(tm) = tone_mapping { + uniforms.tone_mapping_method = match tm { + tooscut_types::ToneMapping::None => 0, + tooscut_types::ToneMapping::Simple => 1, + }; + if *tm != tooscut_types::ToneMapping::None { + // Compute a/b from source format's peak luminance + let peak_nits = source_peak_nits(from_space); + let (a, b) = compute_tone_mapping_ab(peak_nits, 100.0, 9.0); + uniforms.tone_mapping_a = a; + uniforms.tone_mapping_b = b; + } + } + // Always set input CST — even sRGB needs srgb_to_linear() when + // we want corrections to operate in linear space. + uniforms.flags |= FLAG_INPUT_CST; + uniforms.input_cst = color_space_to_u32(from_space); + if let Some(g) = from_gamut { + if *g != tooscut_types::Gamut::Rec709 { + uniforms.flags |= FLAG_INPUT_GAMUT; + uniforms.input_gamut = gamut_to_u32(g); + } } } - // Last CST node: use to_space as output CST (convert from linear to target) - if let Some((_, to_space)) = &last_cst { - if *to_space != tooscut_types::ColorSpace::Srgb { - uniforms.flags |= FLAG_OUTPUT_CST; - uniforms.output_cst = color_space_to_u32(to_space); + // Last CST node: use to_space/to_gamut as output transforms + if let Some(ColorGradingNode::ColorSpaceTransform { + to_space, to_gamut, .. + }) = last_cst + { + // Always set output CST — we must encode back from linear to + // the target transfer function (e.g. linear → sRGB gamma). + uniforms.flags |= FLAG_OUTPUT_CST; + uniforms.output_cst = color_space_to_u32(to_space); + if let Some(g) = to_gamut { + if *g != tooscut_types::Gamut::Rec709 { + uniforms.flags |= FLAG_OUTPUT_GAMUT; + uniforms.output_gamut = gamut_to_u32(g); + } } } @@ -624,4 +721,38 @@ mod tests { let uniforms = ColorGradingUniforms::from_color_grading(&grading); assert_eq!(uniforms.flags & FLAG_BYPASS, FLAG_BYPASS); } + + #[test] + fn from_color_grading_gamut() { + use tooscut_types::{ColorSpace, Gamut}; + let grading = ColorGrading { + bypass: false, + input_color_space: ColorSpace::Srgb, + output_color_space: ColorSpace::Srgb, + nodes: vec![ColorGradingNode::ColorSpaceTransform { + id: "cst-1".into(), + enabled: true, + mix: 1.0, + label: None, + position: None, + from_space: ColorSpace::SLog2, + to_space: ColorSpace::Srgb, + from_gamut: Some(Gamut::SGamut), + to_gamut: Some(Gamut::Rec709), + tone_mapping: None, + }], + }; + let uniforms = ColorGradingUniforms::from_color_grading(&grading); + // Input CST should be set (SLog2 != Srgb) + assert_ne!(uniforms.flags & FLAG_INPUT_CST, 0, "INPUT_CST flag should be set"); + assert_eq!(uniforms.input_cst, 4); // SLog2 = 4 + // Input gamut should be set (SGamut != Rec709) + assert_ne!(uniforms.flags & FLAG_INPUT_GAMUT, 0, "INPUT_GAMUT flag should be set"); + assert_eq!(uniforms.input_gamut, 1); // SGamut = 1 + // Output CST should be set (always encode back, even for sRGB) + assert_ne!(uniforms.flags & FLAG_OUTPUT_CST, 0, "OUTPUT_CST flag should be set"); + assert_eq!(uniforms.output_cst, 0); // Srgb = 0 + // Output gamut should NOT be set (to_gamut = Rec709 = default) + assert_eq!(uniforms.flags & FLAG_OUTPUT_GAMUT, 0, "OUTPUT_GAMUT flag should NOT be set for Rec709"); + } } diff --git a/crates/compositor/src/pipeline.rs b/crates/compositor/src/pipeline.rs index 5a659cd..cf0e2c6 100644 --- a/crates/compositor/src/pipeline.rs +++ b/crates/compositor/src/pipeline.rs @@ -60,7 +60,8 @@ struct ColorGradingUniforms { lut_size: f32, input_cst: u32, output_cst: u32, - _pad_align: vec2, + input_gamut: u32, + output_gamut: u32, // Qualifier correction CDL q_slope: vec4, q_offset: vec4, @@ -75,7 +76,11 @@ struct ColorGradingUniforms { w_power: vec4, w_adjustments: vec4, window_mix: vec4, - _pad: array, 7>, + tone_mapping_method: u32, + tone_mapping_a: f32, + tone_mapping_b: f32, + _pad_tone2: f32, + _pad: array, 6>, }; @group(0) @binding(0) var uniforms: LayerUniforms; @@ -215,57 +220,74 @@ fn linear_to_srgb(linear: vec3) -> vec3 { return select(srgb_low, srgb_high, linear > cutoff); } -// Sony S-Log2 <-> Linear +// Sony S-Log2 <-> Linear (scene-referred) +// S-Log2 is defined as 219/155 * S-Log, with legal-to-full range conversion. +// Input: normalised code value (0.0-1.0). Output: scene-linear reflection. +// Reference: Sony S-Log2 Technical Summary, colour-science library. fn slog2_to_linear(slog: vec3) -> vec3 { - let linear_low = (slog - 0.030001222851889303) / 3.53881278538813; - let linear_high = pow(vec3(10.0), (slog - 0.616596 - 0.03) / 0.432699) - 0.037584; - return select(linear_low, linear_high, slog >= vec3(0.0929)); + // Step 1: legal-to-full range (10-bit: (v*1023 - 64) / 876) + let x = (slog * 1023.0 - 64.0) / 876.0; + // Step 2: S-Log inverse + let threshold = vec3(0.088251); // S-Log encoding of 0.0 as NCV + let linear_low = (x - 0.030001222851889303) / 5.0; + let linear_high = pow(vec3(10.0), (x - 0.616596 - 0.03) / 0.432699) - 0.037584; + let slog_linear = select(linear_low, linear_high, slog >= threshold); + // Step 3: reflection * S-Log2 scale factor (0.9 * 219/155) + return slog_linear * 1.2716129032; } fn linear_to_slog2(linear: vec3) -> vec3 { - let cut = 0.0; - let slog_low = linear * 3.53881278538813 + 0.030001222851889303; - let slog_high = 0.432699 * log(linear + 0.037584) / log(10.0) + 0.616596 + 0.03; - return select(slog_low, slog_high, linear >= vec3(cut)); + // Inverse of slog2_to_linear + let x = linear / 1.2716129032; // undo S-Log2 scale + let slog_low = x * 5.0 + 0.030001222851889303; + let slog_high = 0.432699 * log(x + 0.037584) / log(10.0) + 0.616596 + 0.03; + let full = select(slog_low, slog_high, x >= vec3(0.0)); + // full-to-legal range + return (full * 876.0 + 64.0) / 1023.0; } -// ARRI LogC3 <-> Linear (EI 800) +// ARRI LogC3 <-> Linear (EI 800, SUP 3.x) +// Reference: ARRI LogC3 specification, colour-science library. fn logc_to_linear(logc: vec3) -> vec3 { let a = 5.555556; let b = 0.052272; - let c = 0.247190; + let c = 0.24719; let d = 0.385537; let e_val = 5.367655; - let cut = 0.1496582; - let linear_low = (logc - d) / e_val; - let linear_high = (pow(vec3(10.0), (logc - c) / a) - b) / a; - return select(linear_low, linear_high, logc > vec3(cut)); + let f = 0.092809; + let breakpoint = vec3(0.1496578); // e*cut + f + let linear_low = (logc - f) / e_val; + let linear_high = (pow(vec3(10.0), (logc - d) / c) - b) / a; + return select(linear_low, linear_high, logc > breakpoint); } fn linear_to_logc(linear: vec3) -> vec3 { let a = 5.555556; let b = 0.052272; - let c = 0.247190; + let c = 0.24719; let d = 0.385537; let e_val = 5.367655; - let cut = 0.010591; - let logc_low = e_val * linear + d; - let logc_high = a * log(a * linear + b) / log(10.0) + c; - return select(logc_low, logc_high, linear > vec3(cut)); + let f = 0.092809; + let cut = vec3(0.010591); + let logc_low = e_val * linear + f; + let logc_high = c * log(a * linear + b) / log(10.0) + d; + return select(logc_low, logc_high, linear > cut); } -// Sony S-Log3 <-> Linear +// Sony S-Log3 <-> Linear (scene-referred) +// Reference: Sony S-Log3 Technical Summary, colour-science library. fn slog3_to_linear(slog: vec3) -> vec3 { - let linear_low = (slog - 0.030001222851889303) / 5.26; - let linear_high = pow(vec3(10.0), (slog - 0.410557184750733) / 0.255620723362659) * 0.19 - 0.01; - return select(linear_low, linear_high, slog >= vec3(0.1673609920)); + let threshold = vec3(171.2102946929 / 1023.0); // ~0.16736 + let linear_low = (slog * 1023.0 - 95.0) * 0.01125000 / (171.2102946929 - 95.0); + let linear_high = pow(vec3(10.0), (slog * 1023.0 - 420.0) / 261.5) * 0.19 - 0.01; + return select(linear_low, linear_high, slog >= threshold); } fn linear_to_slog3(linear: vec3) -> vec3 { - let cut = 0.01125000; - let slog_low = linear * 5.26 + 0.030001222851889303; + let cut = vec3(0.01125000); + let slog_low = (linear / 0.01125000 * (171.2102946929 - 95.0) + 95.0) / 1023.0; let slog_high = (420.0 + log((linear + 0.01) / 0.19) / log(10.0) * 261.5) / 1023.0; - return select(slog_low, slog_high, linear >= vec3(cut)); + return select(slog_low, slog_high, linear >= cut); } // Canon CLog3 <-> Linear (simplified) @@ -334,6 +356,204 @@ fn from_linear(color: vec3, cs: u32) -> vec3 { } } +// ============================================================================ +// Gamut Conversion (Color Primaries) +// ============================================================================ + +// Gamut IDs (must match Rust Gamut enum) +const GAMUT_REC709: u32 = 0u; +const GAMUT_SGAMUT: u32 = 1u; +const GAMUT_SGAMUT3: u32 = 2u; +const GAMUT_SGAMUT3_CINE: u32 = 3u; +const GAMUT_ARRI_WIDE: u32 = 4u; +const GAMUT_ACES_AP1: u32 = 5u; +const GAMUT_RED_WIDE: u32 = 6u; +const GAMUT_DCI_P3: u32 = 7u; +const GAMUT_REC2020: u32 = 8u; +const GAMUT_VGAMUT: u32 = 9u; +const GAMUT_BMD_WIDE: u32 = 10u; + +// 3x3 matrices to convert from source gamut to Rec.709 (linear space). +// Pre-computed as: XYZ_to_Rec709 * source_RGB_to_XYZ (D65 white point, no CAT needed). +// Matrices are in WGSL column-major order (each vec3 is a column). + +fn gamut_to_rec709(color: vec3, gamut: u32) -> vec3 { + switch gamut { + case GAMUT_REC709: { return color; } + case GAMUT_SGAMUT, GAMUT_SGAMUT3: { + return mat3x3( + vec3( 1.8775895, -0.1768085, -0.0262071), + vec3(-0.7940379, 1.3510232, -0.1484570), + vec3(-0.0837210, -0.1741716, 1.1747362) + ) * color; + } + case GAMUT_SGAMUT3_CINE: { + return mat3x3( + vec3( 1.6266602, -0.1785165, -0.0444460), + vec3(-0.5400464, 1.4179576, -0.1959648), + vec3(-0.0867831, -0.2393980, 1.2404829) + ) * color; + } + case GAMUT_ARRI_WIDE: { + return mat3x3( + vec3( 1.6172660, -0.0705744, -0.0211068), + vec3(-0.5372011, 1.3346439, -0.2270083), + vec3(-0.0802240, -0.2640464, 1.2483551) + ) * color; + } + case GAMUT_ACES_AP1: { + return mat3x3( + vec3( 1.7309784, -0.1316220, -0.0245741), + vec3(-0.6039470, 1.1348678, -0.1257806), + vec3(-0.0800949, -0.0086797, 1.0658927) + ) * color; + } + case GAMUT_RED_WIDE: { + return mat3x3( + vec3( 1.9816606, -0.1781473, -0.1018204), + vec3(-0.9002885, 1.5005030, -0.5353919), + vec3(-0.0815312, -0.3223326, 1.6374523) + ) * color; + } + case GAMUT_DCI_P3: { + return mat3x3( + vec3( 1.2247450, -0.0420578, -0.0196423), + vec3(-0.2249043, 1.0420810, -0.0786549), + vec3( 0.0000001, -0.0000001, 1.0985372) + ) * color; + } + case GAMUT_REC2020: { + return mat3x3( + vec3( 1.6602266, -0.1245533, -0.0181551), + vec3(-0.5875477, 1.1329261, -0.1006030), + vec3(-0.0728382, -0.0083497, 1.1189982) + ) * color; + } + case GAMUT_VGAMUT: { + return mat3x3( + vec3( 1.8062884, -0.1700943, -0.0252119), + vec3(-0.6955865, 1.3059854, -0.1545054), + vec3(-0.1108609, -0.1358680, 1.1799572) + ) * color; + } + case GAMUT_BMD_WIDE: { + return mat3x3( + vec3( 1.5683008, -0.0863812, -0.0520228), + vec3(-0.5227529, 1.3449488, -0.2491763), + vec3(-0.0457070, -0.2585445, 1.3014390) + ) * color; + } + default: { return color; } + } +} + +fn rec709_to_gamut(color: vec3, gamut: u32) -> vec3 { + switch gamut { + case GAMUT_REC709: { return color; } + case GAMUT_SGAMUT, GAMUT_SGAMUT3: { + return mat3x3( + vec3( 0.5661472, 0.0769741, 0.0223577), + vec3( 0.3427600, 0.7990405, 0.1086252), + vec3( 0.0911672, 0.1239551, 0.8689536) + ) * color; + } + case GAMUT_SGAMUT3_CINE: { + return mat3x3( + vec3( 0.6457935, 0.0875449, 0.0369684), + vec3( 0.2591131, 0.7596906, 0.1292957), + vec3( 0.0951848, 0.1527355, 0.8336765) + ) * color; + } + case GAMUT_ARRI_WIDE: { + return mat3x3( + vec3( 0.6314215, 0.0368258, 0.0173725), + vec3( 0.2707945, 0.7930187, 0.1487858), + vec3( 0.0978548, 0.1701023, 0.8336410) + ) * color; + } + case GAMUT_ACES_AP1: { + return mat3x3( + vec3( 0.6032028, 0.0701291, 0.0221824), + vec3( 0.3263270, 0.9198951, 0.1160756), + vec3( 0.0479841, 0.0127606, 0.9407928) + ) * color; + } + case GAMUT_RED_WIDE: { + return mat3x3( + vec3( 0.5420324, 0.0770016, 0.0588817), + vec3( 0.3601398, 0.7679506, 0.2734883), + vec3( 0.0978822, 0.1550052, 0.6674728) + ) * color; + } + case GAMUT_DCI_P3: { + return mat3x3( + vec3( 0.8225930, 0.0331994, 0.0170854), + vec3( 0.1775339, 0.9667835, 0.0723957), + vec3( 0.0000000, 0.0000000, 0.9103014) + ) * color; + } + case GAMUT_REC2020: { + return mat3x3( + vec3( 0.6275038, 0.0691083, 0.0163941), + vec3( 0.3292755, 0.9195191, 0.0880113), + vec3( 0.0433026, 0.0113596, 0.8953803) + ) * color; + } + case GAMUT_VGAMUT: { + return mat3x3( + vec3( 0.5852893, 0.0786011, 0.0227979), + vec3( 0.3226342, 0.8196082, 0.1142144), + vec3( 0.0921401, 0.1017599, 0.8627817) + ) * color; + } + case GAMUT_BMD_WIDE: { + return mat3x3( + vec3( 0.6549678, 0.0488989, 0.0355435), + vec3( 0.2687242, 0.7919967, 0.1623791), + vec3( 0.0763876, 0.1590558, 0.8018868) + ) * color; + } + default: { return color; } + } +} + +// ============================================================================ +// Tone Mapping +// ============================================================================ + +const TM_NONE: u32 = 0u; +const TM_SIMPLE: u32 = 1u; + +// Per-channel hyperbolic rolloff tone mapping. +// Matches DaVinci Resolve's "Simple" / "DaVinci" tone mapping method. +// Formula: f(x) = a * x / (x + b) +// where a and b are derived from input/output white points. +// Reference: reverse-engineered by Thatcher Freeman +// (github.com/thatcherfreeman/utility-dctls) +// +// For SDR output (100 nits) from camera log (~5500 nits peak): +// input_white = 55.0 (5500/100), output_white = 1.0 +// adaptation = 9.0 +// b = (Wᵢ - (adapt/100)*(Wᵢ/Wₒ)) / ((Wᵢ/Wₒ) - 1) = 0.926852 +// a = Wₒ / (Wᵢ/(Wᵢ+b)) = 1.016852 +// Per-channel hyperbolic rolloff: f(x) = a * x / (x + b) +// a and b are pre-computed from input/output peak luminance and passed via uniforms. +fn apply_tone_mapping(color: vec3, method: u32) -> vec3 { + if (method == TM_NONE) { return color; } + + let a = cg.tone_mapping_a; + let b = cg.tone_mapping_b; + + // Clamp negative values (from gamut conversion of out-of-gamut colors) + let c = max(color, vec3(0.0)); + + return vec3( + a * c.x / (c.x + b), + a * c.y / (c.y + b), + a * c.z / (c.z + b) + ); +} + // ============================================================================ // Color Grading // ============================================================================ @@ -346,6 +566,8 @@ const CG_FLAG_QUALIFIER_ENABLED: u32 = 32u; const CG_FLAG_WINDOW_ENABLED: u32 = 64u; const CG_FLAG_INPUT_CST: u32 = 128u; const CG_FLAG_OUTPUT_CST: u32 = 256u; +const CG_FLAG_INPUT_GAMUT: u32 = 512u; +const CG_FLAG_OUTPUT_GAMUT: u32 = 1024u; fn cg_luminance(rgb: vec3) -> f32 { return dot(rgb, vec3(0.2126, 0.7152, 0.0722)); @@ -587,6 +809,16 @@ fn apply_color_grading(color: vec3, uv: vec2) -> vec3 { result = to_linear(result, cg.input_cst); } + // Input gamut: convert from source primaries to Rec.709 + if ((cg.flags & CG_FLAG_INPUT_GAMUT) != 0u) { + result = gamut_to_rec709(result, cg.input_gamut); + } + + // Tone mapping: compress dynamic range for display + if (cg.tone_mapping_method != TM_NONE) { + result = apply_tone_mapping(result, cg.tone_mapping_method); + } + // Primary correction (operates in linear) if ((cg.flags & CG_FLAG_PRIMARY_ENABLED) != 0u) { result = apply_primary_correction( @@ -624,6 +856,11 @@ fn apply_color_grading(color: vec3, uv: vec2) -> vec3 { result = apply_window(result, uv); } + // Output gamut: convert from Rec.709 to target primaries + if ((cg.flags & CG_FLAG_OUTPUT_GAMUT) != 0u) { + result = rec709_to_gamut(result, cg.output_gamut); + } + // Output CST: convert from linear to output color space if ((cg.flags & CG_FLAG_OUTPUT_CST) != 0u) { result = from_linear(result, cg.output_cst); diff --git a/crates/types/src/color_grading.rs b/crates/types/src/color_grading.rs index 5087b07..af83a6c 100644 --- a/crates/types/src/color_grading.rs +++ b/crates/types/src/color_grading.rs @@ -46,6 +46,62 @@ pub enum ColorSpace { RedLog3G10, } +// ============================================================================ +// Tone Mapping +// ============================================================================ + +/// Tone mapping method for dynamic range compression. +/// +/// When converting from wide-DR log footage to a display-referred space, +/// tone mapping smoothly compresses highlights to avoid hard clipping. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify, Default)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum ToneMapping { + /// No tone mapping — 1:1 linear mapping, highlights clip at 1.0. + #[default] + None, + /// Simple luminance-preserving highlight compression. + /// Smoothly rolls off values above the shoulder threshold. + Simple, +} + +// ============================================================================ +// Color Gamuts (Primaries) +// ============================================================================ + +/// Color gamut (primary) for gamut mapping. +/// +/// Separate from the transfer function (ColorSpace). When source footage +/// uses wide-gamut primaries (e.g. S-Gamut), converting the gamut to +/// Rec.709 is necessary in addition to the transfer function conversion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify, Default)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum Gamut { + /// Rec.709 / sRGB primaries (default display gamut). + #[default] + Rec709, + /// Sony S-Gamut (used with S-Log2/S-Log3). + SGamut, + /// Sony S-Gamut3 (improved S-Gamut). + SGamut3, + /// Sony S-Gamut3.Cine (cinema-optimized, closer to DCI-P3). + SGamut3Cine, + /// ARRI Wide Gamut (ALEXA Wide Gamut 3, used with LogC). + ArriWideGamut, + /// ACES AP1 primaries (ACEScg). + AcesCgAp1, + /// RED Wide Gamut RGB. + RedWideGamut, + /// DCI-P3 (D65 white point variant). + DciP3, + /// Rec.2020 / BT.2020 (UHDTV). + Rec2020, + /// Panasonic V-Gamut. + VGamut, + /// Blackmagic Design Wide Gamut (Gen 5). + BmdWideGamut, +} + // ============================================================================ // Primary Correction (CDL) // ============================================================================ @@ -737,10 +793,22 @@ pub enum ColorGradingNode { #[serde(skip_serializing_if = "Option::is_none")] #[tsify(optional)] position: Option, - /// Source color space. + /// Source transfer function (gamma curve). from_space: ColorSpace, - /// Target color space. + /// Target transfer function (gamma curve). to_space: ColorSpace, + /// Source gamut (color primaries). None = Rec709. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[tsify(optional)] + from_gamut: Option, + /// Target gamut (color primaries). None = Rec709. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[tsify(optional)] + to_gamut: Option, + /// Tone mapping method. None = no tone mapping. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[tsify(optional)] + tone_mapping: Option, }, } diff --git a/packages/render-engine/src/types.ts b/packages/render-engine/src/types.ts index 8cb9ced..f5c2752 100644 --- a/packages/render-engine/src/types.ts +++ b/packages/render-engine/src/types.ts @@ -272,6 +272,21 @@ export type ColorSpace = | "BmFilm" | "RedLog3G10"; +export type Gamut = + | "Rec709" + | "SGamut" + | "SGamut3" + | "SGamut3Cine" + | "ArriWideGamut" + | "AcesCgAp1" + | "RedWideGamut" + | "DciP3" + | "Rec2020" + | "VGamut" + | "BmdWideGamut"; + +export type ToneMapping = "None" | "Simple"; + export type LutInterpolation = "Trilinear" | "Tetrahedral"; /** Primary correction using ASC-CDL model. */ @@ -452,6 +467,9 @@ export type ColorGradingNode = position?: NodePosition; from_space: ColorSpace; to_space: ColorSpace; + from_gamut?: Gamut; + to_gamut?: Gamut; + tone_mapping?: ToneMapping; }; /** Complete color grading configuration. */ diff --git a/packages/render-engine/tests/cst.test.ts b/packages/render-engine/tests/cst.test.ts new file mode 100644 index 0000000..70ab793 --- /dev/null +++ b/packages/render-engine/tests/cst.test.ts @@ -0,0 +1,133 @@ +/** + * Color Space Transform tests. + * + * Validates our transfer function and gamut conversion math against + * reference values from the `colour-science` Python library. + * + * Regenerate fixtures: run the Python script in the commit message. + */ + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, it, expect } from "vitest"; + +const fixtures = JSON.parse(readFileSync(join(__dirname, "fixtures/cst-reference.json"), "utf-8")); + +function slog2ToLinear(slog: number): number { + // legal-to-full (10-bit) + const x = (slog * 1023.0 - 64.0) / 876.0; + const threshold = 0.088251; + let slogLinear: number; + if (slog >= threshold) { + slogLinear = Math.pow(10, (x - 0.616596 - 0.03) / 0.432699) - 0.037584; + } else { + slogLinear = (x - 0.030001222851889303) / 5.0; + } + return slogLinear * 1.2716129032; // 0.9 * 219/155 +} + +function slog3ToLinear(slog: number): number { + const threshold = 171.2102946929 / 1023; + if (slog >= threshold) { + return Math.pow(10, (slog * 1023 - 420) / 261.5) * 0.19 - 0.01; + } + return ((slog * 1023 - 95) * 0.01125) / (171.2102946929 - 95); +} + +function logc3ToLinear(logc: number): number { + const a = 5.555556; + const b = 0.052272; + const c = 0.24719; + const d = 0.385537; + const e = 5.367655; + const f = 0.092809; + const breakpoint = 0.1496578; // e*cut + f + if (logc > breakpoint) { + return (Math.pow(10, (logc - d) / c) - b) / a; + } + return (logc - f) / e; +} + +function vlogToLinear(vlog: number): number { + if (vlog >= 0.181) { + return Math.pow(10, (vlog - 0.598206) / 0.241514) - 0.00873; + } + return (vlog - 0.125) / 5.6; +} + +function linearToSrgb(linear: number): number { + if (linear <= 0.0031308) { + return linear * 12.92; + } + return 1.055 * Math.pow(Math.max(linear, 0), 1 / 2.4) - 0.055; +} + +// Gamut matrix: S-Gamut → Rec.709 (row-major, computed from colour-science) +const SGAMUT_TO_REC709 = [ + [1.8775895, -0.7940379, -0.083721], + [-0.1768085, 1.3510232, -0.1741716], + [-0.0262071, -0.148457, 1.1747362], +]; + +function matMul3(m: number[][], v: [number, number, number]): [number, number, number] { + return [ + m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2], + m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2], + m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2], + ]; +} + +describe("Transfer Functions", () => { + it("S-Log2 → Linear matches colour-science", () => { + for (const { input, expected } of fixtures.slog2_to_linear) { + const actual = slog2ToLinear(input); + expect(actual).toBeCloseTo(expected, 3); + } + }); + + it("S-Log3 → Linear matches colour-science", () => { + for (const { input, expected } of fixtures.slog3_to_linear) { + const actual = slog3ToLinear(input); + expect(actual).toBeCloseTo(expected, 4); + } + }); + + it("LogC3 → Linear matches colour-science", () => { + for (const { input, expected } of fixtures.logc3_to_linear) { + const actual = logc3ToLinear(input); + expect(actual).toBeCloseTo(expected, 4); + } + }); + + it("V-Log → Linear matches colour-science", () => { + for (const { input, expected } of fixtures.vlog_to_linear) { + const actual = vlogToLinear(input); + expect(actual).toBeCloseTo(expected, 5); + } + }); +}); + +describe("Full Pipeline: S-Log2/S-Gamut → sRGB/Rec.709", () => { + it("matches colour-science reference within tolerance", () => { + for (const { input, expected } of fixtures.slog2_sgamut_to_srgb_rec709) { + const [r, g, b] = input; + + // Step 1: S-Log2 → linear + const linear: [number, number, number] = [ + slog2ToLinear(r), + slog2ToLinear(g), + slog2ToLinear(b), + ]; + + // Step 2: S-Gamut → Rec.709 + const rec709Linear = matMul3(SGAMUT_TO_REC709, linear); + + // Step 3: linear → sRGB (clamp negatives before gamma) + const srgb = rec709Linear.map((v) => Math.min(1, Math.max(0, linearToSrgb(Math.max(0, v))))); + + for (let ch = 0; ch < 3; ch++) { + expect(srgb[ch]).toBeCloseTo(expected[ch], 2); + } + } + }); +}); diff --git a/packages/render-engine/tests/fixtures/cst-reference.json b/packages/render-engine/tests/fixtures/cst-reference.json new file mode 100644 index 0000000..7a617c5 --- /dev/null +++ b/packages/render-engine/tests/fixtures/cst-reference.json @@ -0,0 +1,434 @@ +{ + "slog2_to_linear": [ + { + "input": 0.0, + "expected": -0.026210633579493398 + }, + { + "input": 0.05, + "expected": -0.011360633579493396 + }, + { + "input": 0.1, + "expected": 0.003619929299073656 + }, + { + "input": 0.15, + "expected": 0.022355018619810103 + }, + { + "input": 0.2, + "expected": 0.047917346927185286 + }, + { + "input": 0.25, + "expected": 0.08279482294598373 + }, + { + "input": 0.3, + "expected": 0.1303819722390667 + }, + { + "input": 0.35, + "expected": 0.19531031672768767 + }, + { + "input": 0.4, + "expected": 0.28389914754983203 + }, + { + "input": 0.45, + "expected": 0.4047705636970571 + }, + { + "input": 0.5, + "expected": 0.5696886363914269 + }, + { + "input": 0.55, + "expected": 0.7947043726850902 + }, + { + "input": 0.6, + "expected": 1.1017179143338063 + }, + { + "input": 0.65, + "expected": 1.5206100163185121 + }, + { + "input": 0.7, + "expected": 2.0921502557582965 + }, + { + "input": 0.75, + "expected": 2.8719650189185155 + }, + { + "input": 0.8, + "expected": 3.935951459206058 + }, + { + "input": 0.85, + "expected": 5.387664351288298 + }, + { + "input": 0.9, + "expected": 7.368394782841789 + }, + { + "input": 0.95, + "expected": 10.070921614338557 + }, + { + "input": 1.0, + "expected": 13.758274097347455 + } + ], + "slog3_to_linear": [ + { + "input": 0.0, + "expected": -0.014023695936443719 + }, + { + "input": 0.05, + "expected": -0.006473042808558495 + }, + { + "input": 0.1, + "expected": 0.0010776103193267297 + }, + { + "input": 0.15, + "expected": 0.008628263447211949 + }, + { + "input": 0.2, + "expected": 0.01851308652166711 + }, + { + "input": 0.25, + "expected": 0.03473490571895879 + }, + { + "input": 0.3, + "expected": 0.060185729916100474 + }, + { + "input": 0.35, + "expected": 0.10011617448806046 + }, + { + "input": 0.4, + "expected": 0.16276406327012358 + }, + { + "input": 0.45, + "expected": 0.26105392733053484 + }, + { + "input": 0.5, + "expected": 0.4152633917647167 + }, + { + "input": 0.55, + "expected": 0.6572065376669349 + }, + { + "input": 0.6, + "expected": 1.0367972849913025 + }, + { + "input": 0.65, + "expected": 1.632346850642181 + }, + { + "input": 0.7, + "expected": 2.5667196920430464 + }, + { + "input": 0.75, + "expected": 4.032680977386896 + }, + { + "input": 0.8, + "expected": 6.332664875575779 + }, + { + "input": 0.85, + "expected": 9.941168036481097 + }, + { + "input": 0.9, + "expected": 15.602640307011931 + }, + { + "input": 0.95, + "expected": 24.48506796212528 + }, + { + "input": 1.0, + "expected": 38.42093433720254 + } + ], + "logc3_to_linear": [ + { + "input": 0.0, + "expected": -0.01729041825527162 + }, + { + "input": 0.05, + "expected": -0.007975363543297772 + }, + { + "input": 0.1, + "expected": 0.0013396911686760797 + }, + { + "input": 0.15, + "expected": 0.01065489411188905 + }, + { + "input": 0.2, + "expected": 0.022557011654414694 + }, + { + "input": 0.25, + "expected": 0.04151960753378661 + }, + { + "input": 0.3, + "expected": 0.07173104186046103 + }, + { + "input": 0.35, + "expected": 0.11986426000639311 + }, + { + "input": 0.4, + "expected": 0.1965506782688429 + }, + { + "input": 0.45, + "expected": 0.3187283961204467 + }, + { + "input": 0.5, + "expected": 0.513383396023284 + }, + { + "input": 0.55, + "expected": 0.8235100676737995 + }, + { + "input": 0.6, + "expected": 1.3176075864482162 + }, + { + "input": 0.65, + "expected": 2.104809657007795 + }, + { + "input": 0.7, + "expected": 3.3589894015861264 + }, + { + "input": 0.75, + "expected": 5.357163556022022 + }, + { + "input": 0.8, + "expected": 8.540678493745718 + }, + { + "input": 0.85, + "expected": 13.612692530265484 + }, + { + "input": 0.9, + "expected": 21.69348589542808 + }, + { + "input": 0.95, + "expected": 34.5679024369212 + }, + { + "input": 1.0, + "expected": 55.079576698813185 + } + ], + "vlog_to_linear": [ + { + "input": 0.0, + "expected": -0.022321428571428572 + }, + { + "input": 0.05, + "expected": -0.013392857142857144 + }, + { + "input": 0.1, + "expected": -0.004464285714285713 + }, + { + "input": 0.15, + "expected": 0.004464285714285713 + }, + { + "input": 0.2, + "expected": 0.013719643752877516 + }, + { + "input": 0.25, + "expected": 0.027430696720631136 + }, + { + "input": 0.3, + "expected": 0.04951573439629128 + }, + { + "input": 0.35, + "expected": 0.0850891429654538 + }, + { + "input": 0.4, + "expected": 0.14238890472330837 + }, + { + "input": 0.45, + "expected": 0.2346843250826899 + }, + { + "input": 0.5, + "expected": 0.3833488981626514 + }, + { + "input": 0.55, + "expected": 0.6228099158706739 + }, + { + "input": 0.6, + "expected": 1.0085210359700107 + }, + { + "input": 0.65, + "expected": 1.6298040723166047 + }, + { + "input": 0.7, + "expected": 2.630533870183547 + }, + { + "input": 0.75, + "expected": 4.242456407498944 + }, + { + "input": 0.8, + "expected": 6.838855827046132 + }, + { + "input": 0.85, + "expected": 11.020998448522443 + }, + { + "input": 0.9, + "expected": 17.757372203150346 + }, + { + "input": 0.95, + "expected": 28.607966137706466 + }, + { + "input": 1.0, + "expected": 46.08552795674034 + } + ], + "slog2_sgamut_to_srgb_rec709": [ + { + "input": [0.1, 0.1, 0.1], + "expected": [0.0463948, 0.0464056, 0.0464046] + }, + { + "input": [0.2, 0.2, 0.2], + "expected": [0.2424606, 0.2424923, 0.2424892] + }, + { + "input": [0.3, 0.3, 0.3], + "expected": [0.3964033, 0.3964514, 0.3964468] + }, + { + "input": [0.39, 0.39, 0.39], + "expected": [0.5505732, 0.5506378, 0.5506316] + }, + { + "input": [0.5, 0.5, 0.5], + "expected": [0.7794617, 0.7795507, 0.7795421] + }, + { + "input": [0.6, 0.6, 0.6], + "expected": [1.0, 1.0, 1.0] + }, + { + "input": [0.5, 0.3, 0.3], + "expected": [0.9800652, 0.2547766, 0.379422] + }, + { + "input": [0.3, 0.5, 0.3], + "expected": [0.0, 0.8670937, 0.2831792] + }, + { + "input": [0.3, 0.3, 0.5], + "expected": [0.3381213, 0.2573445, 0.8246358] + }, + { + "input": [0.6, 0.4, 0.3], + "expected": [1.0, 0.4445224, 0.3174843] + } + ], + "slog2_sgamut_to_srgb_rec709_tonemapped": [ + { + "input": [0.1, 0.1, 0.1], + "expected": [0.046242, 0.046253, 0.046252] + }, + { + "input": [0.2, 0.2, 0.2], + "expected": [0.236715, 0.236747, 0.236744] + }, + { + "input": [0.3, 0.3, 0.3], + "expected": [0.37393, 0.373976, 0.373972] + }, + { + "input": [0.39, 0.39, 0.39], + "expected": [0.494266, 0.494325, 0.494319] + }, + { + "input": [0.5, 0.5, 0.5], + "expected": [0.636536, 0.636609, 0.636602] + }, + { + "input": [0.6, 0.6, 0.6], + "expected": [0.751015, 0.751101, 0.751093] + }, + { + "input": [0.7, 0.7, 0.7], + "expected": [0.841446, 0.841541, 0.841532] + }, + { + "input": [0.5, 0.3, 0.3], + "expected": [0.888343, 0.227326, 0.340926] + }, + { + "input": [0.3, 0.5, 0.3], + "expected": [0.0, 0.718963, 0.228852] + }, + { + "input": [0.3, 0.3, 0.5], + "expected": [0.32209, 0.244607, 0.788764] + }, + { + "input": [0.6, 0.4, 0.3], + "expected": [1.0, 0.365201, 0.258336] + } + ] +}