@markdy/renderer-dom 0.6.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { SceneAST, ParseWarning } from '@markdy/core';
2
+
1
3
  /**
2
4
  * @markdy/renderer-dom — Player
3
5
  *
@@ -19,10 +21,18 @@
19
21
  * each actor's before-phase state falls through to the inline style we set,
20
22
  * which gives correct initial positions and opacity values.
21
23
  */
24
+
22
25
  interface PlayerOptions {
23
26
  container: HTMLElement;
24
27
  code: string;
25
28
  assets?: Record<string, string>;
29
+ /**
30
+ * Host-resolved ASTs for `import "<path>" as <ns>` statements. When
31
+ * provided, the namespaces' `vars`, `defs`, and `seqs` merge into the
32
+ * parsed scene under `ns.<name>`. Unresolved namespaces emit a soft
33
+ * `import-unresolved` warning.
34
+ */
35
+ imports?: Record<string, SceneAST>;
26
36
  autoplay?: boolean;
27
37
  /** Loop the animation when it reaches the end. Defaults to true. */
28
38
  loop?: boolean;
@@ -30,6 +40,12 @@ interface PlayerOptions {
30
40
  copyright?: boolean;
31
41
  /** Show a rainbow progress bar around the viewport border. Defaults to true. */
32
42
  progressBar?: boolean;
43
+ /**
44
+ * Invoked once per soft `ParseWarning` emitted by the parser — e.g. unknown
45
+ * actions, unknown modifier keys, unresolved imports. Defaults to `console.warn`.
46
+ * Pass a no-op to silence; pass a custom collector to surface warnings in a UI.
47
+ */
48
+ onWarning?: (warning: ParseWarning) => void;
33
49
  }
34
50
  interface Player {
35
51
  play(): void;
package/dist/index.js CHANGED
@@ -14,6 +14,9 @@ function stateFrom(def) {
14
14
  function tx(s) {
15
15
  return `translate(${s.x}px, ${s.y}px) scale(${s.scale}) rotate(${s.rotate}deg)`;
16
16
  }
17
+ function txCaption(s) {
18
+ return `translate(calc(${s.x}px - 50%), calc(${s.y}px - 50%)) scale(${s.scale}) rotate(${s.rotate}deg)`;
19
+ }
17
20
  var EASE_MAP = {
18
21
  linear: "linear",
19
22
  in: "ease-in",
@@ -335,6 +338,31 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
335
338
  el = div;
336
339
  break;
337
340
  }
341
+ case "caption": {
342
+ const div = document.createElement("div");
343
+ div.textContent = def.args[0] ?? "";
344
+ div.dataset.markdyCaption = def.anchor ?? "top";
345
+ Object.assign(div.style, {
346
+ fontSize: `${def.size ?? 32}px`,
347
+ fontFamily: "system-ui, -apple-system, sans-serif",
348
+ fontWeight: "700",
349
+ whiteSpace: "nowrap",
350
+ textAlign: "center",
351
+ lineHeight: "1.1",
352
+ padding: "6px 14px",
353
+ borderRadius: "4px",
354
+ background: "rgba(0, 0, 0, 0.55)",
355
+ color: "#fff",
356
+ textShadow: "0 2px 6px rgba(0, 0, 0, 0.45)",
357
+ userSelect: "none",
358
+ pointerEvents: "none"
359
+ // Center the caption on its (x, y) point (x = sceneWidth/2).
360
+ // We combine translate-centering with the actor transform in the
361
+ // dataset below so the player can re-apply on state changes.
362
+ });
363
+ el = div;
364
+ break;
365
+ }
338
366
  case "figure": {
339
367
  el = createFigureEl(def);
340
368
  break;
@@ -354,9 +382,10 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
354
382
  el.style.left = "0";
355
383
  el.style.top = "0";
356
384
  el.style.transformOrigin = "center center";
357
- el.style.transform = tx(stateFrom(def));
385
+ el.style.transform = def.type === "caption" ? txCaption(stateFrom(def)) : tx(stateFrom(def));
358
386
  el.style.opacity = String(def.opacity ?? 1);
359
387
  if (def.z !== void 0) el.style.zIndex = String(def.z);
388
+ else if (def.type === "caption") el.style.zIndex = "100";
360
389
  return el;
361
390
  }
362
391
 
@@ -380,12 +409,15 @@ function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
380
409
  for (const [name, def] of Object.entries(ast.actors)) {
381
410
  states.set(name, stateFrom(def));
382
411
  }
412
+ const captionActors = /* @__PURE__ */ new Set();
413
+ for (const [name, def] of Object.entries(ast.actors)) {
414
+ if (def.type === "caption") captionActors.add(name);
415
+ }
416
+ const txFor = (actorName) => captionActors.has(actorName) ? txCaption : tx;
383
417
  const events = [...ast.events].sort((a, b) => a.time - b.time);
384
- preInitInlineStyles(ast, actorEls, states, events);
418
+ preInitInlineStyles(ast, actorEls, states, events, txFor);
419
+ const cameraState = freshCameraState();
385
420
  for (const ev of events) {
386
- const el = actorEls.get(ev.actor);
387
- const s = states.get(ev.actor);
388
- if (!el || !s) continue;
389
421
  const delayMs = ev.time * 1e3;
390
422
  const durMs = Math.max(
391
423
  1,
@@ -398,13 +430,39 @@ function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
398
430
  fill: "forwards",
399
431
  easing
400
432
  };
401
- buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, assetOverrides, faceSwaps, anims);
433
+ if (ev.actor === "camera") {
434
+ buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState);
435
+ continue;
436
+ }
437
+ const el = actorEls.get(ev.actor);
438
+ const s = states.get(ev.actor);
439
+ if (!el || !s) continue;
440
+ buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, assetOverrides, faceSwaps, anims, txFor(ev.actor));
402
441
  }
403
442
  return anims;
404
443
  }
405
- function preInitInlineStyles(ast, actorEls, states, events) {
444
+ function offscreenState(s, direction, sceneWidth, sceneHeight) {
445
+ const out = { ...s };
446
+ switch (direction) {
447
+ case "left":
448
+ out.x = -sceneWidth * 1.1;
449
+ break;
450
+ case "right":
451
+ out.x = sceneWidth * 2.1;
452
+ break;
453
+ case "top":
454
+ out.y = -sceneHeight * 1.1;
455
+ break;
456
+ case "bottom":
457
+ out.y = sceneHeight * 2.1;
458
+ break;
459
+ }
460
+ return out;
461
+ }
462
+ function preInitInlineStyles(ast, actorEls, states, events, txFor) {
406
463
  const firstEventByActor = /* @__PURE__ */ new Map();
407
464
  for (const ev of events) {
465
+ if (ev.actor === "camera") continue;
408
466
  if (!firstEventByActor.has(ev.actor)) firstEventByActor.set(ev.actor, ev);
409
467
  }
410
468
  for (const [name, def] of Object.entries(ast.actors)) {
@@ -412,31 +470,78 @@ function preInitInlineStyles(ast, actorEls, states, events) {
412
470
  const s = states.get(name);
413
471
  if (!el || !s) continue;
414
472
  const firstEv = firstEventByActor.get(name);
473
+ const txFn = txFor(name);
415
474
  if (firstEv?.action === "enter") {
416
475
  const from = String(firstEv.params.from ?? "left");
417
- const offscreen = { ...s };
418
- switch (from) {
419
- case "left":
420
- offscreen.x = -ast.meta.width * 1.1;
421
- break;
422
- case "right":
423
- offscreen.x = ast.meta.width * 2.1;
424
- break;
425
- case "top":
426
- offscreen.y = -ast.meta.height * 1.1;
427
- break;
428
- case "bottom":
429
- offscreen.y = ast.meta.height * 2.1;
430
- break;
431
- }
432
- el.style.transform = tx(offscreen);
476
+ const offscreen = offscreenState(s, from, ast.meta.width, ast.meta.height);
477
+ el.style.transform = txFn(offscreen);
433
478
  }
434
479
  if (firstEv?.action === "fade_in" && (def.opacity === void 0 || def.opacity > 0)) {
435
480
  el.style.opacity = "0";
436
481
  }
437
482
  }
438
483
  }
439
- function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, assetOverrides, faceSwaps, anims) {
484
+ function freshCameraState() {
485
+ return { x: 0, y: 0, zoom: 1 };
486
+ }
487
+ function cameraTx(s) {
488
+ return `translate(${-s.x}px, ${-s.y}px) scale(${s.zoom})`;
489
+ }
490
+ function buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState) {
491
+ const s = cameraState;
492
+ switch (ev.action) {
493
+ case "pan": {
494
+ const to = ev.params.to;
495
+ if (!to) break;
496
+ const sceneW = ast.meta.width;
497
+ const sceneH = ast.meta.height;
498
+ const targetX = to[0] - sceneW / 2;
499
+ const targetY = to[1] - sceneH / 2;
500
+ const next = { ...s, x: targetX, y: targetY };
501
+ anims.push(
502
+ scene.animate(
503
+ [{ transform: cameraTx(s) }, { transform: cameraTx(next) }],
504
+ baseOpts
505
+ )
506
+ );
507
+ s.x = next.x;
508
+ s.y = next.y;
509
+ break;
510
+ }
511
+ case "zoom": {
512
+ const to = typeof ev.params.to === "number" ? ev.params.to : s.zoom;
513
+ const next = { ...s, zoom: to };
514
+ anims.push(
515
+ scene.animate(
516
+ [{ transform: cameraTx(s) }, { transform: cameraTx(next) }],
517
+ baseOpts
518
+ )
519
+ );
520
+ s.zoom = next.zoom;
521
+ break;
522
+ }
523
+ case "shake": {
524
+ const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : 8;
525
+ anims.push(
526
+ scene.animate(
527
+ [
528
+ { transform: cameraTx(s), offset: 0 },
529
+ { transform: cameraTx({ ...s, x: s.x - mag, y: s.y - mag * 0.4 }), offset: 0.15 },
530
+ { transform: cameraTx({ ...s, x: s.x + mag, y: s.y + mag * 0.4 }), offset: 0.35 },
531
+ { transform: cameraTx({ ...s, x: s.x - mag * 0.6, y: s.y + mag * 0.3 }), offset: 0.55 },
532
+ { transform: cameraTx({ ...s, x: s.x + mag * 0.5, y: s.y - mag * 0.3 }), offset: 0.75 },
533
+ { transform: cameraTx(s), offset: 1 }
534
+ ],
535
+ { ...baseOpts, easing: "linear" }
536
+ )
537
+ );
538
+ break;
539
+ }
540
+ default:
541
+ break;
542
+ }
543
+ }
544
+ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, assetOverrides, faceSwaps, anims, txFn) {
440
545
  switch (ev.action) {
441
546
  // ── move ────────────────────────────────────────────────────────────────
442
547
  case "move": {
@@ -445,7 +550,7 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
445
550
  const toY = toArr?.[1] ?? s.y;
446
551
  anims.push(
447
552
  el.animate(
448
- [{ transform: tx(s) }, { transform: tx({ ...s, x: toX, y: toY }) }],
553
+ [{ transform: txFn(s) }, { transform: txFn({ ...s, x: toX, y: toY }) }],
449
554
  baseOpts
450
555
  )
451
556
  );
@@ -454,29 +559,45 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
454
559
  break;
455
560
  }
456
561
  // ── enter ───────────────────────────────────────────────────────────────
562
+ // Slides the actor from the given screen edge into its current (s)
563
+ // position while also restoring opacity to 1. The opacity restore makes
564
+ // `enter` symmetric with `exit` so actors can re-enter after exiting
565
+ // without needing a separate fade_in.
457
566
  case "enter": {
458
567
  const from = String(ev.params.from ?? "left");
459
- const fromState = { ...s };
460
- switch (from) {
461
- case "left":
462
- fromState.x = -ast.meta.width;
463
- break;
464
- case "right":
465
- fromState.x = ast.meta.width * 2;
466
- break;
467
- case "top":
468
- fromState.y = -ast.meta.height;
469
- break;
470
- case "bottom":
471
- fromState.y = ast.meta.height * 2;
472
- break;
473
- }
568
+ const fromState = offscreenState(s, from, ast.meta.width, ast.meta.height);
474
569
  anims.push(
475
570
  el.animate(
476
- [{ transform: tx(fromState) }, { transform: tx(s) }],
571
+ [
572
+ { transform: txFn(fromState), opacity: s.opacity },
573
+ { transform: txFn(s), opacity: 1 }
574
+ ],
575
+ baseOpts
576
+ )
577
+ );
578
+ s.opacity = 1;
579
+ break;
580
+ }
581
+ // ── exit ────────────────────────────────────────────────────────────────
582
+ // Mirror of enter: slides off-screen in the given direction while also
583
+ // fading opacity to 0. Universal — works on any actor type (caption, text,
584
+ // figure, box, sprite). Leaves `s` at the off-screen state since the
585
+ // actor is conceptually gone after this.
586
+ case "exit": {
587
+ const to = String(ev.params.to ?? "right");
588
+ const toState = offscreenState(s, to, ast.meta.width, ast.meta.height);
589
+ anims.push(
590
+ el.animate(
591
+ [
592
+ { transform: txFn(s), opacity: s.opacity },
593
+ { transform: txFn(toState), opacity: 0 }
594
+ ],
477
595
  baseOpts
478
596
  )
479
597
  );
598
+ s.x = toState.x;
599
+ s.y = toState.y;
600
+ s.opacity = 0;
480
601
  break;
481
602
  }
482
603
  // ── fade_in ─────────────────────────────────────────────────────────────
@@ -498,7 +619,7 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
498
619
  const toScale = typeof ev.params.to === "number" ? ev.params.to : s.scale;
499
620
  anims.push(
500
621
  el.animate(
501
- [{ transform: tx(s) }, { transform: tx({ ...s, scale: toScale }) }],
622
+ [{ transform: txFn(s) }, { transform: txFn({ ...s, scale: toScale }) }],
502
623
  baseOpts
503
624
  )
504
625
  );
@@ -510,7 +631,7 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
510
631
  const toDeg = typeof ev.params.to === "number" ? ev.params.to : s.rotate;
511
632
  anims.push(
512
633
  el.animate(
513
- [{ transform: tx(s) }, { transform: tx({ ...s, rotate: toDeg }) }],
634
+ [{ transform: txFn(s) }, { transform: txFn({ ...s, rotate: toDeg }) }],
514
635
  baseOpts
515
636
  )
516
637
  );
@@ -523,12 +644,12 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
523
644
  anims.push(
524
645
  el.animate(
525
646
  [
526
- { transform: tx(s), offset: 0 },
527
- { transform: tx({ ...s, x: s.x + mag }), offset: 0.2 },
528
- { transform: tx({ ...s, x: s.x - mag }), offset: 0.4 },
529
- { transform: tx({ ...s, x: s.x + mag }), offset: 0.6 },
530
- { transform: tx({ ...s, x: s.x - mag }), offset: 0.8 },
531
- { transform: tx(s), offset: 1 }
647
+ { transform: txFn(s), offset: 0 },
648
+ { transform: txFn({ ...s, x: s.x + mag }), offset: 0.2 },
649
+ { transform: txFn({ ...s, x: s.x - mag }), offset: 0.4 },
650
+ { transform: txFn({ ...s, x: s.x + mag }), offset: 0.6 },
651
+ { transform: txFn({ ...s, x: s.x - mag }), offset: 0.8 },
652
+ { transform: txFn(s), offset: 1 }
532
653
  ],
533
654
  { ...baseOpts, easing: "linear" }
534
655
  )
@@ -739,12 +860,12 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
739
860
  anims.push(
740
861
  el.animate(
741
862
  [
742
- { transform: tx(s), offset: 0 },
743
- { transform: tx({ ...s, scale: s.scale * 0.9 }), offset: 0.1 },
744
- { transform: tx({ ...s, y: s.y - jHeight, scale: s.scale * 1.1 }), offset: 0.45 },
745
- { transform: tx({ ...s, y: s.y - jHeight * 0.3, scale: s.scale * 1.05 }), offset: 0.7 },
746
- { transform: tx({ ...s, scale: s.scale * 0.92 }), offset: 0.88 },
747
- { transform: tx(s), offset: 1 }
863
+ { transform: txFn(s), offset: 0 },
864
+ { transform: txFn({ ...s, scale: s.scale * 0.9 }), offset: 0.1 },
865
+ { transform: txFn({ ...s, y: s.y - jHeight, scale: s.scale * 1.1 }), offset: 0.45 },
866
+ { transform: txFn({ ...s, y: s.y - jHeight * 0.3, scale: s.scale * 1.05 }), offset: 0.7 },
867
+ { transform: txFn({ ...s, scale: s.scale * 0.92 }), offset: 0.88 },
868
+ { transform: txFn(s), offset: 1 }
748
869
  ],
749
870
  { ...baseOpts, easing: "ease-in-out" }
750
871
  )
@@ -779,24 +900,24 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
779
900
  case "bounce": {
780
901
  const bIntensity = typeof ev.params.intensity === "number" ? ev.params.intensity : 15;
781
902
  const bCount = typeof ev.params.count === "number" ? ev.params.count : 3;
782
- const keyframes = [{ transform: tx(s), offset: 0 }];
903
+ const keyframes = [{ transform: txFn(s), offset: 0 }];
783
904
  for (let bi = 0; bi < bCount; bi++) {
784
905
  const amp = bIntensity * Math.pow(0.55, bi);
785
906
  const baseOffset = (bi + 0.5) / (bCount + 0.5);
786
907
  const peakOffset = Math.min(baseOffset, 0.98);
787
908
  const valleyOffset = Math.min(baseOffset + 0.25 / (bCount + 0.5), 0.99);
788
909
  keyframes.push({
789
- transform: tx({ ...s, y: s.y - amp }),
910
+ transform: txFn({ ...s, y: s.y - amp }),
790
911
  offset: peakOffset
791
912
  });
792
913
  if (bi < bCount - 1) {
793
914
  keyframes.push({
794
- transform: tx(s),
915
+ transform: txFn(s),
795
916
  offset: valleyOffset
796
917
  });
797
918
  }
798
919
  }
799
- keyframes.push({ transform: tx(s), offset: 1 });
920
+ keyframes.push({ transform: txFn(s), offset: 1 });
800
921
  anims.push(
801
922
  el.animate(keyframes, { ...baseOpts, easing: "ease-out" })
802
923
  );
@@ -872,9 +993,20 @@ function bgToTextColor(bg) {
872
993
  return named[bg.toLowerCase()] ?? "#1a1a1a";
873
994
  }
874
995
  function createPlayer(opts) {
875
- const { container, code, assets: assetOverrides = {}, autoplay = true, loop = true, copyright = true, progressBar = true } = opts;
876
- const ast = parse(code);
996
+ const {
997
+ container,
998
+ code,
999
+ assets: assetOverrides = {},
1000
+ imports,
1001
+ autoplay = true,
1002
+ loop = true,
1003
+ copyright = true,
1004
+ progressBar = true,
1005
+ onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message} (${w.kind})`)
1006
+ } = opts;
1007
+ const ast = parse(code, imports ? { imports } : void 0);
877
1008
  const totalDurationMs = (ast.meta.duration ?? 0) * 1e3;
1009
+ for (const w of ast.warnings) onWarning(w);
878
1010
  const viewport = document.createElement("div");
879
1011
  Object.assign(viewport.style, {
880
1012
  position: "relative",
@@ -953,6 +1085,17 @@ function createPlayer(opts) {
953
1085
  transformOrigin: "0 0"
954
1086
  });
955
1087
  viewport.appendChild(scene);
1088
+ const sceneContent = document.createElement("div");
1089
+ Object.assign(sceneContent.style, {
1090
+ position: "absolute",
1091
+ top: "0",
1092
+ left: "0",
1093
+ width: "100%",
1094
+ height: "100%",
1095
+ transformOrigin: "50% 50%",
1096
+ willChange: "transform"
1097
+ });
1098
+ scene.appendChild(sceneContent);
956
1099
  function scaleScene() {
957
1100
  const s = viewport.clientWidth / ast.meta.width;
958
1101
  scene.style.transform = `scale(${s})`;
@@ -963,11 +1106,11 @@ function createPlayer(opts) {
963
1106
  const actorEls = /* @__PURE__ */ new Map();
964
1107
  for (const [name, def] of Object.entries(ast.actors)) {
965
1108
  const el = createActorEl(name, def, ast.assets, assetOverrides);
966
- scene.appendChild(el);
1109
+ sceneContent.appendChild(el);
967
1110
  actorEls.set(name, el);
968
1111
  }
969
1112
  const faceSwaps = [];
970
- const allAnims = buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps);
1113
+ const allAnims = buildAnimations(ast, actorEls, sceneContent, assetOverrides, faceSwaps);
971
1114
  faceSwaps.sort((a, b) => a.timeMs - b.timeMs);
972
1115
  for (const anim of allAnims) {
973
1116
  anim.pause();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Web Animations API renderer for MarkdyScript scenes.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,15 +43,18 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@markdy/core": "0.6.0"
46
+ "@markdy/core": "0.7.1"
47
47
  },
48
48
  "devDependencies": {
49
+ "jsdom": "^26.0.0",
49
50
  "tsup": "^8.5.1",
50
- "typescript": "^5.9.3"
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.1.4"
51
53
  },
52
54
  "scripts": {
53
55
  "build": "tsup",
54
56
  "typecheck": "tsc --noEmit",
55
- "lint": "tsc --noEmit"
57
+ "lint": "tsc --noEmit",
58
+ "test": "vitest run"
56
59
  }
57
60
  }