@hyperframes/studio 0.7.7 → 0.7.9
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/{index-B2YXvFxf.js → index-B7fv-WA3.js} +1 -1
- package/dist/assets/{index-BoASKOeE.js → index-C48wGs63.js} +1 -1
- package/dist/assets/{index-BeRh2hMe.js → index-e4xyxg7-.js} +187 -187
- package/dist/assets/index-svYFaNuq.css +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.html +2 -2
- package/dist/index.js +2715 -2061
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/StudioRightPanel.tsx +5 -1
- package/src/components/editor/DomEditOverlay.tsx +4 -12
- package/src/components/editor/MarqueeOverlay.tsx +40 -0
- package/src/components/editor/OffCanvasIndicators.tsx +2 -7
- package/src/components/editor/PropertyPanel.tsx +12 -0
- package/src/components/editor/Transform3DCube.tsx +313 -0
- package/src/components/editor/gsapAnimationConstants.ts +5 -0
- package/src/components/editor/manualOffsetDrag.test.ts +99 -0
- package/src/components/editor/manualOffsetDrag.ts +38 -7
- package/src/components/editor/marqueeCommit.ts +63 -20
- package/src/components/editor/propertyPanel3dTransform.tsx +311 -92
- package/src/components/editor/propertyPanelHelpers.ts +43 -21
- package/src/components/editor/transform3dProjection.test.ts +99 -0
- package/src/components/editor/transform3dProjection.ts +172 -0
- package/src/components/sidebar/AssetsTab.tsx +23 -213
- package/src/components/sidebar/AudioRow.tsx +202 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/gsapDragCommit.test.ts +9 -5
- package/src/hooks/gsapDragCommit.ts +28 -9
- package/src/hooks/gsapRuntimeKeyframes.ts +50 -5
- package/src/hooks/gsapRuntimePatch.ts +162 -35
- package/src/hooks/useAnimatedPropertyCommit.ts +256 -78
- package/src/hooks/useDomEditCommits.ts +0 -2
- package/src/hooks/useDomEditSession.ts +4 -2
- package/src/hooks/useDomGeometryCommits.ts +3 -31
- package/src/hooks/useEnableKeyframes.test.ts +40 -0
- package/src/hooks/useEnableKeyframes.ts +9 -2
- package/src/hooks/useGsapAwareEditing.ts +31 -1
- package/src/hooks/useGsapKeyframeOps.ts +4 -1
- package/src/hooks/useGsapSelectionHandlers.ts +12 -9
- package/src/hooks/useGsapTweenCache.ts +6 -4
- package/src/utils/marqueeGeometry.test.ts +15 -98
- package/src/utils/marqueeGeometry.ts +1 -158
- package/dist/assets/index-BSkUuN8g.css +0 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
applyStudioPathOffsetDraft,
|
|
5
5
|
beginStudioManualEditGesture,
|
|
6
6
|
captureStudioPathOffset,
|
|
7
|
+
clearStudioPathOffset,
|
|
7
8
|
endStudioManualEditGesture,
|
|
8
9
|
readAppliedStudioPathOffset,
|
|
9
10
|
restoreStudioPathOffset,
|
|
@@ -35,17 +36,17 @@ function getOffsetDragGsap(element: HTMLElement): OffsetDragGsap | null {
|
|
|
35
36
|
function applyOffsetDragDraftViaGsap(
|
|
36
37
|
element: HTMLElement,
|
|
37
38
|
offset: { x: number; y: number },
|
|
39
|
+
baseGsap: { x: number; y: number },
|
|
38
40
|
): boolean {
|
|
39
41
|
const gsap = getOffsetDragGsap(element);
|
|
40
42
|
if (!gsap) return false;
|
|
41
43
|
// GSAP owns the transform; neutralize the CSS translate longhand so the two
|
|
42
44
|
// channels can't compose into a doubled position.
|
|
43
45
|
element.style.setProperty("translate", "none");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const { newX, newY } = computeDraggedGsapPosition(element, offset, fallbackBase);
|
|
46
|
+
// Use the STABLE gesture-start base (captured in JS), NOT `gsap.getProperty`.
|
|
47
|
+
// After `translate: none`, getProperty reads the transform we set last frame,
|
|
48
|
+
// so `base + delta` would integrate frame-over-frame and fling the element.
|
|
49
|
+
const { newX, newY } = computeDraggedGsapPosition(element, offset, baseGsap);
|
|
49
50
|
gsap.set(element, { x: newX, y: newY });
|
|
50
51
|
return true;
|
|
51
52
|
}
|
|
@@ -96,6 +97,14 @@ export interface ManualOffsetDragMember {
|
|
|
96
97
|
selection: DomEditSelection;
|
|
97
98
|
element: HTMLElement;
|
|
98
99
|
initialOffset: { x: number; y: number };
|
|
100
|
+
/**
|
|
101
|
+
* The element's GSAP x/y at gesture start, captured in JS so a mid-drag
|
|
102
|
+
* re-render (which reverts inline style + wipes the `data-hf-drag-gsap-base-*`
|
|
103
|
+
* attrs) can't drop the base. Without this the draft falls back to the LIVE
|
|
104
|
+
* transform — i.e. the value it set last frame — and `base + delta` integrates,
|
|
105
|
+
* making the element accelerate away ("flies"). See applyOffsetDragDraftViaGsap.
|
|
106
|
+
*/
|
|
107
|
+
baseGsap: { x: number; y: number };
|
|
99
108
|
initialPathOffset: StudioPathOffsetSnapshot;
|
|
100
109
|
gestureToken: string;
|
|
101
110
|
screenToOffset: ManualOffsetDragMatrix;
|
|
@@ -343,6 +352,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
343
352
|
scaleX: input.rect.editScaleX,
|
|
344
353
|
scaleY: input.rect.editScaleY,
|
|
345
354
|
});
|
|
355
|
+
const baseGsap = { x: gsapX, y: gsapY };
|
|
346
356
|
if (!measured.ok) {
|
|
347
357
|
// Fallback: when GSAP transforms interfere with probe measurement, use
|
|
348
358
|
// the preview scale as an approximation. The commit path reads the actual
|
|
@@ -357,6 +367,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
357
367
|
selection: input.selection,
|
|
358
368
|
element: input.element,
|
|
359
369
|
initialOffset,
|
|
370
|
+
baseGsap,
|
|
360
371
|
initialPathOffset,
|
|
361
372
|
gestureToken,
|
|
362
373
|
screenToOffset: { a: 1 / scaleX, b: 0, c: 0, d: 1 / scaleY },
|
|
@@ -372,6 +383,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
372
383
|
selection: input.selection,
|
|
373
384
|
element: input.element,
|
|
374
385
|
initialOffset,
|
|
386
|
+
baseGsap,
|
|
375
387
|
initialPathOffset,
|
|
376
388
|
gestureToken,
|
|
377
389
|
screenToOffset: measured.matrix,
|
|
@@ -402,7 +414,7 @@ export function applyManualOffsetDragDraft(
|
|
|
402
414
|
// Position is single-sourced on the GSAP timeline; preview through gsap.set so
|
|
403
415
|
// the live draft matches the committed `tl.set`/keyframe. CSS draft only when
|
|
404
416
|
// gsap is unavailable (no preview iframe runtime).
|
|
405
|
-
if (!applyOffsetDragDraftViaGsap(member.element, offset)) {
|
|
417
|
+
if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
|
|
406
418
|
applyStudioPathOffsetDraft(member.element, offset);
|
|
407
419
|
}
|
|
408
420
|
return offset;
|
|
@@ -413,12 +425,22 @@ export function applyManualOffsetDragCommit(
|
|
|
413
425
|
dx: number,
|
|
414
426
|
dy: number,
|
|
415
427
|
): { x: number; y: number } {
|
|
428
|
+
// Re-stamp the STABLE gesture-start base/offset before the source commit reads
|
|
429
|
+
// them. A mid-drag re-render can wipe these attrs; the commit converts the drop
|
|
430
|
+
// offset → gsap x/y via computeDraggedGsapPosition, which without the base falls
|
|
431
|
+
// back to the live (already-dragged) transform and re-adds the delta — so the
|
|
432
|
+
// element flies off-screen the instant you drop it. The member holds the true
|
|
433
|
+
// gesture-start values in JS, immune to the re-render.
|
|
434
|
+
member.element.setAttribute("data-hf-drag-gsap-base-x", String(member.baseGsap.x));
|
|
435
|
+
member.element.setAttribute("data-hf-drag-gsap-base-y", String(member.baseGsap.y));
|
|
436
|
+
member.element.setAttribute("data-hf-drag-initial-offset-x", String(member.initialOffset.x));
|
|
437
|
+
member.element.setAttribute("data-hf-drag-initial-offset-y", String(member.initialOffset.y));
|
|
416
438
|
const offset = resolveManualOffsetDragMemberOffset(member, dx, dy);
|
|
417
439
|
// Optimistic visual through the GSAP channel (same as the live draft and the
|
|
418
440
|
// committed `tl.set`), so the element holds its dropped position until the
|
|
419
441
|
// source mutation soft-reloads — no transient CSS `--hf-studio-offset` write.
|
|
420
442
|
// CSS apply only when gsap is unavailable.
|
|
421
|
-
if (!applyOffsetDragDraftViaGsap(member.element, offset)) {
|
|
443
|
+
if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
|
|
422
444
|
applyStudioPathOffset(member.element, offset);
|
|
423
445
|
}
|
|
424
446
|
return offset;
|
|
@@ -451,6 +473,15 @@ export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): v
|
|
|
451
473
|
if (member.element.style.getPropertyValue("translate") === "none") {
|
|
452
474
|
member.element.style.removeProperty("translate");
|
|
453
475
|
}
|
|
476
|
+
// Migration: when GSAP owns the position (the committed value lives in the
|
|
477
|
+
// GSAP transform), the legacy `--hf-studio-offset` CSS channel is obsolete.
|
|
478
|
+
// Clear it on the LIVE element — otherwise the leftover `translate:
|
|
479
|
+
// var(--hf-studio-offset)` composes with the GSAP transform and the element
|
|
480
|
+
// renders offset by the stale value until a full page reload (the source is
|
|
481
|
+
// already stripped). clearStudioPathOffset leaves `transform` untouched.
|
|
482
|
+
if (getOffsetDragGsap(member.element)) {
|
|
483
|
+
clearStudioPathOffset(member.element);
|
|
484
|
+
}
|
|
454
485
|
resumeGsapTimelines(member.element);
|
|
455
486
|
}
|
|
456
487
|
}
|
|
@@ -3,7 +3,8 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
3
3
|
import { collectDomEditLayerItems, resolveDomEditSelection } from "./domEditingLayers";
|
|
4
4
|
import { isElementComputedVisible } from "./domEditingElement";
|
|
5
5
|
import { coversComposition } from "../../utils/studioPreviewHelpers";
|
|
6
|
-
import {
|
|
6
|
+
import { rectsOverlap, type Rect } from "../../utils/marqueeGeometry";
|
|
7
|
+
import { toOverlayRect } from "./domEditOverlayGeometry";
|
|
7
8
|
|
|
8
9
|
interface MarqueeState {
|
|
9
10
|
startX: number;
|
|
@@ -16,13 +17,26 @@ interface MarqueeState {
|
|
|
16
17
|
|
|
17
18
|
const MARQUEE_THRESHOLD_PX = 4;
|
|
18
19
|
|
|
20
|
+
interface MarqueeHit {
|
|
21
|
+
element: HTMLElement;
|
|
22
|
+
rect: Rect;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Synchronous core of the marquee: the elements whose overlay-space rect
|
|
27
|
+
* intersects the marquee rect. Uses the SAME `toOverlayRect` basis as the
|
|
28
|
+
* single-selection / group-selection boxes, so what the marquee highlights
|
|
29
|
+
* and selects is exactly the box the user sees when they click an element.
|
|
30
|
+
* Shared by the live candidate highlight (per pointer-move) and the mouse-up
|
|
31
|
+
* commit. No async source probe — that only happens once, on commit.
|
|
32
|
+
*/
|
|
19
33
|
// fallow-ignore-next-line complexity
|
|
20
|
-
|
|
21
|
-
rect:
|
|
34
|
+
function collectMarqueeHits(
|
|
35
|
+
rect: Rect,
|
|
22
36
|
iframe: HTMLIFrameElement,
|
|
23
37
|
overlayEl: HTMLDivElement,
|
|
24
38
|
activeCompositionPath: string,
|
|
25
|
-
):
|
|
39
|
+
): MarqueeHit[] {
|
|
26
40
|
const doc = iframe.contentDocument;
|
|
27
41
|
if (!doc) return [];
|
|
28
42
|
|
|
@@ -38,22 +52,42 @@ async function runMarqueeIntersection(
|
|
|
38
52
|
height: declH > 0 ? declH : rootEl.getBoundingClientRect().height || 1,
|
|
39
53
|
};
|
|
40
54
|
|
|
41
|
-
const hits:
|
|
55
|
+
const hits: MarqueeHit[] = [];
|
|
42
56
|
for (const item of items) {
|
|
43
57
|
const el = item.element;
|
|
44
58
|
if (!isElementComputedVisible(el)) continue;
|
|
45
59
|
if (coversComposition(el.getBoundingClientRect(), viewport)) continue;
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
const overlayRect = toOverlayRect(overlayEl, iframe, el);
|
|
61
|
+
if (!overlayRect) continue;
|
|
62
|
+
const r: Rect = {
|
|
63
|
+
left: overlayRect.left,
|
|
64
|
+
top: overlayRect.top,
|
|
65
|
+
width: overlayRect.width,
|
|
66
|
+
height: overlayRect.height,
|
|
67
|
+
};
|
|
68
|
+
if (!rectsOverlap(rect, r)) continue;
|
|
69
|
+
hits.push({ element: el, rect: r });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return hits;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runMarqueeIntersection(
|
|
76
|
+
rect: Rect,
|
|
77
|
+
iframe: HTMLIFrameElement,
|
|
78
|
+
overlayEl: HTMLDivElement,
|
|
79
|
+
activeCompositionPath: string,
|
|
80
|
+
): Promise<DomEditSelection[]> {
|
|
81
|
+
const isMasterView = !activeCompositionPath || activeCompositionPath === "index.html";
|
|
82
|
+
const hits: DomEditSelection[] = [];
|
|
83
|
+
for (const { element } of collectMarqueeHits(rect, iframe, overlayEl, activeCompositionPath)) {
|
|
84
|
+
const sel = await resolveDomEditSelection(element, {
|
|
50
85
|
activeCompositionPath,
|
|
51
86
|
isMasterView,
|
|
52
87
|
skipSourceProbe: true,
|
|
53
88
|
});
|
|
54
89
|
if (sel) hits.push(sel);
|
|
55
90
|
}
|
|
56
|
-
|
|
57
91
|
return hits;
|
|
58
92
|
}
|
|
59
93
|
|
|
@@ -75,12 +109,12 @@ interface MarqueeGesturesDeps {
|
|
|
75
109
|
// fallow-ignore-next-line complexity
|
|
76
110
|
export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
77
111
|
const marqueeRef = useRef<MarqueeState | null>(null);
|
|
78
|
-
const [marqueeRect, setMarqueeRect] = useState<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
const [marqueeRect, setMarqueeRect] = useState<Rect | null>(null);
|
|
113
|
+
// Live "candidate" highlight: the elements the marquee currently touches,
|
|
114
|
+
// shown before mouse-up so you can see what you're about to select. The
|
|
115
|
+
// iframe DOM doesn't mutate during a drag, so a sync intersection per move
|
|
116
|
+
// is cheap (clean layout → no thrash).
|
|
117
|
+
const [candidateRects, setCandidateRects] = useState<Rect[]>([]);
|
|
84
118
|
|
|
85
119
|
const commitMarquee = useCallback(
|
|
86
120
|
async (
|
|
@@ -111,17 +145,24 @@ export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
|
111
145
|
if (Math.hypot(dx, dy) < MARQUEE_THRESHOLD_PX) return;
|
|
112
146
|
m.pastThreshold = true;
|
|
113
147
|
}
|
|
114
|
-
|
|
148
|
+
const rect: Rect = {
|
|
115
149
|
left: Math.min(m.startX, m.currentX),
|
|
116
150
|
top: Math.min(m.startY, m.currentY),
|
|
117
151
|
width: Math.abs(m.currentX - m.startX),
|
|
118
152
|
height: Math.abs(m.currentY - m.startY),
|
|
119
|
-
}
|
|
153
|
+
};
|
|
154
|
+
setMarqueeRect(rect);
|
|
155
|
+
const iframe = deps.iframeRef.current;
|
|
156
|
+
const overlay = deps.overlayRef.current;
|
|
157
|
+
if (iframe && overlay) {
|
|
158
|
+
const acp = deps.activeCompositionPathRef.current ?? "index.html";
|
|
159
|
+
setCandidateRects(collectMarqueeHits(rect, iframe, overlay, acp).map((h) => h.rect));
|
|
160
|
+
}
|
|
120
161
|
return;
|
|
121
162
|
}
|
|
122
163
|
deps.gestures.onPointerMove(event);
|
|
123
164
|
},
|
|
124
|
-
[deps.gestures, deps.overlayRef],
|
|
165
|
+
[deps.gestures, deps.overlayRef, deps.iframeRef, deps.activeCompositionPathRef],
|
|
125
166
|
);
|
|
126
167
|
|
|
127
168
|
const onPointerUp = useCallback(
|
|
@@ -148,6 +189,7 @@ export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
|
148
189
|
deps.onMarqueeSelectRef.current?.([], false);
|
|
149
190
|
}
|
|
150
191
|
setMarqueeRect(null);
|
|
192
|
+
setCandidateRects([]);
|
|
151
193
|
return;
|
|
152
194
|
}
|
|
153
195
|
deps.gestures.onPointerUp(event);
|
|
@@ -159,10 +201,11 @@ export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
|
159
201
|
if (marqueeRef.current) {
|
|
160
202
|
marqueeRef.current = null;
|
|
161
203
|
setMarqueeRect(null);
|
|
204
|
+
setCandidateRects([]);
|
|
162
205
|
return;
|
|
163
206
|
}
|
|
164
207
|
deps.gestures.clearPointerState(deps.selectionRef);
|
|
165
208
|
}, [deps.gestures, deps.selectionRef]);
|
|
166
209
|
|
|
167
|
-
return { marqueeRef, marqueeRect, onPointerMove, onPointerUp, onPointerCancel };
|
|
210
|
+
return { marqueeRef, marqueeRect, candidateRects, onPointerMove, onPointerUp, onPointerCancel };
|
|
168
211
|
}
|