@hyperframes/studio 0.7.15 → 0.7.17
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-CTXExGxO.js → index-BCAdHPz_.js} +1 -1
- package/dist/assets/{index-Db9KG66z.js → index-BDx28x9R.js} +1 -1
- package/dist/assets/index-DmkOvZns.css +1 -0
- package/dist/assets/index-hiqLDAtz.js +375 -0
- package/dist/{chunk-UUEDZ4UJ.js → chunk-SBGXX7WY.js} +52 -22
- package/dist/chunk-SBGXX7WY.js.map +1 -0
- package/dist/{domEditingLayers-NWCBED4R.js → domEditingLayers-VZMLL4AP.js} +2 -4
- package/dist/index.d.ts +18 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3157 -2808
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -0
- package/src/components/StudioPreviewArea.tsx +0 -41
- package/src/components/StudioRightPanel.tsx +2 -0
- package/src/components/TimelineToolbar.tsx +4 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +2 -2
- package/src/components/editor/GsapAnimationSection.tsx +3 -3
- package/src/components/editor/InspectorHeaderActions.tsx +62 -0
- package/src/components/editor/LayersPanel.tsx +35 -2
- package/src/components/editor/MotionPathOverlay.tsx +24 -8
- package/src/components/editor/PropertyPanel.tsx +48 -60
- package/src/components/editor/Transform3DCube.tsx +84 -85
- package/src/components/editor/domEditOverlayGeometry.ts +28 -0
- package/src/components/editor/domEditingDom.ts +36 -2
- package/src/components/editor/domEditingGroups.ts +41 -0
- package/src/components/editor/domEditingLayers.test.ts +97 -1
- package/src/components/editor/domEditingLayers.ts +13 -29
- package/src/components/editor/domEditingTypes.ts +3 -0
- package/src/components/editor/manualOffsetDrag.ts +23 -2
- package/src/components/editor/propertyPanel3dTransform.tsx +70 -30
- package/src/components/editor/propertyPanelHelpers.ts +2 -0
- package/src/components/editor/useDomEditOverlayRects.ts +8 -3
- package/src/components/editor/useMotionPathData.ts +47 -1
- package/src/contexts/DomEditContext.tsx +16 -0
- package/src/contexts/TimelineEditContext.tsx +0 -1
- package/src/hooks/gsapRuntimeBridge.ts +9 -2
- package/src/hooks/gsapTweenSynth.ts +64 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +126 -26
- package/src/hooks/useAppHotkeys.ts +34 -3
- package/src/hooks/useDomEditSession.ts +48 -0
- package/src/hooks/useDomSelection.ts +73 -9
- package/src/hooks/useEnableKeyframes.test.ts +35 -0
- package/src/hooks/useEnableKeyframes.ts +29 -1
- package/src/hooks/useGroupCommits.ts +188 -0
- package/src/hooks/useGsapSelectionHandlers.ts +4 -0
- package/src/hooks/useGsapTweenCache.ts +56 -72
- package/src/hooks/usePreviewInteraction.ts +60 -2
- package/src/player/components/ShortcutsPanel.tsx +2 -0
- package/src/player/components/Timeline.tsx +0 -15
- package/src/player/components/TimelineCanvas.tsx +0 -13
- package/src/player/components/TimelineClipDiamonds.tsx +4 -125
- package/src/player/components/timelineCallbacks.ts +0 -1
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +47 -4
- package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -4
- package/src/player/store/playerStore.ts +20 -0
- package/src/utils/studioPreviewHelpers.ts +37 -0
- package/dist/assets/index-B7VWFKy1.js +0 -375
- package/dist/assets/index-svYFaNuq.css +0 -1
- package/dist/chunk-UUEDZ4UJ.js.map +0 -1
- package/src/components/editor/keyframeMove.test.ts +0 -101
- package/src/components/editor/keyframeMove.ts +0 -151
- /package/dist/{domEditingLayers-NWCBED4R.js.map → domEditingLayers-VZMLL4AP.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
2
|
+
import { usePlayerStore, type TimelineElement, type DomClipChild } from "../store/playerStore";
|
|
3
3
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
4
4
|
import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
|
|
5
5
|
import { buildTimelineElementKey } from "../lib/timelineElementHelpers";
|
|
@@ -111,6 +111,32 @@ function buildChildElements(
|
|
|
111
111
|
return result;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// Sub-comp DOM children (groups/pills) aren't manifest clips and have no timing
|
|
115
|
+
// of their own — they're "always on" within their sub-comp host, so synthesize
|
|
116
|
+
// clips spanning the host's full bounds. The host element supplies start/duration
|
|
117
|
+
// and the composition file edits write to.
|
|
118
|
+
function domSiblingClips(
|
|
119
|
+
domClipChildren: DomClipChild[],
|
|
120
|
+
siblingParentId: string,
|
|
121
|
+
host: TimelineElement,
|
|
122
|
+
): ClipManifestClip[] {
|
|
123
|
+
return domClipChildren
|
|
124
|
+
.filter((c) => c.parentId === siblingParentId)
|
|
125
|
+
.map((c) => ({
|
|
126
|
+
id: c.id,
|
|
127
|
+
label: c.label,
|
|
128
|
+
start: host.start,
|
|
129
|
+
duration: host.duration,
|
|
130
|
+
track: host.track,
|
|
131
|
+
kind: "element" as const,
|
|
132
|
+
tagName: null,
|
|
133
|
+
compositionId: null,
|
|
134
|
+
parentCompositionId: host.id ?? null,
|
|
135
|
+
compositionSrc: host.compositionSrc ?? null,
|
|
136
|
+
assetUrl: null,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
// Exported for tests.
|
|
115
141
|
export function buildExpandedElements(
|
|
116
142
|
elements: TimelineElement[],
|
|
@@ -118,11 +144,20 @@ export function buildExpandedElements(
|
|
|
118
144
|
parentMap: Map<string, string>,
|
|
119
145
|
topLevelId: string,
|
|
120
146
|
siblingParentId: string,
|
|
147
|
+
domClipChildren: DomClipChild[] = [],
|
|
121
148
|
): TimelineElement[] {
|
|
122
149
|
const topLevelElement = elements.find((el) => el.id === topLevelId || el.domId === topLevelId);
|
|
123
150
|
if (!topLevelElement) return filterToTopLevel(elements, parentMap);
|
|
124
151
|
|
|
125
|
-
|
|
152
|
+
// Prefer real manifest children; fall back to DOM-only sub-comp children
|
|
153
|
+
// (groups/pills) that have no data-start and thus never enter the manifest.
|
|
154
|
+
const siblings = (() => {
|
|
155
|
+
const fromManifest = manifest.filter(
|
|
156
|
+
(c) => c.id != null && parentMap.get(c.id) === siblingParentId,
|
|
157
|
+
);
|
|
158
|
+
if (fromManifest.length > 0) return fromManifest;
|
|
159
|
+
return domSiblingClips(domClipChildren, siblingParentId, topLevelElement);
|
|
160
|
+
})();
|
|
126
161
|
if (siblings.length === 0) return filterToTopLevel(elements, parentMap);
|
|
127
162
|
|
|
128
163
|
// The sub-comp host the children actually live in: top-level host for 1-level
|
|
@@ -154,6 +189,7 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
154
189
|
const elements = usePlayerStore((s) => s.elements);
|
|
155
190
|
const clipManifest = usePlayerStore((s) => s.clipManifest);
|
|
156
191
|
const clipParentMap = usePlayerStore((s) => s.clipParentMap);
|
|
192
|
+
const domClipChildren = usePlayerStore((s) => s.domClipChildren);
|
|
157
193
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
158
194
|
|
|
159
195
|
return useMemo(() => {
|
|
@@ -166,6 +202,13 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
166
202
|
|
|
167
203
|
const immediateParent = clipParentMap.get(rawId)!;
|
|
168
204
|
const topLevel = findTopLevelAncestor(rawId, clipParentMap) ?? immediateParent;
|
|
169
|
-
return buildExpandedElements(
|
|
170
|
-
|
|
205
|
+
return buildExpandedElements(
|
|
206
|
+
elements,
|
|
207
|
+
clipManifest,
|
|
208
|
+
clipParentMap,
|
|
209
|
+
topLevel,
|
|
210
|
+
immediateParent,
|
|
211
|
+
domClipChildren,
|
|
212
|
+
);
|
|
213
|
+
}, [elements, clipManifest, clipParentMap, domClipChildren, selectedElementId]);
|
|
171
214
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { useCallback } from "react";
|
|
12
12
|
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
13
|
-
import type { TimelineElement } from "../store/playerStore";
|
|
13
|
+
import type { TimelineElement, DomClipChild } from "../store/playerStore";
|
|
14
14
|
import type { PlaybackAdapter, ClipManifestClip, IframeWindow } from "../lib/playbackTypes";
|
|
15
15
|
import {
|
|
16
16
|
parseTimelineFromDOM,
|
|
@@ -85,8 +85,8 @@ export function useTimelineSyncCallbacks({
|
|
|
85
85
|
| (Window & { __clipTree?: import("@hyperframes/core/runtime/clipTree").ClipTree })
|
|
86
86
|
| null;
|
|
87
87
|
const clipTree = iframeWin?.__clipTree;
|
|
88
|
+
const parentMap = new Map<string, string>();
|
|
88
89
|
if (clipTree) {
|
|
89
|
-
const parentMap = new Map<string, string>();
|
|
90
90
|
const walk = (nodes: typeof clipTree.roots) => {
|
|
91
91
|
for (const node of nodes) {
|
|
92
92
|
if (node.id && node.parentId) parentMap.set(node.id, node.parentId);
|
|
@@ -94,11 +94,50 @@ export function useTimelineSyncCallbacks({
|
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
walk(clipTree.roots);
|
|
97
|
-
usePlayerStore.getState().setClipParentMap(parentMap);
|
|
98
97
|
}
|
|
98
|
+
|
|
99
|
+
// Descend into each sub-composition host: its internal elements (group
|
|
100
|
+
// wrappers + their children) carry no `data-start`, so the clip
|
|
101
|
+
// tree/manifest never enumerate them. Surface them studio-side as DOM
|
|
102
|
+
// children + parent links so the timeline can expand a sub-comp/group
|
|
103
|
+
// row to show them. Manifest stays lean (timed clips only).
|
|
104
|
+
const domClipChildren: DomClipChild[] = [];
|
|
105
|
+
if (iframeDoc) {
|
|
106
|
+
for (const clip of data.clips) {
|
|
107
|
+
if (clip.kind !== "composition" || !clip.id) continue;
|
|
108
|
+
const hostEl = iframeDoc.getElementById(clip.id);
|
|
109
|
+
if (!hostEl) continue;
|
|
110
|
+
const hostId = clip.id;
|
|
111
|
+
const innerRoot = hostEl.querySelector("[data-hf-inner-root]") ?? hostEl;
|
|
112
|
+
// Collect the sub-comp's id'd descendants (grouped OR ungrouped) so they
|
|
113
|
+
// expand into timeline rows. Descends through id-less structural wrappers
|
|
114
|
+
// (the inlined sub-comp body), and one level into groups for drill-in.
|
|
115
|
+
const collect = (parentEl: Element, parentId: string) => {
|
|
116
|
+
for (const child of Array.from(parentEl.children)) {
|
|
117
|
+
if (!child.id) {
|
|
118
|
+
collect(child, parentId); // unwrap id-less structural containers
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const isGroup = child.hasAttribute("data-hf-group");
|
|
122
|
+
domClipChildren.push({
|
|
123
|
+
id: child.id,
|
|
124
|
+
parentId,
|
|
125
|
+
hostId,
|
|
126
|
+
label: isGroup ? child.getAttribute("data-hf-group") || child.id : child.id,
|
|
127
|
+
});
|
|
128
|
+
parentMap.set(child.id, parentId);
|
|
129
|
+
if (isGroup) collect(child, child.id);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
collect(innerRoot, hostId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
usePlayerStore.getState().setClipParentMap(parentMap);
|
|
136
|
+
usePlayerStore.getState().setDomClipChildren(domClipChildren);
|
|
99
137
|
} catch {
|
|
100
|
-
// cross-origin or __clipTree not available —
|
|
138
|
+
// cross-origin or __clipTree not available — maps stay empty
|
|
101
139
|
}
|
|
140
|
+
|
|
102
141
|
const usedHostEls = new Set<Element>();
|
|
103
142
|
const els: TimelineElement[] = filtered.map((clip, index) => {
|
|
104
143
|
const hostEl = iframeDoc
|
|
@@ -165,6 +165,23 @@ interface PlayerState {
|
|
|
165
165
|
setClipManifest: (clips: ClipManifestClip[] | null) => void;
|
|
166
166
|
clipParentMap: Map<string, string>;
|
|
167
167
|
setClipParentMap: (map: Map<string, string>) => void;
|
|
168
|
+
/**
|
|
169
|
+
* Sub-composition DOM descendants (groups + their children) that have no
|
|
170
|
+
* `data-start`, so they're absent from the clip manifest/tree. Collected
|
|
171
|
+
* studio-side from the live preview so the timeline can expand a sub-comp row
|
|
172
|
+
* to show its DOM-only children. Keeps the manifest lean (timed clips only).
|
|
173
|
+
*/
|
|
174
|
+
domClipChildren: DomClipChild[];
|
|
175
|
+
setDomClipChildren: (children: DomClipChild[]) => void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A sub-comp DOM-only timeline child (no data-start) and its nesting context. */
|
|
179
|
+
export interface DomClipChild {
|
|
180
|
+
id: string;
|
|
181
|
+
parentId: string;
|
|
182
|
+
/** The manifest sub-comp host clip id this descendant ultimately lives under. */
|
|
183
|
+
hostId: string;
|
|
184
|
+
label: string;
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
interface BeatHistoryEntry {
|
|
@@ -296,6 +313,8 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
296
313
|
setClipManifest: (clips) => set({ clipManifest: clips }),
|
|
297
314
|
clipParentMap: new Map(),
|
|
298
315
|
setClipParentMap: (map) => set({ clipParentMap: map }),
|
|
316
|
+
domClipChildren: [],
|
|
317
|
+
setDomClipChildren: (children) => set({ domClipChildren: children }),
|
|
299
318
|
|
|
300
319
|
setIsPlaying: (playing) => {
|
|
301
320
|
if (get().isPlaying === playing) return;
|
|
@@ -380,6 +399,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
380
399
|
beatPersist: null,
|
|
381
400
|
clipManifest: null,
|
|
382
401
|
clipParentMap: new Map(),
|
|
402
|
+
domClipChildren: [],
|
|
383
403
|
}),
|
|
384
404
|
}));
|
|
385
405
|
|
|
@@ -81,6 +81,37 @@ function removePointerEventsOverride(style: HTMLStyleElement | null): void {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Animated group members can move outside their wrapper's static layout box, so
|
|
85
|
+
// the empty space inside a group's *visual* bounds (the member-union the overlay
|
|
86
|
+
// draws) doesn't hit-test to the group via elementsFromPoint. Recover it: if the
|
|
87
|
+
// point falls within a group's live member-union rect, return that wrapper.
|
|
88
|
+
// Innermost (smallest-area) group wins for nested groups.
|
|
89
|
+
function findGroupAtPoint(doc: Document, x: number, y: number): HTMLElement | null {
|
|
90
|
+
let best: HTMLElement | null = null;
|
|
91
|
+
let bestArea = Infinity;
|
|
92
|
+
for (const group of Array.from(doc.querySelectorAll<HTMLElement>("[data-hf-group]"))) {
|
|
93
|
+
let left = Infinity;
|
|
94
|
+
let top = Infinity;
|
|
95
|
+
let right = -Infinity;
|
|
96
|
+
let bottom = -Infinity;
|
|
97
|
+
for (const member of Array.from(group.children)) {
|
|
98
|
+
const r = member.getBoundingClientRect();
|
|
99
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
100
|
+
left = Math.min(left, r.left);
|
|
101
|
+
top = Math.min(top, r.top);
|
|
102
|
+
right = Math.max(right, r.right);
|
|
103
|
+
bottom = Math.max(bottom, r.bottom);
|
|
104
|
+
}
|
|
105
|
+
if (right < left || x < left || x > right || y < top || y > bottom) continue;
|
|
106
|
+
const area = (right - left) * (bottom - top);
|
|
107
|
+
if (area < bestArea) {
|
|
108
|
+
bestArea = area;
|
|
109
|
+
best = group;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return best;
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
// fallow-ignore-next-line complexity
|
|
85
116
|
export function getPreviewTargetFromPointer(
|
|
86
117
|
iframe: HTMLIFrameElement,
|
|
@@ -113,6 +144,12 @@ export function getPreviewTargetFromPointer(
|
|
|
113
144
|
if (visualTarget) return visualTarget;
|
|
114
145
|
}
|
|
115
146
|
|
|
147
|
+
// No element hit (e.g. empty space inside an animated group's overlay) — fall
|
|
148
|
+
// back to the group whose member-union contains the point, so the whole group
|
|
149
|
+
// area is hoverable/selectable, not just where a member currently sits.
|
|
150
|
+
const groupHit = findGroupAtPoint(doc, localPointer.x, localPointer.y);
|
|
151
|
+
if (groupHit && getDomLayerPatchTarget(groupHit, activeCompositionPath)) return groupHit;
|
|
152
|
+
|
|
116
153
|
const fallback = getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
117
154
|
if (!fallback || !getDomLayerPatchTarget(fallback, activeCompositionPath)) return null;
|
|
118
155
|
if (!isElementComputedVisible(fallback)) return null;
|