WannaCut uses a unified Three.js WebGL render engine that powers both the real-time 10 FPS preview player and the final video export. There is no separate “offline renderer” — the sameDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ter-9001/WannaCut/llms.txt
Use this file to discover all available pages before exploring further.
drawFrame function handles both paths. During export, an offscreen WebGLRenderer with a WebGLRenderTarget renders each frame at full project resolution, and the result is piped to Rust’s FFmpeg assembler. This page documents the renderBridge interface, the preview loop, the three-phase export pipeline, the pixel-space coordinate system, and the speed ramp math.
renderBridge Interface
renderBridge.tsx is the bridge between the React editor and the Three.js render engine. It exports two interfaces and two async functions.
RenderEngineContext
The context object passed to drawFrame on every preview tick or export frame render. It gives the render function read-only access to the current editor state without depending on React.
| Field | Purpose | |
|---|---|---|
time | Current playhead time in seconds (composition time). | |
projectConfig | Project settings including width, height, fps, backgroundColor. | |
currentProjectPath | Absolute path to the project directory. Used to resolve asset paths. | |
sceneRef | The shared Three.js Scene. The render function adds/removes THREE.Group objects here. | |
rendererRef | The active WebGLRenderer. During preview this is the canvas renderer; during export it is the offscreen renderer. | |
cameraRef | The camera ref for the active renderer. Note: The interface type is declared as `THREE.OrthographicCamera | null, but App.tsxactually instantiates aTHREE.PerspectiveCamera` (fov=45) in pixel-space coordinates. The interface type definition is inaccurate relative to runtime usage. |
topClips | Clips currently visible at time, sorted by track render order (frontmost first). Updated at 10 FPS by updatePreview(). | |
groupsRef | Map from clip ID → THREE.Group. Allows the render function to update or remove scene objects by clip ID. | |
getInterpolatedValueWithFades | Keyframe interpolation function that evaluates opacity, volume, zoom, position, rotation3d, or speed at a given time for a given clip, including fade-in/out. | |
invoke | Tauri invoke reference, passed in so the render function can call get_video_frame or other commands without importing Tauri directly. | |
topAudios | Audio clips active at time. During export this is always empty ([]) to prevent audio initialization from interfering with the OfflineAudioContext. | |
isPlaying | false during export. The render function uses this to skip audio sync. | |
settingsFolder | Path to the WannaCut settings folder. Used by the render engine to resolve font paths. |
ExportOptions
Options passed to exportVideo() when the user triggers an export.
| Field | Purpose |
|---|---|
targetPath | Absolute output path for the final MP4, chosen by the user via the native save dialog. |
fps | Export frame rate. Must match projectConfig.fps. |
onProgress | Callback receiving a 0–100 percent value as each phase completes. Drives the export progress bar in the UI. |
onError | Callback receiving an error message string if any phase fails. Resets renderStatus to 'idle'. |
getDrawFrameFunction()
Asynchronously loads the render function from the private submodule at ./Render/Render. If the submodule is unavailable, it falls back to a no-op.
App.tsx and the result is stored in drawFrameEngine (useRef):
The actual draw implementation (
professionalDrawFrame) lives in src/Render/Render.tsx, a private submodule not included in the public repository. The public renderBridge.tsx interface is stable and unchanged regardless of the render engine version.Preview Rendering
The preview renders at a throttled 10 FPS to balance visual quality with GPU/CPU headroom during editing. The playhead itself moves at fullrequestAnimationFrame rate (≈60 Hz) via currentTimeRef, but drawFrame is only called when enough time has elapsed.
Preview Loop
updatePreview(currentTime)
Filters the full clips array to find clips that are active at currentTime (i.e., clip.start <= time <= clip.start + clip.duration) and are not audio-only. The result is sorted by track render order (video tracks first, then audio; within type sorted by track ID) and stored in topClips.current.
newDrawFrame(time?)
Calls drawFrameEngine.current with a full RenderEngineContext:
isPlaying is false (paused), newDrawFrame is still called on every scrub (seekTo) so the canvas always reflects the playhead position.
Export Pipeline
The export is a three-phase sequential process. Progress is reported from 0% to 100% via theonProgress callback, and the full pipeline is implemented in exportVideo() in renderBridge.tsx.
Phase 1 — Video Frame Rendering
A freshTHREE.WebGLRenderer and WebGLRenderTarget are created for the export (separate from the preview renderer to avoid disrupting the UI):
drawFrame is called with isPlaying: false (audio sync disabled) and topAudios: { current: [] } (empty audio list). After rendering, the pixel buffer is read back, vertically flipped, encoded as PNG via an auxiliary <canvas>, and sent to Rust via save_export_frame.
Phase 2 — Offline Audio Mixing
renderAudioOffline() is called with all non-muted audio/video clips and uses the Web Audio API’s OfflineAudioContext to produce the mixed audio WAV. It replicates the exact same audio effect chain used during preview (pitch, alien, microphone, volume keyframes, speed ramps), ensuring the exported audio is identical to what the user hears while editing. Using OfflineAudioContext means the mixing runs faster than real time — a 60-second timeline might complete Phase 2 in a few seconds.
Phase 3 — FFmpeg Assembly
The Rustassemble_exported_video command runs FFmpeg with the PNG sequence from <projectPath>/export_frames/ and the WAV from <projectPath>/export_audio.wav, producing the final MP4 at the target path. Progress is emitted as Tauri events that the frontend listens for:
Vertical Flip Utility
WebGL’s coordinate origin is at the bottom-left, but image conventions (and video) use top-left. After reading pixels from the render target,flipVertical corrects this:
Coordinate System
The Three.js camera is configured to map canvas pixel coordinates directly to world units, so no coordinate transformation is needed between the editor’s clip position values and the rendered output.- Top-left corner of the canvas = world coordinates
(0, 0, 0) - Bottom-right corner = world coordinates
(width, -height, 0) - A clip with
position = { x: 100, y: 200 }appears exactly 100 px from the left and 200 px from the top
(W/2, -H/2, zPos) and looking at (W/2, -H/2, 0).
The vertical flip applied after
readRenderTargetPixels() is separate from this Y-axis negation. The camera handles the visual coordinate mapping; the flip handles the raw pixel buffer’s reversed row order in WebGL’s memory layout.Keyframe Interpolation
getInterpolatedValueWithFades is the core function for reading animated clip properties at any point in time. It is passed into the render engine context and called for every clip on every frame.
How it works
- Default values: If a clip has no keyframes for a property, the default is returned (
opacity: 1.0,volume: 0 dB,zoom: 1.0,position: {x:0, y:0},speed: 1.0). - Boundary clamping: Times before the first keyframe return the first keyframe’s value; times after the last return the last keyframe’s value.
- Linear interpolation (LERP): Between two adjacent keyframes, the value is linearly interpolated based on the normalized progress
(t - t0) / (t1 - t0). - Object types: For
position({x, y}) androtation3d({rot, rot3d}), each component is interpolated independently. - Fade-in / Fade-out: Applied as a post-processing step. For opacity, fade uses a linear multiplier. For volume (in dB), the fade uses
20 * log10(fadeModifier)to achieve a perceptually natural fade curve. Silence threshold is-100 dB.
Speed Ramp Math
Speed keyframes allow each clip to have a variable playback rate over time — from 0.1× slow motion to 10× fast forward, with arbitrary curves drawn using many keyframe points.Value Range Mapping
All speed keyframes store real speed values (e.g.0.5 = half speed, 2.0 = double speed). The timeline UI uses a normalized 0–1 slider that maps to the 0.1×–10× range:
Time Mapping Functions
Speed ramps require converting between two time domains: composition time (position on the timeline) and media time (position in the original footage). The relationship is:speed(t) curve using the trapezoid rule. Constant extrapolation is used before the first and after the last keyframe.
Media time → Composition time (inverse, used to anchor non-speed keyframes):
compositionToMediaTime (which is monotonically increasing) to find the inverse within 0.0001 s tolerance.
Keyframe Remapping After Speed Changes
When speed keyframes change, all other keyframe types (opacity, volume, zoom, position, rotation3d) must be remapped so they remain anchored to their original footage position rather than drifting on the composition timeline:originalTime (the media-domain time it was created at). After a speed change, remapKeyframesToSpeed uses mediaToCompositionTime(kf.originalTime, speedKfs) to recalculate the keyframe’s new composition-domain time, keeping the effect visually locked to the same moment in the footage.