bladeworks 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bladeworks/__init__.py +56 -0
- bladeworks/__main__.py +4 -0
- bladeworks/cli.py +728 -0
- bladeworks/core/__init__.py +34 -0
- bladeworks/core/animation.py +893 -0
- bladeworks/core/animation_execution.py +817 -0
- bladeworks/core/audio_enhancements.py +673 -0
- bladeworks/core/audio_execution.py +1848 -0
- bladeworks/core/audio_ir.py +1971 -0
- bladeworks/core/basic_effects.py +168 -0
- bladeworks/core/bindings.py +196 -0
- bladeworks/core/capabilities.py +257 -0
- bladeworks/core/cohort_effects.py +702 -0
- bladeworks/core/color.py +324 -0
- bladeworks/core/compiler.py +3142 -0
- bladeworks/core/composition_diagnostics.py +629 -0
- bladeworks/core/composition_ir.py +3154 -0
- bladeworks/core/composition_planning.py +708 -0
- bladeworks/core/composition_shadow.py +687 -0
- bladeworks/core/compositor.py +1186 -0
- bladeworks/core/effect_parameter_semantics.py +165 -0
- bladeworks/core/effect_parameters.py +193 -0
- bladeworks/core/errors.py +21 -0
- bladeworks/core/filter_text.py +48 -0
- bladeworks/core/geometry.py +1711 -0
- bladeworks/core/group_surface.py +664 -0
- bladeworks/core/keyer.py +280 -0
- bladeworks/core/masks.py +453 -0
- bladeworks/core/missing_media.py +130 -0
- bladeworks/core/model.py +985 -0
- bladeworks/core/multicam_execution.py +496 -0
- bladeworks/core/output_color.py +52 -0
- bladeworks/core/parser.py +1257 -0
- bladeworks/core/pixel_domains.py +433 -0
- bladeworks/core/render_profile.py +311 -0
- bladeworks/core/render_sources.py +475 -0
- bladeworks/core/report.py +118 -0
- bladeworks/core/retime.py +967 -0
- bladeworks/core/retime_execution.py +1515 -0
- bladeworks/core/spatial_intrinsics.py +1749 -0
- bladeworks/core/story_containers.py +524 -0
- bladeworks/core/story_ir.py +1042 -0
- bladeworks/core/text.py +1032 -0
- bladeworks/core/text_templates.py +934 -0
- bladeworks/data/FCPXML_RENDER_CAPABILITIES.yaml +1797 -0
- bladeworks/effects/__init__.py +1 -0
- bladeworks/effects/contract.py +162 -0
- bladeworks/examples/__init__.py +80 -0
- bladeworks/examples/color.fcpxmld/Info.fcpxml +17 -0
- bladeworks/examples/color.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/crop_or_transition.fcpxmld/Info.fcpxml +13 -0
- bladeworks/examples/crop_or_transition.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/crop_or_transition.fcpxmld/Media/b.mp4 +0 -0
- bladeworks/examples/manual_qa/generate_2h_qa_fixtures.py +237 -0
- bladeworks/examples/manual_qa/missing_media_lab.fcpxmld/Info.fcpxml +28 -0
- bladeworks/examples/manual_qa/missing_media_lab.fcpxmld/Media/night_signal.mp4 +0 -0
- bladeworks/examples/manual_qa/portrait_social.fcpxmld/Info.fcpxml +44 -0
- bladeworks/examples/manual_qa/portrait_social.fcpxmld/Media/city_grid.mp4 +0 -0
- bladeworks/examples/manual_qa/portrait_social.fcpxmld/Media/golden_hour.mp4 +0 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Info.fcpxml +113 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/city_grid.mp4 +0 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/golden_hour.mp4 +0 -0
- bladeworks/examples/manual_qa/studio_showcase.fcpxmld/Media/night_signal.mp4 +0 -0
- bladeworks/examples/single_clip.fcpxmld/Info.fcpxml +9 -0
- bladeworks/examples/single_clip.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/spine_and_lane.fcpxmld/Info.fcpxml +13 -0
- bladeworks/examples/spine_and_lane.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/examples/spine_and_lane.fcpxmld/Media/b.mp4 +0 -0
- bladeworks/examples/transform.fcpxmld/Info.fcpxml +11 -0
- bladeworks/examples/transform.fcpxmld/Media/a.mp4 +0 -0
- bladeworks/executor.py +293 -0
- bladeworks/preview/__init__.py +11 -0
- bladeworks/preview/application.py +84 -0
- bladeworks/preview/audio.py +104 -0
- bladeworks/preview/capabilities.py +457 -0
- bladeworks/preview/contracts.py +157 -0
- bladeworks/preview/events.py +55 -0
- bladeworks/preview/export.py +57 -0
- bladeworks/preview/history.py +197 -0
- bladeworks/preview/main.py +19 -0
- bladeworks/preview/media.py +23 -0
- bladeworks/preview/media_library.py +381 -0
- bladeworks/preview/media_reporting.py +55 -0
- bladeworks/preview/media_routes.py +155 -0
- bladeworks/preview/media_visuals.py +225 -0
- bladeworks/preview/producer.py +211 -0
- bladeworks/preview/provider.py +74 -0
- bladeworks/preview/rawframe.py +218 -0
- bladeworks/preview/render_jobs.py +311 -0
- bladeworks/preview/routes.py +375 -0
- bladeworks/preview/runner.py +349 -0
- bladeworks/preview/security.py +61 -0
- bladeworks/preview/service.py +234 -0
- bladeworks/preview/session.py +457 -0
- bladeworks/preview/source.py +603 -0
- bladeworks/preview/source_routes.py +151 -0
- bladeworks/preview/studio.py +77 -0
- bladeworks/preview/webrtc.py +295 -0
- bladeworks/spatial_luts/manifest.json +17 -0
- bladeworks/spatial_luts/rec2020_hlg_to_rec709_sdr_v1.cube +4917 -0
- bladeworks/spatial_luts/rec2020_pq_to_rec709_sdr_v1.cube +4917 -0
- bladeworks/studio_static/__init__.py +1 -0
- bladeworks/studio_static/app.js +6519 -0
- bladeworks/studio_static/build-id.js +1 -0
- bladeworks/studio_static/capability-ui.js +68 -0
- bladeworks/studio_static/clip-state.js +335 -0
- bladeworks/studio_static/fcpxml.js +3313 -0
- bladeworks/studio_static/fixtures.js +393 -0
- bladeworks/studio_static/history.js +50 -0
- bladeworks/studio_static/index.html +24 -0
- bladeworks/studio_static/magnetic-timeline.js +1194 -0
- bladeworks/studio_static/mask-ui.js +189 -0
- bladeworks/studio_static/media-visuals.js +224 -0
- bladeworks/studio_static/mock-capabilities.js +409 -0
- bladeworks/studio_static/retime-ui.js +151 -0
- bladeworks/studio_static/runtime.js +1652 -0
- bladeworks/studio_static/scope-ui.js +150 -0
- bladeworks/studio_static/styles.css +1558 -0
- bladeworks/studio_static/templates/catalog.js +77 -0
- bladeworks/studio_static/templates/icons.js +84 -0
- bladeworks/studio_static/templates/inspector.js +553 -0
- bladeworks/studio_static/templates/library.js +61 -0
- bladeworks/studio_static/templates/media.js +185 -0
- bladeworks/studio_static/templates/shared.js +43 -0
- bladeworks/studio_static/templates/shell.js +94 -0
- bladeworks/studio_static/templates/timeline.js +377 -0
- bladeworks/studio_static/templates/topbar.js +83 -0
- bladeworks/studio_static/templates/types.js +7 -0
- bladeworks/studio_static/templates/viewer.js +174 -0
- bladeworks/studio_static/templates.js +31 -0
- bladeworks/studio_static/types.js +18 -0
- bladeworks/studio_static/ui.js +97 -0
- bladeworks/studio_static/viewer-geometry.js +55 -0
- bladeworks/tensor/__init__.py +131 -0
- bladeworks/tensor/audio_delivery.py +280 -0
- bladeworks/tensor/audio_pyav.py +360 -0
- bladeworks/tensor/blend.py +217 -0
- bladeworks/tensor/color.py +53 -0
- bladeworks/tensor/composite.py +33 -0
- bladeworks/tensor/decode.py +1309 -0
- bladeworks/tensor/decode_policy.py +363 -0
- bladeworks/tensor/effects.py +322 -0
- bladeworks/tensor/encode.py +614 -0
- bladeworks/tensor/errors.py +32 -0
- bladeworks/tensor/expr.py +775 -0
- bladeworks/tensor/fx_basic.py +946 -0
- bladeworks/tensor/fx_branched.py +640 -0
- bladeworks/tensor/fx_cohort.py +404 -0
- bladeworks/tensor/fx_color.py +851 -0
- bladeworks/tensor/fx_keyer.py +140 -0
- bladeworks/tensor/fx_mask.py +352 -0
- bladeworks/tensor/fx_warp.py +337 -0
- bladeworks/tensor/hdr.py +122 -0
- bladeworks/tensor/pipeline.py +401 -0
- bladeworks/tensor/plan.py +2231 -0
- bladeworks/tensor/renderer.py +1199 -0
- bladeworks/tensor/requirements.txt +28 -0
- bladeworks/tensor/resolution.py +246 -0
- bladeworks/tensor/sampler.py +598 -0
- bladeworks/tensor/support.py +169 -0
- bladeworks/tensor/swscale_fixedpoint.py +181 -0
- bladeworks/tensor/tr_equirect.py +420 -0
- bladeworks/tensor/tr_handlers.py +443 -0
- bladeworks/tensor/tr_phase5.py +697 -0
- bladeworks/tensor/transitions.py +424 -0
- bladeworks/transitions/__init__.py +10 -0
- bladeworks/transitions/carryovers.py +299 -0
- bladeworks/transitions/cohort.py +981 -0
- bladeworks/transitions/contract.py +490 -0
- bladeworks/transitions/equirectangular.py +915 -0
- bladeworks/transitions/light_deco.py +141 -0
- bladeworks/transitions/panel_motion.py +495 -0
- bladeworks/transitions/stock.py +270 -0
- bladeworks-0.1.0.dist-info/METADATA +66 -0
- bladeworks-0.1.0.dist-info/RECORD +178 -0
- bladeworks-0.1.0.dist-info/WHEEL +5 -0
- bladeworks-0.1.0.dist-info/entry_points.txt +2 -0
- bladeworks-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Calibrated parameter mappings for the four non-flat carryover transitions.
|
|
2
|
+
|
|
3
|
+
Architecture map
|
|
4
|
+
================
|
|
5
|
+
|
|
6
|
+
genuine Final Cut parameter key/value
|
|
7
|
+
-> capability-registry type/range validation
|
|
8
|
+
-> this module's renderer-owned semantic mapping
|
|
9
|
+
-> radial-zoom strengths/centers or a fixed Black Hole expression
|
|
10
|
+
|
|
11
|
+
This module deliberately knows nothing about XML parsing or FFmpeg graph
|
|
12
|
+
assembly. It converts already validated values into a small immutable plan.
|
|
13
|
+
``transitions.stock`` attaches that plan to the generic stock-transition plan,
|
|
14
|
+
and ``ffmpeg`` performs the final project-size center conversion.
|
|
15
|
+
|
|
16
|
+
Product invariants
|
|
17
|
+
------------------
|
|
18
|
+
|
|
19
|
+
* Final Cut text never becomes an FFmpeg expression fragment.
|
|
20
|
+
* Zoom and Cross Zoom preserve their previously calibrated defaults exactly.
|
|
21
|
+
* A Motion point is converted to a crop center only after project dimensions
|
|
22
|
+
are known, and an off-canvas result is rejected rather than clamped.
|
|
23
|
+
* Black Hole's hidden structural fields are accepted only at their proved
|
|
24
|
+
values; its pixel-inert visible nondefaults are rejected explicitly.
|
|
25
|
+
|
|
26
|
+
Why this exists
|
|
27
|
+
---------------
|
|
28
|
+
|
|
29
|
+
The carryovers do not share the flat-transition geometry and are not 360°
|
|
30
|
+
panorama operations. Keeping their calibration here prevents ``stock.py``
|
|
31
|
+
from turning into another parameter-specific conditional ladder.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import math
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from typing import Any, Literal, Mapping
|
|
39
|
+
|
|
40
|
+
from ..core.errors import FCPXMLCompileError
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Genuine Final Cut 12.3 roundtrip keys. Keeping these beside the mappings
|
|
44
|
+
# makes it obvious which serialized contract each formula consumes.
|
|
45
|
+
ZOOM_AMOUNT_KEY = "9999/988471386/3/999166426/1"
|
|
46
|
+
ZOOM_CENTER_KEY = "9999/988471386/3/999166426/2"
|
|
47
|
+
|
|
48
|
+
CROSS_ZOOM_AMOUNT_KEY = "3"
|
|
49
|
+
CROSS_ZOOM_START_POINT_KEY = "1"
|
|
50
|
+
CROSS_ZOOM_END_POINT_KEY = "2"
|
|
51
|
+
|
|
52
|
+
BLACK_HOLE_DURATION_KEY = "9999/10059/10065/3/989648928/1"
|
|
53
|
+
BLACK_HOLE_TRAIL_OPACITY_KEY = "9999/10059/10065/3/989648928/10001"
|
|
54
|
+
BLACK_HOLE_FLIP_KEY = "9999/10059/10065/3/989648928/10002"
|
|
55
|
+
BLACK_HOLE_INPUT_POINTS_KEY = "9999/10059/10065/3/989648928/10003"
|
|
56
|
+
BLACK_HOLE_ECHOES_KEY = "9999/10059/10065/3/989648928/2"
|
|
57
|
+
BLACK_HOLE_DECAY_KEY = "9999/10059/10065/3/989648928/3"
|
|
58
|
+
BLACK_HOLE_TRAIL_ON_KEY = "9999/10059/10065/3/989648928/4"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
CenterSpace = Literal["normalized", "motion-project-height"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class RadialZoomCenter:
|
|
66
|
+
"""One bounded Final Cut center plus its explicit coordinate space."""
|
|
67
|
+
|
|
68
|
+
value: tuple[float, float]
|
|
69
|
+
space: CenterSpace
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class CarryoverRadialPlan:
|
|
74
|
+
"""The calibrated radial parameters consumed by the stock FFmpeg graph."""
|
|
75
|
+
|
|
76
|
+
strength: float
|
|
77
|
+
spread: float
|
|
78
|
+
outgoing_center: RadialZoomCenter
|
|
79
|
+
incoming_center: RadialZoomCenter
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_radial_plan(
|
|
83
|
+
implementation_id: str,
|
|
84
|
+
values: Mapping[str, Any],
|
|
85
|
+
) -> CarryoverRadialPlan:
|
|
86
|
+
"""Map Zoom-family controls while preserving both old default renders.
|
|
87
|
+
|
|
88
|
+
Main callers:
|
|
89
|
+
- ``transitions.stock.build_stock_transition_plan`` for Zoom and Cross
|
|
90
|
+
Zoom after the generic registry contract has validated every value.
|
|
91
|
+
|
|
92
|
+
Final Cut Zoom publishes Amount with a 100 default. Cross Zoom publishes
|
|
93
|
+
a different FxPlug Amount whose default is 50, plus normalized start/end
|
|
94
|
+
points. The two amount normalizations below therefore produce the exact
|
|
95
|
+
historical ``strength`` and ``spread`` values at their respective defaults.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
if implementation_id == "zoom_blur_default":
|
|
99
|
+
amount = _scalar(values, ZOOM_AMOUNT_KEY, default=100.0)
|
|
100
|
+
center = RadialZoomCenter(
|
|
101
|
+
_vector2(values, ZOOM_CENTER_KEY, default=(0.5, 0.5)),
|
|
102
|
+
"motion-project-height",
|
|
103
|
+
)
|
|
104
|
+
factor = amount / 100.0
|
|
105
|
+
return CarryoverRadialPlan(
|
|
106
|
+
strength=1.70 * factor,
|
|
107
|
+
spread=0.52 * factor,
|
|
108
|
+
outgoing_center=center,
|
|
109
|
+
incoming_center=center,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if implementation_id == "cross_zoom_default":
|
|
113
|
+
amount = _scalar(values, CROSS_ZOOM_AMOUNT_KEY, default=50.0)
|
|
114
|
+
factor = amount / 50.0
|
|
115
|
+
return CarryoverRadialPlan(
|
|
116
|
+
strength=2.00 * factor,
|
|
117
|
+
spread=0.62 * factor,
|
|
118
|
+
outgoing_center=RadialZoomCenter(
|
|
119
|
+
_vector2(values, CROSS_ZOOM_START_POINT_KEY, default=(0.75, 0.5)),
|
|
120
|
+
"normalized",
|
|
121
|
+
),
|
|
122
|
+
incoming_center=RadialZoomCenter(
|
|
123
|
+
_vector2(values, CROSS_ZOOM_END_POINT_KEY, default=(0.25, 0.5)),
|
|
124
|
+
"normalized",
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
raise FCPXMLCompileError(
|
|
129
|
+
f"carryover radial mapper does not own implementation {implementation_id!r}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def resolve_radial_center(
|
|
134
|
+
center: RadialZoomCenter,
|
|
135
|
+
*,
|
|
136
|
+
width: int,
|
|
137
|
+
height: int,
|
|
138
|
+
) -> tuple[float, float]:
|
|
139
|
+
"""Convert a proved Final Cut center to a normalized crop anchor.
|
|
140
|
+
|
|
141
|
+
Main callers:
|
|
142
|
+
- The CPU radial-zoom graph builder, after the output dimensions are known.
|
|
143
|
+
|
|
144
|
+
Why this exists:
|
|
145
|
+
Cross Zoom serializes ordinary normalized points. Motion's Zoom template
|
|
146
|
+
instead serializes offsets in a square coordinate system whose unit scale
|
|
147
|
+
is the project height and whose neutral center is ``0.5 0.5``. A genuine
|
|
148
|
+
540x960 UI roundtrip (75 px, -120 px -> ``133.833 -119.5``) proves this
|
|
149
|
+
conversion. Off-canvas anchors need a different sampling primitive, so
|
|
150
|
+
this stock crop implementation rejects them explicitly.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
if width <= 0 or height <= 0:
|
|
154
|
+
raise FCPXMLCompileError("radial Zoom needs positive project dimensions")
|
|
155
|
+
x, y = center.value
|
|
156
|
+
if center.space == "normalized":
|
|
157
|
+
resolved = (x, y)
|
|
158
|
+
elif center.space == "motion-project-height":
|
|
159
|
+
resolved = (
|
|
160
|
+
0.5 + (x - 0.5) / float(height),
|
|
161
|
+
0.5 + (y - 0.5) / float(height),
|
|
162
|
+
)
|
|
163
|
+
else: # pragma: no cover - the Literal plus frozen constructors own this.
|
|
164
|
+
raise FCPXMLCompileError(f"unknown radial Zoom center space {center.space!r}")
|
|
165
|
+
if any(not math.isfinite(component) for component in resolved):
|
|
166
|
+
raise FCPXMLCompileError("radial Zoom center must be finite")
|
|
167
|
+
if any(component < 0.0 or component > 1.0 for component in resolved):
|
|
168
|
+
raise FCPXMLCompileError(
|
|
169
|
+
"radial Zoom center resolves outside the project canvas; "
|
|
170
|
+
"the stock CPU crop supports normalized centers in [0, 1]"
|
|
171
|
+
)
|
|
172
|
+
return resolved
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _plane_sample(source: str, x: str, y: str) -> str:
|
|
176
|
+
"""Return one color-preserving xfade sample from ``a`` or ``b``."""
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
f"if(eq(PLANE,0),{source}0({x},{y}),"
|
|
180
|
+
f"if(eq(PLANE,1),{source}1({x},{y}),"
|
|
181
|
+
f"if(eq(PLANE,2),{source}2({x},{y}),"
|
|
182
|
+
f"{source}3({x},{y}))))"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# The default expression is byte-for-byte the previously reviewed Black Hole
|
|
187
|
+
# implementation. ``build_black_hole_expression`` returns it unchanged while
|
|
188
|
+
# Trails is disabled, preserving existing default semantics and snapshots.
|
|
189
|
+
_ELAPSED = "(1-P)"
|
|
190
|
+
_REMAINING = f"pow(max(0,1-clip(({_ELAPSED}-0.18)/0.55,0,1)),0.6)"
|
|
191
|
+
_VERTICAL_SCALE = f"(1-clip(({_ELAPSED}-0.48)/0.26,0,1))"
|
|
192
|
+
_HALF_HEIGHT = f"(H/2*{_VERTICAL_SCALE})"
|
|
193
|
+
_Y = f"clip((Y-H/2)/{_VERTICAL_SCALE}+H/2,0,H-1)"
|
|
194
|
+
_VERTICAL_POSITION = f"abs((Y-H/2)/max(1,{_HALF_HEIGHT}))"
|
|
195
|
+
_PROFILE = f"(0.04+0.96*pow({_VERTICAL_POSITION},0.55))"
|
|
196
|
+
_PINCH_PROGRESS = "clip(((1-P)-0.05)/0.46,0,1)"
|
|
197
|
+
_ACTIVE_PROFILE = f"(1-{_PINCH_PROGRESS}*(1-{_PROFILE}))"
|
|
198
|
+
_SCALE = f"({_REMAINING}*{_ACTIVE_PROFILE})"
|
|
199
|
+
_HALF_WIDTH = f"(W/2*{_SCALE})"
|
|
200
|
+
_X = f"clip((X-W/2)/max(0.001,{_SCALE})+W/2,0,W-1)"
|
|
201
|
+
_SAMPLE = _plane_sample("a", _X, _Y)
|
|
202
|
+
_GHOST_SCALE = f"(min(1,{_SCALE}+0.16*{_REMAINING}))"
|
|
203
|
+
_GHOST_WIDTH = f"(W/2*{_GHOST_SCALE})"
|
|
204
|
+
_GHOST_VERTICAL_SCALE = f"min(1,1.08*{_VERTICAL_SCALE})"
|
|
205
|
+
_GHOST_HALF_HEIGHT = f"(H/2*{_GHOST_VERTICAL_SCALE})"
|
|
206
|
+
_GHOST_Y = f"clip((Y-H/2)/max(0.001,{_GHOST_VERTICAL_SCALE})+H/2,0,H-1)"
|
|
207
|
+
_GHOST_X = f"clip((X-W/2)/max(0.001,{_GHOST_SCALE})+W/2,0,W-1)"
|
|
208
|
+
_GHOST_SAMPLE = _plane_sample("a", _GHOST_X, _GHOST_Y)
|
|
209
|
+
_OPACITY_RAW = "clip((P-0.2)/0.4,0,1)"
|
|
210
|
+
_OPACITY = f"({_OPACITY_RAW}*{_OPACITY_RAW}*(3-2*{_OPACITY_RAW}))"
|
|
211
|
+
_ACTIVE_SAMPLE = f"({_OPACITY}*{_SAMPLE}+(1-{_OPACITY})*B)"
|
|
212
|
+
_GHOST_OPACITY = f"(0.16*{_OPACITY})"
|
|
213
|
+
_ACTIVE_GHOST = f"({_GHOST_OPACITY}*{_GHOST_SAMPLE}+(1-{_GHOST_OPACITY})*B)"
|
|
214
|
+
BLACK_HOLE_DEFAULT_EXPRESSION = (
|
|
215
|
+
"if(gte(P,1),A,if(lte(P,0),B,"
|
|
216
|
+
f"if(lte(abs(Y-H/2),{_HALF_HEIGHT})*"
|
|
217
|
+
f"lte(abs(X-W/2),{_HALF_WIDTH}),{_ACTIVE_SAMPLE},"
|
|
218
|
+
f"if(lte(abs(Y-H/2),{_GHOST_HALF_HEIGHT})*"
|
|
219
|
+
f"lte(abs(X-W/2),{_GHOST_WIDTH}),{_ACTIVE_GHOST},B))))"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def build_black_hole_expression(values: Mapping[str, Any]) -> str:
|
|
224
|
+
"""Preserve Black Hole's default and reject its inert serialized controls.
|
|
225
|
+
|
|
226
|
+
Main callers:
|
|
227
|
+
- ``transitions.stock.build_stock_transition_plan``.
|
|
228
|
+
|
|
229
|
+
Final Cut serializes the Trails object and preserves every authored value,
|
|
230
|
+
but genuine renders for all fourteen nondefault settings are byte-identical
|
|
231
|
+
to its default movie. Rendering a portable-only response would invent a
|
|
232
|
+
semantic contract. The registry still types all seven published keys so
|
|
233
|
+
malformed or unknown values fail normally; this boundary accepts only the
|
|
234
|
+
exact visible defaults plus the three fixed structural values.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
duration = _scalar(values, BLACK_HOLE_DURATION_KEY, default=0.0)
|
|
238
|
+
_required_boolean(values, BLACK_HOLE_FLIP_KEY, expected=False)
|
|
239
|
+
_required_boolean(values, BLACK_HOLE_INPUT_POINTS_KEY, expected=True)
|
|
240
|
+
_required_scalar(values, BLACK_HOLE_DECAY_KEY, expected=1.0)
|
|
241
|
+
echoes_raw = _scalar(values, BLACK_HOLE_ECHOES_KEY, default=4.0)
|
|
242
|
+
if not echoes_raw.is_integer():
|
|
243
|
+
raise FCPXMLCompileError("Black Hole Echoes must be an integer")
|
|
244
|
+
echoes = int(echoes_raw)
|
|
245
|
+
opacity = _scalar(values, BLACK_HOLE_TRAIL_OPACITY_KEY, default=1.0)
|
|
246
|
+
trail_on_raw = _scalar(values, BLACK_HOLE_TRAIL_ON_KEY, default=0.0)
|
|
247
|
+
if not trail_on_raw.is_integer() or int(trail_on_raw) not in {0, 1}:
|
|
248
|
+
raise FCPXMLCompileError("Black Hole Trail On must be Light (0) or Dark (1)")
|
|
249
|
+
trail_on = int(trail_on_raw)
|
|
250
|
+
if (duration, opacity, echoes, trail_on) != (0.0, 1.0, 4, 0):
|
|
251
|
+
raise FCPXMLCompileError(
|
|
252
|
+
"Black Hole Trails controls preserve XML but produce no Final Cut "
|
|
253
|
+
"pixel response; nondefault values are explicitly unsupported"
|
|
254
|
+
)
|
|
255
|
+
return BLACK_HOLE_DEFAULT_EXPRESSION
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _scalar(values: Mapping[str, Any], key: str, *, default: float) -> float:
|
|
259
|
+
value = values.get(key, default)
|
|
260
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
261
|
+
raise FCPXMLCompileError(f"validated carryover parameter {key!r} must be scalar")
|
|
262
|
+
number = float(value)
|
|
263
|
+
if not math.isfinite(number):
|
|
264
|
+
raise FCPXMLCompileError(f"validated carryover parameter {key!r} must be finite")
|
|
265
|
+
return number
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _required_scalar(values: Mapping[str, Any], key: str, *, expected: float) -> None:
|
|
269
|
+
value = _scalar(values, key, default=expected)
|
|
270
|
+
if value != expected:
|
|
271
|
+
raise FCPXMLCompileError(
|
|
272
|
+
f"Black Hole structural parameter {key!r} must remain {expected:g}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _required_boolean(values: Mapping[str, Any], key: str, *, expected: bool) -> None:
|
|
277
|
+
"""Validate a registry-typed hidden boolean at its one structural value."""
|
|
278
|
+
|
|
279
|
+
value = values.get(key, expected)
|
|
280
|
+
if not isinstance(value, bool) or value is not expected:
|
|
281
|
+
raise FCPXMLCompileError(
|
|
282
|
+
f"Black Hole structural parameter {key!r} must remain "
|
|
283
|
+
f"{'true' if expected else 'false'}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _vector2(
|
|
288
|
+
values: Mapping[str, Any],
|
|
289
|
+
key: str,
|
|
290
|
+
*,
|
|
291
|
+
default: tuple[float, float],
|
|
292
|
+
) -> tuple[float, float]:
|
|
293
|
+
value = values.get(key, default)
|
|
294
|
+
if not isinstance(value, tuple) or len(value) != 2:
|
|
295
|
+
raise FCPXMLCompileError(f"validated carryover parameter {key!r} must be vec2")
|
|
296
|
+
resolved = (float(value[0]), float(value[1]))
|
|
297
|
+
if any(not math.isfinite(component) for component in resolved):
|
|
298
|
+
raise FCPXMLCompileError(f"validated carryover parameter {key!r} must be finite")
|
|
299
|
+
return resolved
|