vexy-stax-js 3.0.10 → 3.1.1
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/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/schema/vexy-stax-scene.schema.json +13 -0
- package/src/geometry.js +12 -10
- package/src/index.js +22 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
All notable changes to this project are documented here.
|
|
6
6
|
|
|
7
|
+
## [3.0.11] — issues 335, 336, 337
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`video` scene section** (335 / 336): `scene.js` `parseVideo()` + the JSON schema now accept and
|
|
12
|
+
validate the `video` section (`width`/`height`/`fps`/`frames`/`first_hold`/`last_hold`), mirroring
|
|
13
|
+
`vexy_stax.scene.Video`. Previously the strict parser rejected the key with "Unknown key 'video' in
|
|
14
|
+
scene", which broke the Python `playwright` engine (it mounts the scene in `<vexy-stax>`) — issue
|
|
15
|
+
336. The element now accepts a scene carrying `video`.
|
|
16
|
+
- **Held first/last still frames in `toVideo()`** (335 §2): the video export now bookends the clip
|
|
17
|
+
with held stills — it renders the start endpoint and captures it `video.first_hold` times (default
|
|
18
|
+
10), plays the transition, then captures the end endpoint `video.last_hold` times (still →
|
|
19
|
+
transition → still). Mirrors `geometry.py`'s `frame_plan` holds. Verified: the exported
|
|
20
|
+
`airbl-transition.mp4` first/last frames are static.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Compact view reserved empty caption space** (337): `compactCamera` now fits ONLY the frontmost
|
|
25
|
+
slide plate (height `H`, aimed at the slide center `Y = lift`) instead of the slide+caption
|
|
26
|
+
composite, so the compact view fills the frame with no caption padding. Mirrors `geometry.py`
|
|
27
|
+
(issue 337). Verified: the playwright-rendered compact still fills the frame; geometry test updated.
|
|
28
|
+
|
|
7
29
|
## [3.0.10] — issues 331
|
|
8
30
|
|
|
9
31
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vexy-stax-js",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Browser renderer for the vexy-stax shared scene format: 3D glass plates in two views, with morphable opacity. Ships as ESM, Web Component, and a classic-script global.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -70,6 +70,19 @@
|
|
|
70
70
|
"background": { "type": "string", "default": "#ffffff" },
|
|
71
71
|
"juicy": { "type": "boolean", "default": false, "description": "Python-only per-channel color match." },
|
|
72
72
|
"captions": { "type": "boolean", "default": true, "description": "Global captions toggle (issue 332). ON: each slide plate sits on top of its on-floor caption plate (stacked layout). OFF: no caption plates; slide plates sit directly on the floor." },
|
|
73
|
+
"video": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"description": "Video render params (issue 335 §3): dimensions/fps/frame counts/held stills. transition still owns the animation (kind/easing/wait/duration); video owns the output framing. Defaults preserve prior behavior.",
|
|
77
|
+
"properties": {
|
|
78
|
+
"width": { "type": "integer", "minimum": 1, "description": "Video width; omitted ⇒ size.width." },
|
|
79
|
+
"height": { "type": "integer", "minimum": 1, "description": "Video height; omitted ⇒ size.height." },
|
|
80
|
+
"fps": { "type": "integer", "minimum": 1, "description": "Video frames per second; omitted ⇒ transition.fps (else 30). Overrides transition.fps when set." },
|
|
81
|
+
"frames": { "type": "integer", "minimum": 1, "description": "Transition frames PER LEG; omitted ⇒ round(transition.duration × fps)." },
|
|
82
|
+
"first_hold": { "type": "integer", "minimum": 0, "default": 10, "description": "Held copies of the FIRST frame (still intro)." },
|
|
83
|
+
"last_hold": { "type": "integer", "minimum": 0, "default": 10, "description": "Held copies of the LAST frame (still outro)." }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
73
86
|
"caption_defaults": { "$ref": "#/$defs/captionStyle" },
|
|
74
87
|
"caption_fade": {
|
|
75
88
|
"type": "object",
|
package/src/geometry.js
CHANGED
|
@@ -330,13 +330,14 @@ export function expandedCamera(scene, viewportAspect) {
|
|
|
330
330
|
export function compactCamera(scene, viewportAspect) {
|
|
331
331
|
const cam = scene.camera;
|
|
332
332
|
const depth = stackDepth(scene, "compact");
|
|
333
|
-
// Issue
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
333
|
+
// Issue 337: the compact view frames ONLY the frontmost SLIDE plate — not the composite with
|
|
334
|
+
// its caption row. Captions are invisible in compact (they fade in only as the deck expands),
|
|
335
|
+
// so reserving the caption-plate height just padded the frame. The slide is lifted by `lift`
|
|
336
|
+
// (issue 332: it sits on top of the on-floor caption plate), so its center is at Y = lift and
|
|
337
|
+
// it spans height H. Aim at the slide center and fit H so the slide fills the frame tight.
|
|
338
|
+
// Mirrors geometry.py.
|
|
337
339
|
const lift = slideLift(scene);
|
|
338
|
-
const
|
|
339
|
-
const target = [0.0, lift / 2.0, -depth / 2.0];
|
|
340
|
+
const target = [0.0, lift, -depth / 2.0];
|
|
340
341
|
|
|
341
342
|
let isPercent = false;
|
|
342
343
|
let pctVal = 90.0;
|
|
@@ -351,15 +352,16 @@ export function compactCamera(scene, viewportAspect) {
|
|
|
351
352
|
|
|
352
353
|
let distance;
|
|
353
354
|
if (isPercent) {
|
|
354
|
-
// Dual-axis crop-free fit (SPEC.md §3, issue 302 §1): fit the frontmost
|
|
355
|
-
// (width W, height H
|
|
356
|
-
// ever has extra padding (never a crop). distance = max(d_w, d_h).
|
|
355
|
+
// Dual-axis crop-free fit (SPEC.md §3, issue 302 §1, issue 337): fit the frontmost SLIDE
|
|
356
|
+
// plate (width W, height H — NOT the caption composite) so the limiting axis touches P% and
|
|
357
|
+
// the other axis only ever has extra padding (never a crop). distance = max(d_w, d_h).
|
|
358
|
+
// Mirrors geometry.py.
|
|
357
359
|
const hfov = (cam.fov * Math.PI) / 180.0;
|
|
358
360
|
const aspect = viewportAspect || scene.size.width / scene.size.height;
|
|
359
361
|
const vfov = 2.0 * Math.atan(Math.tan(hfov / 2.0) / aspect);
|
|
360
362
|
const frac = pctVal / 100.0;
|
|
361
363
|
const dW = scene.size.width / (2.0 * Math.tan(hfov / 2.0) * frac);
|
|
362
|
-
const dH =
|
|
364
|
+
const dH = scene.size.height / (2.0 * Math.tan(vfov / 2.0) * frac);
|
|
363
365
|
const distToZ0 = Math.max(dW, dH);
|
|
364
366
|
distance = distToZ0 + depth / 2.0;
|
|
365
367
|
} else {
|
package/src/index.js
CHANGED
|
@@ -205,13 +205,28 @@ export class VexyStax {
|
|
|
205
205
|
await this._ready;
|
|
206
206
|
const kind = opts.kind ?? this.scene.transition?.kind;
|
|
207
207
|
if (!kind) throw new Error("VexyStax.toVideo: no kind given and scene.transition is null");
|
|
208
|
-
const fps = this.scene.transition?.fps ?? 30;
|
|
208
|
+
const fps = this.scene.video?.fps ?? this.scene.transition?.fps ?? 30;
|
|
209
209
|
const canvas = this.stage.renderer.domElement;
|
|
210
|
+
// Issue 335 §2: bookend the clip with HELD STILLS — render the start frame and capture it
|
|
211
|
+
// `first_hold` times, then the transition, then capture the end frame `last_hold` times
|
|
212
|
+
// (still → transition → still). Defaults 10/10 from scene.video. Mirrors geometry.py's
|
|
213
|
+
// frame_plan holds (the Python engines get holds via frame_plan; here toVideo drives a
|
|
214
|
+
// real-time capture, so we hold by capturing the boundary frames repeatedly).
|
|
215
|
+
const firstHold = this.scene.video?.first_hold ?? 10;
|
|
216
|
+
const lastHold = this.scene.video?.last_hold ?? 10;
|
|
217
|
+
const { startMorph, endMorph } = transitionEndpoints(kind);
|
|
218
|
+
const aspect = this.stage.camera.aspect;
|
|
210
219
|
|
|
211
220
|
return recordVideo({
|
|
212
221
|
canvas,
|
|
213
222
|
fps,
|
|
214
223
|
run: async (onFrame) => {
|
|
224
|
+
// Held still intro: snap to the start endpoint and capture it `firstHold` times.
|
|
225
|
+
const startState = frameStateAt(this.scene, startMorph, aspect);
|
|
226
|
+
this.stage.applyFrameState(startState, startMorph);
|
|
227
|
+
this.stage.render();
|
|
228
|
+
for (let i = 0; i < firstHold; i++) onFrame(startState);
|
|
229
|
+
|
|
215
230
|
const controller = playTransition(this.scene, (state) => {
|
|
216
231
|
const t = this._morphFromGaps(state.gaps);
|
|
217
232
|
this.stage.applyFrameState(state, t);
|
|
@@ -219,6 +234,12 @@ export class VexyStax {
|
|
|
219
234
|
onFrame(state);
|
|
220
235
|
}, { kind });
|
|
221
236
|
await controller.promise;
|
|
237
|
+
|
|
238
|
+
// Held still outro: snap to the end endpoint and capture it `lastHold` times.
|
|
239
|
+
const endState = frameStateAt(this.scene, endMorph, aspect);
|
|
240
|
+
this.stage.applyFrameState(endState, endMorph);
|
|
241
|
+
this.stage.render();
|
|
242
|
+
for (let i = 0; i < lastHold; i++) onFrame(endState);
|
|
222
243
|
},
|
|
223
244
|
});
|
|
224
245
|
}
|