@hyperframes/studio 0.5.5 → 0.6.0-alpha.2
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-Cd8vYWxP.js +198 -0
- package/dist/assets/index-UWFaHilT.css +1 -0
- package/dist/assets/index-cPJbxeAk.js +107 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +2621 -170
- package/src/components/LintModal.tsx +3 -4
- package/src/components/editor/DomEditOverlay.test.ts +241 -0
- package/src/components/editor/DomEditOverlay.tsx +1300 -0
- package/src/components/editor/MotionPanel.tsx +651 -0
- package/src/components/editor/PropertyPanel.test.ts +67 -0
- package/src/components/editor/PropertyPanel.tsx +2891 -207
- package/src/components/editor/TimelineLayerPanel.test.ts +42 -0
- package/src/components/editor/TimelineLayerPanel.tsx +113 -0
- package/src/components/editor/colorValue.test.ts +82 -0
- package/src/components/editor/colorValue.ts +175 -0
- package/src/components/editor/domEditing.test.ts +872 -0
- package/src/components/editor/domEditing.ts +993 -0
- package/src/components/editor/floatingPanel.test.ts +34 -0
- package/src/components/editor/floatingPanel.ts +54 -0
- package/src/components/editor/fontAssets.ts +32 -0
- package/src/components/editor/fontCatalog.ts +126 -0
- package/src/components/editor/gradientValue.test.ts +89 -0
- package/src/components/editor/gradientValue.ts +445 -0
- package/src/components/editor/manualEditingAvailability.test.ts +129 -0
- package/src/components/editor/manualEditingAvailability.ts +60 -0
- package/src/components/editor/manualEdits.test.ts +945 -0
- package/src/components/editor/manualEdits.ts +1397 -0
- package/src/components/editor/manualOffsetDrag.test.ts +140 -0
- package/src/components/editor/manualOffsetDrag.ts +307 -0
- package/src/components/editor/studioMotion.test.ts +355 -0
- package/src/components/editor/studioMotion.ts +632 -0
- package/src/components/nle/NLELayout.tsx +27 -4
- package/src/components/nle/NLEPreview.tsx +50 -5
- package/src/components/renders/RenderQueue.tsx +13 -62
- package/src/components/renders/useRenderQueue.ts +6 -30
- package/src/components/sidebar/AssetsTab.tsx +3 -4
- package/src/components/sidebar/CompositionsTab.test.ts +16 -1
- package/src/components/sidebar/CompositionsTab.tsx +117 -45
- package/src/components/sidebar/LeftSidebar.tsx +140 -125
- package/src/hooks/usePersistentEditHistory.test.ts +256 -0
- package/src/hooks/usePersistentEditHistory.ts +337 -0
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/CompositionThumbnail.test.ts +19 -0
- package/src/player/components/CompositionThumbnail.tsx +50 -13
- package/src/player/components/EditModal.tsx +5 -20
- package/src/player/components/Player.tsx +18 -2
- package/src/player/components/Timeline.test.ts +20 -0
- package/src/player/components/Timeline.tsx +103 -21
- package/src/player/components/TimelineClip.test.ts +92 -0
- package/src/player/components/TimelineClip.tsx +241 -7
- package/src/player/components/timelineEditing.test.ts +16 -3
- package/src/player/components/timelineEditing.ts +10 -3
- package/src/player/hooks/useTimelinePlayer.test.ts +148 -19
- package/src/player/hooks/useTimelinePlayer.ts +287 -16
- package/src/player/store/playerStore.ts +2 -0
- package/src/utils/clipboard.test.ts +89 -0
- package/src/utils/clipboard.ts +57 -0
- package/src/utils/editHistory.test.ts +244 -0
- package/src/utils/editHistory.ts +218 -0
- package/src/utils/editHistoryStorage.test.ts +37 -0
- package/src/utils/editHistoryStorage.ts +99 -0
- package/src/utils/mediaTypes.ts +1 -1
- package/src/utils/sourcePatcher.test.ts +128 -1
- package/src/utils/sourcePatcher.ts +130 -18
- package/src/utils/studioFileHistory.test.ts +156 -0
- package/src/utils/studioFileHistory.ts +61 -0
- package/src/utils/timelineAssetDrop.test.ts +31 -11
- package/src/utils/timelineAssetDrop.ts +22 -2
- package/src/utils/timelineInspector.test.ts +79 -0
- package/src/utils/timelineInspector.ts +116 -0
- package/dist/assets/hyperframes-player-CEnWY28J.js +0 -417
- package/dist/assets/index-04Mp2wOn.css +0 -1
- package/dist/assets/index-960mgQMI.js +0 -93
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { EditHistoryKind } from "./editHistory";
|
|
2
|
+
|
|
3
|
+
interface SaveProjectFilesWithHistoryInput {
|
|
4
|
+
projectId: string;
|
|
5
|
+
label: string;
|
|
6
|
+
kind: EditHistoryKind;
|
|
7
|
+
coalesceKey?: string;
|
|
8
|
+
files: Record<string, string>;
|
|
9
|
+
readFile: (path: string) => Promise<string>;
|
|
10
|
+
writeFile: (path: string, content: string) => Promise<void>;
|
|
11
|
+
recordEdit: (entry: {
|
|
12
|
+
label: string;
|
|
13
|
+
kind: EditHistoryKind;
|
|
14
|
+
coalesceKey?: string;
|
|
15
|
+
files: Record<string, { before: string; after: string }>;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function saveProjectFilesWithHistory({
|
|
20
|
+
label,
|
|
21
|
+
kind,
|
|
22
|
+
coalesceKey,
|
|
23
|
+
files,
|
|
24
|
+
readFile,
|
|
25
|
+
writeFile,
|
|
26
|
+
recordEdit,
|
|
27
|
+
}: SaveProjectFilesWithHistoryInput): Promise<string[]> {
|
|
28
|
+
const snapshots: Record<string, { before: string; after: string }> = {};
|
|
29
|
+
for (const [path, after] of Object.entries(files)) {
|
|
30
|
+
const before = await readFile(path);
|
|
31
|
+
if (before !== after) {
|
|
32
|
+
snapshots[path] = { before, after };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const changedPaths = Object.keys(snapshots);
|
|
37
|
+
if (changedPaths.length === 0) return [];
|
|
38
|
+
|
|
39
|
+
const writtenPaths: string[] = [];
|
|
40
|
+
try {
|
|
41
|
+
for (const path of changedPaths) {
|
|
42
|
+
await writeFile(path, snapshots[path].after);
|
|
43
|
+
writtenPaths.push(path);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await recordEdit({ label, kind, coalesceKey, files: snapshots });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
try {
|
|
49
|
+
for (const path of writtenPaths.reverse()) {
|
|
50
|
+
await writeFile(path, snapshots[path].before);
|
|
51
|
+
}
|
|
52
|
+
} catch (rollbackError) {
|
|
53
|
+
throw new AggregateError(
|
|
54
|
+
[error, rollbackError],
|
|
55
|
+
"Failed to save project files and rollback did not complete",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
return changedPaths;
|
|
61
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
buildTimelineAssetInsertHtml,
|
|
5
5
|
getTimelineAssetKind,
|
|
6
6
|
insertTimelineAssetIntoSource,
|
|
7
|
+
resolveTimelineAssetInitialGeometry,
|
|
7
8
|
resolveTimelineAssetSrc,
|
|
8
9
|
} from "./timelineAssetDrop";
|
|
9
10
|
|
|
@@ -19,17 +20,21 @@ describe("getTimelineAssetKind", () => {
|
|
|
19
20
|
|
|
20
21
|
describe("buildTimelineAssetInsertHtml", () => {
|
|
21
22
|
it("builds an image clip with explicit timing and track", () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
const html = buildTimelineAssetInsertHtml({
|
|
24
|
+
id: "photo_asset",
|
|
25
|
+
assetPath: "assets/photo.png",
|
|
26
|
+
kind: "image",
|
|
27
|
+
start: 1.25,
|
|
28
|
+
duration: 3,
|
|
29
|
+
track: 2,
|
|
30
|
+
zIndex: 4,
|
|
31
|
+
geometry: { left: 0, top: 0, width: 1280, height: 720 },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(html).toContain('img id="photo_asset"');
|
|
35
|
+
expect(html).toContain("left: 0px");
|
|
36
|
+
expect(html).toContain("width: 1280px");
|
|
37
|
+
expect(html).not.toContain("inset:");
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
it("builds an audio clip without visual layout styles", () => {
|
|
@@ -47,6 +52,21 @@ describe("buildTimelineAssetInsertHtml", () => {
|
|
|
47
52
|
});
|
|
48
53
|
});
|
|
49
54
|
|
|
55
|
+
describe("resolveTimelineAssetInitialGeometry", () => {
|
|
56
|
+
it("uses the target composition dimensions for visual media", () => {
|
|
57
|
+
expect(
|
|
58
|
+
resolveTimelineAssetInitialGeometry(
|
|
59
|
+
`<div data-composition-id="main" data-width="330" data-height="228"></div>`,
|
|
60
|
+
),
|
|
61
|
+
).toEqual({
|
|
62
|
+
left: 0,
|
|
63
|
+
top: 0,
|
|
64
|
+
width: 330,
|
|
65
|
+
height: 228,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
50
70
|
describe("resolveTimelineAssetSrc", () => {
|
|
51
71
|
it("keeps project-root asset paths for index.html", () => {
|
|
52
72
|
expect(resolveTimelineAssetSrc("index.html", "assets/photo.png")).toBe("assets/photo.png");
|
|
@@ -76,6 +76,23 @@ export function buildTimelineFileDropPlacements(
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export function resolveTimelineAssetInitialGeometry(source: string): {
|
|
80
|
+
left: number;
|
|
81
|
+
top: number;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
} {
|
|
85
|
+
const width = Number.parseFloat(source.match(/\bdata-width=(["'])([^"']+)\1/i)?.[2] ?? "");
|
|
86
|
+
const height = Number.parseFloat(source.match(/\bdata-height=(["'])([^"']+)\1/i)?.[2] ?? "");
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
left: 0,
|
|
90
|
+
top: 0,
|
|
91
|
+
width: Number.isFinite(width) && width > 0 ? Math.round(width) : 640,
|
|
92
|
+
height: Number.isFinite(height) && height > 0 ? Math.round(height) : 360,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
export function buildTimelineAssetInsertHtml(input: {
|
|
80
97
|
id: string;
|
|
81
98
|
assetPath: string;
|
|
@@ -84,15 +101,18 @@ export function buildTimelineAssetInsertHtml(input: {
|
|
|
84
101
|
duration: number;
|
|
85
102
|
track: number;
|
|
86
103
|
zIndex: number;
|
|
104
|
+
geometry?: { left: number; top: number; width: number; height: number };
|
|
87
105
|
}): string {
|
|
88
106
|
const sharedAttrs = `id="${input.id}" class="clip" src="${input.assetPath}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
|
|
107
|
+
const geometry = input.geometry ?? { left: 0, top: 0, width: 640, height: 360 };
|
|
108
|
+
const visualStyles = `position: absolute; left: ${geometry.left}px; top: ${geometry.top}px; width: ${geometry.width}px; height: ${geometry.height}px; object-fit: contain; z-index: ${input.zIndex}`;
|
|
89
109
|
|
|
90
110
|
if (input.kind === "image") {
|
|
91
|
-
return `<img ${sharedAttrs} style="
|
|
111
|
+
return `<img ${sharedAttrs} style="${visualStyles}" />`;
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
if (input.kind === "video") {
|
|
95
|
-
return `<video ${sharedAttrs} muted playsinline style="
|
|
115
|
+
return `<video ${sharedAttrs} muted playsinline style="${visualStyles}"></video>`;
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
return `<audio ${sharedAttrs} style="z-index: ${input.zIndex}"></audio>`;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Window } from "happy-dom";
|
|
3
|
+
import {
|
|
4
|
+
canInspectTimelineElement,
|
|
5
|
+
getTimelineLayerVisibilityInPreview,
|
|
6
|
+
getTimelineElementKey,
|
|
7
|
+
isAudioTimelineElement,
|
|
8
|
+
isTimelineElementActiveAtTime,
|
|
9
|
+
isTimelineLayerVisibleInPreview,
|
|
10
|
+
shouldShowTimelineInspectorBounds,
|
|
11
|
+
} from "./timelineInspector";
|
|
12
|
+
|
|
13
|
+
function createDocument(markup: string): Document {
|
|
14
|
+
const window = new Window();
|
|
15
|
+
window.document.body.innerHTML = markup;
|
|
16
|
+
return window.document;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function attachVisibleBox(element: HTMLElement) {
|
|
20
|
+
Object.defineProperty(element, "getBoundingClientRect", {
|
|
21
|
+
configurable: true,
|
|
22
|
+
value: () => ({
|
|
23
|
+
bottom: 34,
|
|
24
|
+
height: 24,
|
|
25
|
+
left: 10,
|
|
26
|
+
right: 90,
|
|
27
|
+
top: 10,
|
|
28
|
+
width: 80,
|
|
29
|
+
x: 10,
|
|
30
|
+
y: 10,
|
|
31
|
+
toJSON: () => ({}),
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("timeline inspector", () => {
|
|
37
|
+
it("keeps visual clips inspectable and audio-only clips out of the visual panel", () => {
|
|
38
|
+
expect(canInspectTimelineElement({ tag: "section" })).toBe(true);
|
|
39
|
+
expect(canInspectTimelineElement({ tag: "video", src: "assets/demo.mp4" })).toBe(true);
|
|
40
|
+
expect(canInspectTimelineElement({ tag: "audio" })).toBe(false);
|
|
41
|
+
expect(canInspectTimelineElement({ tag: "div", src: "assets/narration.mp3" })).toBe(false);
|
|
42
|
+
expect(isAudioTimelineElement({ tag: "sfx" })).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("uses stable timeline keys and only shows bounds at clip edges", () => {
|
|
46
|
+
expect(getTimelineElementKey({ id: "card", key: "index.html#card" })).toBe("index.html#card");
|
|
47
|
+
expect(shouldShowTimelineInspectorBounds(2, { start: 2, duration: 4 })).toBe(true);
|
|
48
|
+
expect(shouldShowTimelineInspectorBounds(6, { start: 2, duration: 4 })).toBe(true);
|
|
49
|
+
expect(shouldShowTimelineInspectorBounds(4, { start: 2, duration: 4 })).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("keeps selected layer bounds visible only while the clip is active", () => {
|
|
53
|
+
expect(isTimelineElementActiveAtTime(1.99, { start: 2, duration: 4 }, 0)).toBe(false);
|
|
54
|
+
expect(isTimelineElementActiveAtTime(2, { start: 2, duration: 4 }, 0)).toBe(true);
|
|
55
|
+
expect(isTimelineElementActiveAtTime(4, { start: 2, duration: 4 }, 0)).toBe(true);
|
|
56
|
+
expect(isTimelineElementActiveAtTime(6, { start: 2, duration: 4 }, 0)).toBe(true);
|
|
57
|
+
expect(isTimelineElementActiveAtTime(6.01, { start: 2, duration: 4 }, 0)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("uses composite visibility for nested layers", () => {
|
|
61
|
+
const hiddenDoc = createDocument(`<div style="opacity: 0"><span id="label">Label</span></div>`);
|
|
62
|
+
const hiddenLabel = hiddenDoc.getElementById("label") as HTMLElement;
|
|
63
|
+
attachVisibleBox(hiddenLabel);
|
|
64
|
+
expect(isTimelineLayerVisibleInPreview(hiddenLabel)).toBe(false);
|
|
65
|
+
|
|
66
|
+
const visibleDoc = createDocument(
|
|
67
|
+
`<div style="opacity: 1"><span id="label">Label</span></div>`,
|
|
68
|
+
);
|
|
69
|
+
const visibleLabel = visibleDoc.getElementById("label") as HTMLElement;
|
|
70
|
+
attachVisibleBox(visibleLabel);
|
|
71
|
+
expect(isTimelineLayerVisibleInPreview(visibleLabel)).toBe(true);
|
|
72
|
+
expect(getTimelineLayerVisibilityInPreview(visibleLabel)).toMatchObject({
|
|
73
|
+
compositeOpacity: 1,
|
|
74
|
+
hasBox: true,
|
|
75
|
+
inViewport: true,
|
|
76
|
+
visible: true,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { TimelineElement } from "../player";
|
|
2
|
+
|
|
3
|
+
export const TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS = 0.08;
|
|
4
|
+
|
|
5
|
+
const AUDIO_TIMELINE_TAGS = new Set(["audio", "music", "sfx", "sound", "narration"]);
|
|
6
|
+
const AUDIO_SOURCE_EXT_RE = /\.(aac|flac|m4a|mp3|ogg|opus|wav)(?:[?#].*)?$/i;
|
|
7
|
+
|
|
8
|
+
export function getTimelineElementKey(
|
|
9
|
+
element: Pick<TimelineElement, "id" | "key"> | null | undefined,
|
|
10
|
+
): string | null {
|
|
11
|
+
if (!element) return null;
|
|
12
|
+
return element.key ?? element.id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isAudioTimelineElement(
|
|
16
|
+
element: Pick<TimelineElement, "tag" | "src"> | null | undefined,
|
|
17
|
+
): boolean {
|
|
18
|
+
if (!element) return false;
|
|
19
|
+
const tag = element.tag.trim().toLowerCase();
|
|
20
|
+
if (AUDIO_TIMELINE_TAGS.has(tag)) return true;
|
|
21
|
+
return Boolean(element.src && AUDIO_SOURCE_EXT_RE.test(element.src));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function canInspectTimelineElement(
|
|
25
|
+
element: Pick<TimelineElement, "tag" | "src"> | null | undefined,
|
|
26
|
+
): boolean {
|
|
27
|
+
return !isAudioTimelineElement(element);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldShowTimelineInspectorBounds(
|
|
31
|
+
currentTime: number,
|
|
32
|
+
element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
|
|
33
|
+
epsilonSeconds = TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!element) return false;
|
|
36
|
+
if (!Number.isFinite(currentTime)) return false;
|
|
37
|
+
if (!Number.isFinite(element.start) || !Number.isFinite(element.duration)) return false;
|
|
38
|
+
const start = Math.max(0, element.start);
|
|
39
|
+
const end = Math.max(start, start + Math.max(0, element.duration));
|
|
40
|
+
const epsilon = Math.max(0, epsilonSeconds);
|
|
41
|
+
return Math.abs(currentTime - start) <= epsilon || Math.abs(currentTime - end) <= epsilon;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isTimelineElementActiveAtTime(
|
|
45
|
+
currentTime: number,
|
|
46
|
+
element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
|
|
47
|
+
epsilonSeconds = TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS,
|
|
48
|
+
): boolean {
|
|
49
|
+
if (!element) return false;
|
|
50
|
+
if (!Number.isFinite(currentTime)) return false;
|
|
51
|
+
if (!Number.isFinite(element.start) || !Number.isFinite(element.duration)) return false;
|
|
52
|
+
const start = Math.max(0, element.start);
|
|
53
|
+
const end = Math.max(start, start + Math.max(0, element.duration));
|
|
54
|
+
const epsilon = Math.max(0, epsilonSeconds);
|
|
55
|
+
return currentTime >= start - epsilon && currentTime <= end + epsilon;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TimelineLayerVisibility {
|
|
59
|
+
visible: boolean;
|
|
60
|
+
compositeOpacity: number;
|
|
61
|
+
hasBox: boolean;
|
|
62
|
+
inViewport: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getTimelineLayerVisibilityInPreview(
|
|
66
|
+
element: HTMLElement,
|
|
67
|
+
options: { minCompositeOpacity?: number } = {},
|
|
68
|
+
): TimelineLayerVisibility {
|
|
69
|
+
const hidden: TimelineLayerVisibility = {
|
|
70
|
+
visible: false,
|
|
71
|
+
compositeOpacity: 0,
|
|
72
|
+
hasBox: false,
|
|
73
|
+
inViewport: false,
|
|
74
|
+
};
|
|
75
|
+
if (!element.isConnected) return hidden;
|
|
76
|
+
const doc = element.ownerDocument;
|
|
77
|
+
const win = doc.defaultView;
|
|
78
|
+
if (!win) return hidden;
|
|
79
|
+
|
|
80
|
+
const minCompositeOpacity = options.minCompositeOpacity ?? 0.01;
|
|
81
|
+
let compositeOpacity = 1;
|
|
82
|
+
let current: HTMLElement | null = element;
|
|
83
|
+
while (current && current !== doc.body && current !== doc.documentElement) {
|
|
84
|
+
const style = win.getComputedStyle(current);
|
|
85
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
86
|
+
return { ...hidden, compositeOpacity };
|
|
87
|
+
}
|
|
88
|
+
compositeOpacity *= Number.parseFloat(style.opacity || "1");
|
|
89
|
+
if (compositeOpacity <= minCompositeOpacity) {
|
|
90
|
+
return { ...hidden, compositeOpacity };
|
|
91
|
+
}
|
|
92
|
+
current = current.parentElement;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rect = element.getBoundingClientRect();
|
|
96
|
+
const hasBox = rect.width > 0.5 && rect.height > 0.5;
|
|
97
|
+
if (!hasBox) return { visible: false, compositeOpacity, hasBox, inViewport: false };
|
|
98
|
+
|
|
99
|
+
const viewportWidth = win.innerWidth || doc.documentElement.clientWidth;
|
|
100
|
+
const viewportHeight = win.innerHeight || doc.documentElement.clientHeight;
|
|
101
|
+
const inViewport =
|
|
102
|
+
rect.right > 0 && rect.bottom > 0 && rect.left < viewportWidth && rect.top < viewportHeight;
|
|
103
|
+
return {
|
|
104
|
+
visible: inViewport,
|
|
105
|
+
compositeOpacity,
|
|
106
|
+
hasBox,
|
|
107
|
+
inViewport,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isTimelineLayerVisibleInPreview(
|
|
112
|
+
element: HTMLElement,
|
|
113
|
+
options: { minCompositeOpacity?: number } = {},
|
|
114
|
+
): boolean {
|
|
115
|
+
return getTimelineLayerVisibilityInPreview(element, options).visible;
|
|
116
|
+
}
|