@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.
- package/dist/assets/{hyperframes-player-DMzBHhbH.js → hyperframes-player-ColOZ_78.js} +1 -1
- package/dist/assets/{index-B-YDp8hN.js → index-BuSTrK-Z.js} +146 -146
- package/dist/assets/{index-CBRX2eZX.js → index-CVuUfURE.js} +1 -1
- package/dist/assets/{index-t1LH6H5H.css → index-D11atkBt.css} +1 -1
- package/dist/assets/{index-lyjhSEzi.js → index-jQEqFKMk.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +1250 -1043
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/FrameyGlyph.tsx +52 -0
- package/src/components/OpenInDesktopButton.dom.test.tsx +130 -0
- package/src/components/OpenInDesktopButton.tsx +160 -0
- package/src/components/StudioHeader.dom.test.tsx +2 -0
- package/src/components/StudioHeader.tsx +2 -0
- package/src/player/components/AudibleVideoClipContent.test.tsx +67 -0
- package/src/player/components/AudibleVideoClipContent.tsx +5 -4
- package/src/player/components/AudioWaveform.tsx +2 -1
- package/src/player/components/ClipPeakMarks.tsx +5 -2
- package/src/player/components/OutOfSyncBadge.tsx +1 -1
- package/src/player/components/Timeline.trackPadding.test.tsx +1 -3
- package/src/player/components/TimelineCanvas.tsx +1 -0
- package/src/player/components/TimelineClip.filmstrip.test.tsx +27 -0
- package/src/player/components/TimelineClipFades.test.tsx +2 -1
- package/src/player/components/timelineClipDragPreview.test.ts +31 -0
- package/src/player/components/timelineClipDragPreview.ts +42 -37
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +11 -0
- package/src/styles/components.css +82 -1
- 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
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
}
|
package/src/styles/theme.css
CHANGED
|
@@ -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
|