@hyperframes/studio 0.8.122 → 0.8.124

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 (29) hide show
  1. package/dist/assets/{hyperframes-player-DMzBHhbH.js → hyperframes-player-ColOZ_78.js} +1 -1
  2. package/dist/assets/{index-B-YDp8hN.js → index-BuSTrK-Z.js} +146 -146
  3. package/dist/assets/{index-CBRX2eZX.js → index-CVuUfURE.js} +1 -1
  4. package/dist/assets/{index-t1LH6H5H.css → index-D11atkBt.css} +1 -1
  5. package/dist/assets/{index-lyjhSEzi.js → index-jQEqFKMk.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +1250 -1043
  8. package/dist/index.js.map +1 -1
  9. package/dist/styles.css +1 -1
  10. package/package.json +8 -8
  11. package/src/components/FrameyGlyph.tsx +52 -0
  12. package/src/components/OpenInDesktopButton.dom.test.tsx +130 -0
  13. package/src/components/OpenInDesktopButton.tsx +160 -0
  14. package/src/components/StudioHeader.dom.test.tsx +2 -0
  15. package/src/components/StudioHeader.tsx +2 -0
  16. package/src/player/components/AudibleVideoClipContent.test.tsx +67 -0
  17. package/src/player/components/AudibleVideoClipContent.tsx +5 -4
  18. package/src/player/components/AudioWaveform.tsx +2 -1
  19. package/src/player/components/ClipPeakMarks.tsx +5 -2
  20. package/src/player/components/OutOfSyncBadge.tsx +1 -1
  21. package/src/player/components/Timeline.trackPadding.test.tsx +1 -3
  22. package/src/player/components/TimelineCanvas.tsx +1 -0
  23. package/src/player/components/TimelineClip.filmstrip.test.tsx +27 -0
  24. package/src/player/components/TimelineClipFades.test.tsx +2 -1
  25. package/src/player/components/timelineClipDragPreview.test.ts +31 -0
  26. package/src/player/components/timelineClipDragPreview.ts +42 -37
  27. package/src/player/hooks/useTimelinePlayer.seek.test.ts +11 -0
  28. package/src/styles/components.css +82 -1
  29. package/src/styles/theme.css +3 -0
@@ -241,7 +241,7 @@ export function computeDragPreview(
241
241
  drag.element.key ?? drag.element.id,
242
242
  !isMusicTrack(drag.element),
243
243
  );
244
- const snap = snapMoveToTargets(
244
+ const snapped = snapMoveToTargets(
245
245
  nextMove.start,
246
246
  drag.element.duration,
247
247
  targets,
@@ -251,6 +251,11 @@ export function computeDragPreview(
251
251
  dragMaxStart + drag.element.duration,
252
252
  ctx.gridStep,
253
253
  );
254
+ // A snap the saved clip would miss jumps on release, so the clip stays where the pointer put it.
255
+ const target = snapTarget(snapped);
256
+ const missed =
257
+ target !== null && !guideIfSaved(drag.element, { start: snapped.start }, target, pps);
258
+ const snap = missed ? { start: nextMove.start, snapTime: null, snapType: null } : snapped;
254
259
  // A group moves rigidly: the grabbed clip stops where any mover would cross its host's start.
255
260
  const group = resolveGroupDrag(drag, ctx);
256
261
  const dragKey = drag.element.key ?? drag.element.id;
@@ -268,11 +273,7 @@ export function computeDragPreview(
268
273
  return { ...drag, started: true };
269
274
  }
270
275
  const { track: previewTrack, insertRow } = placement;
271
- const target =
272
- placement.start === snap.start && snap.snapTime !== null && snap.snapType !== null
273
- ? { time: snap.snapTime, type: snap.snapType }
274
- : null;
275
- const guide = guideIfSaved(drag.element, { start: snap.start }, target, pps);
276
+ const guide = placement.start === snap.start ? snapTarget(snap) : null;
276
277
  return {
277
278
  ...drag,
278
279
  started: true,
@@ -289,6 +290,9 @@ export function computeDragPreview(
289
290
  };
290
291
  }
291
292
 
293
+ const snapTarget = (s: { snapTime: number | null; snapType: TimelineSnapType | null }) =>
294
+ s.snapTime !== null && s.snapType !== null ? { time: s.snapTime, type: s.snapType } : null;
295
+
292
296
  /** A snap's guide, kept only when the clip's edge as its file will save it is within a pixel of it. */
293
297
  export function guideIfSaved(
294
298
  element: TimelineElement,
@@ -392,37 +396,38 @@ export function computeResizePreview(
392
396
  );
393
397
  const gridStep = ctx.gridStep ?? 0;
394
398
  let snap: TimelineSnapTarget | null = null;
395
- if (trimTargets.length > 0 || gridStep > 0) {
396
- const snapSecs = TIMELINE_SNAP_PX / Math.max(pps, 1);
397
- if (resize.edge === "end") {
398
- const edgeTime = nextResize.start + nextResize.duration;
399
- const { time: snapped, target } = snapTimelineTime(edgeTime, trimTargets, snapSecs, gridStep);
400
- // Stay within [start+minDuration, maxEnd] so the snap can't create a
401
- // degenerate clip or run past the source/composition limit.
402
- const snappedDuration = Math.round((snapped - nextResize.start) * 1000) / 1000;
403
- if (
404
- target &&
405
- snapped <= maxEnd + 1e-6 &&
406
- snappedDuration >= resolveTimelineMinDuration() - 1e-6
407
- ) {
408
- // An edge already on the target still owns the guide; only move it when off.
409
- if (snapped !== edgeTime) nextResize = { ...nextResize, duration: snappedDuration };
410
- snap = guideIfSaved(resize.element, nextResize, target, pps);
411
- }
412
- } else {
413
- const { time: snapped, target } = snapTimelineTime(
414
- nextResize.start,
415
- trimTargets,
416
- snapSecs,
417
- gridStep,
418
- );
419
- const clip = { ...nextResize, playbackRate: resize.element.playbackRate };
420
- const delta = snapped - nextResize.start;
421
- const bounds = clipStartTrimDeltaBounds(clip, minStart, resolveTimelineMinDuration());
422
- if (target && delta >= bounds.minDelta - 1e-6 && delta <= bounds.maxDelta + 1e-6) {
423
- if (snapped !== nextResize.start) nextResize = applyClipStartTrimDelta(clip, delta);
424
- snap = guideIfSaved(resize.element, nextResize, target, pps);
425
- }
399
+ const snapSecs = TIMELINE_SNAP_PX / Math.max(pps, 1);
400
+ if (resize.edge === "end") {
401
+ const edgeTime = nextResize.start + nextResize.duration;
402
+ const { time: snapped, target } = snapTimelineTime(edgeTime, trimTargets, snapSecs, gridStep);
403
+ // Stay within [start+minDuration, maxEnd] so the snap can't create a
404
+ // degenerate clip or run past the source/composition limit.
405
+ const snappedDuration = Math.round((snapped - nextResize.start) * 1000) / 1000;
406
+ if (
407
+ target &&
408
+ snapped <= maxEnd + 1e-6 &&
409
+ snappedDuration >= resolveTimelineMinDuration() - 1e-6
410
+ ) {
411
+ const onTarget =
412
+ snapped === edgeTime ? nextResize : { ...nextResize, duration: snappedDuration };
413
+ snap = guideIfSaved(resize.element, onTarget, target, pps);
414
+ if (snap) nextResize = onTarget;
415
+ }
416
+ } else {
417
+ const { time: snapped, target } = snapTimelineTime(
418
+ nextResize.start,
419
+ trimTargets,
420
+ snapSecs,
421
+ gridStep,
422
+ );
423
+ const clip = { ...nextResize, playbackRate: resize.element.playbackRate };
424
+ const delta = snapped - nextResize.start;
425
+ const bounds = clipStartTrimDeltaBounds(clip, minStart, resolveTimelineMinDuration());
426
+ if (target && delta >= bounds.minDelta - 1e-6 && delta <= bounds.maxDelta + 1e-6) {
427
+ const onTarget =
428
+ snapped === nextResize.start ? nextResize : applyClipStartTrimDelta(clip, delta);
429
+ snap = guideIfSaved(resize.element, onTarget, target, pps);
430
+ if (snap) nextResize = onTarget;
426
431
  }
427
432
  }
428
433
 
@@ -406,6 +406,17 @@ describe("useTimelinePlayer preview frame (a trim's dragged edge)", () => {
406
406
  unmountWithAct(root);
407
407
  });
408
408
 
409
+ it("reads the playing transport's own time, not the playhead's, under a preview frame", () => {
410
+ const { api, root, adapter } = renderAttachedTimelinePlayer();
411
+ seekWithAct(api, 2);
412
+ setStorePlaying();
413
+ previewFrame(5);
414
+ adapter.seek(7);
415
+ act(() => api.pause());
416
+ expect(usePlayerStore.getState().currentTime).toBe(7);
417
+ unmountWithAct(root);
418
+ });
419
+
409
420
  it("leaves live playback alone", () => {
410
421
  const { api, root, adapter } = renderAttachedTimelinePlayer();
411
422
  seekWithAct(api, 2);
@@ -308,6 +308,22 @@
308
308
  }
309
309
  }
310
310
 
311
+ /* A video's sound strip is too short for the peak readout: only the mark stays, on a chip clear of the clip's
312
+ rounded corner and above the waveform canvas layer (z 10), below the fade handles (z 30). */
313
+ .audible-video-wave [data-peak-text] {
314
+ display: none;
315
+ }
316
+
317
+ .audible-video-wave [data-peak-badge] {
318
+ top: 50%;
319
+ right: var(--timeline-clip-radius);
320
+ translate: 0 -50%;
321
+ padding: 1px 3px;
322
+ border-radius: 3px;
323
+ background: var(--timeline-clip-chip-audio-bg);
324
+ z-index: 20;
325
+ }
326
+
311
327
  .timeline-clip.is-audio.is-hovered {
312
328
  background-color: var(--timeline-clip-audio-bg-hover);
313
329
  }
@@ -350,7 +366,6 @@
350
366
  position: absolute;
351
367
  top: 5px;
352
368
  left: 6px;
353
- z-index: 6;
354
369
  width: fit-content;
355
370
  max-width: calc(100% - 22px);
356
371
  overflow: hidden;
@@ -411,6 +426,7 @@
411
426
  .timeline-clip.is-audio .timeline-clip__label {
412
427
  top: 11px;
413
428
  left: 9px;
429
+ z-index: 11;
414
430
  background: var(--timeline-clip-chip-audio-bg);
415
431
  }
416
432
 
@@ -935,3 +951,68 @@
935
951
  transform: scaleY(1);
936
952
  }
937
953
  }
954
+
955
+ /* Framey on the header's "Edit with Framey" (OpenInDesktopButton.tsx): a pulse as the page opens, a wiggle
956
+ * on hover, a flight towards the app while it opens. The eye follows the pointer from script. */
957
+ @keyframes hf-framey-wiggle {
958
+ 0%,
959
+ 100% {
960
+ transform: rotate(0deg);
961
+ }
962
+ 25% {
963
+ transform: rotate(-14deg);
964
+ }
965
+ 60% {
966
+ transform: rotate(10deg);
967
+ }
968
+ }
969
+
970
+ @keyframes hf-framey-fly {
971
+ 70% {
972
+ opacity: 1;
973
+ }
974
+ 100% {
975
+ transform: translate(48px, -28px) rotate(25deg) scale(0.6);
976
+ opacity: 0;
977
+ }
978
+ }
979
+
980
+ @keyframes hf-framey-pulse {
981
+ 0% {
982
+ box-shadow: 0 0 0 0 color-mix(in oklch, var(--color-accent) 45%, transparent);
983
+ }
984
+ 100% {
985
+ box-shadow: 0 0 0 8px transparent;
986
+ }
987
+ }
988
+
989
+ .hf-framey-trigger {
990
+ animation: hf-framey-pulse 1.6s ease-out 3;
991
+ }
992
+
993
+ .hf-framey {
994
+ transform-origin: 30% 30%;
995
+ }
996
+
997
+ .hf-framey-eye {
998
+ transition: transform 80ms linear;
999
+ }
1000
+
1001
+ .hf-framey-trigger:hover .hf-framey {
1002
+ animation: hf-framey-wiggle 0.5s ease-in-out;
1003
+ }
1004
+
1005
+ .hf-framey-trigger[data-opening] .hf-framey {
1006
+ animation: hf-framey-fly 0.9s ease-in forwards;
1007
+ }
1008
+
1009
+ @media (prefers-reduced-motion: reduce) {
1010
+ .hf-framey-trigger,
1011
+ .hf-framey-trigger:hover .hf-framey,
1012
+ .hf-framey-trigger[data-opening] .hf-framey {
1013
+ animation: none;
1014
+ }
1015
+ .hf-framey-eye {
1016
+ transition: none;
1017
+ }
1018
+ }
@@ -436,6 +436,9 @@
436
436
  --color-violet-400: oklch(70.2% 0.183 293.541);
437
437
  --color-violet-500: oklch(60.6% 0.25 292.717);
438
438
  --color-white: #fff;
439
+ /* Framey, the desktop app's cursor mascot (hyperframes-internal packages/nib): the same in both themes. */
440
+ --color-framey: #3ce6ac;
441
+ --color-framey-ink: #16121a;
439
442
  }
440
443
 
441
444
  /* The colour-scheme picks each token's light or dark half: dark unless the document element