@vosjs/studio-core 0.24.0 → 0.25.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.
package/dist/index.js CHANGED
@@ -236,6 +236,210 @@ var MEDIA_FRAME_KEYS = [
236
236
  "focusFollow"
237
237
  ];
238
238
 
239
+ // src/callout.ts
240
+ var CALLOUT_SHAPES = ["tag", "note", "code"];
241
+ var CALLOUT_TYPE_RATIO = 1.35;
242
+ var CALLOUT_DEFAULT_BODY_PX = 14;
243
+ var HOUSE_REGISTER = {
244
+ ground: "#ffffff",
245
+ accent: "#7c3aed"
246
+ };
247
+ var DEFAULT_FACE = "'Inter',-apple-system,system-ui,sans-serif";
248
+ var DEFAULT_MONO = "'JetBrains Mono',ui-monospace,monospace";
249
+ function calloutLook(register, scale = 1) {
250
+ const ground = hexToRgb(register.ground) ?? [255, 255, 255];
251
+ const accent = hexToRgb(register.accent) ?? [124, 58, 237];
252
+ const light = relativeLuminance(ground) > 0.4;
253
+ const [h, s] = rgbToHsl(accent);
254
+ const cardGround = light ? hslToHex(h, Math.min(0.35, s * 0.6), 0.07) : hslToHex(h, Math.min(0.25, s * 0.35), 0.97);
255
+ const bodyPx = register.body ?? CALLOUT_DEFAULT_BODY_PX;
256
+ const titlePx = Math.max(20, Math.round(CALLOUT_TYPE_RATIO * bodyPx * scale));
257
+ return {
258
+ light,
259
+ cardGround,
260
+ cardInk: light ? "#fafafa" : "#101012",
261
+ cardMuted: light ? "rgba(255,255,255,0.64)" : "rgba(16,16,18,0.6)",
262
+ hairline: light ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.1)",
263
+ accent: rgbToHex(accent),
264
+ shadow: "0 24px 60px rgba(0,0,0,0.35), 0 2px 6px rgba(0,0,0,0.16)",
265
+ face: register.face ? quoteFamily(register.face) : DEFAULT_FACE,
266
+ mono: register.mono ? quoteFamily(register.mono) : DEFAULT_MONO,
267
+ kickerPx: Math.max(11, Math.round(titlePx * 0.55)),
268
+ titlePx,
269
+ bodyPx: Math.max(15, Math.round(titlePx * 0.75)),
270
+ codePx: Math.max(14, Math.round(titlePx * 0.68)),
271
+ radius: 16,
272
+ padX: 24,
273
+ padY: 20
274
+ };
275
+ }
276
+ function calloutBox(shape, look, words) {
277
+ const em = (px, chars, adv) => Math.ceil(px * adv * chars);
278
+ if (shape === "tag") {
279
+ const text = words.kicker ?? words.title ?? "";
280
+ const w = em(look.kickerPx * 1.1, text.length, 0.62) + 14 + 18 * 2;
281
+ return {
282
+ width: clamp(w, 120, 720),
283
+ height: Math.max(36, Math.round(look.kickerPx * 1.1 * 2.6))
284
+ };
285
+ }
286
+ if (shape === "code") {
287
+ const lines = (words.code ?? "").split("\n");
288
+ const longest = Math.max(1, ...lines.map((l) => l.length));
289
+ const lineH = Math.round(look.codePx * 1.7);
290
+ const w = em(look.codePx, longest, 0.62) + look.padX * 2;
291
+ const h = lines.length * lineH + look.padY * 2 + (words.kicker ? look.kickerPx * 2 : 0);
292
+ return { width: clamp(w, 360, 820), height: clamp(h, 72, 640) };
293
+ }
294
+ const title = words.title ?? "";
295
+ const body = words.body ?? "";
296
+ const titleW = em(look.titlePx, title.length, 0.56);
297
+ const width = clamp(Math.max(titleW, 320) + look.padX * 2, 360, 640);
298
+ const inner = width - look.padX * 2;
299
+ const bodyLines = body ? Math.max(1, Math.ceil(em(look.bodyPx, body.length, 0.54) / inner)) : 0;
300
+ const titleLines = Math.max(1, Math.ceil(titleW / inner));
301
+ const height = look.padY * 2 + (words.kicker ? Math.round(look.kickerPx * 1.6) : 0) + titleLines * Math.round(look.titlePx * 1.25) + (bodyLines ? 8 + bodyLines * Math.round(look.bodyPx * 1.5) : 0);
302
+ return { width, height: clamp(height, 64, 480) };
303
+ }
304
+ function calloutSource(shape, look, words, box) {
305
+ const base = `box-sizing:border-box;width:${box.width}px;height:${box.height}px;background:${look.cardGround};color:${look.cardInk};border:1px solid ${look.hairline};border-radius:${look.radius}px;box-shadow:${look.shadow};-webkit-font-smoothing:antialiased;`;
306
+ if (shape === "tag") {
307
+ const text = esc(words.kicker ?? words.title ?? "");
308
+ return {
309
+ html: `<div class="tag"><span class="dot"></span><span class="t">${text}</span></div>`,
310
+ css: `.tag{font-family:${look.face};display:flex;align-items:center;gap:10px;${base}border-radius:999px;padding:0 18px;background:${look.accent};color:#fff;border-color:rgba(255,255,255,0.18);font-size:${Math.round(look.kickerPx * 1.1)}px;font-weight:600;letter-spacing:0.02em;text-transform:uppercase;white-space:nowrap}.dot{width:8px;height:8px;border-radius:99px;background:#fff;opacity:0.9;flex:none}`
311
+ };
312
+ }
313
+ if (shape === "code") {
314
+ const lines = (words.code ?? "").split("\n").map(
315
+ (l) => /^\s*(\/\/|#|\$)/.test(l) ? `<span class="mut">${esc(l)}</span>` : esc(l)
316
+ ).join("\n");
317
+ const kicker2 = words.kicker ? `<div class="k">${esc(words.kicker)}</div>` : "";
318
+ return {
319
+ html: `<div class="code">${kicker2}<pre class="p">${lines}</pre></div>`,
320
+ css: `.code{font-family:${look.mono};${base}padding:${look.padY}px ${look.padX}px;font-size:${look.codePx}px;line-height:1.7;letter-spacing:-0.01em}.k{font-family:${look.face};font-size:${look.kickerPx}px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:${look.accent};margin:0 0 ${Math.round(look.kickerPx * 0.9)}px}.p{margin:0;white-space:pre;font:inherit}.mut{color:${look.cardMuted}}`
321
+ };
322
+ }
323
+ const kicker = words.kicker ? `<div class="k"><span class="dot"></span>${esc(words.kicker)}</div>` : "";
324
+ const body = words.body ? `<div class="b">${esc(words.body)}</div>` : "";
325
+ return {
326
+ html: `<div class="note">${kicker}<div class="t">${esc(words.title ?? "")}</div>${body}</div>`,
327
+ css: `.note{font-family:${look.face};${base}padding:${look.padY}px ${look.padX}px;display:flex;flex-direction:column;justify-content:center}.k{display:flex;align-items:center;gap:8px;font-size:${look.kickerPx}px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:${look.accent};margin-bottom:${Math.round(look.kickerPx * 0.6)}px}.dot{width:7px;height:7px;border-radius:99px;background:${look.accent};box-shadow:0 0 10px ${look.accent}}.t{font-size:${look.titlePx}px;font-weight:600;line-height:1.25;letter-spacing:-0.01em}.b{margin-top:8px;font-size:${look.bodyPx}px;line-height:1.5;color:${look.cardMuted}}`
328
+ };
329
+ }
330
+ function calloutClip(shape, register, words, opts) {
331
+ const look = calloutLook(register, opts.scale ?? 1);
332
+ const box = calloutBox(shape, look, words);
333
+ const { html, css } = calloutSource(shape, look, words, box);
334
+ return {
335
+ id: opts.id,
336
+ kind: "html",
337
+ start: opts.start,
338
+ duration: opts.duration,
339
+ html,
340
+ css,
341
+ box,
342
+ transform: {
343
+ x: opts.at?.x ?? 0.5,
344
+ y: opts.at?.y ?? 0.5,
345
+ scale: 1,
346
+ rotation: 0
347
+ },
348
+ anim: { enter: "rise", exit: "fade" },
349
+ ...opts.pin ? { pin: opts.pin } : {}
350
+ };
351
+ }
352
+ function calloutGroundOf(css) {
353
+ const m = /background(?:-color)?\s*:\s*(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\))/.exec(css);
354
+ if (!m) return null;
355
+ const rgb = parseCssColour(m[1]);
356
+ return rgb ? rgbToHex(rgb) : null;
357
+ }
358
+ function hexToRgb(hex) {
359
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim());
360
+ if (!m) return null;
361
+ let h = m[1];
362
+ if (h.length === 3)
363
+ h = h.split("").map((c) => c + c).join("");
364
+ return [
365
+ parseInt(h.slice(0, 2), 16),
366
+ parseInt(h.slice(2, 4), 16),
367
+ parseInt(h.slice(4, 6), 16)
368
+ ];
369
+ }
370
+ function rgbToHex([r, g, b]) {
371
+ const c = (v) => Math.round(clamp(v, 0, 255)).toString(16).padStart(2, "0");
372
+ return `#${c(r)}${c(g)}${c(b)}`;
373
+ }
374
+ function parseCssColour(s) {
375
+ const hex = hexToRgb(s);
376
+ if (hex) return hex;
377
+ const m = /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/.exec(s);
378
+ if (!m) return null;
379
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
380
+ }
381
+ function relativeLuminance([r, g, b]) {
382
+ const lin = (v) => {
383
+ const c = v / 255;
384
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
385
+ };
386
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
387
+ }
388
+ function rgbToHsl([r, g, b]) {
389
+ const rr = r / 255;
390
+ const gg = g / 255;
391
+ const bb = b / 255;
392
+ const max = Math.max(rr, gg, bb);
393
+ const min = Math.min(rr, gg, bb);
394
+ const l = (max + min) / 2;
395
+ if (max === min) return [0, 0, l];
396
+ const d = max - min;
397
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
398
+ let h = 0;
399
+ if (max === rr) h = (gg - bb) / d + (gg < bb ? 6 : 0);
400
+ else if (max === gg) h = (bb - rr) / d + 2;
401
+ else h = (rr - gg) / d + 4;
402
+ return [h / 6, s, l];
403
+ }
404
+ function hslToHex(h, s, l) {
405
+ const f = (n) => {
406
+ const k = (n + h * 12) % 12;
407
+ const a = s * Math.min(l, 1 - l);
408
+ return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
409
+ };
410
+ return rgbToHex([f(0) * 255, f(8) * 255, f(4) * 255]);
411
+ }
412
+ function deltaE(a, b) {
413
+ const la = rgbToLab(a);
414
+ const lb = rgbToLab(b);
415
+ return Math.hypot(la[0] - lb[0], la[1] - lb[1], la[2] - lb[2]);
416
+ }
417
+ function rgbToLab([r, g, b]) {
418
+ const lin = (v) => {
419
+ const c = v / 255;
420
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
421
+ };
422
+ const R = lin(r);
423
+ const G = lin(g);
424
+ const B = lin(b);
425
+ const x = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
426
+ const y = R * 0.2126 + G * 0.7152 + B * 0.0722;
427
+ const z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 1.08883;
428
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
429
+ return [116 * f(y) - 16, 500 * (f(x) - f(y)), 200 * (f(y) - f(z))];
430
+ }
431
+ function quoteFamily(family) {
432
+ const first = family.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
433
+ const rest = family.includes(",") ? family.slice(family.indexOf(",") + 1).trim() : /mono/i.test(first) ? "ui-monospace,monospace" : "-apple-system,system-ui,sans-serif";
434
+ return /^[a-z-]+$/i.test(first) && /^(ui-|system-ui|sans-serif|serif|monospace)/.test(first) ? `${first},${rest}` : `'${first}',${rest}`;
435
+ }
436
+ function esc(s) {
437
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
438
+ }
439
+ function clamp(v, lo, hi) {
440
+ return Math.max(lo, Math.min(hi, v));
441
+ }
442
+
239
443
  // src/capture.ts
240
444
  var CHROME_TOP_MIN = 20;
241
445
  var CHROME_TOP_MAX = 220;
@@ -1102,7 +1306,7 @@ function sampleAt(points, t) {
1102
1306
  return { x: last.x, y: last.y };
1103
1307
  }
1104
1308
  function smoothCursor(track, options = {}) {
1105
- const factor = clamp(options.factor ?? 0.15, 0.01, 1);
1309
+ const factor = clamp2(options.factor ?? 0.15, 0.01, 1);
1106
1310
  const fps = options.fps ?? 60;
1107
1311
  const raw = track.filter((e) => e.type === "move" || e.type === "down" || e.type === "up").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y }));
1108
1312
  if (raw.length === 0) return [];
@@ -1121,7 +1325,7 @@ function smoothCursor(track, options = {}) {
1121
1325
  if (clicks.length) {
1122
1326
  while (ci < clicks.length - 1 && t > clicks[ci].t + SNAP_AFTER) ci++;
1123
1327
  const ck = clicks[ci];
1124
- const u = clamp(
1328
+ const u = clamp2(
1125
1329
  (t - (ck.t - SNAP_BEFORE)) / SNAP_BEFORE,
1126
1330
  0,
1127
1331
  t <= ck.t + SNAP_AFTER ? 1 : 0
@@ -1136,7 +1340,7 @@ function smoothCursor(track, options = {}) {
1136
1340
  }
1137
1341
  return out;
1138
1342
  }
1139
- function clamp(v, lo, hi) {
1343
+ function clamp2(v, lo, hi) {
1140
1344
  return Math.max(lo, Math.min(hi, v));
1141
1345
  }
1142
1346
 
@@ -1782,7 +1986,7 @@ function focusFor(cluster, width, height, targetFill, minLevel, maxLevel) {
1782
1986
  const cx = clamp012(sx / n / width);
1783
1987
  const cy = clamp012(sy / n / height);
1784
1988
  const level = fitLevel({ x: 0, y: 0, w: maxW, h: maxH }, width, height, targetFill) ?? maxLevel;
1785
- return { cx, cy, level: clamp2(level, minLevel, maxLevel) };
1989
+ return { cx, cy, level: clamp3(level, minLevel, maxLevel) };
1786
1990
  }
1787
1991
  function clusterFocus(events, width, height) {
1788
1992
  let sx = 0;
@@ -1815,11 +2019,11 @@ function clusterFocus(events, width, height) {
1815
2019
  } : null;
1816
2020
  return { cx: clamp012(sx / n / width), cy: clamp012(sy / n / height), rect };
1817
2021
  }
1818
- function clamp2(v, lo, hi) {
2022
+ function clamp3(v, lo, hi) {
1819
2023
  return Math.max(lo, Math.min(hi, v));
1820
2024
  }
1821
2025
  function clamp012(v) {
1822
- return clamp2(v, 0, 1);
2026
+ return clamp3(v, 0, 1);
1823
2027
  }
1824
2028
  function round(v) {
1825
2029
  return Math.round(v * 1e3) / 1e3;
@@ -3058,7 +3262,7 @@ function round2(v) {
3058
3262
  }
3059
3263
 
3060
3264
  // src/lower/pin.ts
3061
- import { lerpArray, sample } from "@vosjs/timeline";
3265
+ import { lerpArray, mapTime, sample } from "@vosjs/timeline";
3062
3266
  var PIN_GAP = 24;
3063
3267
  var PIN_MARGIN = 24;
3064
3268
  var PIN_SAMPLE_HZ = 30;
@@ -3077,19 +3281,27 @@ function pinReferent(doc, pin) {
3077
3281
  if (pin.step !== void 0) {
3078
3282
  const step = findStep(meta.steps ?? [], pin.step);
3079
3283
  if (!step || step.skipped) return null;
3284
+ const carry = dragCarry(doc, step.tStart - 0.05, step.tEnd + 0.05);
3080
3285
  if (step.rect && validRect(step.rect) && w > 0 && h > 0) {
3081
3286
  return {
3082
3287
  rect: normRect(step.rect, w, h),
3083
3288
  by: "step",
3084
3289
  step,
3085
- at: step.tStart
3290
+ at: step.tStart,
3291
+ ...carry ? { carry } : {}
3086
3292
  };
3087
3293
  }
3088
3294
  const union = unionRects(
3089
3295
  pressRects(doc, step.tStart - 0.05, step.tEnd + 0.05)
3090
3296
  );
3091
3297
  if (!union || w <= 0 || h <= 0) return null;
3092
- return { rect: normRect(union, w, h), by: "step", step, at: step.tStart };
3298
+ return {
3299
+ rect: normRect(union, w, h),
3300
+ by: "step",
3301
+ step,
3302
+ at: step.tStart,
3303
+ ...carry ? { carry } : {}
3304
+ };
3093
3305
  }
3094
3306
  if (pin.press !== void 0 && Number.isFinite(pin.press)) {
3095
3307
  let best = null;
@@ -3101,10 +3313,42 @@ function pinReferent(doc, pin) {
3101
3313
  if (!best || d < Math.abs(best.t - pin.press)) best = { t, rect: e.rect };
3102
3314
  }
3103
3315
  if (!best || w <= 0 || h <= 0) return null;
3104
- return { rect: normRect(best.rect, w, h), by: "press", at: best.t };
3316
+ const carry = dragCarry(doc, best.t - 0.01, best.t + 0.01);
3317
+ return {
3318
+ rect: normRect(best.rect, w, h),
3319
+ by: "press",
3320
+ at: best.t,
3321
+ ...carry ? { carry } : {}
3322
+ };
3105
3323
  }
3106
3324
  return null;
3107
3325
  }
3326
+ function dragCarry(doc, from, to) {
3327
+ const meta = doc.source.meta;
3328
+ if (!(meta.width > 0) || !(meta.height > 0)) return null;
3329
+ const d = dragsFromTrack(doc.source.cursor, meta.width, meta.height).find(
3330
+ (g) => g.t0 >= from && g.t0 <= to
3331
+ );
3332
+ if (!d) return null;
3333
+ return { t0: d.t0, t1: d.t1, x0: d.nx, y0: d.ny };
3334
+ }
3335
+ function cursorAt(cursor, t, space) {
3336
+ let prev = null;
3337
+ for (const e of cursor) {
3338
+ if (e.type !== "move" && e.type !== "down" && e.type !== "up") continue;
3339
+ const cur = { t: e.t / 1e3, nx: e.x / space.w, ny: e.y / space.h };
3340
+ if (cur.t >= t) {
3341
+ if (!prev) return { nx: cur.nx, ny: cur.ny };
3342
+ const k = cur.t > prev.t ? (t - prev.t) / (cur.t - prev.t) : 1;
3343
+ return {
3344
+ nx: prev.nx + (cur.nx - prev.nx) * k,
3345
+ ny: prev.ny + (cur.ny - prev.ny) * k
3346
+ };
3347
+ }
3348
+ prev = cur;
3349
+ }
3350
+ return prev ? { nx: prev.nx, ny: prev.ny } : null;
3351
+ }
3108
3352
  function findStep(steps, ref) {
3109
3353
  const byId = steps.find((s) => s.id !== void 0 && s.id === ref);
3110
3354
  if (byId) return byId;
@@ -3188,6 +3432,20 @@ function pinRectOnScreen(rect, t, layout, camera, zoomTrack) {
3188
3432
  const b = map(rect.x + rect.w, rect.y + rect.h);
3189
3433
  return { x: a.x, y: a.y, w: b.x - a.x, h: b.y - a.y };
3190
3434
  }
3435
+ function pinReferentAt(referent, t, input) {
3436
+ const { carry, cursor, space, rated } = input;
3437
+ if (!carry || !cursor || !space || !rated) return referent;
3438
+ const s = mapTime(rated, t);
3439
+ const sc = Math.max(carry.t0, Math.min(carry.t1, s));
3440
+ const p = cursorAt(cursor, sc, space);
3441
+ if (!p) return referent;
3442
+ return {
3443
+ x: referent.x + (p.nx - carry.x0),
3444
+ y: referent.y + (p.ny - carry.y0),
3445
+ w: referent.w,
3446
+ h: referent.h
3447
+ };
3448
+ }
3191
3449
  function pinPlacement(input) {
3192
3450
  const { clip, referent, layout, camera, zoomTrack, base, motion } = input;
3193
3451
  const dur = Math.max(1e-3, clip.duration);
@@ -3195,7 +3453,13 @@ function pinPlacement(input) {
3195
3453
  const box = pinBox(clip, layout);
3196
3454
  const W = layout.W;
3197
3455
  const H = layout.H;
3198
- const refAt = (local) => pinRectOnScreen(referent, clip.start + local, layout, camera, zoomTrack);
3456
+ const refAt = (local) => pinRectOnScreen(
3457
+ pinReferentAt(referent, clip.start + local, input),
3458
+ clip.start + local,
3459
+ layout,
3460
+ camera,
3461
+ zoomTrack
3462
+ );
3199
3463
  const centreFor = (r, side2) => {
3200
3464
  switch (side2) {
3201
3465
  case "right":
@@ -6670,6 +6934,15 @@ var ON_FRAME = `(ctx, content, dt) => {
6670
6934
  if (pn.dur - pnT < 0.35) { var pnV = (pn.dur - pnT) / 0.35; pnA = Math.min(pnA, 1 - Math.pow(1 - pnV, 3)) }
6671
6935
  if (pnA <= 0.004) continue
6672
6936
  var pnX = dx + pn.x * dw, pnY = dy + pn.y * dh, pnW = pn.w * dw, pnH = pn.h * dh
6937
+ // a carried referent (a drag): moved by where the pointer has taken
6938
+ // it at the on-screen source moment, held at the release
6939
+ if (pn.carry) {
6940
+ var pcC = (actM ? actM.cursor : d.cursor) || []
6941
+ var pcT = Math.max(pn.carry.t0, Math.min(pn.carry.t1, srcT))
6942
+ var pcx = -1, pcy = -1
6943
+ for (var pk = 0; pk < pcC.length; pk++) { if (pcC[pk].t <= pcT) { pcx = pcC[pk].x; pcy = pcC[pk].y } }
6944
+ if (pcx >= 0) { pnX += (pcx / (space.w || vw) - pn.carry.x0) * dw; pnY += (pcy / (space.h || vh) - pn.carry.y0) * dh }
6945
+ }
6673
6946
  var pnLw = 2 * s2 / ceZs, pnPad = 6 * s2 / ceZs
6674
6947
  var pnCol = pn.color || 'rgba(255,255,255,0.95)'
6675
6948
  c.save()
@@ -6839,6 +7112,15 @@ var ON_FRAME = `(ctx, content, dt) => {
6839
7112
  // the referent's corners in frame space under the live camera
6840
7113
  var pqAx = dx + pq.x * dw, pqAy = dy + pq.y * dh
6841
7114
  var pqBx = dx + (pq.x + pq.w) * dw, pqBy = dy + (pq.y + pq.h) * dh
7115
+ if (pq.carry) {
7116
+ var pqcT = Math.max(pq.carry.t0, Math.min(pq.carry.t1, srcT))
7117
+ var pqcx = -1, pqcy = -1
7118
+ for (var pqk = 0; pqk < cur.length; pqk++) { if (cur[pqk].t <= pqcT) { pqcx = cur[pqk].x; pqcy = cur[pqk].y } }
7119
+ if (pqcx >= 0) {
7120
+ var pqOx = (pqcx / (space.w || vw) - pq.carry.x0) * dw, pqOy = (pqcy / (space.h || vh) - pq.carry.y0) * dh
7121
+ pqAx += pqOx; pqBx += pqOx; pqAy += pqOy; pqBy += pqOy
7122
+ }
7123
+ }
6842
7124
  if (lvl > 1.001 && !d.zoomSuppressed) {
6843
7125
  if (camStage) {
6844
7126
  pqAx = W / 2 + (pqAx - wcx) * lvl; pqAy = H / 2 + (pqAy - wcy) * lvl
@@ -7423,6 +7705,8 @@ function docZoomTrack(input) {
7423
7705
  }
7424
7706
  function resolvePins(doc, layout, camera, zoomTrack) {
7425
7707
  const out = /* @__PURE__ */ new Map();
7708
+ const rated = ratedSegments(doc);
7709
+ const space = { w: doc.source.meta.width, h: doc.source.meta.height };
7426
7710
  for (const o of doc.overlays ?? []) {
7427
7711
  if (!o.pin) continue;
7428
7712
  const ref = pinReferent(doc, o.pin);
@@ -7442,12 +7726,39 @@ function resolvePins(doc, layout, camera, zoomTrack) {
7442
7726
  camera,
7443
7727
  zoomTrack,
7444
7728
  base,
7445
- motion
7729
+ motion,
7730
+ carry: ref.carry ?? null,
7731
+ cursor: doc.source.cursor,
7732
+ space,
7733
+ rated
7446
7734
  })
7447
7735
  );
7448
7736
  }
7449
7737
  return out;
7450
7738
  }
7739
+ function pinCandidates(doc, clip) {
7740
+ const rated = ratedSegments(doc);
7741
+ const out = [];
7742
+ const a0 = clip.start;
7743
+ const a1 = clip.start + clip.duration;
7744
+ for (const s of doc.source.meta.steps ?? []) {
7745
+ if (s.skipped) continue;
7746
+ const ref = pinReferent(doc, { step: s.id ?? s.step });
7747
+ if (!ref) continue;
7748
+ const ext = spanOutputExtent(rated, s.tStart, s.tEnd);
7749
+ if (!ext) continue;
7750
+ const overlap = Math.min(a1, ext.end) - Math.max(a0, ext.start);
7751
+ if (overlap <= 0) continue;
7752
+ out.push({
7753
+ step: s.id ?? s.step,
7754
+ do: s.do,
7755
+ ...s.selector ? { selector: s.selector } : {},
7756
+ output: { start: round7(ext.start), end: round7(ext.end) },
7757
+ overlap: round7(overlap)
7758
+ });
7759
+ }
7760
+ return out.sort((x, y) => y.overlap - x.overlap);
7761
+ }
7451
7762
  function overlayPinPoseAt(doc, o, t) {
7452
7763
  if (!o.pin) return null;
7453
7764
  const layout = docCardLayout(doc);
@@ -7484,6 +7795,16 @@ function pinsDataOf(doc, pins) {
7484
7795
  mark,
7485
7796
  leader: !!p.leader,
7486
7797
  ...p.color ? { color: p.color } : {},
7798
+ // A carried referent (a drag): ON_FRAME moves the mark and the
7799
+ // leader's end with the pointer through [t0, t1], source seconds.
7800
+ ...ref.carry ? {
7801
+ carry: {
7802
+ t0: round7(ref.carry.t0),
7803
+ t1: round7(ref.carry.t1),
7804
+ x0: round43(ref.carry.x0),
7805
+ y0: round43(ref.carry.y0)
7806
+ }
7807
+ } : {},
7487
7808
  enter: enterKey(o),
7488
7809
  tip: placed.tip
7489
7810
  });
@@ -8439,12 +8760,12 @@ function round10(v) {
8439
8760
  }
8440
8761
 
8441
8762
  // src/timeline/rangeActions.ts
8442
- import { mapTime } from "@vosjs/timeline";
8763
+ import { mapTime as mapTime2 } from "@vosjs/timeline";
8443
8764
  function outputRangeToSource(doc, t0, t1) {
8444
8765
  const rated = ratedSegments(doc);
8445
8766
  return {
8446
- srcIn: mapTime(rated, Math.max(0, t0)),
8447
- srcOut: mapTime(rated, Math.max(0, t1))
8767
+ srcIn: mapTime2(rated, Math.max(0, t0)),
8768
+ srcOut: mapTime2(rated, Math.max(0, t1))
8448
8769
  };
8449
8770
  }
8450
8771
  var EPS = 1e-6;
@@ -8985,24 +9306,31 @@ function destinationsForChannel(channel) {
8985
9306
  }
8986
9307
 
8987
9308
  // src/htmlStarters.ts
9309
+ function grammarStarter(id, name, words) {
9310
+ const clip = calloutClip(id, HOUSE_REGISTER, words, {
9311
+ id,
9312
+ start: 0,
9313
+ duration: 1,
9314
+ scale: 1.4
9315
+ });
9316
+ return { id, name, html: clip.html, css: clip.css ?? "", box: clip.box };
9317
+ }
8988
9318
  var WORDS = "font-family:'Inter',-apple-system,system-ui,sans-serif;color:#fafafa;-webkit-font-smoothing:antialiased;";
8989
9319
  var MONO = "font-family:'JetBrains Mono',ui-monospace,monospace;";
8990
9320
  var CARD = "background:#0b0b0d;border:1px solid rgba(255,255,255,.08);border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.45);";
8991
9321
  var HTML_STARTERS = [
8992
- {
8993
- id: "callout",
8994
- name: "Callout",
8995
- box: { width: 420, height: 132 },
8996
- html: '<div class="callout"><div class="head"><span class="dot"></span><span class="title">Title</span></div><div class="body">One line that says what this is.</div></div>',
8997
- css: `.callout{${WORDS}${CARD}box-sizing:border-box;width:420px;height:132px;padding:22px 24px}.head{display:flex;align-items:center;gap:10px}.dot{width:8px;height:8px;border-radius:99px;background:#4ade80;box-shadow:0 0 10px rgba(74,222,128,.9)}.title{font-size:20px;font-weight:600;letter-spacing:-0.01em}.body{margin-top:12px;font-size:15px;line-height:1.5;color:#8b8b94}`
8998
- },
8999
- {
9000
- id: "code",
9001
- name: "Code",
9002
- box: { width: 460, height: 168 },
9003
- html: '<div class="code"><span class="mut">// button.tsx</span>\n<span class="kw">export function</span> <span class="fn">Button</span>({ variant }) {\n <span class="kw">return</span> <span class="tag">&lt;button</span> className={<span class="fn">cn</span>(variant)} <span class="tag">/&gt;</span>\n}</div>',
9004
- css: `.code{${MONO}${CARD}color:#e8e8ec;box-sizing:border-box;width:460px;height:168px;padding:22px 24px;font-size:14px;line-height:1.8;white-space:pre;letter-spacing:-0.01em}.kw{color:#c4b5fd}.fn{color:#7dd3fc}.tag{color:#f0abfc}.mut{color:#5b5b66}`
9005
- },
9322
+ // The callout grammar (three shapes a viewer learns once), in the house
9323
+ // register: a dark card in the house hue over a light app.
9324
+ grammarStarter("note", "Note", {
9325
+ kicker: "Presets",
9326
+ title: "One of 43, picked here.",
9327
+ body: "One line that says what this is about."
9328
+ }),
9329
+ grammarStarter("tag", "Tag", { kicker: "New in 2.0" }),
9330
+ grammarStarter("code", "Code", {
9331
+ kicker: "index.css",
9332
+ code: ":root {\n --primary: oklch(0.63 0.06 4.6);\n --radius: 2.25rem;\n}"
9333
+ }),
9006
9334
  {
9007
9335
  id: "terminal",
9008
9336
  name: "Terminal",
@@ -9209,7 +9537,7 @@ function applyLook(frame, look, size, video, placement) {
9209
9537
 
9210
9538
  // src/timeline/lanes.ts
9211
9539
  import {
9212
- mapTime as mapTime2,
9540
+ mapTime as mapTime3,
9213
9541
  removeSegment,
9214
9542
  segmentRate as segmentRate3,
9215
9543
  sourceToTimeline as sourceToTimeline2,
@@ -9277,7 +9605,7 @@ var videoLane = {
9277
9605
  index === 0 || !contiguous(segments, index)
9278
9606
  );
9279
9607
  const anchorOut = sourceToTimeline2(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
9280
- const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
9608
+ const sourceT = mapTime3(fullMap, anchorOut + (g.t - edgeOutNow));
9281
9609
  const next = trimSegment(
9282
9610
  segments,
9283
9611
  index,
@@ -9313,7 +9641,7 @@ var videoLane = {
9313
9641
  return null;
9314
9642
  }
9315
9643
  const sourceT = round12(
9316
- mapTime2(segmentPieces(s, speeds, freezes, leading), local)
9644
+ mapTime3(segmentPieces(s, speeds, freezes, leading), local)
9317
9645
  );
9318
9646
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
9319
9647
  const next = [
@@ -9403,7 +9731,7 @@ var freezeLane = {
9403
9731
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
9404
9732
  }
9405
9733
  if (g.t > totalDuration7(rated) + 1e-9) return null;
9406
- const srcT = round12(mapTime2(rated, Math.max(0, g.t)));
9734
+ const srcT = round12(mapTime3(rated, Math.max(0, g.t)));
9407
9735
  if (!kept(srcT) || !clear(srcT)) return null;
9408
9736
  const id = nextFreezeId(doc);
9409
9737
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
@@ -9446,7 +9774,7 @@ var freezeLane = {
9446
9774
  const mine = marks.get(f.id);
9447
9775
  const endOut = mine ? mine.t + mine.duration : null;
9448
9776
  const srcT = round12(
9449
- Math.min(sourceDuration, mapTime2(base, Math.max(0, g.t)))
9777
+ Math.min(sourceDuration, mapTime3(base, Math.max(0, g.t)))
9450
9778
  );
9451
9779
  if (!kept(srcT) || !clear(srcT, f.id)) return null;
9452
9780
  return (d) => {
@@ -9517,7 +9845,7 @@ var zoomLane = {
9517
9845
  const rated = ratedSegments(doc);
9518
9846
  const sourceDuration = anchorSourceDuration(doc);
9519
9847
  if (g.type === "create") {
9520
- const srcT = mapTime2(rated, Math.max(0, g.t));
9848
+ const srcT = mapTime3(rated, Math.max(0, g.t));
9521
9849
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
9522
9850
  const here = spans.filter((z) => (z.media ?? "") === mediaHere);
9523
9851
  const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
@@ -9551,7 +9879,7 @@ var zoomLane = {
9551
9879
  const len = sp.out - sp.in;
9552
9880
  let newIn = clampToKept(
9553
9881
  effectiveSegments(doc),
9554
- mapTime2(rated, Math.max(0, g.t)),
9882
+ mapTime3(rated, Math.max(0, g.t)),
9555
9883
  len
9556
9884
  );
9557
9885
  for (const o of others) {
@@ -9581,7 +9909,7 @@ var zoomLane = {
9581
9909
  sourceDuration,
9582
9910
  ...others.filter((o) => o.in >= sp.out).map((o) => o.in)
9583
9911
  );
9584
- const sourceT = mapTime2(rated, Math.max(0, g.t));
9912
+ const sourceT = mapTime3(rated, Math.max(0, g.t));
9585
9913
  const minSrc = Math.min(
9586
9914
  ZOOM_SPAN_MIN * rateAt(rated, sp.in),
9587
9915
  sp.out - sp.in
@@ -9646,7 +9974,7 @@ var tiltLane = {
9646
9974
  const rated = ratedSegments(doc);
9647
9975
  const sourceDuration = anchorSourceDuration(doc);
9648
9976
  if (g.type === "create") {
9649
- const srcT = mapTime2(rated, Math.max(0, g.t));
9977
+ const srcT = mapTime3(rated, Math.max(0, g.t));
9650
9978
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
9651
9979
  const here = spans.filter((z) => (z.media ?? "") === mediaHere);
9652
9980
  const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
@@ -9678,7 +10006,7 @@ var tiltLane = {
9678
10006
  const len = sp.out - sp.in;
9679
10007
  let newIn = clampToKept(
9680
10008
  effectiveSegments(doc),
9681
- mapTime2(rated, Math.max(0, g.t)),
10009
+ mapTime3(rated, Math.max(0, g.t)),
9682
10010
  len
9683
10011
  );
9684
10012
  for (const o of others) {
@@ -9708,7 +10036,7 @@ var tiltLane = {
9708
10036
  sourceDuration,
9709
10037
  ...others.filter((o) => o.in >= sp.out).map((o) => o.in)
9710
10038
  );
9711
- const sourceT = mapTime2(rated, Math.max(0, g.t));
10039
+ const sourceT = mapTime3(rated, Math.max(0, g.t));
9712
10040
  const minSrc = Math.min(
9713
10041
  TILT_SPAN_MIN * rateAt(rated, sp.in),
9714
10042
  sp.out - sp.in
@@ -9764,7 +10092,7 @@ var camMoveLane = {
9764
10092
  const sourceDuration = anchorSourceDuration(doc);
9765
10093
  if (g.type === "create") {
9766
10094
  if (!doc.source.camKey) return null;
9767
- const srcT = mapTime2(rated, Math.max(0, g.t));
10095
+ const srcT = mapTime3(rated, Math.max(0, g.t));
9768
10096
  if (spans.some((z) => srcT >= z.in && srcT < z.out)) return null;
9769
10097
  const next = spans.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
9770
10098
  const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
@@ -9791,7 +10119,7 @@ var camMoveLane = {
9791
10119
  const len = sp.out - sp.in;
9792
10120
  let newIn = clampToKept(
9793
10121
  effectiveSegments(doc),
9794
- mapTime2(rated, Math.max(0, g.t)),
10122
+ mapTime3(rated, Math.max(0, g.t)),
9795
10123
  len
9796
10124
  );
9797
10125
  for (const o of others) {
@@ -9821,7 +10149,7 @@ var camMoveLane = {
9821
10149
  sourceDuration,
9822
10150
  ...others.filter((o) => o.in >= sp.out).map((o) => o.in)
9823
10151
  );
9824
- const sourceT = mapTime2(rated, Math.max(0, g.t));
10152
+ const sourceT = mapTime3(rated, Math.max(0, g.t));
9825
10153
  const minSrc = Math.min(
9826
10154
  CAM_SPAN_MIN * rateAt(rated, sp.in),
9827
10155
  sp.out - sp.in
@@ -9895,7 +10223,7 @@ var camLane = {
9895
10223
  if (!seg) return null;
9896
10224
  const clipSrcStart = Math.max(seg.in, Math.max(current.in, first.in));
9897
10225
  const span = Math.max(0.05, current.out - current.in);
9898
- const delta = mapTime2(rated, Math.max(0, g.t)) - clipSrcStart;
10226
+ const delta = mapTime3(rated, Math.max(0, g.t)) - clipSrcStart;
9899
10227
  const base = Math.max(first.in, current.in);
9900
10228
  const newIn = Math.min(
9901
10229
  Math.max(first.in, base + delta),
@@ -9911,7 +10239,7 @@ var camLane = {
9911
10239
  })).filter((x) => x.len > 1e-6);
9912
10240
  const edgeIdx = g.edge === "start" ? overlapping.at(0)?.i : overlapping.at(-1)?.i;
9913
10241
  if (edgeIdx === void 0 || g.id !== `cam-${edgeIdx}`) return null;
9914
- const sourceT = Math.min(Math.max(mapTime2(rated, g.t), 0), sourceDuration);
10242
+ const sourceT = Math.min(Math.max(mapTime3(rated, g.t), 0), sourceDuration);
9915
10243
  const next = g.edge === "start" ? { in: Math.min(sourceT, current.out - 0.05), out: current.out } : { in: current.in, out: Math.max(sourceT, current.in + 0.05) };
9916
10244
  return (d) => {
9917
10245
  d.cam.window = {
@@ -10005,7 +10333,7 @@ var speedLane = {
10005
10333
  const rated = ratedSegments(doc);
10006
10334
  const sourceDuration = anchorSourceDuration(doc);
10007
10335
  if (g.type === "create") {
10008
- const srcT = mapTime2(rated, Math.max(0, g.t));
10336
+ const srcT = mapTime3(rated, Math.max(0, g.t));
10009
10337
  if (spans.some((s) => srcT >= s.in && srcT < s.out)) return null;
10010
10338
  const next = spans.filter((s) => s.in > srcT).sort((a, b) => a.in - b.in).at(0);
10011
10339
  const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
@@ -10035,7 +10363,7 @@ var speedLane = {
10035
10363
  const len = sp.out - sp.in;
10036
10364
  let newIn = clampToKept(
10037
10365
  effectiveSegments(doc),
10038
- mapTime2(base, Math.max(0, g.t)),
10366
+ mapTime3(base, Math.max(0, g.t)),
10039
10367
  len
10040
10368
  );
10041
10369
  for (const o of others) {
@@ -10064,7 +10392,7 @@ var speedLane = {
10064
10392
  const minSrc = Math.min(SPEED_SPAN_MIN * sp.rate, sp.out - sp.in);
10065
10393
  let next;
10066
10394
  if (g.edge === "start") {
10067
- const sourceT = mapTime2(base, Math.max(0, g.t));
10395
+ const sourceT = mapTime3(base, Math.max(0, g.t));
10068
10396
  next = {
10069
10397
  in: Math.min(Math.max(lo, sourceT), sp.out - minSrc),
10070
10398
  out: sp.out
@@ -10432,7 +10760,7 @@ function computePeaks(channels, buckets) {
10432
10760
  }
10433
10761
 
10434
10762
  // src/lower/duckCurve.ts
10435
- import { mapTime as mapTime3 } from "@vosjs/timeline";
10763
+ import { mapTime as mapTime4 } from "@vosjs/timeline";
10436
10764
  var DEFAULT_DUCK = {
10437
10765
  threshold: 0.02,
10438
10766
  duckTo: 0.25,
@@ -10468,7 +10796,7 @@ function duckCurve(rms, segments, durationSec2, opts = DEFAULT_DUCK) {
10468
10796
  const steps = Math.ceil(durationSec2 * opts.gridHz);
10469
10797
  for (let i = 0; i <= steps; i++) {
10470
10798
  const t = Math.min(durationSec2, i * dt);
10471
- const srcT = mapTime3(segments, t);
10799
+ const srcT = mapTime4(segments, t);
10472
10800
  const w = Math.min(
10473
10801
  rms.values.length - 1,
10474
10802
  Math.max(0, Math.floor(srcT * rms.rate))
@@ -10543,6 +10871,9 @@ export {
10543
10871
  AUDIO_PLAN_STEP,
10544
10872
  BACKGROUND_Z,
10545
10873
  BASE_FRAME_STYLE,
10874
+ CALLOUT_DEFAULT_BODY_PX,
10875
+ CALLOUT_SHAPES,
10876
+ CALLOUT_TYPE_RATIO,
10546
10877
  CAMERA_CENTRE_RAMP,
10547
10878
  CAMERA_COVER_MIN,
10548
10879
  CAMERA_FAR,
@@ -10617,6 +10948,7 @@ export {
10617
10948
  FREEZE_SECONDS_MAX,
10618
10949
  FREEZE_SECONDS_MIN,
10619
10950
  HOUSE_GRADIENT,
10951
+ HOUSE_REGISTER,
10620
10952
  HTML_LAYER_FOREIGN_FIELDS,
10621
10953
  HTML_LAYER_MAX_BYTES,
10622
10954
  HTML_LAYER_SVG_CODE,
@@ -10708,6 +11040,11 @@ export {
10708
11040
  audioLane,
10709
11041
  backdropMedia,
10710
11042
  buildDigest,
11043
+ calloutBox,
11044
+ calloutClip,
11045
+ calloutGroundOf,
11046
+ calloutLook,
11047
+ calloutSource,
10711
11048
  camBubbleRect,
10712
11049
  camBubbleRectAt,
10713
11050
  camLane,
@@ -10742,6 +11079,7 @@ export {
10742
11079
  cssFamilies,
10743
11080
  cssWeights,
10744
11081
  cursorIdleFade,
11082
+ deltaE,
10745
11083
  deriveViewportCrop,
10746
11084
  destinationById,
10747
11085
  destinationsForChannel,
@@ -10779,6 +11117,7 @@ export {
10779
11117
  frameGeometry,
10780
11118
  freezeLane,
10781
11119
  freezeOutputExtents,
11120
+ hexToRgb,
10782
11121
  hexToRgbTriplet,
10783
11122
  houseLook,
10784
11123
  htmlLayerBleed,
@@ -10849,12 +11188,15 @@ export {
10849
11188
  overlaysLane,
10850
11189
  ownFreezes,
10851
11190
  pageDisplayUrl,
11191
+ parseCssColour,
10852
11192
  parsePoseId,
10853
11193
  pickStyle,
10854
11194
  pinBox,
11195
+ pinCandidates,
10855
11196
  pinPlacement,
10856
11197
  pinRectOnScreen,
10857
11198
  pinReferent,
11199
+ pinReferentAt,
10858
11200
  placeFreezes,
10859
11201
  planAutoSpeed,
10860
11202
  planAutoTilt,
@@ -10868,6 +11210,7 @@ export {
10868
11210
  recommendedExportResolution,
10869
11211
  refillAudioBeds,
10870
11212
  rejectSpan,
11213
+ relativeLuminance,
10871
11214
  removeSourceRange,
10872
11215
  removeSpeedInRange,
10873
11216
  resolveExportSize,
@@ -10878,6 +11221,7 @@ export {
10878
11221
  resolveZoomStyle,
10879
11222
  restLeanOf,
10880
11223
  restSpansThroughTransitions,
11224
+ rgbToHex,
10881
11225
  sameMedia,
10882
11226
  sceneChanges,
10883
11227
  scrollRuns,