Filmtone / Code Generation
TypeScript の色変換数学から Swift コードを機械生成する — Filmtone のクロスプラットフォーム色一致パイプライン
色変換・モーションブラー・露出計算を `packages/film-lab-renderer/src/` の TypeScript に集約し、`bun run generate:filmtone-ios-swift` で `FilmtonePhase0Generated.swift` を機械生成する。Swift 側は手書き禁止。Phase 0 contract test (`verify:swift-contract`) が生成された数式と TypeScript 出力の一致を検証する。WebGL fragment shader は GLSL 定数として、WebGPU compute は WGSL として、iOS Swift export は生成モジュールとして同じ数式を読む。色一致を「規律」ではなく「構造」で守る実装。
- Published
- 2026年4月29日
- Topics
- Filmtone · WebGL · WebGPU · Swift · Cross-platform
TypeScript を単一参照源に置く
Filmtone はプレビュー (web / WebGL / WebGPU) と書き出し (Desktop / iOS Swift) を別ランタイムで動かしている。それでも色を割らないために、色変換・モーションブラー・露出計算・LUT の制約・preset 定義などの数学を `packages/film-lab-renderer/src/` の TypeScript に集約する。Swift は手書きせず、TS から機械生成する。GLSL fragment shader と WGSL compute shader は同じ TS module から定数を読み出して文字列を組み立てる。
generate:filmtone-ios-swift
ルートの `bun run generate:filmtone-ios-swift` が `scripts/generate-filmtone-ios-swift.ts` を実行し、`apps/capacitor-film-lab-ios/ios/App/App/FilmtonePhase0Generated.swift` を書き出す。`dev:ios` / `build:ios` の前段に組み込まれているので、TS を変更すれば次の Capacitor sync / sim build に自動で反映される。
// 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"
}
}生成された Swift モジュールの形
出力は `enum FilmtonePhase0Generated` 1 つに閉じた static let の集合体。`schemaVersion` / `paramKeys` / `quickAxisIds` / `defaultQuickState` / `outputProfile` / `resetParams` / preset 定義などが TS の値から逐語コピーで Swift constant に写る。このファイルは手動編集禁止で、編集すると次の generate で消える。Swift モジュール側は `FilmtonePhase0Generated.resetParams` のように同じ key で参照するだけ。
// 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 フィールド
)
}contract test — `verify-phase0-contract.sh`
生成された Swift と TS の出力が一致するかを検証するのが `apps/capacitor-film-lab-ios/scripts/verify-phase0-contract.sh`。`xcrun --sdk iphonesimulator swiftc -typecheck` で simulator target の型を通したあと、ホストの `xcrun swiftc` でバイナリにコンパイルし、固定 fixture (`canonical-export-request.json` / `legacy-project-state.json` / `hlg-export-request.json`) と一致するか実行で確認する。`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"7 系統の検証レイヤー
verify-phase0-contract.sh は単一の test ではなく、7 系統の独立 test を 1 シェル経由で順に走らせる。1 つでも壊れたら commit gate で止まる。
- Phase 0 contract — DTO スキーマと preset 定数を canonical / legacy / HLG fixture に対して検証
- Motion blur math — `FilmtoneMotionBlurMath` の active frames と weights を境界値で検証
- Cube parser — `.cube` の DOMAIN_MIN / DOMAIN_MAX 解釈と LUT サイズ制約
- Cache store — retention / cleanup policy
- Source-color-classifier — SDR / HDR PQ / HDR HLG / Apple Log / Apple Log 2 の分類と HDR preparation policy
- Ray-angle optics — `FilmtoneRayAngleOptics` の depth × 光線角度の数式
- Sidecar builder — `FilmtoneExportSidecarBuilder` の HLG fixture 検証
WebGL / WebGPU と iOS の数式が割れない理由
TS から落ちる経路は 3 系統に分かれる: WebGL Backend は GLSL 文字列を `packages/film-lab-renderer/src/webgl/` で組み立てる、WebGPU Backend は WGSL を `packages/film-lab-renderer/src/webgpu/` で生成する、iOS Swift は `FilmtonePhase0Generated.swift` を `bun run generate:filmtone-ios-swift` で書き出す。3 経路すべての入口は同じ TS 関数なので、中間で誰かが「ここだけ違う数値を入れた」状況が物理的に作れない。