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,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inspector: Video / Color / Audio / Info plus effect stack and masks.
|
|
3
|
+
*
|
|
4
|
+
* Architecture map:
|
|
5
|
+
* selected clip or transition + InspectorTab
|
|
6
|
+
* -> inspectorTemplate HTML
|
|
7
|
+
* -> data-parameter-path and data-action controls
|
|
8
|
+
*
|
|
9
|
+
* Main callers: BladeworksEditorApp.renderInspector
|
|
10
|
+
*/
|
|
11
|
+
import { itemHasKeyframeAt } from "../magnetic-timeline.js";
|
|
12
|
+
import { supportDetail, supportLabel } from "../capability-ui.js";
|
|
13
|
+
import { escapeHtml, formatTimecode } from "../ui.js";
|
|
14
|
+
import { maskKeyframeKey, maskUiValues } from "../mask-ui.js";
|
|
15
|
+
import { icon } from "./icons.js";
|
|
16
|
+
import { capitalize, waveBars } from "./shared.js";
|
|
17
|
+
export function inspectorTemplate(state) {
|
|
18
|
+
const item = state.selectedItem;
|
|
19
|
+
const transition = state.selectedTransition;
|
|
20
|
+
const inspectorTabs = [
|
|
21
|
+
["video", "film"],
|
|
22
|
+
["color", "color"],
|
|
23
|
+
["audio", "speaker"],
|
|
24
|
+
["info", "info"],
|
|
25
|
+
];
|
|
26
|
+
return `
|
|
27
|
+
<div class="inspector-tabs">
|
|
28
|
+
${inspectorTabs.map(([tab, ico]) => `<button class="inspector-tab ${state.inspectorTab === tab ? "active" : ""}" data-action="inspector-tab" data-tab="${tab}" title="${capitalize(tab)} Inspector">${icon(ico)}</button>`).join("")}
|
|
29
|
+
<span class="inspector-clip-name">${escapeHtml(state.selectionCount > 1 ? `${state.selectionCount} Clips Selected` : transition?.name ?? item?.name ?? "Nothing Selected")}</span>
|
|
30
|
+
<span class="inspector-source-time" id="inspector-source-time">${item ? formatTimecode(item.sourceStart + Math.max(0, state.currentTime - item.timelineStart), state.fps) : ""}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="inspector-scroll">
|
|
33
|
+
${transition ? transitionInspector(transition, state) : item ? inspectorBody(item, state) : `<div class="empty-inspector">${icon("inspector")}<strong>Select a timeline clip</strong><span>Video, audio, and metadata controls appear here.</span></div>`}
|
|
34
|
+
</div>
|
|
35
|
+
${state.connectionMode === "mock" ? '<div class="inspector-footer"><button data-action="save-effects-preset">Save Effects Preset</button></div>' : ""}
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
function inspectorBody(item, state) {
|
|
39
|
+
if (state.inspectorTab === "audio")
|
|
40
|
+
return audioInspector(item, state);
|
|
41
|
+
if (state.inspectorTab === "color")
|
|
42
|
+
return colorInspector(item, state);
|
|
43
|
+
if (state.inspectorTab === "info")
|
|
44
|
+
return infoInspector(item, state.fps);
|
|
45
|
+
return videoInspector(item, state);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The applied-effect stack for the selected clip. Effects render top-to-bottom
|
|
49
|
+
* in array order (matching FCP). Each row exposes enable, reorder, an intensity
|
|
50
|
+
* amount, and remove; the "+" opens the Effects browser. The pixel result is a
|
|
51
|
+
* labeled mock, but placement and ordering are real reducer state.
|
|
52
|
+
*/
|
|
53
|
+
function effectsSection(item, state, visibleEffects = item.effects) {
|
|
54
|
+
const isFilteredProjection = visibleEffects !== item.effects;
|
|
55
|
+
const rows = isFilteredProjection
|
|
56
|
+
? visibleEffects.map((effect, index) => effectRow(effect, index, visibleEffects.length, state, false)).join("")
|
|
57
|
+
: item.effectStack.map((entry, index) => entry.kind === "effect"
|
|
58
|
+
? effectRow(entry.effect, index, item.effectStack.length, state, state.connectionMode !== "localhost")
|
|
59
|
+
: state.connectionMode === "localhost"
|
|
60
|
+
? `<div class="effect-row disabled"><div class="effect-meta"><strong>Masked Effect</strong><small>Preserved, not editable in Studio</small></div></div>`
|
|
61
|
+
: maskedEffectRow(entry.maskedEffect, index, item.effectStack.length, state)).join("");
|
|
62
|
+
const body = `
|
|
63
|
+
${rows || '<p class="effect-empty">No effects. Add one from the Effects browser.</p>'}
|
|
64
|
+
<button class="effect-add" data-action="open-effects-browser">${icon("plus")}<span>Add Effect</span></button>
|
|
65
|
+
`;
|
|
66
|
+
const enabled = item.effectStack.every((entry) => entry.kind === "effect" ? entry.effect.enabled : entry.maskedEffect.enabled);
|
|
67
|
+
return section(`Effects${rows ? ` (${isFilteredProjection ? visibleEffects.length : item.effectStack.length})` : ""}`, "effects-stack", enabled, "", body, Boolean(rows), "toggle-effects-section");
|
|
68
|
+
}
|
|
69
|
+
function effectRow(effect, index, total, state, allowMask) {
|
|
70
|
+
const capability = state.capabilities.effects.find((candidate) => candidate.handler === effect.handler || candidate.resource.uid === effect.resourceUid || candidate.name === effect.name);
|
|
71
|
+
const parameters = capability?.parameters ?? [];
|
|
72
|
+
const maskKinds = state.capabilities.mechanics.find((mechanic) => mechanic.id === "masks")?.sourceKinds ?? [];
|
|
73
|
+
return `<div class="effect-row ${effect.enabled ? "" : "disabled"}" data-effect-id="${escapeHtml(effect.id)}">
|
|
74
|
+
<label class="effect-toggle"><input type="checkbox" data-action="toggle-effect" data-effect-id="${escapeHtml(effect.id)}" ${effect.enabled ? "checked" : ""}></label>
|
|
75
|
+
<div class="effect-meta"><strong>${escapeHtml(effect.name)}</strong><small>${escapeHtml(effect.category)}</small></div>
|
|
76
|
+
<div class="effect-actions">
|
|
77
|
+
<button data-action="reorder-effect" data-effect-id="${escapeHtml(effect.id)}" data-dir="-1" ${index === 0 ? "disabled" : ""} title="Move up">▲</button>
|
|
78
|
+
<button data-action="reorder-effect" data-effect-id="${escapeHtml(effect.id)}" data-dir="1" ${index === total - 1 ? "disabled" : ""} title="Move down">▼</button>
|
|
79
|
+
<button data-action="remove-effect" data-effect-id="${escapeHtml(effect.id)}" title="Remove effect">${icon("trash")}</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="capability-controls">
|
|
82
|
+
${parameters.length ? capabilityParameterRows("effect", effect.id, parameters, effect.parameters) : ""}
|
|
83
|
+
${state.connectionMode === "mock" ? capability?.notes.map((note) => `<p class="capability-note">${escapeHtml(note)}</p>`).join("") ?? "" : ""}
|
|
84
|
+
${allowMask && maskKinds.length ? `<div class="mask-add-row"><span>Add Effect Mask</span>${maskKinds.map((kind) => `<button data-action="wrap-effect-mask" data-effect-id="${escapeHtml(effect.id)}" data-mask-kind="${kind.id}">${escapeHtml(kind.id[0].toUpperCase() + kind.id.slice(1))}</button>`).join("")}</div>` : ""}
|
|
85
|
+
</div>
|
|
86
|
+
</div>`;
|
|
87
|
+
}
|
|
88
|
+
function maskedEffectRow(group, index, total, state) {
|
|
89
|
+
const maskMechanic = state.capabilities.mechanics.find((mechanic) => mechanic.id === "masks");
|
|
90
|
+
const sourceKinds = maskMechanic?.sourceKinds ?? [];
|
|
91
|
+
const blendModes = (maskMechanic?.blendModes ?? []);
|
|
92
|
+
const outside = group.filters[1];
|
|
93
|
+
return `<div class="masked-effect-group ${group.enabled ? "" : "disabled"}" data-mask-group-id="${escapeHtml(group.id)}">
|
|
94
|
+
<header class="masked-effect-header">
|
|
95
|
+
<label><input type="checkbox" data-action="toggle-mask-group" data-group-id="${escapeHtml(group.id)}" ${group.enabled ? "checked" : ""}><strong>Masked Effect</strong></label>
|
|
96
|
+
${maskMechanic?.invert ? `<label><input type="checkbox" data-action="toggle-mask-invert" data-group-id="${escapeHtml(group.id)}" ${group.inverted ? "checked" : ""}>Invert</label>` : ""}
|
|
97
|
+
<span class="support-badge support-${maskMechanic?.support ?? "approximate"}">${supportLabel(maskMechanic?.support ?? "approximate")}</span>
|
|
98
|
+
<button data-action="reorder-mask-group" data-group-id="${escapeHtml(group.id)}" data-dir="-1" ${index === 0 ? "disabled" : ""}>▲</button>
|
|
99
|
+
<button data-action="reorder-mask-group" data-group-id="${escapeHtml(group.id)}" data-dir="1" ${index === total - 1 ? "disabled" : ""}>▼</button>
|
|
100
|
+
<button data-action="remove-mask-group" data-group-id="${escapeHtml(group.id)}">${icon("trash")}</button>
|
|
101
|
+
</header>
|
|
102
|
+
<div class="masked-filter-stack">
|
|
103
|
+
${maskedFilterRow(group.id, group.filters[0], 0, "Inside", state)}
|
|
104
|
+
${outside ? maskedFilterRow(group.id, outside, 1, "Outside", state) : `<label class="outside-effect-picker"><span>Outside</span><select data-action="set-mask-outside-effect" data-group-id="${escapeHtml(group.id)}"><option value="">Add outside effect…</option>${state.capabilities.effects.filter((effect) => effect.authorable && effect.support !== "unsupported").map((effect) => `<option value="${escapeHtml(effect.id)}">${escapeHtml(effect.name)}</option>`).join("")}</select></label>`}
|
|
105
|
+
${outside ? `<div class="masked-filter-actions"><button data-action="swap-mask-filters" data-group-id="${escapeHtml(group.id)}">Swap Inside / Outside</button><button data-action="remove-mask-outside-effect" data-group-id="${escapeHtml(group.id)}">Remove Outside</button></div>` : ""}
|
|
106
|
+
</div>
|
|
107
|
+
<div class="mask-source-list">${group.masks.map((mask, maskIndex) => maskSourceRow(group.id, mask, maskIndex, group.masks.length, sourceKinds, blendModes, state)).join("")}</div>
|
|
108
|
+
<div class="mask-add-row"><span>Add Mask</span>${sourceKinds.map((kind) => `<button data-action="add-mask-source" data-group-id="${escapeHtml(group.id)}" data-mask-kind="${kind.id}">${escapeHtml(kind.id[0].toUpperCase() + kind.id.slice(1))}</button>`).join("")}</div>
|
|
109
|
+
<p class="capability-note">Tracked, Magnetic, Auto, opaque isolation, and ML masks are rejected rather than flattened.</p>
|
|
110
|
+
</div>`;
|
|
111
|
+
}
|
|
112
|
+
function maskedFilterRow(groupId, effect, filterIndex, label, state) {
|
|
113
|
+
const capability = state.capabilities.effects.find((candidate) => candidate.resource.uid === effect.resourceUid || candidate.handler === effect.handler);
|
|
114
|
+
return `<section class="masked-filter"><header><span>${label}</span><strong>${escapeHtml(effect.name)}</strong></header>${capability?.parameters.length
|
|
115
|
+
? capabilityParameterRows("masked-filter", `${groupId}:${filterIndex}`, capability.parameters, effect.parameters)
|
|
116
|
+
: `<p class="capability-note">${supportDetail(capability?.support ?? effect.support)}</p>`}</section>`;
|
|
117
|
+
}
|
|
118
|
+
function maskSourceRow(groupId, mask, index, total, sourceKinds, blendModes, state) {
|
|
119
|
+
const source = sourceKinds.find((candidate) => candidate.id === mask.kind);
|
|
120
|
+
const values = maskUiValues(mask);
|
|
121
|
+
return `<section class="mask-source" data-mask-id="${escapeHtml(mask.id)}">
|
|
122
|
+
<header>
|
|
123
|
+
<label><input type="checkbox" data-action="toggle-mask-source" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" ${mask.enabled ? "checked" : ""}><strong>${escapeHtml(mask.name)}</strong></label>
|
|
124
|
+
<select data-action="set-mask-blend" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}">${blendModes.map((mode) => `<option value="${mode}" ${mask.blendMode === mode ? "selected" : ""}>${capitalize(mode)}</option>`).join("")}</select>
|
|
125
|
+
<button data-action="reorder-mask-source" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-dir="-1" ${index === 0 ? "disabled" : ""}>▲</button>
|
|
126
|
+
<button data-action="reorder-mask-source" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-dir="1" ${index === total - 1 ? "disabled" : ""}>▼</button>
|
|
127
|
+
<button data-action="remove-mask-source" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}">${icon("trash")}</button>
|
|
128
|
+
</header>
|
|
129
|
+
<div class="mask-parameters">${source?.parameters.map((parameter) => maskParameterRow(groupId, mask, parameter, values[parameter.key], state)).join("") ?? ""}</div>
|
|
130
|
+
${source?.notes ? `<p class="capability-note">${escapeHtml(source.notes)}</p>` : ""}
|
|
131
|
+
</section>`;
|
|
132
|
+
}
|
|
133
|
+
function maskParameterRow(groupId, mask, parameter, value, state) {
|
|
134
|
+
const attributes = `data-action="set-mask-parameter" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-mask-key="${escapeHtml(parameter.key)}"`;
|
|
135
|
+
let control;
|
|
136
|
+
if (parameter.type === "point_list") {
|
|
137
|
+
const points = Array.isArray(value) ? value : [];
|
|
138
|
+
control = `<div class="mask-point-list">${points.map((point, pointIndex) => `<label><small>${pointIndex + 1}</small><input type="number" step="0.1" value="${point.x}" data-point-index="${pointIndex}" data-component="x" ${attributes}><input type="number" step="0.1" value="${point.y}" data-point-index="${pointIndex}" data-component="y" ${attributes}><button data-action="remove-mask-point" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-point-index="${pointIndex}" ${points.length <= (parameter.minimumItems ?? 3) ? "disabled" : ""}>−</button></label>`).join("")}<button class="mask-point-add" data-action="add-mask-point" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" ${points.length >= (parameter.maximumItems ?? 64) ? "disabled" : ""}>+ Add Point</button></div>`;
|
|
139
|
+
}
|
|
140
|
+
else if (parameter.type === "point") {
|
|
141
|
+
const point = typeof value === "object" && value !== null && "x" in value ? value : { x: 0, y: 0 };
|
|
142
|
+
control = `<div class="capability-compound"><label><small>X</small><input type="number" min="${parameter.min ?? ""}" max="${parameter.max ?? ""}" step="0.1" value="${point.x}" data-component="x" ${attributes}></label><label><small>Y</small><input type="number" min="${parameter.min ?? ""}" max="${parameter.max ?? ""}" step="0.1" value="${point.y}" data-component="y" ${attributes}></label></div>`;
|
|
143
|
+
}
|
|
144
|
+
else if (parameter.type === "color") {
|
|
145
|
+
control = `<input type="color" value="${colorHex(value)}" ${attributes}>`;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
control = `<input type="number" ${parameter.min !== undefined ? `min="${parameter.min}"` : ""} ${parameter.max !== undefined ? `max="${parameter.max}"` : ""} step="0.01" value="${Number(value ?? parameter.default ?? 0)}" ${attributes}>`;
|
|
149
|
+
}
|
|
150
|
+
const key = maskKeyframeKey(mask, parameter.key);
|
|
151
|
+
const frames = mask.parameterKeyframes[key] ?? [];
|
|
152
|
+
const local = state.currentTime - (state.selectedItem?.timelineStart ?? 0);
|
|
153
|
+
const active = frames.some((frame) => Math.abs(frame.time.seconds - local) < 1 / Math.max(1, state.fps));
|
|
154
|
+
return `<div class="mask-parameter-row"><label><span>${escapeHtml(parameter.name)}</span><small>${escapeHtml(parameter.units)}</small></label>${control}${parameter.animatable ? `<div class="mask-keyframe-controls"><button data-action="previous-mask-keyframe" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-mask-key="${escapeHtml(parameter.key)}" ${frames.length ? "" : "disabled"}>‹</button><button class="keyframe-diamond ${active ? "active" : ""}" data-action="toggle-mask-keyframe" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-mask-key="${escapeHtml(parameter.key)}">${icon("diamond")}</button><button data-action="next-mask-keyframe" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-mask-key="${escapeHtml(parameter.key)}" ${frames.length ? "" : "disabled"}>›</button><button data-action="clear-mask-keyframes" data-group-id="${escapeHtml(groupId)}" data-mask-id="${escapeHtml(mask.id)}" data-mask-key="${escapeHtml(parameter.key)}" ${frames.length ? "" : "disabled"}>×</button></div>` : '<span class="mask-static">Static</span>'}</div>`;
|
|
155
|
+
}
|
|
156
|
+
function videoInspector(item, state) {
|
|
157
|
+
const fullState = state;
|
|
158
|
+
const localhost = fullState.connectionMode === "localhost";
|
|
159
|
+
return `
|
|
160
|
+
${containerInspector(item, fullState)}
|
|
161
|
+
${generatedClipInspector(item)}
|
|
162
|
+
${effectsSection(item, fullState)}
|
|
163
|
+
${section("Compositing", "compositing", item.video.blendEnabled, "video.blendEnabled", `
|
|
164
|
+
${selectRow("Blend Mode", "video.blendMode", item.video.blendMode, fullState.capabilities.blendModes
|
|
165
|
+
.filter((mode) => mode.authorable && mode.support !== "unsupported")
|
|
166
|
+
.map((mode) => [mode.fcpxmlValue, mode.name]))}
|
|
167
|
+
${sliderRow("Opacity", "transform.opacity", item.transform.opacity, 0, 1, .01, "%", state, 100)}
|
|
168
|
+
`)}
|
|
169
|
+
${section("Transform", "transform", item.transform.enabled, "transform.enabled", `
|
|
170
|
+
${xyRow("Position", "transform.x", item.transform.x, "transform.y", item.transform.y, "%", state, "transform.position")}
|
|
171
|
+
${knobRow("Rotation", "transform.rotation", item.transform.rotation, -180, 180, 0.1, "°", state)}
|
|
172
|
+
${sliderRow("Scale (All)", "transform.scale", item.transform.scale, .1, 4, .01, "%", state, 100, "transform.scale")}
|
|
173
|
+
${xyRow("Anchor", "transform.anchorX", item.transform.anchorX, "transform.anchorY", item.transform.anchorY, "%", state, "transform.anchor")}
|
|
174
|
+
`)}
|
|
175
|
+
${section("Crop", "crop", item.video.crop.enabled, "video.crop.enabled", `
|
|
176
|
+
${localhost && item.video.crop.type === "ken-burns"
|
|
177
|
+
? `<div class="parameter-row"><label>Type</label><strong>Ken Burns</strong><span class="row-spacer"></span></div>
|
|
178
|
+
<p class="capability-note">Ken Burns is preserved but not editable in the live preview.</p>`
|
|
179
|
+
: `${selectRow("Type", "video.crop.type", item.video.crop.type, localhost ? [["trim", "Trim"], ["crop", "Crop"]] : [["trim", "Trim"], ["crop", "Crop"], ["ken-burns", "Ken Burns"]])}
|
|
180
|
+
${item.video.crop.type === "ken-burns" ? `
|
|
181
|
+
${segmentedRow("Edit", "video.crop.activeKenWindow", item.video.crop.activeKenWindow, [["start", "Start"], ["end", "End"]])}
|
|
182
|
+
${selectRow("Motion", "video.crop.easing", item.video.crop.easing, [["linear", "Linear (supported)"]])}
|
|
183
|
+
<button class="inline-action" data-action="swap-ken-burns">Swap Start and End</button>
|
|
184
|
+
` : `
|
|
185
|
+
${sliderRow("Left", "video.crop.left", item.video.crop.left, 0, 100, 1, "%", state, 1, null)}
|
|
186
|
+
${sliderRow("Right", "video.crop.right", item.video.crop.right, 0, 100, 1, "%", state, 1, null)}
|
|
187
|
+
${sliderRow("Top", "video.crop.top", item.video.crop.top, 0, 100, 1, "%", state, 1, null)}
|
|
188
|
+
${sliderRow("Bottom", "video.crop.bottom", item.video.crop.bottom, 0, 100, 1, "%", state, 1, null)}
|
|
189
|
+
`}
|
|
190
|
+
`}
|
|
191
|
+
`)}
|
|
192
|
+
${section("Distort", "distort", item.video.distort.enabled, "video.distort.enabled", `
|
|
193
|
+
${xyRow("Bottom Left", "video.distort.bottomLeftX", item.video.distort.bottomLeftX, "video.distort.bottomLeftY", item.video.distort.bottomLeftY, "%", state, "video.distort.bottomleft")}
|
|
194
|
+
${xyRow("Bottom Right", "video.distort.bottomRightX", item.video.distort.bottomRightX, "video.distort.bottomRightY", item.video.distort.bottomRightY, "%", state, "video.distort.bottomright")}
|
|
195
|
+
${xyRow("Top Right", "video.distort.topRightX", item.video.distort.topRightX, "video.distort.topRightY", item.video.distort.topRightY, "%", state, "video.distort.topright")}
|
|
196
|
+
${xyRow("Top Left", "video.distort.topLeftX", item.video.distort.topLeftX, "video.distort.topLeftY", item.video.distort.topLeftY, "%", state, "video.distort.topleft")}
|
|
197
|
+
`)}
|
|
198
|
+
${section("Spatial Conform", "spatial", item.video.spatialConform !== "none", "", selectRow("Type", "video.spatialConform", item.video.spatialConform, [["fit", "Fit"], ["fill", "Fill"], ["none", "None"]]), true, "toggle-spatial-conform")}
|
|
199
|
+
${retimeSection(item)}
|
|
200
|
+
${localhost ? "" : `
|
|
201
|
+
${toggleSectionRow("Stabilization", "video.stabilization", item.video.stabilization)}
|
|
202
|
+
${toggleSectionRow("Rolling Shutter", "video.rollingShutter", item.video.rollingShutter)}
|
|
203
|
+
${section("Color Conform", "color-conform", item.video.colorConform, "video.colorConform", selectRow("Type", "video.colorConformType", item.video.colorConformType, [["automatic", "Automatic"], ["sdr", "SDR"], ["hdr", "HDR"]]))}
|
|
204
|
+
<div class="tracker-row"><span>Trackers</span><button data-action="add-tracker">+</button></div>
|
|
205
|
+
`}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
function containerInspector(item, state) {
|
|
209
|
+
const container = item.container;
|
|
210
|
+
const root = state.rootProject;
|
|
211
|
+
if (!container || !root)
|
|
212
|
+
return "";
|
|
213
|
+
const scopeName = (scopeId) => root.scopes?.[scopeId]?.name ?? scopeId;
|
|
214
|
+
let body = `<button class="inline-action scope-open-inspector" data-action="enter-clip-scope" data-item-id="${escapeHtml(item.id)}">Open Timeline</button>`;
|
|
215
|
+
if (container.kind === "multicam") {
|
|
216
|
+
const angles = Object.entries(container.angleScopeIds);
|
|
217
|
+
const options = (selected) => angles.map(([angleId, scopeId]) => `<option value="${escapeHtml(angleId)}" ${angleId === selected ? "selected" : ""}>${escapeHtml(scopeName(scopeId))}</option>`).join("");
|
|
218
|
+
body += `<label class="inspector-row"><span>Video Angle</span><select data-action="set-multicam-video-angle" data-item-id="${escapeHtml(item.id)}">${options(container.videoAngleId)}</select></label><label class="inspector-row"><span>Audio Angle</span><select data-action="set-multicam-audio-angle" data-item-id="${escapeHtml(item.id)}">${options(container.audioAngleId)}</select></label>`;
|
|
219
|
+
}
|
|
220
|
+
else if (container.kind === "audition") {
|
|
221
|
+
body += `<label class="inspector-row"><span>Active Pick</span><select data-action="set-audition-choice" data-item-id="${escapeHtml(item.id)}">${container.choiceScopeIds.map((scopeId) => `<option value="${escapeHtml(scopeId)}" ${scopeId === container.activeChoiceId ? "selected" : ""}>${escapeHtml(scopeName(scopeId))}</option>`).join("")}</select></label><p class="capability-note">All alternatives remain in the FCPXML; changing the pick only moves the active selector.</p>`;
|
|
222
|
+
}
|
|
223
|
+
else if (container.kind === "sync") {
|
|
224
|
+
body += `<div class="sync-source-controls">${container.sources.map((source) => `<div class="sync-source-row"><strong>${escapeHtml(source.sourceId)}</strong><input type="text" value="${escapeHtml(source.role ?? "")}" data-action="set-sync-source-role" data-item-id="${escapeHtml(item.id)}" data-source-id="${escapeHtml(source.sourceId)}" ${source.role === null ? "disabled" : ""}><label><input type="checkbox" data-action="set-sync-source-enabled" data-item-id="${escapeHtml(item.id)}" data-source-id="${escapeHtml(source.sourceId)}" ${source.enabled ? "checked" : ""} ${source.role === null ? "disabled" : ""}>Enabled</label><label><input type="checkbox" data-action="set-sync-source-active" data-item-id="${escapeHtml(item.id)}" data-source-id="${escapeHtml(source.sourceId)}" ${source.active ? "checked" : ""} ${source.role === null ? "disabled" : ""}>Active</label></div>`).join("")}</div>`;
|
|
225
|
+
}
|
|
226
|
+
return section(`${capitalize(container.kind)} Timeline`, "container", true, "", body, false);
|
|
227
|
+
}
|
|
228
|
+
function audioInspector(item, state) {
|
|
229
|
+
const fullState = state;
|
|
230
|
+
const localhost = fullState.connectionMode === "localhost";
|
|
231
|
+
return `
|
|
232
|
+
${section("Volume", "audio-volume", true, "", `
|
|
233
|
+
${sliderRow("Volume", "audio.gainDb", item.audio.gainDb, -96, 24, .1, "dB", state)}
|
|
234
|
+
${sliderRow("Pan", "audio.pan", item.audio.pan, -1, 1, .01, "", state)}
|
|
235
|
+
${sliderRow("Fade In", "audio.fadeIn", item.audio.fadeIn, 0, Math.min(5, item.duration), .01, "s", state, 1, null)}
|
|
236
|
+
${sliderRow("Fade Out", "audio.fadeOut", item.audio.fadeOut, 0, Math.min(5, item.duration), .01, "s", state, 1, null)}
|
|
237
|
+
<div class="button-row"><button class="square-toggle ${item.audio.muted ? "active" : ""}" data-action="toggle-audio-mute">Mute</button>${localhost ? "" : `<button class="square-toggle ${item.audio.solo ? "active" : ""}" data-action="toggle-item-solo">Solo</button>`}</div>
|
|
238
|
+
`)}
|
|
239
|
+
${localhost ? "" : section("Audio Configuration", "audio-config", true, "", `
|
|
240
|
+
${selectRow("Channels", "audio.channels", "stereo", [["stereo", "Stereo"], ["dual-mono", "Dual Mono"], ["mono", "Mono"]])}
|
|
241
|
+
<div class="channel-strip"><span>L</span><div class="channel-wave">${waveBars(26)}</div><span>R</span></div>
|
|
242
|
+
`)}
|
|
243
|
+
${localhost ? "" : section("Audio Enhancements", "audio-enhance", true, "", `
|
|
244
|
+
${sliderRow("Loudness", "audio.loudness", 0, 0, 100, 1, "%", state)}
|
|
245
|
+
${sliderRow("Noise Removal", "audio.noiseRemoval", 0, 0, 100, 1, "%", state)}
|
|
246
|
+
`)}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
function colorInspector(item, state) {
|
|
250
|
+
const color = item.video.color;
|
|
251
|
+
const zones = ["shadows", "midtones", "highlights"];
|
|
252
|
+
if (state.connectionMode === "localhost") {
|
|
253
|
+
const colorEffects = item.effects.filter((effect) => {
|
|
254
|
+
const capability = state.capabilities.effects.find((candidate) => candidate.handler === effect.handler || candidate.resource.uid === effect.resourceUid);
|
|
255
|
+
return /color|tint|vibran|board|wheel/i.test(`${capability?.id ?? ""} ${capability?.handler ?? ""}`);
|
|
256
|
+
});
|
|
257
|
+
return colorCorrectionsInspector(item, state, colorEffects);
|
|
258
|
+
}
|
|
259
|
+
return `
|
|
260
|
+
${section("Color Adjustments", "color", true, "", `
|
|
261
|
+
${sliderRow("Exposure", "video.color.exposure", color.exposure, -100, 100, 1, "", state)}
|
|
262
|
+
${sliderRow("Brightness", "video.color.brightness", color.brightness, -100, 100, 1, "", state)}
|
|
263
|
+
${sliderRow("Contrast", "video.color.contrast", color.contrast, -100, 100, 1, "", state)}
|
|
264
|
+
${sliderRow("Saturation", "video.color.saturation", color.saturation, -100, 100, 1, "", state)}
|
|
265
|
+
${sliderRow("Temperature", "video.color.temperature", color.temperature, -100, 100, 1, "", state)}
|
|
266
|
+
${sliderRow("Tint", "video.color.tint", color.tint, -100, 100, 1, "", state)}
|
|
267
|
+
${sliderRow("Highlights", "video.color.highlights", color.highlights, -100, 100, 1, "", state)}
|
|
268
|
+
${sliderRow("Midtones", "video.color.midtones", color.midtones, -100, 100, 1, "", state)}
|
|
269
|
+
${sliderRow("Shadows", "video.color.shadows", color.shadows, -100, 100, 1, "", state)}
|
|
270
|
+
${sliderRow("Black Point", "video.color.blackPoint", color.blackPoint, -100, 100, 1, "", state)}
|
|
271
|
+
${sliderRow("Hue", "video.color.hue", color.hue, -180, 180, 1, "°", state)}
|
|
272
|
+
`)}
|
|
273
|
+
${state.connectionMode === "mock" ? `<div class="color-board" aria-label="Color tonal-range controls">
|
|
274
|
+
${zones.map((zone) => {
|
|
275
|
+
const offset = color.pucks[zone];
|
|
276
|
+
const px = Number(offset.x) * .14;
|
|
277
|
+
const py = Number(offset.y) * .14;
|
|
278
|
+
return `<button class="color-puck ${zone} ${state.activeColorZone === zone ? "active" : ""}" data-action="select-color-zone" data-zone="${zone}" data-color-wheel="${zone}" style="--wheel-x:${px}px;--wheel-y:${py}px" title="Select or drag ${capitalize(zone)} (is still a mock)"><i><b></b></i><span>${capitalize(zone)}</span><small>${Math.round(offset.x)}, ${Math.round(offset.y)}</small></button>`;
|
|
279
|
+
}).join("")}
|
|
280
|
+
</div>
|
|
281
|
+
<div class="color-zone-context">
|
|
282
|
+
<strong>${capitalize(state.activeColorZone)}</strong>
|
|
283
|
+
${sliderRow("Level", `video.color.${state.activeColorZone}`, color[state.activeColorZone], -100, 100, 1, "", state)}
|
|
284
|
+
</div>` : '<p class="capability-note inspector-note">Bladeworks exposes the supported portable Color Adjustments controls above. Proprietary wheels and curves are intentionally omitted.</p>'}
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Project Bladeworks color effects into the same visual hierarchy as Final
|
|
289
|
+
* Cut's Color Inspector while retaining the capability catalog as truth.
|
|
290
|
+
*
|
|
291
|
+
* Main callers:
|
|
292
|
+
* - colorInspector in localhost mode.
|
|
293
|
+
*
|
|
294
|
+
* Why this exists:
|
|
295
|
+
* Color Adjustments is one semantic correction with Light and Color groups,
|
|
296
|
+
* not an undifferentiated list of effect parameters. Unsupported native-only
|
|
297
|
+
* controls are omitted instead of being presented as controls that do nothing.
|
|
298
|
+
*/
|
|
299
|
+
function colorCorrectionsInspector(item, state, colorEffects) {
|
|
300
|
+
const selected = colorEffects[0] ?? null;
|
|
301
|
+
const selectedCapability = selected
|
|
302
|
+
? state.capabilities.effects.find((candidate) => candidate.handler === selected.handler || candidate.resource.uid === selected.resourceUid) ?? null
|
|
303
|
+
: null;
|
|
304
|
+
const correctionName = selected?.name ?? "No Corrections";
|
|
305
|
+
return `<div class="fcp-color-inspector">
|
|
306
|
+
<header class="color-correction-selector">
|
|
307
|
+
<label><input type="checkbox" ${selected?.enabled ? "checked" : ""} ${selected ? `data-action="toggle-effect" data-effect-id="${escapeHtml(selected.id)}"` : "disabled"}><span>${escapeHtml(correctionName)}</span></label>
|
|
308
|
+
<span class="color-selector-chevron">⌄</span>
|
|
309
|
+
</header>
|
|
310
|
+
${selected && selectedCapability?.id === "effect-color-adjustments"
|
|
311
|
+
? colorAdjustmentsPanel(selected, selectedCapability)
|
|
312
|
+
: selected
|
|
313
|
+
? `<div class="color-generic-correction">${effectRow(selected, 0, colorEffects.length, state, false)}</div>`
|
|
314
|
+
: `<div class="color-correction-empty"><p>No color correction is applied.</p><button data-action="open-effects-browser">Add Color Adjustments</button></div>`}
|
|
315
|
+
${selected ? '<footer class="color-correction-footer"><button data-action="open-effects-browser">Add Correction</button></footer>' : ""}
|
|
316
|
+
</div>`;
|
|
317
|
+
}
|
|
318
|
+
function colorAdjustmentsPanel(effect, capability) {
|
|
319
|
+
const byName = new Map(capability.parameters.map((parameter) => [parameter.name, parameter]));
|
|
320
|
+
const light = ["Exposure", "Contrast", "Brightness", "Highlights", "Black Point", "Shadows"];
|
|
321
|
+
const color = [
|
|
322
|
+
"Saturation",
|
|
323
|
+
"Highlights Warmth",
|
|
324
|
+
"Highlights Tint",
|
|
325
|
+
"Midtones Warmth",
|
|
326
|
+
"Midtones Tint",
|
|
327
|
+
"Shadows Warmth",
|
|
328
|
+
"Shadows Tint",
|
|
329
|
+
];
|
|
330
|
+
const row = (name) => {
|
|
331
|
+
const parameter = byName.get(name);
|
|
332
|
+
if (!parameter)
|
|
333
|
+
return "";
|
|
334
|
+
const raw = effect.parameters[parameter.key] ?? parameter.default;
|
|
335
|
+
const value = typeof raw === "number" ? raw : 0;
|
|
336
|
+
const minimum = parameter.min ?? -100;
|
|
337
|
+
const maximum = parameter.max ?? 100;
|
|
338
|
+
const step = parameter.type === "integer" ? 1 : 0.01;
|
|
339
|
+
const normalizedValue = value < 0
|
|
340
|
+
? (minimum < 0 ? value / Math.abs(minimum) * 100 : 0)
|
|
341
|
+
: (maximum > 0 ? value / maximum * 100 : 0);
|
|
342
|
+
const oneSidedNegative = maximum <= 0;
|
|
343
|
+
const attributes = `data-action="set-capability-parameter" data-owner="effect" data-owner-id="${escapeHtml(effect.id)}" data-parameter-key="${escapeHtml(parameter.key)}"`;
|
|
344
|
+
return `<label class="fcp-color-row">
|
|
345
|
+
<span>${escapeHtml(name)}</span>
|
|
346
|
+
<span class="fcp-color-slider-shell ${oneSidedNegative ? "one-sided-negative" : ""}">
|
|
347
|
+
<input class="fcp-color-slider" type="range" min="-100" max="${oneSidedNegative ? 0 : 100}" step="0.1" value="${normalizedValue}" data-actual-min="${minimum}" data-actual-max="${maximum}" ${attributes} aria-label="${escapeHtml(name)} ${value}">
|
|
348
|
+
</span>
|
|
349
|
+
<input class="fcp-color-value" type="number" min="${minimum}" max="${maximum}" step="${step}" value="${value}" ${attributes} aria-label="${escapeHtml(name)} value">
|
|
350
|
+
</label>`;
|
|
351
|
+
};
|
|
352
|
+
const controlRange = byName.get("Control Range");
|
|
353
|
+
const controlValue = controlRange
|
|
354
|
+
? String(effect.parameters[controlRange.key] ?? controlRange.default ?? controlRange.choices?.[0] ?? "0 (SDR)")
|
|
355
|
+
: "0 (SDR)";
|
|
356
|
+
return `<div class="fcp-color-adjustments">
|
|
357
|
+
<label class="fcp-control-range"><span>Control Range</span><select ${controlRange ? `data-action="set-capability-parameter" data-owner="effect" data-owner-id="${escapeHtml(effect.id)}" data-parameter-key="${escapeHtml(controlRange.key)}"` : "disabled"}>${(controlRange?.choices ?? [controlValue]).map((choice) => `<option value="${escapeHtml(choice)}" ${choice === controlValue ? "selected" : ""}>${escapeHtml(choice.replace(/^0 \(/, "").replace(/\)$/, ""))}</option>`).join("")}</select></label>
|
|
358
|
+
<section class="fcp-color-group">
|
|
359
|
+
<header><strong>LIGHT</strong></header>
|
|
360
|
+
${light.map(row).join("")}
|
|
361
|
+
</section>
|
|
362
|
+
<section class="fcp-color-group">
|
|
363
|
+
<header><strong>COLOR</strong></header>
|
|
364
|
+
${color.map(row).join("")}
|
|
365
|
+
</section>
|
|
366
|
+
</div>`;
|
|
367
|
+
}
|
|
368
|
+
function retimeSection(item) {
|
|
369
|
+
if (["gap", "title", "caption", "generator"].includes(item.kind))
|
|
370
|
+
return "";
|
|
371
|
+
const points = item.timeMap?.points ?? [];
|
|
372
|
+
const sourceSpan = points.length >= 2
|
|
373
|
+
? Math.abs(points[points.length - 1].value.seconds - points[0].value.seconds)
|
|
374
|
+
: item.duration;
|
|
375
|
+
const rate = item.duration > 0 ? sourceSpan / item.duration : 1;
|
|
376
|
+
const reverse = points.length >= 2
|
|
377
|
+
&& points[points.length - 1].value.seconds < points[0].value.seconds;
|
|
378
|
+
const signedPercent = Number(((reverse ? -1 : 1) * rate * 100).toFixed(2));
|
|
379
|
+
return section("Re-time", "retime", item.timeMap !== null, "", `
|
|
380
|
+
<label class="retime-custom"><span>Speed</span><span class="retime-number"><input type="number" min="-10000" max="10000" step="0.1" value="${signedPercent}" data-action="set-retime-custom" aria-label="Speed %" title="Use a negative value to reverse playback"><em>%</em></span></label>
|
|
381
|
+
<label class="retime-pitch"><input type="checkbox" data-action="toggle-retime-pitch" ${item.timeMap?.preservesPitch === false ? "" : "checked"}><span>Preserve Pitch</span></label>
|
|
382
|
+
`, true, "toggle-retime-section");
|
|
383
|
+
}
|
|
384
|
+
function generatedClipInspector(item) {
|
|
385
|
+
if (item.kind === "generator") {
|
|
386
|
+
return section("Custom Solid", "generator", true, "", `
|
|
387
|
+
<label class="inspector-row"><span>Color</span><input type="color" value="${colorHex(item.generatorColor ?? undefined)}" data-action="set-generator-color"></label>
|
|
388
|
+
`, false);
|
|
389
|
+
}
|
|
390
|
+
if (item.kind !== "title" && item.kind !== "caption")
|
|
391
|
+
return "";
|
|
392
|
+
const style = item.textStyle;
|
|
393
|
+
const caption = item.caption;
|
|
394
|
+
return section(item.kind === "caption" ? "Caption" : "Basic Title", "title", true, "", `
|
|
395
|
+
<label class="inspector-row title-text-row"><span>Text</span><input type="text" value="${escapeHtml(item.text ?? item.name)}" data-action="set-title-text"></label>
|
|
396
|
+
${style ? `
|
|
397
|
+
<label class="inspector-row"><span>Font</span><input type="text" value="${escapeHtml(style.font)}" data-action="set-text-style" data-style-key="font"></label>
|
|
398
|
+
<label class="inspector-row"><span>Face</span><input type="text" value="${escapeHtml(style.fontFace)}" data-action="set-text-style" data-style-key="fontFace"></label>
|
|
399
|
+
<label class="inspector-row capability-number-row"><span>Size</span><input type="number" min="1" max="500" step="1" value="${style.fontSize}" data-action="set-text-style" data-style-key="fontSize"></label>
|
|
400
|
+
<label class="inspector-row"><span>Color</span><input type="color" value="${colorHex(style.fontColor)}" data-action="set-text-style" data-style-key="fontColor"></label>
|
|
401
|
+
${selectActionRow("Alignment", "set-text-style", "data-style-key=\"alignment\"", style.alignment, [["left", "Left"], ["center", "Center"], ["right", "Right"]])}
|
|
402
|
+
` : '<p class="capability-note">This title does not expose the certified Basic Title style model and remains preserved.</p>'}
|
|
403
|
+
${item.kind === "caption" && caption ? `
|
|
404
|
+
${selectActionRow("Placement", "set-caption-field", "data-caption-key=\"placement\"", caption.placement, [["bottom", "Bottom"], ["top", "Top"]])}
|
|
405
|
+
${selectActionRow("Alignment", "set-caption-field", "data-caption-key=\"alignment\"", caption.alignment, [["left", "Left"], ["center", "Center"], ["right", "Right"]])}
|
|
406
|
+
<label class="inspector-row"><span>Role</span><input type="text" value="${escapeHtml(caption.role)}" data-action="set-caption-field" data-caption-key="role"></label>
|
|
407
|
+
` : ""}
|
|
408
|
+
`, false);
|
|
409
|
+
}
|
|
410
|
+
function selectActionRow(label, action, extraAttributes, value, options) {
|
|
411
|
+
return `<label class="inspector-row"><span>${escapeHtml(label)}</span><select data-action="${action}" ${extraAttributes}>${options.map(([id, name]) => `<option value="${id}" ${id === value ? "selected" : ""}>${escapeHtml(name)}</option>`).join("")}</select></label>`;
|
|
412
|
+
}
|
|
413
|
+
function transitionInspector(transition, state) {
|
|
414
|
+
const capability = state.capabilities.transitions.find((candidate) => candidate.name === transition.name) ?? state.capabilities.transitions.find((candidate) => candidate.handler === transition.handler
|
|
415
|
+
|| candidate.resource.uid === transition.resourceUid);
|
|
416
|
+
return `
|
|
417
|
+
${section("Transition", "transition", true, "", `
|
|
418
|
+
<div class="transition-heading"><strong>${escapeHtml(transition.name)}</strong></div>
|
|
419
|
+
<label class="inspector-row capability-number-row"><span>Duration</span><input type="number" min="0.033" step="0.033" value="${transition.duration}" data-action="set-transition-duration" data-transition-id="${escapeHtml(transition.id)}"><em>s</em></label>
|
|
420
|
+
${capability?.parameters.length ? capabilityParameterRows("transition", transition.id, capability.parameters, transition.parameters) : ""}
|
|
421
|
+
<button class="danger-inline" data-action="remove-transition" data-transition-id="${escapeHtml(transition.id)}">Remove Transition</button>
|
|
422
|
+
`, false)}
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
function capabilityParameterRows(owner, ownerId, parameters, values) {
|
|
426
|
+
return parameters.map((parameter) => capabilityParameterRow(owner, ownerId, parameter, values[parameter.key] ?? parameter.default)).join("");
|
|
427
|
+
}
|
|
428
|
+
function capabilityParameterRow(owner, ownerId, parameter, value) {
|
|
429
|
+
const attributes = `data-action="set-capability-parameter" data-owner="${owner}" data-owner-id="${escapeHtml(ownerId)}" data-parameter-key="${escapeHtml(parameter.key)}"`;
|
|
430
|
+
if (parameter.type === "boolean") {
|
|
431
|
+
return `<label class="inspector-row capability-toggle"><span>${escapeHtml(parameter.name)}</span><input type="checkbox" ${value ? "checked" : ""} ${attributes}></label>`;
|
|
432
|
+
}
|
|
433
|
+
if (parameter.type === "enum") {
|
|
434
|
+
const inherited = value === undefined ? '<option value="" selected disabled>Renderer default</option>' : "";
|
|
435
|
+
return `<label class="inspector-row"><span>${escapeHtml(parameter.name)}</span><select ${attributes}>${inherited}${(parameter.choices ?? []).map((choice) => `<option value="${escapeHtml(choice)}" ${choice === value ? "selected" : ""}>${escapeHtml(choice)}</option>`).join("")}</select></label>`;
|
|
436
|
+
}
|
|
437
|
+
if (parameter.components?.length && (parameter.type === "number" || parameter.type === "integer")) {
|
|
438
|
+
const record = typeof value === "object" && value !== null ? value : {};
|
|
439
|
+
return `<div class="capability-compound"><span>${escapeHtml(parameter.name)}</span><div>${parameter.components.map((component) => `<label><small>${escapeHtml(component)}</small><input type="number" ${parameter.min !== undefined ? `min="${parameter.min}"` : ""} ${parameter.max !== undefined ? `max="${parameter.max}"` : ""} step="${parameter.type === "integer" ? 1 : 0.01}" value="${record[component] === undefined ? "" : Number(record[component])}" placeholder="Default" data-component="${escapeHtml(component)}" ${attributes}></label>`).join("")}</div></div>`;
|
|
440
|
+
}
|
|
441
|
+
if (parameter.type === "color") {
|
|
442
|
+
return `<label class="inspector-row"><span>${escapeHtml(parameter.name)}</span><input type="color" value="${colorHex(value)}" ${attributes}></label>`;
|
|
443
|
+
}
|
|
444
|
+
if (parameter.type === "point" || parameter.type === "rect") {
|
|
445
|
+
const components = parameter.components ?? (parameter.type === "point" ? ["x", "y"] : ["left", "top", "right", "bottom"]);
|
|
446
|
+
const record = typeof value === "object" && value !== null ? value : {};
|
|
447
|
+
return `<div class="capability-compound"><span>${escapeHtml(parameter.name)}</span><div>${components.map((component) => `<label><small>${escapeHtml(component)}</small><input type="number" step="0.01" value="${record[component] === undefined ? "" : Number(record[component])}" placeholder="Default" data-component="${escapeHtml(component)}" ${attributes}></label>`).join("")}</div></div>`;
|
|
448
|
+
}
|
|
449
|
+
return `<label class="inspector-row capability-number-row"><span>${escapeHtml(parameter.name)}</span><input type="number" ${parameter.min !== undefined ? `min="${parameter.min}"` : ""} ${parameter.max !== undefined ? `max="${parameter.max}"` : ""} step="${parameter.type === "integer" ? 1 : 0.01}" value="${value === undefined ? "" : Number(value)}" placeholder="Renderer default" ${attributes}></label>`;
|
|
450
|
+
}
|
|
451
|
+
function colorHex(value) {
|
|
452
|
+
if (typeof value !== "object" || value === null || !("red" in value))
|
|
453
|
+
return "#000000";
|
|
454
|
+
const color = value;
|
|
455
|
+
const byte = (component) => Math.round(Math.max(0, Math.min(1, component)) * 255).toString(16).padStart(2, "0");
|
|
456
|
+
return `#${byte(color.red)}${byte(color.green)}${byte(color.blue)}`;
|
|
457
|
+
}
|
|
458
|
+
function infoInspector(item, fps) {
|
|
459
|
+
return `
|
|
460
|
+
<div class="info-table">
|
|
461
|
+
${infoRow("Name", item.name)}
|
|
462
|
+
${infoRow("Role", item.role === "storyline" ? "Dialogue" : item.role)}
|
|
463
|
+
${infoRow("Start", formatTimecode(item.timelineStart, fps))}
|
|
464
|
+
${infoRow("Duration", formatTimecode(item.duration, fps))}
|
|
465
|
+
${infoRow("Source In", formatTimecode(item.sourceStart, fps))}
|
|
466
|
+
${infoRow("Asset ID", item.assetId ?? "Generated")}
|
|
467
|
+
${infoRow("FCPXML ID", item.id)}
|
|
468
|
+
</div>
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
function infoRow(label, value) {
|
|
472
|
+
return `<div class="info-row"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
473
|
+
}
|
|
474
|
+
function sectionKeyframePath(sectionId) {
|
|
475
|
+
const paths = {
|
|
476
|
+
compositing: "transform.opacity",
|
|
477
|
+
transform: "transform.position",
|
|
478
|
+
crop: "video.crop.left",
|
|
479
|
+
distort: "video.distort.topleft",
|
|
480
|
+
"audio-volume": "audio.gainDb",
|
|
481
|
+
"audio-enhance": "audio.loudness",
|
|
482
|
+
color: "video.color.exposure",
|
|
483
|
+
};
|
|
484
|
+
return paths[sectionId] ?? "";
|
|
485
|
+
}
|
|
486
|
+
function section(title, sectionId, enabled, enablePath, body, resettable = true, enableAction = "") {
|
|
487
|
+
return `
|
|
488
|
+
<section class="inspector-section ${enabled ? "" : "disabled"}" data-section="${sectionId}">
|
|
489
|
+
<div class="section-header">
|
|
490
|
+
${enablePath
|
|
491
|
+
? `<input class="section-check" type="checkbox" data-parameter-path="${enablePath}" ${enabled ? "checked" : ""}>`
|
|
492
|
+
: enableAction
|
|
493
|
+
? `<input class="section-check" type="checkbox" data-action="${enableAction}" ${enabled ? "checked" : ""}>`
|
|
494
|
+
: '<span class="section-check-spacer"></span>'}
|
|
495
|
+
<button class="section-disclosure" data-action="toggle-inspector-section" data-section="${sectionId}" aria-label="Show or hide ${escapeHtml(title)}" aria-expanded="true">${icon("disclosure-open")}</button>
|
|
496
|
+
<strong class="section-title">${escapeHtml(title)}</strong>
|
|
497
|
+
${sectionKeyframePath(sectionId) ? `<button class="section-keyframe-menu" data-action="keyframe-menu" data-path="${sectionKeyframePath(sectionId)}" title="Keyframe menu">${icon("keyframe-menu")}</button>` : '<span class="section-keyframe-spacer"></span>'}
|
|
498
|
+
${resettable ? `<button class="section-reset" data-action="reset-section" data-section="${sectionId}" title="Reset ${escapeHtml(title)}">${icon("reset")}</button>` : '<span class="section-reset-spacer"></span>'}
|
|
499
|
+
</div>
|
|
500
|
+
<div class="section-body">${body}</div>
|
|
501
|
+
</section>
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
function sliderRow(label, path, value, min, max, step, unit, state, multiplier = 1, keyframePath = path) {
|
|
505
|
+
const shown = value * multiplier;
|
|
506
|
+
return `
|
|
507
|
+
<div class="parameter-row slider-parameter">
|
|
508
|
+
<label>${escapeHtml(label)}</label>
|
|
509
|
+
<input class="fcp-slider" type="range" min="${min}" max="${max}" step="${step}" value="${value}" data-parameter-path="${path}">
|
|
510
|
+
<div class="numeric-field"><input type="number" min="${min * multiplier}" max="${max * multiplier}" step="${step * multiplier}" value="${round(shown, step * multiplier)}" data-number-path="${path}" data-number-multiplier="${multiplier}"><span>${unit}</span></div>
|
|
511
|
+
${keyframePath ? keyframeButton(keyframePath, state) : '<span class="row-spacer"></span>'}
|
|
512
|
+
</div>
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
function xyRow(label, pathX, valueX, pathY, valueY, unit, state, keyframePath = pathX) {
|
|
516
|
+
return `
|
|
517
|
+
<div class="parameter-row xy-parameter">
|
|
518
|
+
<label>${escapeHtml(label)}</label>
|
|
519
|
+
<div class="axis-field"><span>X</span><input type="number" step="0.1" value="${round(valueX, .1)}" data-number-path="${pathX}"><i>${unit}</i></div>
|
|
520
|
+
<div class="axis-field"><span>Y</span><input type="number" step="0.1" value="${round(valueY, .1)}" data-number-path="${pathY}"><i>${unit}</i></div>
|
|
521
|
+
${keyframeButton(keyframePath, state)}
|
|
522
|
+
</div>
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
function knobRow(label, path, value, min, max, step, unit, state) {
|
|
526
|
+
return `
|
|
527
|
+
<div class="parameter-row knob-parameter">
|
|
528
|
+
<label>${escapeHtml(label)}</label>
|
|
529
|
+
<div class="fcp-knob" data-knob-path="${path}" data-min="${min}" data-max="${max}" data-step="${step}" style="--knob-angle:${value}deg" title="Drag vertically or horizontally"><i></i></div>
|
|
530
|
+
<div class="numeric-field"><input type="number" min="${min}" max="${max}" step="${step}" value="${round(value, step)}" data-number-path="${path}"><span>${unit}</span></div>
|
|
531
|
+
${keyframeButton(path, state)}
|
|
532
|
+
</div>
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
function selectRow(label, path, value, options) {
|
|
536
|
+
return `<div class="parameter-row select-parameter"><label>${escapeHtml(label)}</label><select data-parameter-path="${path}">${options.map(([id, name]) => `<option value="${id}" ${id === value ? "selected" : ""}>${escapeHtml(name)}</option>`).join("")}</select><span class="row-spacer"></span></div>`;
|
|
537
|
+
}
|
|
538
|
+
function segmentedRow(label, path, value, options) {
|
|
539
|
+
return `<div class="parameter-row segmented-parameter"><label>${escapeHtml(label)}</label><div class="inspector-segmented">${options.map(([id, name]) => `<button class="${id === value ? "active" : ""}" data-action="set-parameter" data-path="${path}" data-value="${id}">${escapeHtml(name)}</button>`).join("")}</div><span class="row-spacer"></span></div>`;
|
|
540
|
+
}
|
|
541
|
+
function toggleSectionRow(label, path, value) {
|
|
542
|
+
return `<div class="standalone-toggle-row"><input type="checkbox" data-parameter-path="${path}" ${value ? "checked" : ""}><strong>${escapeHtml(label)}</strong><button data-action="reset-path" data-path="${path}">${icon("reset")}</button></div>`;
|
|
543
|
+
}
|
|
544
|
+
function keyframeButton(path, state) {
|
|
545
|
+
const item = state.selectedItem ?? null;
|
|
546
|
+
const active = item ? itemHasKeyframeAt(item, path, state.currentTime, state.fps) : false;
|
|
547
|
+
return `<button class="keyframe-diamond ${active ? "active" : ""}" data-action="toggle-keyframe" data-path="${path}" title="Add or remove keyframe">${icon("diamond")}</button>`;
|
|
548
|
+
}
|
|
549
|
+
function round(value, step) {
|
|
550
|
+
const fraction = String(step).split(".")[1];
|
|
551
|
+
const decimals = fraction ? fraction.length : 0;
|
|
552
|
+
return Number(value.toFixed(Math.min(3, decimals)));
|
|
553
|
+
}
|