Filmtone / Code Generation
Generating Swift from TypeScript math — Filmtone's cross-platform color parity pipeline
Color transforms, motion blur, and exposure math live in TypeScript inside `packages/film-lab-renderer/src/`. `bun run generate:filmtone-ios-swift` mechanically produces `FilmtonePhase0Generated.swift`; hand-editing the generated module is prohibited. The Phase 0 contract test (`verify:swift-contract`) confirms the generated math matches the TypeScript output. WebGL fragment shaders read the math as GLSL constants, WebGPU compute shaders as WGSL, and the iOS Swift export session as the generated module. Color parity is held by structure, not discipline.
- Published
- April 29, 2026
- Topics
- Filmtone · WebGL · WebGPU · Swift · Cross-platform
Putting TypeScript at the center
Filmtone runs preview (web / WebGL / WebGPU) and export (Desktop / iOS Swift) on separate runtimes. Keeping color identical across them depends on a single source of truth for the math. Color transforms, motion blur, exposure, LUT constraints, and preset definitions all live in TypeScript under `packages/film-lab-renderer/src/`. Swift isn't hand-written; it's generated from TS. GLSL fragment shaders and WGSL compute shaders read constants from the same TS module.
generate:filmtone-ios-swift
The root `bun run generate:filmtone-ios-swift` runs `scripts/generate-filmtone-ios-swift.ts`, which writes `apps/capacitor-film-lab-ios/ios/App/App/FilmtonePhase0Generated.swift`. It's chained into `dev:ios` and `build:ios`, so any TS change reaches the next Capacitor sync / sim build automatically.
// package.json (root, scripts excerpt)
{
"scripts": {
"build:core": "...",
"build:renderer": "...",
"generate:filmtone-ios-swift": "bun run scripts/generate-filmtone-ios-swift.ts",
"dev:ios": "bun run build:core && bun run generate:filmtone-ios-swift && bun run --cwd apps/capacitor-film-lab-ios dev",
"build:ios": "bun run build:core && bun run generate:filmtone-ios-swift && bun run --cwd apps/capacitor-film-lab-ios build && bun run --cwd apps/capacitor-film-lab-ios cap:sync:ios"
}
}Shape of the generated Swift module
The output is a single `enum FilmtonePhase0Generated` of static lets. `schemaVersion`, `paramKeys`, `quickAxisIds`, `defaultQuickState`, `outputProfile`, `resetParams`, and the preset definitions are copied verbatim from TS values into Swift constants. The file is marked do-not-edit; any manual change is overwritten by the next generation. Swift call-sites only read it: `FilmtonePhase0Generated.resetParams`, etc.
// apps/capacitor-film-lab-ios/ios/App/App/FilmtonePhase0Generated.swift
enum FilmtonePhase0Generated {
static let schemaVersion = 2
static let presetVersion = "v1"
static let presetDefault = "reset"
static let presetStrengthDefault = 1.0
static let paramKeys: [String] = [
"exposure", "contrast", "saturation", "temperature", "tint",
"rgbShift", "lensSoftness", "grainRadialMix", "grainSize",
"bloomThreshold", "bloomStrength", "bloomRadius", "diffusion",
"halationIntensity", "halationSpread", "halationHue",
"halationThreshold", "halationRadius", "bloomSoftKnee",
"halationSoftKnee", "compressionAmount", "compressionRange",
"printContrast", "cyan", "magenta", "yellow",
"shutterAngle", "trailIntensity", "fade", "vignette", "grainIntensity",
]
static let outputProfile = Phase0OutputProfileDTO(
longEdge: 1920, fps: 24, codec: "h264", container: "mp4", preserveAudio: true
)
static let resetParams: FilmtonePhase0Params = .init(
exposure: 0.0, contrast: 1.0, saturation: 1.0,
// ... 27 more fields
)
}Contract test — `verify-phase0-contract.sh`
What proves the generated Swift agrees with TS is `apps/capacitor-film-lab-ios/scripts/verify-phase0-contract.sh`. It runs `xcrun --sdk iphonesimulator swiftc -typecheck` against the simulator target first, then compiles a native binary with `xcrun swiftc` and runs it against frozen fixtures (`canonical-export-request.json`, `legacy-project-state.json`, `hlg-export-request.json`). Triggered by `bun run --cwd apps/capacitor-film-lab-ios verify:swift-contract`.
# apps/capacitor-film-lab-ios/scripts/verify-phase0-contract.sh (excerpt)
xcrun --sdk iphonesimulator swiftc \
-target "$SIMULATOR_TARGET" \
-typecheck \
"$SWIFT_SUPPORT" \
"$PHASE0_GENERATED" \
"$PHASE0_MATH" \
"$MOTION_MATH" \
"$SWIFT_CHECK"
xcrun swiftc \
-o "$HOST_BINARY" \
"$SWIFT_SUPPORT" \
"$PHASE0_GENERATED" \
"$PHASE0_MATH" \
"$MOTION_MATH" \
"$SWIFT_CHECK"
"$HOST_BINARY" "$CANONICAL_FIXTURE" "$LEGACY_FIXTURE" "$HLG_FIXTURE"Seven verification layers
verify-phase0-contract.sh isn't one test — it's seven independent ones, run sequentially through one shell. Any single failure stops the commit gate.
- Phase 0 contract — DTO schema and preset constants verified against canonical, legacy, and HLG fixtures
- Motion blur math — `FilmtoneMotionBlurMath` active frames and weights at boundary values
- Cube parser — `.cube` DOMAIN_MIN / DOMAIN_MAX semantics and LUT size constraints
- Cache store — retention and cleanup policy
- Source-color-classifier — SDR / HDR PQ / HDR HLG / Apple Log / Apple Log 2 classification and HDR preparation policy
- Ray-angle optics — depth × ray-angle math in `FilmtoneRayAngleOptics`
- Sidecar builder — `FilmtoneExportSidecarBuilder` against the HLG fixture
Why the math doesn't drift between WebGL/WebGPU and iOS
TypeScript fans out into three downstream paths: the WebGL Backend assembles GLSL strings from TS constants under `packages/film-lab-renderer/src/webgl/`, the WebGPU Backend emits WGSL constants from `packages/film-lab-renderer/src/webgpu/`, and iOS Swift gets `FilmtonePhase0Generated.swift` from `bun run generate:filmtone-ios-swift`. All three converge on the same TypeScript functions at their entry. There is no place where someone could quietly substitute a slightly different number — the path doesn't exist.