@markdy/renderer-dom 0.5.7 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +240 -22
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -5,10 +5,12 @@ Web Animations API renderer for [MarkdyScript](../../docs/SYNTAX.md) scenes. Tra
5
5
  ## Features
6
6
 
7
7
  - **Browser-native** — Web Animations API + CSS transforms, no Canvas or GSAP
8
- - **Emoji stick figures** — `figure` actor type with articulatable limbs (punch, kick, rotate_part)
8
+ - **Emoji stick figures** — `figure` actor type with articulatable limbs, shoulder/hip joints, and body-part rig
9
+ - **Expressive gestures** — built-in `wave`, `nod`, `jump`, `bounce`, and multi-part `pose` actions
9
10
  - **Seek-safe** — manual `currentTime` control enables reliable `seek()` in any direction
10
11
  - **Face expressions** — instant emoji face swaps that work correctly on seek-back
11
12
  - **Speech bubbles** — auto-positioned bubbles with fade-in/fade-out
13
+ - **Z-index layering** — `z` modifier for actor depth ordering
12
14
  - **Single dependency** — only `@markdy/core`
13
15
 
14
16
  ## Installation
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ function createFigureEl(def) {
29
29
  const skinColor = def.args[0] || "#ffdbac";
30
30
  const isFemale = def.args[1] === "f";
31
31
  const startFace = def.args[2] || (isFemale ? "\u{1F642}" : "\u{1F636}");
32
- const ink = "#2a2a2a";
32
+ const ink = "currentColor";
33
33
  const WRAP_W = 80;
34
34
  const FACE_FS = 40;
35
35
  const SHIRT_FS = isFemale ? 48 : 44;
@@ -39,6 +39,7 @@ function createFigureEl(def) {
39
39
  const shY = Math.round(SHIRT_FS * 0.28);
40
40
  const ARM_W = 36, ARM_H = 22;
41
41
  const LEG_H = 54, LEG_STICK_H = 34;
42
+ const JOINT_SIZE = 6;
42
43
  const wrap = document.createElement("div");
43
44
  Object.assign(wrap.style, {
44
45
  position: "relative",
@@ -62,13 +63,13 @@ function createFigureEl(def) {
62
63
  });
63
64
  const neck = document.createElement("div");
64
65
  Object.assign(neck.style, {
65
- width: "8px",
66
- height: "8px",
66
+ width: "10px",
67
+ height: "10px",
67
68
  background: skinColor,
68
- borderRadius: "3px",
69
+ borderRadius: "4px",
69
70
  flexShrink: "0",
70
- marginTop: "-2px",
71
- marginBottom: "-2px",
71
+ marginTop: "-3px",
72
+ marginBottom: "-3px",
72
73
  zIndex: "4"
73
74
  });
74
75
  const shirtRow = document.createElement("div");
@@ -91,6 +92,12 @@ function createFigureEl(def) {
91
92
  pointerEvents: "none"
92
93
  });
93
94
  shirtRow.appendChild(torso);
95
+ const shoulderL = buildJoint("left", skinColor, JOINT_SIZE, shLx, shY, WRAP_W);
96
+ const shoulderR = buildJoint("right", skinColor, JOINT_SIZE, shRx, shY, WRAP_W);
97
+ shoulderL.dataset.figShoulderL = "";
98
+ shoulderR.dataset.figShoulderR = "";
99
+ shirtRow.appendChild(shoulderL);
100
+ shirtRow.appendChild(shoulderR);
94
101
  const armHandEmoji = isFemale ? "\u{1F485}" : "\u{1F91C}";
95
102
  shirtRow.appendChild(
96
103
  buildArm("left", armHandEmoji, skinColor, {
@@ -112,21 +119,57 @@ function createFigureEl(def) {
112
119
  flipFist: false
113
120
  })
114
121
  );
122
+ const hipRow = document.createElement("div");
123
+ Object.assign(hipRow.style, {
124
+ position: "relative",
125
+ display: "flex",
126
+ flexDirection: "column",
127
+ alignItems: "center",
128
+ flexShrink: "0",
129
+ overflow: "visible"
130
+ });
131
+ const hipBridge = document.createElement("div");
132
+ Object.assign(hipBridge.style, {
133
+ width: "28px",
134
+ height: "4px",
135
+ background: skinColor,
136
+ borderRadius: "2px",
137
+ marginBottom: "1px",
138
+ zIndex: "3"
139
+ });
140
+ hipRow.appendChild(hipBridge);
115
141
  const legsRow = document.createElement("div");
116
142
  Object.assign(legsRow.style, {
117
143
  display: "flex",
118
144
  justifyContent: "center",
119
145
  gap: "10px",
120
146
  flexShrink: "0",
121
- overflow: "visible"
147
+ overflow: "visible",
148
+ position: "relative"
122
149
  });
123
- legsRow.append(
124
- buildLeg(true, isFemale, ink, LEG_H, LEG_STICK_H),
125
- buildLeg(false, isFemale, ink, LEG_H, LEG_STICK_H)
126
- );
127
- wrap.append(faceEl, neck, shirtRow, legsRow);
150
+ const legL = buildLeg(true, isFemale, ink, skinColor, LEG_H, LEG_STICK_H, JOINT_SIZE);
151
+ const legR = buildLeg(false, isFemale, ink, skinColor, LEG_H, LEG_STICK_H, JOINT_SIZE);
152
+ legsRow.append(legL, legR);
153
+ hipRow.appendChild(legsRow);
154
+ wrap.append(faceEl, neck, shirtRow, hipRow);
128
155
  return wrap;
129
156
  }
157
+ function buildJoint(side, skinColor, size, anchorX, anchorY, wrapW) {
158
+ const joint = document.createElement("div");
159
+ const isLeft = side === "left";
160
+ Object.assign(joint.style, {
161
+ position: "absolute",
162
+ width: `${size}px`,
163
+ height: `${size}px`,
164
+ borderRadius: "50%",
165
+ background: skinColor,
166
+ ...isLeft ? { right: `${wrapW - anchorX - size / 2}px` } : { left: `${anchorX - size / 2}px` },
167
+ top: `${anchorY - size / 2}px`,
168
+ zIndex: "5",
169
+ pointerEvents: "none"
170
+ });
171
+ return joint;
172
+ }
130
173
  function buildArm(side, handEmoji, skinColor, g) {
131
174
  const arm = document.createElement("div");
132
175
  const ds = arm.dataset;
@@ -150,7 +193,7 @@ function buildArm(side, handEmoji, skinColor, g) {
150
193
  ...isLeft ? { right: "5px" } : { left: "5px" },
151
194
  top: `${g.h / 2 - 2}px`,
152
195
  width: "18px",
153
- height: "3px",
196
+ height: "4px",
154
197
  background: skinColor,
155
198
  borderRadius: "2px"
156
199
  });
@@ -169,7 +212,7 @@ function buildArm(side, handEmoji, skinColor, g) {
169
212
  arm.append(stick, fist);
170
213
  return arm;
171
214
  }
172
- function buildLeg(isLeft, isFemale, ink, legH, stickH) {
215
+ function buildLeg(isLeft, isFemale, ink, skinColor, legH, stickH, jointSize) {
173
216
  const leg = document.createElement("div");
174
217
  const ds = leg.dataset;
175
218
  ds[isLeft ? "figLegL" : "figLegR"] = "";
@@ -181,17 +224,44 @@ function buildLeg(isLeft, isFemale, ink, legH, stickH) {
181
224
  transform: "rotate(0deg)",
182
225
  overflow: "visible"
183
226
  });
227
+ const hipJoint = document.createElement("div");
228
+ hipJoint.dataset[isLeft ? "figHipL" : "figHipR"] = "";
229
+ Object.assign(hipJoint.style, {
230
+ position: "absolute",
231
+ width: `${jointSize}px`,
232
+ height: `${jointSize}px`,
233
+ borderRadius: "50%",
234
+ background: skinColor,
235
+ left: "50%",
236
+ top: `-${jointSize / 2}px`,
237
+ transform: "translateX(-50%)",
238
+ zIndex: "3",
239
+ pointerEvents: "none"
240
+ });
184
241
  const stick = document.createElement("div");
185
242
  Object.assign(stick.style, {
186
243
  position: "absolute",
187
- width: "3px",
244
+ width: "4px",
188
245
  height: `${stickH}px`,
189
246
  background: ink,
190
- borderRadius: "1px",
247
+ borderRadius: "2px",
191
248
  left: "50%",
192
249
  top: "0",
193
250
  transform: "translateX(-50%)"
194
251
  });
252
+ const knee = document.createElement("div");
253
+ Object.assign(knee.style, {
254
+ position: "absolute",
255
+ width: "4px",
256
+ height: "4px",
257
+ borderRadius: "50%",
258
+ background: skinColor,
259
+ left: "50%",
260
+ top: `${stickH * 0.55}px`,
261
+ transform: "translateX(-50%)",
262
+ zIndex: "2",
263
+ pointerEvents: "none"
264
+ });
195
265
  const shoe = document.createElement("span");
196
266
  shoe.textContent = isFemale ? "\u{1F460}" : "\u{1F45F}";
197
267
  Object.assign(shoe.style, {
@@ -208,7 +278,7 @@ function buildLeg(isLeft, isFemale, ink, legH, stickH) {
208
278
  } else {
209
279
  shoe.style.right = "0";
210
280
  }
211
- leg.append(stick, shoe);
281
+ leg.append(hipJoint, stick, knee, shoe);
212
282
  return leg;
213
283
  }
214
284
  var PART_SEL = {
@@ -286,10 +356,24 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
286
356
  el.style.transformOrigin = "center center";
287
357
  el.style.transform = tx(stateFrom(def));
288
358
  el.style.opacity = String(def.opacity ?? 1);
359
+ if (def.z !== void 0) el.style.zIndex = String(def.z);
289
360
  return el;
290
361
  }
291
362
 
292
363
  // src/animations.ts
364
+ function isSceneDark(scene) {
365
+ const bg = scene.style.background || "white";
366
+ let hex = bg.trim().replace(/^#/, "");
367
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
368
+ if (hex.length === 6) {
369
+ const r = parseInt(hex.slice(0, 2), 16);
370
+ const g = parseInt(hex.slice(2, 4), 16);
371
+ const b = parseInt(hex.slice(4, 6), 16);
372
+ return 0.299 * r + 0.587 * g + 0.114 * b <= 140;
373
+ }
374
+ const dark = { black: true, "#000": true, "#000000": true };
375
+ return dark[bg.toLowerCase()] ?? false;
376
+ }
293
377
  function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
294
378
  const anims = [];
295
379
  const states = /* @__PURE__ */ new Map();
@@ -529,6 +613,11 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
529
613
  case "say": {
530
614
  const text = String(ev.params.text ?? "");
531
615
  const inverseScale = 1 / (s.scale || 1);
616
+ const sceneDark = isSceneDark(scene);
617
+ const bubbleBg = sceneDark ? "#1e2530" : "white";
618
+ const bubbleBorder = sceneDark ? "#475569" : "#222";
619
+ const bubbleText = sceneDark ? "#e2e8f0" : "#222";
620
+ const bubbleShadow = sceneDark ? "0 2px 8px rgba(0,0,0,0.35)" : "0 2px 8px rgba(0,0,0,0.12)";
532
621
  const bubble = document.createElement("div");
533
622
  bubble.textContent = text;
534
623
  bubble.style.opacity = "0";
@@ -538,9 +627,9 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
538
627
  left: "50%",
539
628
  transform: `translateX(-50%) scale(${inverseScale})`,
540
629
  transformOrigin: "center bottom",
541
- background: "white",
542
- border: "2px solid #222",
543
- color: "#222",
630
+ background: bubbleBg,
631
+ border: `2px solid ${bubbleBorder}`,
632
+ color: bubbleText,
544
633
  borderRadius: "12px",
545
634
  padding: "6px 14px",
546
635
  fontFamily: "system-ui, sans-serif",
@@ -552,7 +641,7 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
552
641
  textOverflow: "ellipsis",
553
642
  pointerEvents: "none",
554
643
  zIndex: "10",
555
- boxShadow: "0 2px 8px rgba(0,0,0,0.12)"
644
+ boxShadow: bubbleShadow
556
645
  });
557
646
  const tail = document.createElement("span");
558
647
  Object.assign(tail.style, {
@@ -564,7 +653,7 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
564
653
  height: "0",
565
654
  borderLeft: "7px solid transparent",
566
655
  borderRight: "7px solid transparent",
567
- borderTop: "10px solid #222"
656
+ borderTop: `10px solid ${bubbleBorder}`
568
657
  });
569
658
  bubble.appendChild(tail);
570
659
  el.style.overflow = "visible";
@@ -584,6 +673,135 @@ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, as
584
673
  );
585
674
  break;
586
675
  }
676
+ // ── pose ────────────────────────────────────────────────────────────────
677
+ // Sets multiple body parts to target angles simultaneously.
678
+ // Usage: @1.0: hero.pose(arm_left=45, arm_right=-45, leg_left=10, dur=0.4)
679
+ case "pose": {
680
+ const poseParts = ["arm_left", "arm_right", "leg_left", "leg_right", "head", "body"];
681
+ for (const partName of poseParts) {
682
+ if (typeof ev.params[partName] !== "number") continue;
683
+ const pSel = PART_SEL[partName];
684
+ if (!pSel) continue;
685
+ const pEl = el.querySelector(pSel);
686
+ if (!pEl) continue;
687
+ const fromDeg = readRotation(pEl);
688
+ const toDeg = ev.params[partName];
689
+ anims.push(
690
+ pEl.animate(
691
+ [
692
+ { transform: `rotate(${fromDeg}deg)` },
693
+ { transform: `rotate(${toDeg}deg)` }
694
+ ],
695
+ { ...baseOpts, fill: "forwards" }
696
+ )
697
+ );
698
+ pEl.style.transform = pEl.style.transform.replace(
699
+ /rotate\([^)]*\)/,
700
+ `rotate(${toDeg}deg)`
701
+ );
702
+ }
703
+ break;
704
+ }
705
+ // ── wave ────────────────────────────────────────────────────────────────
706
+ // Built-in wave gesture: raises arm, oscillates, returns.
707
+ // Usage: @2.0: hero.wave(side=right, dur=0.8)
708
+ case "wave": {
709
+ const wSide = String(ev.params.side ?? "right");
710
+ const wArmEl = el.querySelector(
711
+ wSide === "left" ? "[data-fig-arm-l]" : "[data-fig-arm-r]"
712
+ );
713
+ if (!wArmEl) break;
714
+ const wRest = readRotation(wArmEl);
715
+ const wUp = wSide === "left" ? 70 : -70;
716
+ const wMid1 = wSide === "left" ? 50 : -50;
717
+ const wMid2 = wSide === "left" ? 70 : -70;
718
+ anims.push(
719
+ wArmEl.animate(
720
+ [
721
+ { transform: `rotate(${wRest}deg)`, offset: 0 },
722
+ { transform: `rotate(${wUp}deg)`, offset: 0.2 },
723
+ { transform: `rotate(${wMid1}deg)`, offset: 0.4 },
724
+ { transform: `rotate(${wMid2}deg)`, offset: 0.55 },
725
+ { transform: `rotate(${wMid1}deg)`, offset: 0.7 },
726
+ { transform: `rotate(${wMid2}deg)`, offset: 0.85 },
727
+ { transform: `rotate(${wRest}deg)`, offset: 1 }
728
+ ],
729
+ { ...baseOpts, easing: "ease-in-out", fill: "forwards" }
730
+ )
731
+ );
732
+ break;
733
+ }
734
+ // ── jump ────────────────────────────────────────────────────────────────
735
+ // Jumps the actor up with squash/stretch effect.
736
+ // Usage: @3.0: hero.jump(height=30, dur=0.5)
737
+ case "jump": {
738
+ const jHeight = typeof ev.params.height === "number" ? ev.params.height : 30;
739
+ anims.push(
740
+ el.animate(
741
+ [
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 }
748
+ ],
749
+ { ...baseOpts, easing: "ease-in-out" }
750
+ )
751
+ );
752
+ break;
753
+ }
754
+ // ── nod ─────────────────────────────────────────────────────────────────
755
+ // Nods the head down and back up. Figure actors only.
756
+ // Usage: @2.0: hero.nod(dur=0.4)
757
+ case "nod": {
758
+ const nHeadEl = el.querySelector("[data-fig-head]");
759
+ if (!nHeadEl) break;
760
+ const nRest = readRotation(nHeadEl);
761
+ const nDown = 15;
762
+ anims.push(
763
+ nHeadEl.animate(
764
+ [
765
+ { transform: `rotate(${nRest}deg)`, offset: 0 },
766
+ { transform: `rotate(${nDown}deg)`, offset: 0.35 },
767
+ { transform: `rotate(${nRest}deg)`, offset: 0.65 },
768
+ { transform: `rotate(${nDown}deg)`, offset: 0.8 },
769
+ { transform: `rotate(${nRest}deg)`, offset: 1 }
770
+ ],
771
+ { ...baseOpts, easing: "ease-in-out", fill: "forwards" }
772
+ )
773
+ );
774
+ break;
775
+ }
776
+ // ── bounce ──────────────────────────────────────────────────────────────
777
+ // Bounces the actor vertically with diminishing amplitude.
778
+ // Usage: @1.0: hero.bounce(intensity=15, count=3, dur=0.6)
779
+ case "bounce": {
780
+ const bIntensity = typeof ev.params.intensity === "number" ? ev.params.intensity : 15;
781
+ const bCount = typeof ev.params.count === "number" ? ev.params.count : 3;
782
+ const keyframes = [{ transform: tx(s), offset: 0 }];
783
+ for (let bi = 0; bi < bCount; bi++) {
784
+ const amp = bIntensity * Math.pow(0.55, bi);
785
+ const baseOffset = (bi + 0.5) / (bCount + 0.5);
786
+ const peakOffset = Math.min(baseOffset, 0.98);
787
+ const valleyOffset = Math.min(baseOffset + 0.25 / (bCount + 0.5), 0.99);
788
+ keyframes.push({
789
+ transform: tx({ ...s, y: s.y - amp }),
790
+ offset: peakOffset
791
+ });
792
+ if (bi < bCount - 1) {
793
+ keyframes.push({
794
+ transform: tx(s),
795
+ offset: valleyOffset
796
+ });
797
+ }
798
+ }
799
+ keyframes.push({ transform: tx(s), offset: 1 });
800
+ anims.push(
801
+ el.animate(keyframes, { ...baseOpts, easing: "ease-out" })
802
+ );
803
+ break;
804
+ }
587
805
  // ── throw ───────────────────────────────────────────────────────────────
588
806
  case "throw": {
589
807
  const assetName = String(ev.params.asset ?? "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "Web Animations API renderer for MarkdyScript scenes.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@markdy/core": "0.5.7"
46
+ "@markdy/core": "0.6.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "tsup": "^8.5.1",