@dailies-studio/shot-editor 0.1.0
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.
- package/dist/assets/index-B7notge7.css +1 -0
- package/dist/assets/index-BBE6Ta9L.js +52 -0
- package/dist/index.html +13 -0
- package/package.json +45 -0
- package/src/api.ts +289 -0
- package/src/app/App.tsx +394 -0
- package/src/audio/index.tsx +74 -0
- package/src/audio/preview.ts +364 -0
- package/src/bridge.ts +45 -0
- package/src/bridgeTypes.ts +582 -0
- package/src/channels.ts +40 -0
- package/src/main.tsx +15 -0
- package/src/panels/ResizeHandle.tsx +113 -0
- package/src/panels/agent/AgentPanel.tsx +478 -0
- package/src/panels/agent/agent.css +322 -0
- package/src/panels/export/ExportDialog.tsx +284 -0
- package/src/panels/files/FilePanel.tsx +564 -0
- package/src/panels/files/McpSection.tsx +66 -0
- package/src/panels/files/icons.tsx +118 -0
- package/src/panels/header/DocSwitcher.tsx +85 -0
- package/src/panels/header/Header.tsx +132 -0
- package/src/panels/header/HistoryControls.tsx +20 -0
- package/src/panels/header/StatusBadges.tsx +49 -0
- package/src/panels/header/TransportBar.tsx +86 -0
- package/src/panels/header/ZoomControls.tsx +38 -0
- package/src/panels/help/HelpSection.tsx +190 -0
- package/src/panels/inspector/FramingRows.tsx +69 -0
- package/src/panels/inspector/InspectorPanel.tsx +178 -0
- package/src/panels/inspector/MarkerSection.tsx +63 -0
- package/src/panels/inspector/ParamRow.tsx +71 -0
- package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
- package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
- package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
- package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
- package/src/panels/inspector/animate/ShotSection.tsx +126 -0
- package/src/panels/inspector/animate/useBlocks.ts +43 -0
- package/src/panels/inspector/design/AlignRow.tsx +63 -0
- package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
- package/src/panels/inspector/design/ClonerSection.tsx +142 -0
- package/src/panels/inspector/design/ComponentSection.tsx +94 -0
- package/src/panels/inspector/design/CornerSection.tsx +52 -0
- package/src/panels/inspector/design/FillSection.tsx +89 -0
- package/src/panels/inspector/design/HtmlSection.tsx +50 -0
- package/src/panels/inspector/design/InstanceSection.tsx +302 -0
- package/src/panels/inspector/design/MediaSection.tsx +251 -0
- package/src/panels/inspector/design/MeshSection.tsx +172 -0
- package/src/panels/inspector/design/ShadowSection.tsx +73 -0
- package/src/panels/inspector/design/StrokeSection.tsx +85 -0
- package/src/panels/inspector/design/TransformSection.tsx +91 -0
- package/src/panels/inspector/design/TypographySection.tsx +170 -0
- package/src/panels/inspector/scene/CameraSection.tsx +59 -0
- package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
- package/src/panels/inspector/scene/SceneSection.tsx +71 -0
- package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
- package/src/panels/inspector/useDocWrite.ts +268 -0
- package/src/panels/material/MaterialPicker.tsx +172 -0
- package/src/panels/stage/StageMenu.tsx +38 -0
- package/src/panels/stage/StagePanel.tsx +131 -0
- package/src/panels/timeline/AudioLane.tsx +340 -0
- package/src/panels/timeline/BlockRow.tsx +242 -0
- package/src/panels/timeline/ChannelRow.tsx +266 -0
- package/src/panels/timeline/LifespanRow.tsx +165 -0
- package/src/panels/timeline/MarkerLane.tsx +146 -0
- package/src/panels/timeline/SceneStrip.tsx +145 -0
- package/src/panels/timeline/TimelineDock.tsx +488 -0
- package/src/panels/timeline/model.ts +641 -0
- package/src/panels/timeline/snap.ts +191 -0
- package/src/panels/toolbar/AddMenu.tsx +230 -0
- package/src/panels/toolbar/CameraViewBar.tsx +70 -0
- package/src/panels/toolbar/ShadingBar.tsx +42 -0
- package/src/panels/toolbar/ToolDockBar.tsx +110 -0
- package/src/panels/toolbar/ViewModeBar.tsx +49 -0
- package/src/shotTokens.css +131 -0
- package/src/stage/FrustumGizmo.tsx +184 -0
- package/src/stage/GridPlane.tsx +274 -0
- package/src/stage/MoveGizmo.tsx +411 -0
- package/src/stage/PerfHud.tsx +206 -0
- package/src/stage/SplitRenderer.tsx +511 -0
- package/src/stage/StageApp.tsx +3423 -0
- package/src/stage/StageTools.tsx +581 -0
- package/src/stage/bridgeClient.ts +278 -0
- package/src/stage/feed.ts +243 -0
- package/src/stage/frustumGeometry.ts +172 -0
- package/src/stage/gizmoMath.ts +494 -0
- package/src/stage/handles.ts +151 -0
- package/src/stage/instanceModules.ts +152 -0
- package/src/stage/interactions/move.ts +172 -0
- package/src/stage/interactions/panZoom.ts +60 -0
- package/src/stage/isolation.ts +155 -0
- package/src/stage/main.tsx +110 -0
- package/src/stage/modalState.ts +427 -0
- package/src/stage/overlays/Overlays.tsx +1046 -0
- package/src/stage/panes.ts +121 -0
- package/src/stage/penState.ts +112 -0
- package/src/stage/project.ts +402 -0
- package/src/stage/shading.ts +55 -0
- package/src/stage/tokenColor.ts +26 -0
- package/src/stage/view.ts +182 -0
- package/src/stage/workingView.ts +636 -0
- package/src/state/agentActions.ts +203 -0
- package/src/state/clientId.ts +25 -0
- package/src/state/context.tsx +33 -0
- package/src/state/insert.ts +507 -0
- package/src/state/keymap.ts +560 -0
- package/src/state/menu.ts +141 -0
- package/src/state/panelSize.ts +26 -0
- package/src/state/renderEvents.ts +100 -0
- package/src/state/store.ts +4094 -0
- package/src/state/triangles.ts +70 -0
- package/src/state/types.ts +384 -0
- package/src/styles.css +2948 -0
- package/src/tools.ts +181 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
// The shot chrome <-> stage protocol, and the ONE place both halves agree.
|
|
2
|
+
//
|
|
3
|
+
// ── WHY A BRIDGE AT ALL ───────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// The chrome is a PREBUILT STATIC bundle (so user tooling can never break the
|
|
6
|
+
// editor) and the stage is served through the PROJECT's Vite (so the three
|
|
7
|
+
// stack resolves to the studio's pinned copies). They are two documents, and
|
|
8
|
+
// the only thing that crosses is this interface — installed on
|
|
9
|
+
// `window.__shotStudio` by the stage's `boot()` before first paint, and polled
|
|
10
|
+
// for by the chrome's `connectStage`.
|
|
11
|
+
//
|
|
12
|
+
// SYNCHRONOUS BY DESIGN, and same-origin only. The motion and canvas lanes made
|
|
13
|
+
// the same call for the same reason: a seek that has to await a postMessage
|
|
14
|
+
// round trip cannot be driven from a pointermove without tearing. Hosting this
|
|
15
|
+
// chrome cross-origin would need an async rewrite; that is a known ceiling and
|
|
16
|
+
// not a bug.
|
|
17
|
+
//
|
|
18
|
+
// ── THREE-FREE ────────────────────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// Nothing in this file — or anywhere under src/ outside src/stage/ — may import
|
|
21
|
+
// `three`. The stage PROJECTS; the chrome receives screen-space numbers as
|
|
22
|
+
// plain data. test/sourceHygiene.test.ts is the gate, and the temptation it
|
|
23
|
+
// exists to catch is importing `Vector3` "just for the projection maths".
|
|
24
|
+
import type { Doc3D, FramingDelta, GestureDelta, GestureKind, PathData, SnapGuide } from "@dailies-studio/shot/doc";
|
|
25
|
+
// ONE definition of the tool union, in the table that also answers what each
|
|
26
|
+
// tool's key, surface, cursor and draw behaviour are. Re-exported here so the
|
|
27
|
+
// stage can take the protocol from one import.
|
|
28
|
+
import type { InsertTool, ShotTool } from "./tools.js";
|
|
29
|
+
// The pane vocabulary lives with the geometry that defines it. Importing it
|
|
30
|
+
// here rather than restating it is safe for the three-free rule and load
|
|
31
|
+
// bearing for the lane: `panes.ts` and `workingView.ts` are plain arithmetic
|
|
32
|
+
// with no three import precisely so both documents can share these types, and
|
|
33
|
+
// a second declaration of "what a split is" is a second thing to keep in step.
|
|
34
|
+
import type { StageLayout, StagePane } from "./stage/panes.js";
|
|
35
|
+
import type { ShadingMode } from "./stage/shading.js";
|
|
36
|
+
import type { WorkingView } from "./stage/workingView.js";
|
|
37
|
+
|
|
38
|
+
export type { InsertTool, ShotTool };
|
|
39
|
+
|
|
40
|
+
export type { FramingDelta, GestureDelta, GestureKind, SnapGuide };
|
|
41
|
+
export type { ShadingMode, StageLayout, StagePane, WorkingView };
|
|
42
|
+
|
|
43
|
+
/** Bumped when a change to this file would make an old stage and a new chrome
|
|
44
|
+
* disagree silently. `connectStage` refuses a mismatch rather than running.
|
|
45
|
+
*
|
|
46
|
+
* 2: the split view. A v1 stage has no `setStageLayout`, so a v2 chrome asking
|
|
47
|
+
* for a working pane would get a silently unchanged camera pane — exactly the
|
|
48
|
+
* half-working state the refusal exists for.
|
|
49
|
+
*
|
|
50
|
+
* 3: the camera lane. A v2 stage has no `setCameraFollowsView`, so a v3 chrome
|
|
51
|
+
* turning follow on would throw inside the push and, worse, would show its
|
|
52
|
+
* armed button over a pane that is not following anything.
|
|
53
|
+
*
|
|
54
|
+
* 4: `onWorkingViewChange` carries a WorkingViewOrigin. A v3 stage sends none,
|
|
55
|
+
* so every one of its view changes reads as "the author moved the view" — and
|
|
56
|
+
* the stage's own fit is one of them, which under follow rewrites the camera
|
|
57
|
+
* of every document that is merely OPENED while follow happens to be armed.
|
|
58
|
+
* Silent, dirtying and autosaved, which is exactly what this refusal is for.
|
|
59
|
+
*
|
|
60
|
+
* 5: `onDraw`. A v4 stage has no draw lane at all, so with the rect tool armed
|
|
61
|
+
* a drag would fall through to its marquee: the author draws a rectangle, sees
|
|
62
|
+
* a selection band, and gets no layer and no error. Every tool in the dock
|
|
63
|
+
* would look armed and half of them would do nothing.
|
|
64
|
+
*
|
|
65
|
+
* 6: `closeDoc` + `clearCaches`. Document close now really closes on the
|
|
66
|
+
* stage: the doc is dropped (unmounting every plane) and THEN the raster LRU
|
|
67
|
+
* — a module singleton in the STAGE's bundle, which the chrome may not
|
|
68
|
+
* import — is freed. A v5 stage has neither method and the push would throw;
|
|
69
|
+
* worse, it would keep rendering the closed document behind the file picker
|
|
70
|
+
* with every raster resident.
|
|
71
|
+
*
|
|
72
|
+
* 7: `onPenCommit`. The pen draws BEZIERS now, and a v6 stage can only report
|
|
73
|
+
* the polyline `onDraw` carries — so a v6 stage under a v7 chrome would hand
|
|
74
|
+
* back a path whose curves had been flattened to corners on the way, silently
|
|
75
|
+
* and only for paths the author had actually curved. The handle geometry has
|
|
76
|
+
* no representation in the older lane at all, which is what makes this a
|
|
77
|
+
* version and not an optional extra.
|
|
78
|
+
*
|
|
79
|
+
* 8: `setIsolation` and `onEnterLayer`. A v7 stage has no isolation at all, so a v8 chrome
|
|
80
|
+
* entering a subtree would show the whole scene, undimmed, at the scene
|
|
81
|
+
* camera — while its outline, its keymap and its inspector all believed the
|
|
82
|
+
* author was inside one card. Every pick would land on a layer the chrome
|
|
83
|
+
* considers out of scope, so clicking anything would read as a dead canvas.
|
|
84
|
+
*
|
|
85
|
+
* 9: `setShadingMode`, and `"roll"` in `GestureKind`. Two silent
|
|
86
|
+
* disagreements at once. A v8 stage has no `setShadingMode`, so a v9 chrome's
|
|
87
|
+
* ShadingBar would show an armed wireframe button over a pane still rendering
|
|
88
|
+
* clay — the armed-control-over-a-dead-pane state the refusal exists for. And
|
|
89
|
+
* a v9 stage can emit `{kind: "roll"}` across `onGesture`, which a v8
|
|
90
|
+
* chrome's `transformFor` has no arm for — `undefined` reaches `unchanged()`
|
|
91
|
+
* and throws inside the stage's own pointer path.
|
|
92
|
+
*
|
|
93
|
+
* 10: `setIsolationOrbit` + `onIsolationOrbitTaken`, and `"scale"` in
|
|
94
|
+
* `GestureKind`. Two more silent disagreements. A v9 stage keeps isolation's
|
|
95
|
+
* DERIVED orthographic rig in the working camera's slot forever, so a v10
|
|
96
|
+
* chrome that believed the author had taken the camera would refuse nothing,
|
|
97
|
+
* let follow mode write the shot camera from a pose with no pixels, and
|
|
98
|
+
* autosave it. And a v10 chrome can emit `{kind: "scale"}` across `onGesture`,
|
|
99
|
+
* which a v9 `transformFor` has no arm for — `undefined` reaches `unchanged()`
|
|
100
|
+
* and throws inside the stage's own pointer path, exactly as `"roll"` did at
|
|
101
|
+
* version 9.
|
|
102
|
+
*
|
|
103
|
+
* 11: `onContextMenu`, `onMeshStats`, and the key-light trio
|
|
104
|
+
* (`onKeyLightStart`/`onKeyLight`/`onKeyLightEnd`). All five are stage ->
|
|
105
|
+
* chrome, and the mismatch bites in both directions.
|
|
106
|
+
*
|
|
107
|
+
* A v11 stage under a v10 chrome calls handlers that are not there. Every one
|
|
108
|
+
* of them fires from inside a pointer lane — a right press, a gizmo drag, a
|
|
109
|
+
* model finishing its load — so the failure is a throw in the middle of a
|
|
110
|
+
* gesture, which is the same shape `"roll"` and `"scale"` had at 9 and 10.
|
|
111
|
+
*
|
|
112
|
+
* A v10 stage under a v11 chrome is the quiet direction and worse for it: the
|
|
113
|
+
* chrome would show a right-click menu that never opens, a key-light gizmo
|
|
114
|
+
* that never appears on a toon mesh, and an outliner whose triangle budget
|
|
115
|
+
* reports an empty scene however heavy the models are. Nothing throws and
|
|
116
|
+
* nothing is wrong on screen; the author simply believes three features are
|
|
117
|
+
* broken and one number is true. */
|
|
118
|
+
export const SHOT_BRIDGE_VERSION = 11;
|
|
119
|
+
|
|
120
|
+
/** What the stage reports once it has a renderer and a document. */
|
|
121
|
+
export interface ShotStageInfo {
|
|
122
|
+
/** The design canvas, design px. */
|
|
123
|
+
width: number;
|
|
124
|
+
height: number;
|
|
125
|
+
/** Master-timeline extent. */
|
|
126
|
+
durationFrames: number;
|
|
127
|
+
/** "webgpu" or "webgl" — which backend actually came up, for the status
|
|
128
|
+
* badge. A WebGPU request that fell back to WebGL2 is normal, not an error,
|
|
129
|
+
* but it is worth showing because it changes what the post stack costs. */
|
|
130
|
+
backend: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A screen-space quad for one layer: TL, TR, BR, BL as x,y pairs, in CSS
|
|
134
|
+
* pixels relative to the stage viewport's top-left.
|
|
135
|
+
*
|
|
136
|
+
* A QUAD AND NOT A RECT. Layers tilt, so a selected layer's outline is a
|
|
137
|
+
* trapezoid; an axis-aligned box would lie about the tilt in every frame and
|
|
138
|
+
* would put the resize handles somewhere the layer is not. */
|
|
139
|
+
export interface ShotScreenQuad {
|
|
140
|
+
id: string;
|
|
141
|
+
pts: readonly [number, number, number, number, number, number, number, number];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The stage's view of the working area: how much of the frame is on screen and
|
|
145
|
+
* where. Owned by the stage (it is a camera `setViewOffset`), mirrored to the
|
|
146
|
+
* chrome so the zoom readout and "fit" have something to read and write. */
|
|
147
|
+
export interface ShotView {
|
|
148
|
+
/** Screen px per design px at z = 0. */
|
|
149
|
+
zoom: number;
|
|
150
|
+
/** Pan of the working area, design px. */
|
|
151
|
+
panX: number;
|
|
152
|
+
panY: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* WHY the working view moved, which only the stage can know.
|
|
157
|
+
*
|
|
158
|
+
* "Camera follows this view" turns a view change into a DOCUMENT EDIT, so the
|
|
159
|
+
* three cases stop being the same event the moment follow is armed:
|
|
160
|
+
*
|
|
161
|
+
* "gesture" — an orbit or a pan. The stage brackets it with the framing
|
|
162
|
+
* gesture lane, so the whole drag is one undo entry.
|
|
163
|
+
* "wheel" — a dolly. It has no press and no release, so nothing brackets
|
|
164
|
+
* it; the chrome closes it over silence instead.
|
|
165
|
+
* "fit" — the stage MIRRORING a view it derived itself, on a document
|
|
166
|
+
* open or an iframe reload. Nobody moved anything, so follow must not
|
|
167
|
+
* write. Discriminating this by value (is it equal to the fit?) would be a
|
|
168
|
+
* guess; the caller is the only half that knows.
|
|
169
|
+
*/
|
|
170
|
+
export type WorkingViewOrigin = "gesture" | "wheel" | "fit";
|
|
171
|
+
|
|
172
|
+
/** What the chrome installs on the stage. Every handler must be total and must
|
|
173
|
+
* never throw back across the boundary — the stage calls them from inside its
|
|
174
|
+
* own render loop. */
|
|
175
|
+
export interface ShotBridgeHandlers {
|
|
176
|
+
/** The renderer is up and the stage is showing `doc`. Fires once per
|
|
177
|
+
* document load, and again after a Vite reload remints the stage. */
|
|
178
|
+
onReady(info: ShotStageInfo): void;
|
|
179
|
+
/** The stage failed in a way the author needs to see. */
|
|
180
|
+
onError(message: string): void;
|
|
181
|
+
/**
|
|
182
|
+
* A pick, in DOCUMENT order front-to-back — the WHOLE ordered hit list, not
|
|
183
|
+
* just the nearest.
|
|
184
|
+
*
|
|
185
|
+
* The chrome owns the policy (plain click takes the front, Cmd-click walks
|
|
186
|
+
* behind), because "what does a modified click mean" is a chrome question. A
|
|
187
|
+
* stage that answered it would have to know about modifier state, and then
|
|
188
|
+
* two places would decide selection.
|
|
189
|
+
*
|
|
190
|
+
* `pane` says which view the hits were read in, and it is not decoration:
|
|
191
|
+
* the camera pane orders them by PAINT ORDER and the working pane by
|
|
192
|
+
* DISTANCE (see `pickLayersByDistance`), so the meaning of "the front-most
|
|
193
|
+
* hit" — and therefore what a meta-click walks behind — depends on it.
|
|
194
|
+
*/
|
|
195
|
+
onPick(hits: readonly string[], modifiers: PickModifiers, pane: StagePane): void;
|
|
196
|
+
/**
|
|
197
|
+
* A DOUBLE-CLICK: go inside whatever is under the cursor.
|
|
198
|
+
*
|
|
199
|
+
* The same ordered hit list `onPick` sends, and for the same reason — the
|
|
200
|
+
* chrome owns "which of these did the author mean", and it is the only half
|
|
201
|
+
* that knows what is already isolated. A stage that answered would need the
|
|
202
|
+
* isolation stack, and then two places would decide what entering means.
|
|
203
|
+
*/
|
|
204
|
+
onEnterLayer(hits: readonly string[], pane: StagePane): void;
|
|
205
|
+
/**
|
|
206
|
+
* A RIGHT PRESS: the same ordered hit stack `onPick` sends, plus the point to
|
|
207
|
+
* open a menu at.
|
|
208
|
+
*
|
|
209
|
+
* `onPick`'s division exactly — the stage reports what is under the pointer
|
|
210
|
+
* and the chrome decides what a menu of it should say. A stage that built the
|
|
211
|
+
* entries would need the selection, the isolation stack and every store verb,
|
|
212
|
+
* and the chrome already has all three.
|
|
213
|
+
*
|
|
214
|
+
* `at` is in CLIENT coordinates and needs no translation: the stage iframe is
|
|
215
|
+
* `position: fixed; inset: 0`, so a point in the stage's document is the same
|
|
216
|
+
* point in the chrome's.
|
|
217
|
+
*/
|
|
218
|
+
onContextMenu(hits: readonly string[], pane: StagePane, at: { x: number; y: number }): void;
|
|
219
|
+
/**
|
|
220
|
+
* A mesh layer's triangle count, once its model has loaded — or null when the
|
|
221
|
+
* layer unmounts or its model is swapped.
|
|
222
|
+
*
|
|
223
|
+
* PUSHED, not polled. The count is known exactly once, when the GLB is parsed,
|
|
224
|
+
* and it cannot change until a different asset arrives; a chrome that asked
|
|
225
|
+
* for it per frame would be asking a question whose answer it already had.
|
|
226
|
+
*
|
|
227
|
+
* It crosses the bridge at all because the chrome is three-free by test: the
|
|
228
|
+
* outliner wants to badge a heavy model and the budget wants to warn about a
|
|
229
|
+
* heavy scene, and neither half can read a `BufferGeometry`.
|
|
230
|
+
*/
|
|
231
|
+
onMeshStats(layerId: string, triangles: number | null): void;
|
|
232
|
+
/**
|
|
233
|
+
* The key-light gizmo's drag, as the same three-call shape every other
|
|
234
|
+
* gesture uses: open a scrub, re-derive from its base on every emit, commit
|
|
235
|
+
* or discard once.
|
|
236
|
+
*
|
|
237
|
+
* `onKeyLightStart` returns false when the chrome refuses to open one (a
|
|
238
|
+
* scrub is already held), and the stage abandons the drag rather than
|
|
239
|
+
* emitting into nothing. `dir` is a direction in the DESIGN frame, already
|
|
240
|
+
* unit length. `commit: false` restores the base, which is what Escape means.
|
|
241
|
+
*
|
|
242
|
+
* There is no light in this lane and this is not one: it writes
|
|
243
|
+
* `mesh.shading.keyDir`, a shader uniform that fakes a key direction. See
|
|
244
|
+
* `MeshShading` — "no lights, ever".
|
|
245
|
+
*/
|
|
246
|
+
onKeyLightStart(layerId: string): boolean;
|
|
247
|
+
onKeyLight(dir: readonly [number, number, number]): void;
|
|
248
|
+
onKeyLightEnd(commit: boolean): void;
|
|
249
|
+
/**
|
|
250
|
+
* The author just took the working camera INSIDE isolation.
|
|
251
|
+
*
|
|
252
|
+
* ── WHY ISOLATION HANDS THE CAMERA OVER AT ALL ──────────────────────────
|
|
253
|
+
*
|
|
254
|
+
* Entering a subtree puts a DERIVED orthographic head-on rig in the working
|
|
255
|
+
* camera's slot (stage/isolation.ts), because that is the only camera that
|
|
256
|
+
* can frame a rolled or face-down card: the orbit rig resets its up vector to
|
|
257
|
+
* world +y every frame and clamps its polar angle off both poles. The pose is
|
|
258
|
+
* 100 percent derived, so there is no parameter an orbit, a pan or a dolly
|
|
259
|
+
* can move — and the author's first instinct is to orbit, which did nothing
|
|
260
|
+
* at all and said nothing about why. Blender's local view scopes what you see
|
|
261
|
+
* and leaves the viewport camera completely free.
|
|
262
|
+
*
|
|
263
|
+
* So the derived rig is the ENTRY pose, not a cage. The first navigation
|
|
264
|
+
* gesture fires this, the chrome flips its bit and pushes it back down, and
|
|
265
|
+
* from then on the ordinary perspective orbit rig owns the pane. Home and
|
|
266
|
+
* numpad-dot clear it, which makes head-on the RESET rather than the only
|
|
267
|
+
* option.
|
|
268
|
+
*
|
|
269
|
+
* It is a separate handler rather than a flavour of `onWorkingViewChange`
|
|
270
|
+
* because it is a MODE change the chrome must persist and re-push, while a
|
|
271
|
+
* view change is a mirror the chrome is free to throttle.
|
|
272
|
+
*/
|
|
273
|
+
onIsolationOrbitTaken(): void;
|
|
274
|
+
/** The stage's own pan/zoom changed. Camera pane only: the working pane's
|
|
275
|
+
* view is a `WorkingView`, and the two are not the same shape. */
|
|
276
|
+
onViewChange(view: ShotView): void;
|
|
277
|
+
/** The working pane's orbit / pan / dolly changed. `origin` says WHAT moved
|
|
278
|
+
* it, and follow mode is the reason it has to: see `WorkingViewOrigin`. */
|
|
279
|
+
onWorkingViewChange(view: WorkingView, origin?: WorkingViewOrigin): void;
|
|
280
|
+
/** Which pane the author last pressed, wheeled or dragged in. The chrome
|
|
281
|
+
* points its zoom controls and its fit shortcut at it, so it must follow the
|
|
282
|
+
* POINTER rather than the selection — an author can orbit the working pane
|
|
283
|
+
* with a camera-pane layer selected. */
|
|
284
|
+
onActivePane(pane: StagePane): void;
|
|
285
|
+
|
|
286
|
+
// ── the gesture lane ─────────────────────────────────────────────────────
|
|
287
|
+
//
|
|
288
|
+
// Split into three calls rather than one, because the middle one fires on
|
|
289
|
+
// every pointermove and the outer two are what make the whole drag ONE undo
|
|
290
|
+
// entry: `start` opens a history scrub, every `onGesture` re-derives from the
|
|
291
|
+
// scrub's base, and `end` commits or discards it. A single "here is the new
|
|
292
|
+
// document" call could not express that, and would cost an undo entry per
|
|
293
|
+
// pixel moved.
|
|
294
|
+
/** `screenScale` is SCREEN PX PER DESIGN PX in the pane the press landed in,
|
|
295
|
+
* which the stage is the only half able to answer: the camera pane's is
|
|
296
|
+
* `ShotView.zoom` and the working pane's is a perspective scale derived from
|
|
297
|
+
* the orbit distance. The chrome divides its snap threshold by it, so
|
|
298
|
+
* omitting it snaps a working-pane drag at the camera pane's zoom. */
|
|
299
|
+
/**
|
|
300
|
+
* …and it ANSWERS whether the scrub opened.
|
|
301
|
+
*
|
|
302
|
+
* Engine allows one scrub, so a start that arrives while another lane holds
|
|
303
|
+
* the history has to be refused. A pointer drag can absorb that silently —
|
|
304
|
+
* the author releases and tries again — but a MODAL transform arms and stays
|
|
305
|
+
* armed, so one that opened over somebody else's scrub would drive that
|
|
306
|
+
* scrub with its own deltas and commit it. The stage honours the answer.
|
|
307
|
+
*/
|
|
308
|
+
onGestureStart(kind: GestureKind, layerIds: readonly string[], screenScale?: number): boolean;
|
|
309
|
+
/** The delta MEASURED FROM THE PRESS, never since the last move — the scrub
|
|
310
|
+
* re-derives from its base, so a coalesced move cannot make a drag drift. */
|
|
311
|
+
onGesture(delta: GestureDelta): void;
|
|
312
|
+
/** `false` = Escape or a lost pointer: restore exactly what was there. */
|
|
313
|
+
onGestureEnd(commit: boolean): void;
|
|
314
|
+
|
|
315
|
+
// ── the CAMERA's gesture lane ────────────────────────────────────────────
|
|
316
|
+
//
|
|
317
|
+
// A second lane rather than a `subject` on the first, because the two write
|
|
318
|
+
// different things through different ops and only one of them can be refused
|
|
319
|
+
// for being under a shot. One scrub is open at a time either way (engine's
|
|
320
|
+
// history throws on a second), which the pointer already guarantees.
|
|
321
|
+
//
|
|
322
|
+
// It brackets TWO gestures: a drag on the frustum gizmo, which sends deltas;
|
|
323
|
+
// and — while "camera follows this view" is armed — a working-pane orbit or
|
|
324
|
+
// pan, which sends none at all. The view change IS the payload there, and it
|
|
325
|
+
// already crosses as `onWorkingViewChange`.
|
|
326
|
+
/** Answers whether the scrub opened, for `onGestureStart`'s reason. */
|
|
327
|
+
onFramingGestureStart(): boolean;
|
|
328
|
+
/** Measured FROM THE PRESS, same rule as `onGesture`. `dzoom` is a factor. */
|
|
329
|
+
onFramingGesture(delta: FramingDelta): void;
|
|
330
|
+
onFramingGestureEnd(commit: boolean): void;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* INSERT A LAYER. The camera pane's draw lane, and the pen's commit.
|
|
334
|
+
*
|
|
335
|
+
* ── THE POINTS ARE DESIGN COORDINATES, NOT SCREEN ONES ─────────────────
|
|
336
|
+
*
|
|
337
|
+
* The stage is the only half that can convert: the chrome knows the view but
|
|
338
|
+
* not the pane's pixel size, and `screenToDesign` needs both. So the numbers
|
|
339
|
+
* that cross are already in the schema's own space — design px from the
|
|
340
|
+
* CANVAS CENTRE, +y down (types.ts's coordinate contract) — exactly like the
|
|
341
|
+
* `SnapGuide` positions that cross the other way. The chrome never divides by
|
|
342
|
+
* a zoom, and there is no second place a pane origin could be subtracted.
|
|
343
|
+
*
|
|
344
|
+
* ── A CLICK IS TWO IDENTICAL POINTS ────────────────────────────────────
|
|
345
|
+
*
|
|
346
|
+
* "Did the pointer travel far enough to be a drag" is a SCREEN-px question
|
|
347
|
+
* and therefore the stage's, so a sub-threshold drag arrives here collapsed
|
|
348
|
+
* to `from === to` and the chrome's one rule is "a zero-size box means the
|
|
349
|
+
* default box, centred on the point". A `dragged` flag beside the points
|
|
350
|
+
* would be a second thing that can disagree with them.
|
|
351
|
+
*
|
|
352
|
+
* Two points for every draw tool. The PEN no longer rides this lane — it
|
|
353
|
+
* carries handles now and crosses as `onPenCommit` below — but `onDraw`
|
|
354
|
+
* still accepts `"path"`, because `layerFromDraw` remains the way an
|
|
355
|
+
* agent-free caller mints a polyline path.
|
|
356
|
+
*/
|
|
357
|
+
onDraw(tool: InsertTool, points: readonly (readonly [number, number])[]): void;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* COMMIT THE PEN'S PATH: anchors, their bezier handles, and whether the loop
|
|
361
|
+
* closed.
|
|
362
|
+
*
|
|
363
|
+
* ONE ARGUMENT, not `(points, closed)`. `closed` lives inside `PathData`
|
|
364
|
+
* already, and a flag beside the points would be a second thing that can
|
|
365
|
+
* disagree with them — the same rule `onDraw` states above for `dragged`.
|
|
366
|
+
*
|
|
367
|
+
* Coordinates follow `onDraw` exactly: design px from the canvas centre,
|
|
368
|
+
* +y down, converted by the stage because only it knows the pane's size.
|
|
369
|
+
* The chrome normalises them into the layer's own viewBox on arrival.
|
|
370
|
+
*
|
|
371
|
+
* Fewer than two anchors never reaches here — `penData` returns null and the
|
|
372
|
+
* stage commits nothing, because one anchor is a layer with no geometry and
|
|
373
|
+
* no way to select it.
|
|
374
|
+
*/
|
|
375
|
+
onPenCommit(data: PathData): void;
|
|
376
|
+
|
|
377
|
+
/** A key the STAGE received. Returns true when the chrome consumed it, so the
|
|
378
|
+
* stage knows whether to preventDefault. Keyboard events go to ONE document,
|
|
379
|
+
* and once the canvas has focus that document is the stage. */
|
|
380
|
+
onKey(event: KeyEventFields, editing: boolean): boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The fields the keymap reads. A plain object rather than a KeyboardEvent
|
|
384
|
+
* so it crosses the bridge as data and a test can build one. */
|
|
385
|
+
export interface KeyEventFields {
|
|
386
|
+
key: string;
|
|
387
|
+
shiftKey: boolean;
|
|
388
|
+
altKey: boolean;
|
|
389
|
+
ctrlKey: boolean;
|
|
390
|
+
metaKey: boolean;
|
|
391
|
+
/** `KeyboardEvent.code` — the numpad lane resolves on it. Optional so a
|
|
392
|
+
* probe or an older sender without it keeps today's answers. */
|
|
393
|
+
code?: string;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The chrome's furniture, as an inset into the stage, in CSS px per side.
|
|
398
|
+
*
|
|
399
|
+
* Zero on every side is the honest default: a stage that has not been told
|
|
400
|
+
* anything draws in the corners, which is where it drew before this existed.
|
|
401
|
+
*/
|
|
402
|
+
export interface StageInset {
|
|
403
|
+
top: number;
|
|
404
|
+
right: number;
|
|
405
|
+
bottom: number;
|
|
406
|
+
left: number;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface PickModifiers {
|
|
410
|
+
shift: boolean;
|
|
411
|
+
meta: boolean;
|
|
412
|
+
alt: boolean;
|
|
413
|
+
/**
|
|
414
|
+
* The hits came from a MARQUEE, not from a click.
|
|
415
|
+
*
|
|
416
|
+
* ── WHY THE CHROME CANNOT TELL THEM APART WITHOUT THIS ──────────────────
|
|
417
|
+
*
|
|
418
|
+
* `onPick`'s list means two different things. For a click it is the ordered
|
|
419
|
+
* HIT STACK under one point — front-most first, and a plain click takes the
|
|
420
|
+
* front one while meta walks behind it. For a marquee it is the SET the box
|
|
421
|
+
* enclosed, and every one of them is selected.
|
|
422
|
+
*
|
|
423
|
+
* Without the discriminator the chrome ran the click policy over both, so a
|
|
424
|
+
* box drawn over five layers took `hits[0]` and selected exactly one. Box
|
|
425
|
+
* select has never worked.
|
|
426
|
+
*
|
|
427
|
+
* It also decides what an EMPTY list means. BOTH DESELECT — a rubber band
|
|
428
|
+
* that caught nothing is still the author saying "not that", which is what
|
|
429
|
+
* Figma and Blender both do. Only the ISOLATION half differs: an empty click
|
|
430
|
+
* leaves the subtree, and an empty marquee does not, because a band drawn
|
|
431
|
+
* inside a card that caught nothing is not a request to leave the card.
|
|
432
|
+
*/
|
|
433
|
+
marquee?: boolean;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** What the stage installs on `window.__shotStudio`. */
|
|
437
|
+
export interface ShotStageBridge {
|
|
438
|
+
version: number;
|
|
439
|
+
/** Register the chrome's handlers. The stage renders NOTHING until this is
|
|
440
|
+
* called: there is nothing worth painting before a chrome exists, and the
|
|
441
|
+
* document arrives immediately after. */
|
|
442
|
+
connect(handlers: ShotBridgeHandlers): void;
|
|
443
|
+
/** Replace the document. Cheap when the reference is unchanged. */
|
|
444
|
+
loadDoc(doc: Doc3D): void;
|
|
445
|
+
/** Park the studio clock on an integer MASTER frame and repaint. */
|
|
446
|
+
seek(frame: number): void;
|
|
447
|
+
/** Which layers are selected, for the overlays. */
|
|
448
|
+
setSelection(ids: readonly string[]): void;
|
|
449
|
+
/** Screen quads for the current selection at the current frame, or null when
|
|
450
|
+
* nothing is selected. The chrome polls this from its own render rather than
|
|
451
|
+
* being pushed at, so an overlay re-render is never scheduled by the stage.
|
|
452
|
+
*
|
|
453
|
+
* The pane defaults to "camera" — the pane that IS the export, and the only
|
|
454
|
+
* one that existed at v1 — so a caller that does not know about the split
|
|
455
|
+
* keeps getting the answer it always got. */
|
|
456
|
+
selectionQuads(pane?: StagePane): readonly ShotScreenQuad[] | null;
|
|
457
|
+
/**
|
|
458
|
+
* Which tool is armed, because it decides the PLANE a move unprojects onto:
|
|
459
|
+
* "select" slides a tilted layer along its own surface, "move" slides it flat
|
|
460
|
+
* with z untouched. See stage/interactions/move.ts — that distinction is the
|
|
461
|
+
* whole difference between the two, and it only exists once layers can tilt.
|
|
462
|
+
*/
|
|
463
|
+
setTool(tool: ShotTool): void;
|
|
464
|
+
/** Smart-guide lines the chrome's snapping landed on, in DESIGN frame
|
|
465
|
+
* coordinates. Pushed rather than polled because they change only while a
|
|
466
|
+
* gesture is running and must vanish the instant it ends. */
|
|
467
|
+
setSnapGuides(guides: readonly SnapGuide[]): void;
|
|
468
|
+
/** Drive the working-area view. */
|
|
469
|
+
setView(view: ShotView): void;
|
|
470
|
+
/** Fit the design frame to the CAMERA PANE with a margin. Returns the view it
|
|
471
|
+
* landed on so the chrome's readout stays in step. */
|
|
472
|
+
fitView(): ShotView;
|
|
473
|
+
/**
|
|
474
|
+
* How many views are on screen, and how the width is divided.
|
|
475
|
+
*
|
|
476
|
+
* WORKSPACE STATE, not document state: it is never written to a Doc3D and
|
|
477
|
+
* never reaches an export. The stage holds no copy it was not handed, so this
|
|
478
|
+
* is re-pushed on every attach.
|
|
479
|
+
*/
|
|
480
|
+
setStageLayout(layout: StageLayout): void;
|
|
481
|
+
/**
|
|
482
|
+
* ISOLATE A SUBTREE, or null for the whole scene.
|
|
483
|
+
*
|
|
484
|
+
* WORKSPACE STATE, like the layout: never written to a Doc3D, never in an
|
|
485
|
+
* export. The stage is handed one id and derives everything from the document
|
|
486
|
+
* it already has — the subtree's members, the head-on camera, and which
|
|
487
|
+
* layers to dim — so the chrome never sends a set that could disagree with
|
|
488
|
+
* the tree.
|
|
489
|
+
*
|
|
490
|
+
* It takes the WORKING PANE's slot rather than adding a third view. A
|
|
491
|
+
* scissored pane per mode is the trap SplitRenderer's header is about, and
|
|
492
|
+
* the two views an author wants while inside a card are "this card, flat" and
|
|
493
|
+
* "the shot", which is exactly the split that already exists.
|
|
494
|
+
*/
|
|
495
|
+
setIsolation(layerId: string | null): void;
|
|
496
|
+
/**
|
|
497
|
+
* Inside isolation, does the AUTHOR own the working camera?
|
|
498
|
+
*
|
|
499
|
+
* False is the entry state and means the derived orthographic head-on rig
|
|
500
|
+
* draws the pane. True means the ordinary perspective orbit rig does, driven
|
|
501
|
+
* by `workingView` exactly as it is outside isolation. See
|
|
502
|
+
* `onIsolationOrbitTaken` for why the mode exists at all.
|
|
503
|
+
*
|
|
504
|
+
* Pushed rather than inferred, because the CHROME owns it: it is the half
|
|
505
|
+
* that clears the mode on Home, on numpad-dot and on leaving the subtree, and
|
|
506
|
+
* the half whose `writeFramingFromView` must refuse while the derived rig is
|
|
507
|
+
* in the slot. A stage that decided for itself would be a second opinion on
|
|
508
|
+
* which camera is drawing.
|
|
509
|
+
*
|
|
510
|
+
* `setIsolation` resets it to false on its own, so entering a subtree never
|
|
511
|
+
* inherits the last one's mode.
|
|
512
|
+
*/
|
|
513
|
+
setIsolationOrbit(taken: boolean): void;
|
|
514
|
+
/**
|
|
515
|
+
* How far the chrome's own furniture reaches INTO the stage, per side, in CSS
|
|
516
|
+
* px.
|
|
517
|
+
*
|
|
518
|
+
* ── WHY THE STAGE CANNOT MEASURE THIS ITSELF ────────────────────────────
|
|
519
|
+
*
|
|
520
|
+
* A pane is the WHOLE WINDOW (`paneRects`), because the stage iframe is
|
|
521
|
+
* pinned `position: fixed; inset: 0` and the chrome's panels float ON TOP of
|
|
522
|
+
* it. So "the top-right corner of the working pane" is behind the shell
|
|
523
|
+
* header, and there is no measurement the stage's document can take to
|
|
524
|
+
* discover that — the panels are in the other document.
|
|
525
|
+
*
|
|
526
|
+
* `Overlays` carried a hardcoded 116 for the top edge and a note saying the
|
|
527
|
+
* real fix was this message, "worth it when a second overlay needs the same
|
|
528
|
+
* answer". Four now do, and two of them — the isolation bar and the follow
|
|
529
|
+
* bar — sat entirely behind the opaque header, while the G/R/S pill, the only
|
|
530
|
+
* thing that says a modal transform is live, has never been visible in the
|
|
531
|
+
* default layout at all.
|
|
532
|
+
*
|
|
533
|
+
* PAINT ONLY. `paneRects` must not change: insetting the pane rects would
|
|
534
|
+
* move the scissor and the hit test together and shrink the rendered picture
|
|
535
|
+
* to fit the chrome. This positions overlay FURNITURE and nothing else.
|
|
536
|
+
*
|
|
537
|
+
* Editor state, never a document field.
|
|
538
|
+
*/
|
|
539
|
+
setStageInset(inset: StageInset): void;
|
|
540
|
+
/**
|
|
541
|
+
* The working pane's shading (solid / wireframe / material / rendered).
|
|
542
|
+
*
|
|
543
|
+
* WORKSPACE STATE like the layout: never written to a Doc3D, never in an
|
|
544
|
+
* export, re-pushed on every attach. The camera pane is not addressable
|
|
545
|
+
* here — it is always the graded render, by construction.
|
|
546
|
+
*/
|
|
547
|
+
setShadingMode(mode: ShadingMode): void;
|
|
548
|
+
/** Drive the working pane's orbit camera. */
|
|
549
|
+
setWorkingView(view: WorkingView): void;
|
|
550
|
+
/** Frame the whole canvas in the working pane. Returns the view it landed on,
|
|
551
|
+
* the same contract `fitView` has — the stage owns the derivation because it
|
|
552
|
+
* owns the lens the distance is derived from. */
|
|
553
|
+
fitWorkingView(): WorkingView;
|
|
554
|
+
/**
|
|
555
|
+
* "Camera follows this view" is armed.
|
|
556
|
+
*
|
|
557
|
+
* The stage needs it for two things it is the only half able to do: bracket
|
|
558
|
+
* an orbit with the framing gesture lane, so one drag is one undo entry; and
|
|
559
|
+
* draw the affordance INSIDE the working pane. Blender's long-running
|
|
560
|
+
* complaint about its equivalent is that it is easy to forget it is on, and a
|
|
561
|
+
* checkbox in a panel the author is not looking at is exactly how.
|
|
562
|
+
*/
|
|
563
|
+
setCameraFollowsView(on: boolean): void;
|
|
564
|
+
/** Drop the document: the stage renders nothing and every layer plane
|
|
565
|
+
* unmounts, releasing its raster pins and texture bindings. The close
|
|
566
|
+
* half of `loadDoc`. */
|
|
567
|
+
closeDoc(): void;
|
|
568
|
+
/** Drop the stage's raster cache. Called on document CLOSE, AFTER
|
|
569
|
+
* `closeDoc` — the bitmaps are known dead because the doc-clear that
|
|
570
|
+
* precedes this call unmounts every plane, not merely because the chrome
|
|
571
|
+
* closed a panel. Not called on scene switch: surviving a switch warm is
|
|
572
|
+
* the whole reason the cache is module-level. */
|
|
573
|
+
clearCaches(): void;
|
|
574
|
+
/** Tear the stage down (a chrome unmount, or a bridge version mismatch). */
|
|
575
|
+
dispose(): void;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
declare global {
|
|
579
|
+
interface Window {
|
|
580
|
+
__shotStudio?: ShotStageBridge;
|
|
581
|
+
}
|
|
582
|
+
}
|
package/src/channels.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The eight channels the block system writes, each with its label and its
|
|
2
|
+
// resolved-value formatter.
|
|
3
|
+
//
|
|
4
|
+
// This list IS resolve.ts's combinator table, in the order the composition is
|
|
5
|
+
// written there — and it is the chrome's ONE copy. The inspector's Resolved
|
|
6
|
+
// section and the dock's channel rows both read it, so an eighth channel added
|
|
7
|
+
// in resolve.ts without a row here is a value the author has no way to see in
|
|
8
|
+
// either place, which is how a delta model becomes folklore.
|
|
9
|
+
import { formatValue } from "@dailies-studio/ui";
|
|
10
|
+
import type { LayerDelta, LayerState } from "@dailies-studio/shot/doc";
|
|
11
|
+
|
|
12
|
+
/** The channel names, exactly as `LayerDelta` declares them. */
|
|
13
|
+
export type Channel = keyof Required<LayerDelta>;
|
|
14
|
+
|
|
15
|
+
export interface ChannelSpec {
|
|
16
|
+
key: Channel;
|
|
17
|
+
label: string;
|
|
18
|
+
/** The channel's value as the author sees it — from `resolveLayerAt`'s
|
|
19
|
+
* output, never a second fold. */
|
|
20
|
+
read(state: LayerState): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const CHANNELS: readonly ChannelSpec[] = [
|
|
24
|
+
{ key: "opacity", label: "Opacity", read: (s) => formatValue(s.opacity, 0.01) },
|
|
25
|
+
// Position, size, tilt and roll read the layer's own value WITH the delta
|
|
26
|
+
// folded in, because that is the number on screen.
|
|
27
|
+
{ key: "offset", label: "Position", read: (s) => s.position.map((v) => formatValue(v, 0.1)).join(", ") },
|
|
28
|
+
{ key: "scale", label: "Size", read: (s) => `${formatValue(s.size[0], 0.1)} × ${formatValue(s.size[1], 0.1)}` },
|
|
29
|
+
{ key: "tilt", label: "Tilt", read: (s) => `${formatValue(s.tilt[0], 0.1)}°, ${formatValue(s.tilt[1], 0.1)}°` },
|
|
30
|
+
{ key: "roll", label: "Roll", read: (s) => `${formatValue(s.rollDeg, 0.1)}°` },
|
|
31
|
+
// Blur, reveal and stagger have no base at all: they exist only as block
|
|
32
|
+
// output, so the resolved value IS the fold.
|
|
33
|
+
{ key: "blurPx", label: "Blur", read: (s) => `${formatValue(s.blurPx, 0.1)} px` },
|
|
34
|
+
{ key: "reveal", label: "Reveal", read: (s) => `${formatValue(s.reveal * 100, 1)}%` },
|
|
35
|
+
{ key: "textPhase", label: "Stagger", read: (s) => `${formatValue(s.textPhase * 100, 1)}%` },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const CHANNEL_BY_KEY: ReadonlyMap<Channel, ChannelSpec> = new Map(
|
|
39
|
+
CHANNELS.map((c) => [c.key, c]),
|
|
40
|
+
);
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { createShotEditorStore } from "./state/store.js";
|
|
4
|
+
import { StoreProvider } from "./state/context.js";
|
|
5
|
+
import { App } from "./app/App.js";
|
|
6
|
+
import "./styles.css";
|
|
7
|
+
|
|
8
|
+
const store = createShotEditorStore();
|
|
9
|
+
createRoot(document.getElementById("root")!).render(
|
|
10
|
+
<StrictMode>
|
|
11
|
+
<StoreProvider store={store}>
|
|
12
|
+
<App />
|
|
13
|
+
</StoreProvider>
|
|
14
|
+
</StrictMode>,
|
|
15
|
+
);
|