@hyperframes/studio 0.7.6 → 0.7.8

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 (58) hide show
  1. package/dist/assets/{index-B_NnmU6q.js → index--lOAjFJl.js} +1 -1
  2. package/dist/assets/{index-ysftPins.js → index-BSyZKYmH.js} +204 -203
  3. package/dist/assets/{index-DsBhGFPe.js → index-Dgeszckd.js} +1 -1
  4. package/dist/assets/index-svYFaNuq.css +1 -0
  5. package/dist/index.d.ts +5 -1
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +3650 -2431
  8. package/dist/index.js.map +1 -1
  9. package/package.json +5 -5
  10. package/src/components/StudioRightPanel.tsx +7 -1
  11. package/src/components/editor/AnimationCard.tsx +6 -0
  12. package/src/components/editor/DomEditOverlay.tsx +4 -12
  13. package/src/components/editor/EaseCurveSection.tsx +175 -99
  14. package/src/components/editor/GsapAnimationSection.tsx +2 -0
  15. package/src/components/editor/KeyframeEaseList.tsx +67 -3
  16. package/src/components/editor/MarqueeOverlay.tsx +40 -0
  17. package/src/components/editor/OffCanvasIndicators.tsx +2 -7
  18. package/src/components/editor/PropertyPanel.tsx +23 -2
  19. package/src/components/editor/Transform3DCube.tsx +313 -0
  20. package/src/components/editor/gsapAnimationCallbacks.ts +2 -0
  21. package/src/components/editor/gsapAnimationConstants.ts +11 -35
  22. package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
  23. package/src/components/editor/marqueeCommit.ts +63 -20
  24. package/src/components/editor/propertyPanel3dTransform.tsx +311 -92
  25. package/src/components/editor/propertyPanelHelpers.ts +46 -18
  26. package/src/components/editor/propertyPanelTimingSection.tsx +35 -2
  27. package/src/components/editor/transform3dProjection.test.ts +99 -0
  28. package/src/components/editor/transform3dProjection.ts +172 -0
  29. package/src/components/editor/useMotionPathData.ts +2 -1
  30. package/src/components/sidebar/AssetContextMenu.tsx +97 -0
  31. package/src/components/sidebar/AssetsTab.tsx +364 -266
  32. package/src/components/sidebar/AudioRow.tsx +202 -0
  33. package/src/components/sidebar/assetHelpers.ts +40 -0
  34. package/src/contexts/DomEditContext.tsx +8 -0
  35. package/src/hooks/gsapDragCommit.test.ts +9 -5
  36. package/src/hooks/gsapDragCommit.ts +129 -9
  37. package/src/hooks/gsapRuntimeBridge.ts +22 -0
  38. package/src/hooks/gsapRuntimeKeyframes.test.ts +47 -0
  39. package/src/hooks/gsapRuntimeKeyframes.ts +63 -5
  40. package/src/hooks/gsapRuntimePatch.ts +162 -35
  41. package/src/hooks/useAnimatedPropertyCommit.ts +256 -78
  42. package/src/hooks/useDomEditCommits.ts +0 -2
  43. package/src/hooks/useDomEditSession.ts +24 -2
  44. package/src/hooks/useDomEditWiring.ts +3 -0
  45. package/src/hooks/useDomGeometryCommits.ts +3 -31
  46. package/src/hooks/useGestureCommit.ts +0 -4
  47. package/src/hooks/useGsapAwareEditing.ts +31 -5
  48. package/src/hooks/useGsapKeyframeOps.ts +4 -1
  49. package/src/hooks/useGsapScriptCommits.ts +0 -12
  50. package/src/hooks/useGsapSelectionHandlers.ts +12 -9
  51. package/src/hooks/useGsapTweenCache.test.ts +45 -1
  52. package/src/hooks/useGsapTweenCache.ts +125 -42
  53. package/src/hooks/useMusicBeatAnalysis.ts +36 -21
  54. package/src/hooks/useRazorSplit.ts +0 -3
  55. package/src/utils/marqueeGeometry.test.ts +15 -98
  56. package/src/utils/marqueeGeometry.ts +1 -158
  57. package/dist/assets/index-wJZf6FK3.css +0 -1
  58. package/src/utils/editDebugLog.ts +0 -9
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { projectAxes, projectCubeFaces, rotate, wrapDeg } from "./transform3dProjection";
3
+
4
+ const OPTS = { cx: 50, cy: 50, r: 30, persp: 4 };
5
+
6
+ describe("projectCubeFaces", () => {
7
+ it("shows the front face (and only front-facing faces) at identity", () => {
8
+ const faces = projectCubeFaces(0, 0, 0, OPTS);
9
+ const ids = faces.map((f) => f.id);
10
+ expect(ids).toContain("front");
11
+ // Head-on: side/top/bottom normals are edge-on (n.z ≈ 0) → culled.
12
+ expect(ids).not.toContain("back");
13
+ expect(faces.length).toBeGreaterThanOrEqual(1);
14
+ expect(faces.length).toBeLessThanOrEqual(3);
15
+ });
16
+
17
+ it("reveals the top face when tilted forward on X", () => {
18
+ const ids = projectCubeFaces(45, 0, 0, OPTS).map((f) => f.id);
19
+ expect(ids).toContain("front");
20
+ expect(ids).toContain("top");
21
+ expect(ids).not.toContain("bottom");
22
+ });
23
+
24
+ it("reveals a side face when rotated on Y (CSS rotateY convention)", () => {
25
+ const ids = projectCubeFaces(0, 45, 0, OPTS).map((f) => f.id);
26
+ expect(ids).toContain("front");
27
+ expect(ids).toContain("left");
28
+ expect(ids).not.toContain("right");
29
+ });
30
+
31
+ it("never returns more than 3 faces (a cube shows at most 3 at once)", () => {
32
+ for (const [rx, ry, rz] of [
33
+ [30, 30, 0],
34
+ [60, 60, 45],
35
+ [135, 20, 90],
36
+ ]) {
37
+ expect(projectCubeFaces(rx, ry, rz, OPTS).length).toBeLessThanOrEqual(3);
38
+ }
39
+ });
40
+
41
+ it("rotationZ rolls the silhouette without changing which faces are visible", () => {
42
+ const a = projectCubeFaces(0, 0, 0, OPTS)
43
+ .map((f) => f.id)
44
+ .sort();
45
+ const b = projectCubeFaces(0, 0, 90, OPTS)
46
+ .map((f) => f.id)
47
+ .sort();
48
+ expect(b).toEqual(a);
49
+ // …but the projected coordinates differ (it rolled).
50
+ expect(projectCubeFaces(0, 0, 90, OPTS)[0]?.points).not.toEqual(
51
+ projectCubeFaces(0, 0, 0, OPTS)[0]?.points,
52
+ );
53
+ });
54
+
55
+ it("paints far faces before near faces (painter's order)", () => {
56
+ const faces = projectCubeFaces(35, 35, 0, OPTS);
57
+ for (let i = 1; i < faces.length; i++) {
58
+ expect(faces[i]!.depth).toBeGreaterThanOrEqual(faces[i - 1]!.depth);
59
+ }
60
+ });
61
+ });
62
+
63
+ describe("projectAxes", () => {
64
+ it("returns the three X/Y/Z axes with standard colors", () => {
65
+ const axes = projectAxes(0, 0, 0, OPTS);
66
+ expect(axes.map((a) => a.id).sort()).toEqual(["x", "y", "z"]);
67
+ const z = axes.find((a) => a.id === "z")!;
68
+ expect(z.color).toMatch(/#/);
69
+ });
70
+
71
+ it("flags the toward-viewer axis as front at identity (Z points at the camera)", () => {
72
+ const z = projectAxes(0, 0, 0, OPTS).find((a) => a.id === "z")!;
73
+ expect(z.front).toBe(true);
74
+ });
75
+
76
+ it("rotating 180° on Y flips the Z axis away from the viewer", () => {
77
+ const z = projectAxes(0, 180, 0, OPTS).find((a) => a.id === "z")!;
78
+ expect(z.front).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("rotate", () => {
83
+ it("90° on Y maps +x to -z (right edge swings to the back)", () => {
84
+ const v = rotate({ x: 1, y: 0, z: 0 }, 0, 90, 0);
85
+ expect(v.x).toBeCloseTo(0, 5);
86
+ expect(v.z).toBeCloseTo(-1, 5);
87
+ });
88
+ });
89
+
90
+ describe("wrapDeg", () => {
91
+ it("wraps into (-180, 180]", () => {
92
+ expect(wrapDeg(0)).toBe(0);
93
+ expect(wrapDeg(180)).toBe(180);
94
+ expect(wrapDeg(190)).toBe(-170);
95
+ expect(wrapDeg(-190)).toBe(170);
96
+ expect(wrapDeg(360)).toBe(0);
97
+ expect(wrapDeg(540)).toBe(180);
98
+ });
99
+ });
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Pure cube projection for the 3D-transform widget. Given an element's
3
+ * rotationX/Y/Z (degrees), project a unit cube to 2D SVG polygons with
4
+ * back-face culling and painter's-order sorting — no DOM, no React, so it
5
+ * unit-tests in isolation. Used by Transform3DCube to draw a live preview of
6
+ * the element's orientation.
7
+ */
8
+
9
+ export interface Vec3 {
10
+ x: number;
11
+ y: number;
12
+ z: number;
13
+ }
14
+
15
+ export interface ProjectedFace {
16
+ /** Stable face id (front/back/left/right/top/bottom) for keying + theming. */
17
+ id: string;
18
+ /** SVG polygon points: "x,y x,y x,y x,y". */
19
+ points: string;
20
+ /** 0..1 brightness from how front-facing the rotated normal is (lighting cue). */
21
+ shade: number;
22
+ /** Mean depth of the face's corners; larger = nearer the viewer. */
23
+ depth: number;
24
+ }
25
+
26
+ const DEG = Math.PI / 180;
27
+
28
+ /** Rotate a point intrinsically by X, then Y, then Z (degrees). */
29
+ export function rotate(v: Vec3, rx: number, ry: number, rz: number): Vec3 {
30
+ let { x, y, z } = v;
31
+ const cx = Math.cos(rx * DEG);
32
+ const sx = Math.sin(rx * DEG);
33
+ [y, z] = [y * cx - z * sx, y * sx + z * cx];
34
+ const cy = Math.cos(ry * DEG);
35
+ const sy = Math.sin(ry * DEG);
36
+ [x, z] = [x * cy + z * sy, -x * sy + z * cy];
37
+ const cz = Math.cos(rz * DEG);
38
+ const sz = Math.sin(rz * DEG);
39
+ [x, y] = [x * cz - y * sz, x * sz + y * cz];
40
+ return { x, y, z };
41
+ }
42
+
43
+ // Unit-cube corners (±1) — 0-3 back face (z=-1), 4-7 front face (z=1).
44
+ const CORNERS: Vec3[] = [
45
+ { x: -1, y: -1, z: -1 },
46
+ { x: 1, y: -1, z: -1 },
47
+ { x: 1, y: 1, z: -1 },
48
+ { x: -1, y: 1, z: -1 },
49
+ { x: -1, y: -1, z: 1 },
50
+ { x: 1, y: -1, z: 1 },
51
+ { x: 1, y: 1, z: 1 },
52
+ { x: -1, y: 1, z: 1 },
53
+ ];
54
+
55
+ const FACES: { id: string; idx: [number, number, number, number]; normal: Vec3 }[] = [
56
+ { id: "front", idx: [4, 5, 6, 7], normal: { x: 0, y: 0, z: 1 } },
57
+ { id: "back", idx: [1, 0, 3, 2], normal: { x: 0, y: 0, z: -1 } },
58
+ { id: "left", idx: [0, 4, 7, 3], normal: { x: -1, y: 0, z: 0 } },
59
+ { id: "right", idx: [5, 1, 2, 6], normal: { x: 1, y: 0, z: 0 } },
60
+ // y=+1 corners project to screen-top (SVG y is flipped), so that face is "top".
61
+ { id: "top", idx: [7, 6, 2, 3], normal: { x: 0, y: 1, z: 0 } },
62
+ { id: "bottom", idx: [4, 5, 1, 0], normal: { x: 0, y: -1, z: 0 } },
63
+ ];
64
+
65
+ export interface ProjectOpts {
66
+ /** Center of the SVG viewport. */
67
+ cx: number;
68
+ cy: number;
69
+ /** Half-extent of the cube in SVG units (drawn cube ≈ 2·r before perspective). */
70
+ r: number;
71
+ /** Weak-perspective strength in units of `r` (larger = flatter; ~3-6 reads as 3D). */
72
+ persp?: number;
73
+ }
74
+
75
+ function round(n: number): number {
76
+ return Math.round(n * 100) / 100;
77
+ }
78
+
79
+ // Directional light (upper-front-right), normalized — drives per-face shading so
80
+ // top/front/side read as distinct planes instead of one flat fill.
81
+ const LIGHT = (() => {
82
+ const v = { x: 0.32, y: 0.56, z: 0.76 };
83
+ const m = Math.hypot(v.x, v.y, v.z);
84
+ return { x: v.x / m, y: v.y / m, z: v.z / m };
85
+ })();
86
+
87
+ /**
88
+ * Project the cube at the given orientation. Returns only the front-facing
89
+ * faces (≤3), painter-sorted far→near so the SVG draws nearer faces on top.
90
+ * Screen Y is flipped (SVG y grows downward). `shade` is a 0..1 lambert term
91
+ * from {@link LIGHT} for clean directional face tones.
92
+ */
93
+ export function projectCubeFaces(
94
+ rx: number,
95
+ ry: number,
96
+ rz: number,
97
+ opts: ProjectOpts,
98
+ ): ProjectedFace[] {
99
+ const { cx, cy, r } = opts;
100
+ const persp = opts.persp ?? 4;
101
+ const view = (v: Vec3) => rotate(v, rx, ry, rz);
102
+ const rotated = CORNERS.map(view);
103
+ const faces: ProjectedFace[] = [];
104
+ for (const f of FACES) {
105
+ const n = view(f.normal);
106
+ if (n.z <= 1e-6) continue; // back-face cull: normal must point toward viewer
107
+ const corners = f.idx.map((i) => rotated[i]!);
108
+ const depth = corners.reduce((s, p) => s + p.z, 0) / 4;
109
+ const points = corners
110
+ .map((p) => {
111
+ // Weak perspective: nearer corners (higher z) project slightly larger.
112
+ const s = persp / (persp - p.z);
113
+ return `${round(cx + p.x * r * s)},${round(cy - p.y * r * s)}`;
114
+ })
115
+ .join(" ");
116
+ const lum = Math.max(0, n.x * LIGHT.x + n.y * LIGHT.y + n.z * LIGHT.z);
117
+ faces.push({ id: f.id, points, shade: 0.3 + lum * 0.7, depth });
118
+ }
119
+ faces.sort((a, b) => a.depth - b.depth);
120
+ return faces;
121
+ }
122
+
123
+ export interface ProjectedAxis {
124
+ id: "x" | "y" | "z";
125
+ /** Axis color (standard X=red, Y=green, Z=blue). */
126
+ color: string;
127
+ /** Tip position in SVG coords. */
128
+ x2: number;
129
+ y2: number;
130
+ /** Whether the tip points toward the viewer (drawn in front of the cube). */
131
+ front: boolean;
132
+ }
133
+
134
+ const AXES: { id: "x" | "y" | "z"; dir: Vec3; color: string }[] = [
135
+ { id: "x", dir: { x: 1, y: 0, z: 0 }, color: "#ff6b81" },
136
+ { id: "y", dir: { x: 0, y: 1, z: 0 }, color: "#5ff08a" },
137
+ { id: "z", dir: { x: 0, y: 0, z: 1 }, color: "#62b6ff" },
138
+ ];
139
+
140
+ /**
141
+ * Project the 3 orientation axes (from the cube center) for an X/Y/Z gizmo.
142
+ * Orthographic — thin lines don't need perspective. `front` lets the caller draw
143
+ * away-facing axes behind the cube and toward-facing axes on top.
144
+ */
145
+ export function projectAxes(
146
+ rx: number,
147
+ ry: number,
148
+ rz: number,
149
+ opts: ProjectOpts,
150
+ ): ProjectedAxis[] {
151
+ const { cx, cy, r } = opts;
152
+ const len = r * 1.5;
153
+ const view = (v: Vec3) => rotate(v, rx, ry, rz);
154
+ return AXES.map((a) => {
155
+ const p = view(a.dir);
156
+ return {
157
+ id: a.id,
158
+ color: a.color,
159
+ x2: round(cx + p.x * len),
160
+ y2: round(cy - p.y * len),
161
+ front: p.z >= -1e-6,
162
+ };
163
+ });
164
+ }
165
+
166
+ /** Wrap an angle to (-180, 180] so drag accumulation never runs away. */
167
+ export function wrapDeg(deg: number): number {
168
+ let d = deg % 360;
169
+ if (d > 180) d -= 360;
170
+ if (d <= -180) d += 360;
171
+ return d;
172
+ }
@@ -119,7 +119,8 @@ export function useMotionPathData(
119
119
  return;
120
120
  }
121
121
  const recompute = () => {
122
- const read = readRuntimeKeyframes(iframeRef.current, selector);
122
+ // Position-only: never let a co-located size/scale tween shadow the path.
123
+ const read = readRuntimeKeyframes(iframeRef.current, selector, undefined, ["x", "y"]);
123
124
  const next = buildMotionPathGeometry(read);
124
125
  setGeometry((prev) =>
125
126
  prev?.points === next?.points && prev?.kind === next?.kind ? prev : next,
@@ -0,0 +1,97 @@
1
+ export function ContextMenu({
2
+ x,
3
+ y,
4
+ asset,
5
+ onClose,
6
+ onCopy,
7
+ onDelete,
8
+ onRename,
9
+ }: {
10
+ x: number;
11
+ y: number;
12
+ asset: string;
13
+ onClose: () => void;
14
+ onCopy: (path: string) => void;
15
+ onDelete?: (path: string) => void;
16
+ onRename?: (oldPath: string, newPath: string) => void;
17
+ }) {
18
+ return (
19
+ <div
20
+ className="fixed inset-0 z-[200]"
21
+ onClick={onClose}
22
+ onContextMenu={(e) => {
23
+ e.preventDefault();
24
+ onClose();
25
+ }}
26
+ >
27
+ <div
28
+ className="absolute bg-neutral-900 border border-neutral-700 rounded-lg shadow-xl py-1 min-w-[140px] text-xs"
29
+ style={{ left: x, top: y }}
30
+ >
31
+ <button
32
+ onClick={(e) => {
33
+ e.stopPropagation();
34
+ onCopy(asset);
35
+ onClose();
36
+ }}
37
+ className="w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 transition-colors"
38
+ >
39
+ Copy path
40
+ </button>
41
+ {onRename && (
42
+ <button
43
+ onClick={(e) => {
44
+ e.stopPropagation();
45
+ onClose();
46
+ }}
47
+ className="w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 transition-colors"
48
+ >
49
+ Rename
50
+ </button>
51
+ )}
52
+ {onDelete && (
53
+ <button
54
+ onClick={(e) => {
55
+ e.stopPropagation();
56
+ onDelete(asset);
57
+ onClose();
58
+ }}
59
+ className="w-full text-left px-3 py-1.5 text-red-400 hover:bg-neutral-800 transition-colors"
60
+ >
61
+ Delete
62
+ </button>
63
+ )}
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export function DeleteConfirm({
70
+ name,
71
+ onConfirm,
72
+ onCancel,
73
+ }: {
74
+ name: string;
75
+ onConfirm: () => void;
76
+ onCancel: () => void;
77
+ }) {
78
+ return (
79
+ <div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
80
+ <span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
81
+ <div className="flex items-center gap-1 flex-shrink-0">
82
+ <button
83
+ onClick={onConfirm}
84
+ className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white hover:bg-red-500 transition-colors"
85
+ >
86
+ Delete
87
+ </button>
88
+ <button
89
+ onClick={onCancel}
90
+ className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
91
+ >
92
+ Cancel
93
+ </button>
94
+ </div>
95
+ </div>
96
+ );
97
+ }