@tongue-core/studio 0.1.0-rc.2 → 0.1.0-rc.3
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/README.md +1 -1
- package/assets/studio.css +4 -2
- package/dist/studio.js +22 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ What it shows: the film on a stage (a quiet "drawing…" while the frame on scre
|
|
|
20
20
|
playhead's, sharp); play, step, jump to edits, zoom; the timeline with a ruler you scrub, filmstrips
|
|
21
21
|
of each clip, what is loaded (the green line under the ruler) and the playhead; an inspector for the
|
|
22
22
|
selected clip (its timing, source, props and where it is written) or the film. Keys: Space plays,
|
|
23
|
-
←/→ step (Shift: a second), ↑/↓ jump between edits, Home/End, = and − zoom, 0 fits.
|
|
23
|
+
←/→ step (Shift: a second), ↑/↓ jump between edits, Home/End, = and − zoom, 0 fits. The inspector and timeline also show what film.json says about the drawing: a clip's blend and opacity, and each transition's kind, noise and edge. What a scene draws inside itself (masks, strokes, perspective) is in its code, and shows in the picture.
|
|
24
24
|
|
|
25
25
|
`window.tongueStudio` drives it from outside (an agent, a check): the frame, the state, seek, play,
|
|
26
26
|
select, whether it has settled, and a hash of any frame's pixels.
|
package/assets/studio.css
CHANGED
|
@@ -79,8 +79,9 @@ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
|
79
79
|
/* ------------------------------------------------------------ stage and inspector */
|
|
80
80
|
|
|
81
81
|
.studio-main { display: grid; grid-template-columns: minmax(0, 1fr) 300px; min-height: 0; }
|
|
82
|
-
.stage { position: relative; display: grid; place-items: center; padding: 20px; min-height: 0; min-width: 0; background: var(--bg); }
|
|
83
|
-
|
|
82
|
+
.stage { position: relative; display: grid; place-items: center; padding: 20px; min-height: 0; min-width: 0; background: var(--bg); container-type: size; }
|
|
83
|
+
/* Sized by the stage (its width is set inline from the film's shape), never stretched. */
|
|
84
|
+
.stage-frame { position: relative; background: #000; border-radius: 3px; box-shadow: 0 0 0 1px var(--line); overflow: hidden; }
|
|
84
85
|
.stage-canvas { display: block; width: 100%; height: 100%; }
|
|
85
86
|
.stage-note { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); padding: 24px; text-align: center; }
|
|
86
87
|
.stage-note.bad { color: var(--bad); }
|
|
@@ -169,6 +170,7 @@ button.icon.play[data-playing] { color: var(--accent); background: var(--accent-
|
|
|
169
170
|
[data-tl-run] { top: 5px !important; bottom: 5px !important; border-radius: 2px; background: repeating-linear-gradient(90deg, var(--scene) 0 3px, var(--scene-edge) 3px 4px); opacity: 0.8; cursor: pointer; }
|
|
170
171
|
.row[data-kind="audio"] [data-tl-run] { background: repeating-linear-gradient(90deg, var(--audio) 0 3px, var(--audio-edge) 3px 4px); }
|
|
171
172
|
[data-tl-transition] { top: 5px !important; bottom: 5px !important; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); border-radius: 4px; }
|
|
173
|
+
[data-tl-transition][data-style="dissolve"] { background: linear-gradient(90deg, transparent, rgba(255, 183, 101, 0.42), transparent); }
|
|
172
174
|
|
|
173
175
|
.playhead { width: 0; z-index: 3; }
|
|
174
176
|
.playhead::before { content: ""; position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; background: var(--accent); }
|
package/dist/studio.js
CHANGED
|
@@ -93,8 +93,11 @@ function Stage(props) {
|
|
|
93
93
|
// Whether what is on screen is the playhead's frame, sharp: written to the element, not rendered.
|
|
94
94
|
useAtomEffect(timeline.shown, (s) => { if (stage.current)
|
|
95
95
|
stage.current.dataset["exact"] = s?.exact ? "yes" : "no"; });
|
|
96
|
-
const
|
|
97
|
-
|
|
96
|
+
const size = props.film ? props.film.doc.film : { width: 16, height: 9 };
|
|
97
|
+
// As large as the stage allows in both directions, in the film's shape (the stage is a size
|
|
98
|
+
// container: 100cqw and 100cqh are its width and height).
|
|
99
|
+
const frame = { aspectRatio: `${size.width} / ${size.height}`, width: `min(100cqw, calc(100cqh * ${size.width / size.height}))` };
|
|
100
|
+
return (_jsxs("section", { className: "stage", ref: stage, children: [_jsxs("div", { className: "stage-frame", style: frame, children: [_jsx("canvas", { ref: props.canvas, className: "stage-canvas" }), !props.film && !props.loadError ? _jsx("div", { className: "stage-note", children: "Opening the film\u2026" }) : null, props.loadError ? _jsx("div", { className: "stage-note bad", children: props.loadError }) : null, props.buildError ? (_jsxs("div", { className: "stage-error", role: "alert", children: [_jsx("strong", { children: "Build failed" }), _jsx("pre", { children: props.buildError }), _jsx("span", { children: "The last build that worked stays up; save a fix and it reloads." })] })) : null] }), _jsx("div", { className: "stage-drafting", "aria-hidden": "true", children: "drawing\u2026" })] }));
|
|
98
101
|
}
|
|
99
102
|
// ---------------------------------------------------------------- transport
|
|
100
103
|
function TransportBar(props) {
|
|
@@ -124,16 +127,30 @@ function Inspector(props) {
|
|
|
124
127
|
}
|
|
125
128
|
const mb = (bytes) => (bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.ceil(bytes / 1024)} KB`);
|
|
126
129
|
const KIND_NAMES = { scene: "Scene", video: "Video", image: "Image", audio: "Audio" };
|
|
130
|
+
/** A transition in words: what film.json says about it. */
|
|
131
|
+
function transitionText(t) {
|
|
132
|
+
switch (t.style) {
|
|
133
|
+
case "fade": return `Fade, ${t.frames} frames`;
|
|
134
|
+
case "wipe": return `Wipe ${t.direction}, ${t.frames} frames`;
|
|
135
|
+
case "push": return `Push ${t.direction}, ${t.frames} frames`;
|
|
136
|
+
case "morph": return `Morph, ${t.frames} frames`;
|
|
137
|
+
case "dissolve": return `Dissolve, ${t.noise} noise at ${t.size} px${t.edge ? `, ${t.edge} edge` : ""}, ${t.frames} frames`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
127
140
|
function ClipDetails(props) {
|
|
128
141
|
const { clip, fps } = props;
|
|
129
142
|
const doc = clip.doc;
|
|
130
143
|
const transport = useTransport();
|
|
131
|
-
|
|
144
|
+
const { timeline } = useTimelineContext();
|
|
145
|
+
const model = useAtomValue(timeline.model);
|
|
146
|
+
const transitions = model?.rows.find((r) => r.id === clip.row)?.transitions ?? [];
|
|
147
|
+
const into = transitions.find((t) => t.to === clip.id), outOf = transitions.find((t) => t.from === clip.id);
|
|
148
|
+
return (_jsxs("section", { className: "panel", children: [_jsxs("h2", { className: "clip-title", children: [_jsx("span", { className: "kind", "data-kind": clip.kind, children: KIND_NAMES[clip.kind] }), " ", clip.id] }), _jsxs("dl", { children: [_jsx(Field, { name: doc.kind === "scene" ? "Scene" : "File", children: clip.label }), _jsx(Field, { name: "Track", children: clip.row }), _jsx(Field, { name: "Starts", children: timecode(clip.start, fps) }), _jsx(Field, { name: "Ends", children: timecode(clip.end, fps) }), _jsxs(Field, { name: "Length", children: [seconds(clip.end - clip.start, fps), " \u00B7 ", clip.end - clip.start, " frames"] }), doc.kind === "video" || doc.kind === "audio" ? _jsxs(Field, { name: "Source", children: [doc.in.toFixed(2), " s \u2192 ", doc.out.toFixed(2), " s", doc.speed !== 1 ? ` at ${doc.speed}×` : ""] }) : null, doc.kind === "audio" ? _jsxs(Field, { name: "Gain", children: [doc.gain, " dB \u00B7 fades ", doc.fadeIn, "/", doc.fadeOut, " frames"] }) : null, doc.kind === "video" && (doc.reverse || doc.freeze) ? _jsx(Field, { name: "Plays", children: doc.freeze ? "frozen" : "reversed" }) : null, doc.kind !== "audio" && doc.blend ? _jsx(Field, { name: "Blend", children: doc.blend }) : null, doc.kind !== "audio" && doc.opacity !== 1 ? _jsx(Field, { name: "Opacity", children: doc.opacity }) : null, into ? _jsx(Field, { name: "Comes in", children: transitionText(into.doc) }) : null, outOf ? _jsx(Field, { name: "Goes out", children: transitionText(outOf.doc) }) : null, doc.origin ? _jsxs(Field, { name: "Written at", children: [doc.origin.file, ":", doc.origin.line] }) : null] }), doc.kind === "scene" ? _jsx("pre", { className: "props", children: JSON.stringify(doc.props, null, 2) }) : null, _jsxs("div", { className: "panel-actions", children: [_jsx("button", { type: "button", className: "text", onClick: () => transport.seek(clip.start), children: "Go to its start" }), _jsx("button", { type: "button", className: "text", onClick: () => transport.send({ type: "range", from: clip.start, to: clip.end - 1 }), children: "Play just this" })] })] }));
|
|
132
149
|
}
|
|
133
150
|
// ---------------------------------------------------------------- timeline
|
|
134
151
|
const StudioClip = memo(function StudioClip(props) {
|
|
135
152
|
const { clip } = props;
|
|
136
|
-
return (_jsxs(Timeline.Clip, { clip: clip, className: "clip", children: [props.picture && clip.kind !== "audio" ? _jsx(Filmstrip, { clip: clip, className: "clip-strip" }) : null, _jsxs("span", { className: "clip-label", children: [_jsx("span", { className: "clip-name", children: clip.detail ?? clip.label }), clip.detail ? _jsx("span", { className: "clip-sub", children: clip.label }) : null] })] }));
|
|
153
|
+
return (_jsxs(Timeline.Clip, { clip: clip, className: "clip", children: [props.picture && clip.kind !== "audio" ? _jsx(Filmstrip, { clip: clip, className: "clip-strip" }) : null, _jsxs("span", { className: "clip-label", children: [_jsx("span", { className: "clip-name", children: clip.detail ?? clip.label }), clip.detail ? _jsx("span", { className: "clip-sub", children: clip.label }) : clip.doc.kind !== "audio" && clip.doc.blend ? _jsx("span", { className: "clip-sub", children: clip.doc.blend }) : null] })] }));
|
|
137
154
|
});
|
|
138
155
|
function RowHeader(props) {
|
|
139
156
|
return (_jsxs("div", { className: "row-header", "data-kind": props.row.kind, children: [props.row.kind === "audio" ? _jsx(SoundIcon, {}) : _jsx(PictureIcon, {}), _jsx("span", { children: props.row.id })] }));
|
|
@@ -141,5 +158,5 @@ function RowHeader(props) {
|
|
|
141
158
|
function TimelinePanel() {
|
|
142
159
|
const { timeline } = useTimelineContext();
|
|
143
160
|
const rows = useAtomValue(timeline.rows);
|
|
144
|
-
return (_jsxs("section", { className: "timeline", children: [_jsxs("div", { className: "timeline-headers", children: [_jsx("div", { className: "ruler-spacer" }), rows.map((row) => _jsx(RowHeader, { row: row }, row.id))] }), _jsx(Timeline.Root, { className: "timeline-body", children: _jsxs(Timeline.Scroller, { className: "timeline-scroller", children: [_jsx(Timeline.Ruler, { className: "ruler", renderTick: (tick) => (tick.label ? _jsx("span", { className: "tick-label", children: tick.label }) : null) }), _jsx(Timeline.Ready, { className: "ready" }), _jsx(Timeline.PlayRange, { className: "range" }), _jsx(Timeline.Rows, { children: (row) => (_jsx(Timeline.Row, { row: row, className: "row", renderClip: (clip) => _jsx(StudioClip, { clip: clip, picture: row.kind === "visual" }, clip.id) }, row.id)) }), _jsx(Timeline.Playhead, { className: "playhead", children: _jsx("span", { className: "playhead-handle" }) })] }) })] }));
|
|
161
|
+
return (_jsxs("section", { className: "timeline", children: [_jsxs("div", { className: "timeline-headers", children: [_jsx("div", { className: "ruler-spacer" }), rows.map((row) => _jsx(RowHeader, { row: row }, row.id))] }), _jsx(Timeline.Root, { className: "timeline-body", children: _jsxs(Timeline.Scroller, { className: "timeline-scroller", children: [_jsx(Timeline.Ruler, { className: "ruler", renderTick: (tick) => (tick.label ? _jsx("span", { className: "tick-label", children: tick.label }) : null) }), _jsx(Timeline.Ready, { className: "ready" }), _jsx(Timeline.PlayRange, { className: "range" }), _jsx(Timeline.Rows, { children: (row) => (_jsx(Timeline.Row, { row: row, className: "row", renderClip: (clip) => _jsx(StudioClip, { clip: clip, picture: row.kind === "visual" }, clip.id), renderTransition: (t) => _jsx(Timeline.Transition, { transition: t, title: transitionText(t.doc) }, `${t.from}>${t.to}`) }, row.id)) }), _jsx(Timeline.Playhead, { className: "playhead", children: _jsx("span", { className: "playhead-handle" }) })] }) })] }));
|
|
145
162
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tongue-core/studio",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.3",
|
|
4
4
|
"description": "Tongue's studio: a film's player, timeline and inspector in the browser, built only from @tongue-core/ui. Served by `tongue studio`, for a project on this machine or a build anywhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@effect/atom-react": "4.0.6",
|
|
31
|
-
"@tongue-core/project": "0.1.0-rc.
|
|
32
|
-
"@tongue-core/ui": "0.1.0-rc.
|
|
31
|
+
"@tongue-core/project": "0.1.0-rc.3",
|
|
32
|
+
"@tongue-core/ui": "0.1.0-rc.3",
|
|
33
33
|
"effect": "4.0.6",
|
|
34
34
|
"react": "19.3.0",
|
|
35
35
|
"react-dom": "19.3.0"
|