@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.
Files changed (74) hide show
  1. package/dist/assets/hyperframes-player-Cd8vYWxP.js +198 -0
  2. package/dist/assets/index-UWFaHilT.css +1 -0
  3. package/dist/assets/index-cPJbxeAk.js +107 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +2621 -170
  7. package/src/components/LintModal.tsx +3 -4
  8. package/src/components/editor/DomEditOverlay.test.ts +241 -0
  9. package/src/components/editor/DomEditOverlay.tsx +1300 -0
  10. package/src/components/editor/MotionPanel.tsx +651 -0
  11. package/src/components/editor/PropertyPanel.test.ts +67 -0
  12. package/src/components/editor/PropertyPanel.tsx +2891 -207
  13. package/src/components/editor/TimelineLayerPanel.test.ts +42 -0
  14. package/src/components/editor/TimelineLayerPanel.tsx +113 -0
  15. package/src/components/editor/colorValue.test.ts +82 -0
  16. package/src/components/editor/colorValue.ts +175 -0
  17. package/src/components/editor/domEditing.test.ts +872 -0
  18. package/src/components/editor/domEditing.ts +993 -0
  19. package/src/components/editor/floatingPanel.test.ts +34 -0
  20. package/src/components/editor/floatingPanel.ts +54 -0
  21. package/src/components/editor/fontAssets.ts +32 -0
  22. package/src/components/editor/fontCatalog.ts +126 -0
  23. package/src/components/editor/gradientValue.test.ts +89 -0
  24. package/src/components/editor/gradientValue.ts +445 -0
  25. package/src/components/editor/manualEditingAvailability.test.ts +129 -0
  26. package/src/components/editor/manualEditingAvailability.ts +60 -0
  27. package/src/components/editor/manualEdits.test.ts +945 -0
  28. package/src/components/editor/manualEdits.ts +1397 -0
  29. package/src/components/editor/manualOffsetDrag.test.ts +140 -0
  30. package/src/components/editor/manualOffsetDrag.ts +307 -0
  31. package/src/components/editor/studioMotion.test.ts +355 -0
  32. package/src/components/editor/studioMotion.ts +632 -0
  33. package/src/components/nle/NLELayout.tsx +27 -4
  34. package/src/components/nle/NLEPreview.tsx +50 -5
  35. package/src/components/renders/RenderQueue.tsx +13 -62
  36. package/src/components/renders/useRenderQueue.ts +6 -30
  37. package/src/components/sidebar/AssetsTab.tsx +3 -4
  38. package/src/components/sidebar/CompositionsTab.test.ts +16 -1
  39. package/src/components/sidebar/CompositionsTab.tsx +117 -45
  40. package/src/components/sidebar/LeftSidebar.tsx +140 -125
  41. package/src/hooks/usePersistentEditHistory.test.ts +256 -0
  42. package/src/hooks/usePersistentEditHistory.ts +337 -0
  43. package/src/icons/SystemIcons.tsx +2 -0
  44. package/src/player/components/CompositionThumbnail.test.ts +19 -0
  45. package/src/player/components/CompositionThumbnail.tsx +50 -13
  46. package/src/player/components/EditModal.tsx +5 -20
  47. package/src/player/components/Player.tsx +18 -2
  48. package/src/player/components/Timeline.test.ts +20 -0
  49. package/src/player/components/Timeline.tsx +103 -21
  50. package/src/player/components/TimelineClip.test.ts +92 -0
  51. package/src/player/components/TimelineClip.tsx +241 -7
  52. package/src/player/components/timelineEditing.test.ts +16 -3
  53. package/src/player/components/timelineEditing.ts +10 -3
  54. package/src/player/hooks/useTimelinePlayer.test.ts +148 -19
  55. package/src/player/hooks/useTimelinePlayer.ts +287 -16
  56. package/src/player/store/playerStore.ts +2 -0
  57. package/src/utils/clipboard.test.ts +89 -0
  58. package/src/utils/clipboard.ts +57 -0
  59. package/src/utils/editHistory.test.ts +244 -0
  60. package/src/utils/editHistory.ts +218 -0
  61. package/src/utils/editHistoryStorage.test.ts +37 -0
  62. package/src/utils/editHistoryStorage.ts +99 -0
  63. package/src/utils/mediaTypes.ts +1 -1
  64. package/src/utils/sourcePatcher.test.ts +128 -1
  65. package/src/utils/sourcePatcher.ts +130 -18
  66. package/src/utils/studioFileHistory.test.ts +156 -0
  67. package/src/utils/studioFileHistory.ts +61 -0
  68. package/src/utils/timelineAssetDrop.test.ts +31 -11
  69. package/src/utils/timelineAssetDrop.ts +22 -2
  70. package/src/utils/timelineInspector.test.ts +79 -0
  71. package/src/utils/timelineInspector.ts +116 -0
  72. package/dist/assets/hyperframes-player-CEnWY28J.js +0 -417
  73. package/dist/assets/index-04Mp2wOn.css +0 -1
  74. package/dist/assets/index-960mgQMI.js +0 -93
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveFloatingPanelPosition } from "./floatingPanel";
3
+
4
+ describe("resolveFloatingPanelPosition", () => {
5
+ it("places the panel below the anchor when there is space", () => {
6
+ expect(
7
+ resolveFloatingPanelPosition(
8
+ { left: 100, top: 100, right: 220, bottom: 140, width: 120, height: 40 },
9
+ { width: 800, height: 600 },
10
+ { width: 280, height: 220 },
11
+ ),
12
+ ).toMatchObject({ top: 148, placement: "bottom" });
13
+ });
14
+
15
+ it("places the panel above the anchor when the bottom would be clipped", () => {
16
+ expect(
17
+ resolveFloatingPanelPosition(
18
+ { left: 100, top: 500, right: 220, bottom: 540, width: 120, height: 40 },
19
+ { width: 800, height: 600 },
20
+ { width: 280, height: 220 },
21
+ ),
22
+ ).toMatchObject({ top: 272, placement: "top" });
23
+ });
24
+
25
+ it("clamps the panel horizontally inside the viewport", () => {
26
+ expect(
27
+ resolveFloatingPanelPosition(
28
+ { left: 760, top: 100, right: 800, bottom: 140, width: 40, height: 40 },
29
+ { width: 800, height: 600 },
30
+ { width: 280, height: 220 },
31
+ ).left,
32
+ ).toBe(508);
33
+ });
34
+ });
@@ -0,0 +1,54 @@
1
+ export interface FloatingRect {
2
+ left: number;
3
+ top: number;
4
+ right: number;
5
+ bottom: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+
10
+ export interface FloatingSize {
11
+ width: number;
12
+ height: number;
13
+ }
14
+
15
+ export interface FloatingPosition {
16
+ left: number;
17
+ top: number;
18
+ placement: "top" | "bottom";
19
+ }
20
+
21
+ function clamp(value: number, min: number, max: number): number {
22
+ return Math.max(min, Math.min(max, value));
23
+ }
24
+
25
+ export function resolveFloatingPanelPosition(
26
+ anchor: FloatingRect,
27
+ viewport: FloatingSize,
28
+ panel: FloatingSize,
29
+ options?: { offset?: number; margin?: number },
30
+ ): FloatingPosition {
31
+ const offset = options?.offset ?? 8;
32
+ const margin = options?.margin ?? 12;
33
+ const maxLeft = Math.max(margin, viewport.width - panel.width - margin);
34
+ const preferredLeft = anchor.left + anchor.width / 2 - panel.width / 2;
35
+ const left = clamp(preferredLeft, margin, maxLeft);
36
+ const belowTop = anchor.bottom + offset;
37
+ const aboveTop = anchor.top - panel.height - offset;
38
+ const fitsBelow = belowTop + panel.height <= viewport.height - margin;
39
+ const fitsAbove = aboveTop >= margin;
40
+
41
+ if (fitsBelow || !fitsAbove) {
42
+ return {
43
+ left,
44
+ top: clamp(belowTop, margin, Math.max(margin, viewport.height - panel.height - margin)),
45
+ placement: "bottom",
46
+ };
47
+ }
48
+
49
+ return {
50
+ left,
51
+ top: clamp(aboveTop, margin, Math.max(margin, viewport.height - panel.height - margin)),
52
+ placement: "top",
53
+ };
54
+ }
@@ -0,0 +1,32 @@
1
+ export interface ImportedFontAsset {
2
+ family: string;
3
+ path: string;
4
+ url: string;
5
+ }
6
+
7
+ const FONT_EXT_RE = /\.(eot|otf|ttc|ttf|woff2?)$/i;
8
+ const FONT_STYLE_SUFFIX_RE =
9
+ /\s+(thin|extralight|extra light|light|regular|roman|medium|semibold|semi bold|bold|extrabold|extra bold|black|italic|oblique|variable)$/i;
10
+
11
+ export function cssString(value: string): string {
12
+ return JSON.stringify(value);
13
+ }
14
+
15
+ export function fontFamilyFromAssetPath(path: string): string {
16
+ const fileName = decodeURIComponent(path.split(/[\\/]/).pop() ?? path).replace(FONT_EXT_RE, "");
17
+ let family = fileName
18
+ .replace(/[_-]+/g, " ")
19
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
20
+ .replace(/\s+/g, " ")
21
+ .trim();
22
+
23
+ while (FONT_STYLE_SUFFIX_RE.test(family)) {
24
+ family = family.replace(FONT_STYLE_SUFFIX_RE, "").trim();
25
+ }
26
+
27
+ return family || fileName;
28
+ }
29
+
30
+ export function importedFontFaceCss(asset: ImportedFontAsset, url: string = asset.url): string {
31
+ return `@font-face { font-family: ${cssString(asset.family)}; src: url(${cssString(url)}); font-display: swap; }`;
32
+ }
@@ -0,0 +1,126 @@
1
+ export const POPULAR_GOOGLE_FONT_FAMILIES = [
2
+ "ABeeZee",
3
+ "Abel",
4
+ "Abril Fatface",
5
+ "Alegreya",
6
+ "Alegreya Sans",
7
+ "Anton",
8
+ "Archivo",
9
+ "Archivo Black",
10
+ "Arimo",
11
+ "Assistant",
12
+ "Barlow",
13
+ "Barlow Condensed",
14
+ "Bebas Neue",
15
+ "Bitter",
16
+ "Bricolage Grotesque",
17
+ "Cabin",
18
+ "Cardo",
19
+ "Catamaran",
20
+ "Caveat",
21
+ "Chivo",
22
+ "Cormorant Garamond",
23
+ "Crimson Text",
24
+ "Dancing Script",
25
+ "DM Sans",
26
+ "DM Serif Display",
27
+ "Domine",
28
+ "EB Garamond",
29
+ "Exo 2",
30
+ "Figtree",
31
+ "Fira Code",
32
+ "Fira Sans",
33
+ "Fraunces",
34
+ "Fredoka",
35
+ "IBM Plex Mono",
36
+ "IBM Plex Sans",
37
+ "IBM Plex Serif",
38
+ "Inconsolata",
39
+ "Instrument Sans",
40
+ "Instrument Serif",
41
+ "Inter",
42
+ "JetBrains Mono",
43
+ "Josefin Sans",
44
+ "Jost",
45
+ "Kanit",
46
+ "Karla",
47
+ "Lato",
48
+ "League Gothic",
49
+ "Lexend",
50
+ "Libre Baskerville",
51
+ "Libre Franklin",
52
+ "Lora",
53
+ "Manrope",
54
+ "Merriweather",
55
+ "Montserrat",
56
+ "Mukta",
57
+ "Mulish",
58
+ "Newsreader",
59
+ "Noto Sans",
60
+ "Noto Sans JP",
61
+ "Noto Serif",
62
+ "Nunito",
63
+ "Nunito Sans",
64
+ "Open Sans",
65
+ "Oswald",
66
+ "Outfit",
67
+ "Overpass",
68
+ "Pacifico",
69
+ "Pathway Extreme",
70
+ "Permanent Marker",
71
+ "Playfair Display",
72
+ "Plus Jakarta Sans",
73
+ "Poppins",
74
+ "Prata",
75
+ "PT Sans",
76
+ "PT Serif",
77
+ "Public Sans",
78
+ "Quicksand",
79
+ "Raleway",
80
+ "Red Hat Display",
81
+ "Roboto",
82
+ "Roboto Condensed",
83
+ "Roboto Mono",
84
+ "Roboto Serif",
85
+ "Rubik",
86
+ "Schibsted Grotesk",
87
+ "Signika",
88
+ "Source Code Pro",
89
+ "Source Sans 3",
90
+ "Source Serif 4",
91
+ "Space Grotesk",
92
+ "Space Mono",
93
+ "Spectral",
94
+ "Sora",
95
+ "Syne",
96
+ "Teko",
97
+ "Titillium Web",
98
+ "Ubuntu",
99
+ "Ubuntu Mono",
100
+ "Unbounded",
101
+ "Urbanist",
102
+ "Varela Round",
103
+ "Work Sans",
104
+ "Young Serif",
105
+ "Zilla Slab",
106
+ ] as const;
107
+
108
+ export const COMMON_LOCAL_FONT_FAMILIES = [
109
+ "TT Norms Pro",
110
+ "SF Pro Display",
111
+ "SF Pro Text",
112
+ "Avenir",
113
+ "Avenir Next",
114
+ "Helvetica Neue",
115
+ "Arial",
116
+ "Georgia",
117
+ "Times New Roman",
118
+ "Menlo",
119
+ "Monaco",
120
+ "Courier New",
121
+ ] as const;
122
+
123
+ export function googleFontStylesheetUrl(family: string): string {
124
+ const encodedFamily = encodeURIComponent(family.trim()).replace(/%20/g, "+");
125
+ return `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@300;400;500;600;700;800;900&display=swap`;
126
+ }
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildDefaultGradientModel,
4
+ insertGradientStop,
5
+ parseGradient,
6
+ serializeGradient,
7
+ } from "./gradientValue";
8
+
9
+ describe("parseGradient", () => {
10
+ it("parses linear gradients", () => {
11
+ expect(
12
+ parseGradient("linear-gradient(135deg, rgba(15, 23, 42, 0.58), rgba(255, 255, 255, 0.04))"),
13
+ ).toMatchObject({
14
+ kind: "linear",
15
+ repeating: false,
16
+ angle: 135,
17
+ stops: [
18
+ { color: "rgba(15, 23, 42, 0.58)", position: 0 },
19
+ { color: "rgba(255, 255, 255, 0.04)", position: 100 },
20
+ ],
21
+ });
22
+ });
23
+
24
+ it("parses radial gradients", () => {
25
+ expect(
26
+ parseGradient("radial-gradient(circle closest-side at 20% 35%, #ff0000 10%, #0000ff 90%)"),
27
+ ).toMatchObject({
28
+ kind: "radial",
29
+ shape: "circle",
30
+ radialSize: "closest-side",
31
+ centerX: 20,
32
+ centerY: 35,
33
+ });
34
+ });
35
+
36
+ it("parses conic gradients", () => {
37
+ expect(
38
+ parseGradient("conic-gradient(from 45deg at 40% 60%, #111111 0%, #ffffff 100%)"),
39
+ ).toMatchObject({
40
+ kind: "conic",
41
+ angle: 45,
42
+ centerX: 40,
43
+ centerY: 60,
44
+ });
45
+ });
46
+
47
+ it("parses repeating gradients", () => {
48
+ expect(
49
+ parseGradient("repeating-linear-gradient(90deg, #000000 0%, #ffffff 50%)"),
50
+ ).toMatchObject({
51
+ kind: "linear",
52
+ repeating: true,
53
+ angle: 90,
54
+ });
55
+ });
56
+ });
57
+
58
+ describe("serializeGradient", () => {
59
+ it("serializes default gradient models", () => {
60
+ expect(serializeGradient(buildDefaultGradientModel("rgba(60, 230, 172, 0.18)"))).toBe(
61
+ "linear-gradient(135deg, rgba(60, 230, 172, 0.18) 0%, rgba(255, 255, 255, 0.04) 100%)",
62
+ );
63
+ });
64
+
65
+ it("round-trips parsed gradients", () => {
66
+ const parsed = parseGradient(
67
+ "repeating-conic-gradient(from 90deg at 25% 75%, rgba(0, 0, 0, 0.5) 0%, rgba(255, 255, 255, 0.1) 100%)",
68
+ );
69
+ expect(parsed).not.toBeNull();
70
+ expect(serializeGradient(parsed!)).toBe(
71
+ "repeating-conic-gradient(from 90deg at 25% 75%, rgba(0, 0, 0, 0.5) 0%, rgba(255, 255, 255, 0.1) 100%)",
72
+ );
73
+ });
74
+ });
75
+
76
+ describe("insertGradientStop", () => {
77
+ it("inserts a stop at the clicked position with an interpolated color", () => {
78
+ const parsed = parseGradient("linear-gradient(90deg, #000000 0%, #ffffff 100%)");
79
+ expect(parsed).not.toBeNull();
80
+
81
+ expect(insertGradientStop(parsed!, 25)).toMatchObject({
82
+ stops: [
83
+ { color: "#000000", position: 0 },
84
+ { color: "#404040", position: 25 },
85
+ { color: "#ffffff", position: 100 },
86
+ ],
87
+ });
88
+ });
89
+ });