@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 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
- .stage-frame { position: relative; max-width: 100%; max-height: 100%; height: 100%; background: #000; border-radius: 3px; box-shadow: 0 0 0 1px var(--line); overflow: hidden; }
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 aspect = props.film ? `${props.film.doc.film.width} / ${props.film.doc.film.height}` : "16 / 9";
97
- return (_jsxs("section", { className: "stage", ref: stage, children: [_jsxs("div", { className: "stage-frame", style: { aspectRatio: aspect }, 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" })] }));
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
- 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.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" })] })] }));
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.2",
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.2",
32
- "@tongue-core/ui": "0.1.0-rc.2",
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"