@hyperframes/studio 0.4.38 → 0.5.0-alpha.10

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 (51) hide show
  1. package/dist/assets/index-DKaNgV2Z.css +1 -0
  2. package/dist/assets/index-peNJzL-4.js +105 -0
  3. package/dist/index.html +2 -2
  4. package/package.json +4 -4
  5. package/src/App.tsx +1431 -196
  6. package/src/components/LintModal.tsx +3 -4
  7. package/src/components/editor/DomEditOverlay.tsx +445 -0
  8. package/src/components/editor/PropertyPanel.tsx +2466 -206
  9. package/src/components/editor/colorValue.test.ts +82 -0
  10. package/src/components/editor/colorValue.ts +175 -0
  11. package/src/components/editor/domEditing.test.ts +537 -0
  12. package/src/components/editor/domEditing.ts +762 -0
  13. package/src/components/editor/floatingPanel.test.ts +34 -0
  14. package/src/components/editor/floatingPanel.ts +54 -0
  15. package/src/components/editor/fontAssets.ts +32 -0
  16. package/src/components/editor/fontCatalog.ts +126 -0
  17. package/src/components/editor/gradientValue.test.ts +89 -0
  18. package/src/components/editor/gradientValue.ts +445 -0
  19. package/src/components/nle/NLELayout.tsx +17 -47
  20. package/src/components/nle/NLEPreview.tsx +50 -5
  21. package/src/components/sidebar/AssetsTab.tsx +3 -4
  22. package/src/components/sidebar/CompositionsTab.test.ts +16 -1
  23. package/src/components/sidebar/CompositionsTab.tsx +117 -45
  24. package/src/components/sidebar/LeftSidebar.tsx +34 -55
  25. package/src/icons/SystemIcons.tsx +0 -2
  26. package/src/player/components/CompositionThumbnail.test.ts +19 -0
  27. package/src/player/components/CompositionThumbnail.tsx +42 -10
  28. package/src/player/components/EditModal.tsx +5 -20
  29. package/src/player/components/Player.tsx +18 -70
  30. package/src/player/components/PlayerControls.tsx +44 -3
  31. package/src/player/components/Timeline.test.ts +12 -0
  32. package/src/player/components/Timeline.tsx +51 -20
  33. package/src/player/components/TimelineClip.tsx +20 -7
  34. package/src/player/components/timelineEditing.test.ts +2 -4
  35. package/src/player/components/timelineEditing.ts +1 -3
  36. package/src/player/components/timelineTheme.ts +3 -3
  37. package/src/player/hooks/useTimelinePlayer.test.ts +59 -0
  38. package/src/player/hooks/useTimelinePlayer.ts +74 -32
  39. package/src/player/lib/time.test.ts +1 -11
  40. package/src/player/lib/time.ts +0 -6
  41. package/src/utils/clipboard.test.ts +88 -0
  42. package/src/utils/clipboard.ts +57 -0
  43. package/src/utils/mediaTypes.ts +1 -1
  44. package/src/utils/sourcePatcher.test.ts +128 -1
  45. package/src/utils/sourcePatcher.ts +130 -18
  46. package/src/utils/timelineAssetDrop.test.ts +31 -11
  47. package/src/utils/timelineAssetDrop.ts +22 -2
  48. package/dist/assets/index-18P_dZeo.js +0 -93
  49. package/dist/assets/index-BLrgRQSu.css +0 -1
  50. package/src/utils/frameCapture.test.ts +0 -26
  51. package/src/utils/frameCapture.ts +0 -38
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ formatCssColor,
4
+ hsvToRgb,
5
+ mergeColorWithExistingAlpha,
6
+ parseCssColor,
7
+ rgbToHsv,
8
+ toColorPickerValue,
9
+ toHexColor,
10
+ } from "./colorValue";
11
+
12
+ describe("parseCssColor", () => {
13
+ it("parses rgb values", () => {
14
+ expect(parseCssColor("rgb(12, 34, 56)")).toEqual({
15
+ red: 12,
16
+ green: 34,
17
+ blue: 56,
18
+ alpha: 1,
19
+ });
20
+ });
21
+
22
+ it("parses rgba values", () => {
23
+ expect(parseCssColor("rgba(15, 23, 42, 0.64)")).toEqual({
24
+ red: 15,
25
+ green: 23,
26
+ blue: 42,
27
+ alpha: 0.64,
28
+ });
29
+ });
30
+
31
+ it("parses transparent", () => {
32
+ expect(parseCssColor("transparent")).toEqual({
33
+ red: 0,
34
+ green: 0,
35
+ blue: 0,
36
+ alpha: 0,
37
+ });
38
+ });
39
+ });
40
+
41
+ describe("toColorPickerValue", () => {
42
+ it("converts css color to hex", () => {
43
+ expect(toColorPickerValue("rgba(15, 23, 42, 0.64)")).toBe("#0f172a");
44
+ });
45
+ });
46
+
47
+ describe("toHexColor", () => {
48
+ it("formats rgb channels as hex", () => {
49
+ expect(toHexColor({ red: 15, green: 23, blue: 42 })).toBe("#0f172a");
50
+ });
51
+ });
52
+
53
+ describe("formatCssColor", () => {
54
+ it("formats opaque colors as rgb", () => {
55
+ expect(formatCssColor({ red: 18, green: 52, blue: 86, alpha: 1 })).toBe("rgb(18, 52, 86)");
56
+ });
57
+
58
+ it("formats translucent colors as rgba", () => {
59
+ expect(formatCssColor({ red: 18, green: 52, blue: 86, alpha: 0.64 })).toBe(
60
+ "rgba(18, 52, 86, 0.64)",
61
+ );
62
+ });
63
+ });
64
+
65
+ describe("rgb hsv conversion", () => {
66
+ it("round-trips primary color values", () => {
67
+ const hsv = rgbToHsv({ red: 47, green: 198, blue: 127 });
68
+ expect(hsvToRgb(hsv)).toEqual({ red: 47, green: 198, blue: 127 });
69
+ });
70
+ });
71
+
72
+ describe("mergeColorWithExistingAlpha", () => {
73
+ it("preserves alpha when the previous color was translucent", () => {
74
+ expect(mergeColorWithExistingAlpha("#123456", "rgba(15, 23, 42, 0.64)")).toBe(
75
+ "rgba(18, 52, 86, 0.64)",
76
+ );
77
+ });
78
+
79
+ it("returns rgb when the previous color was opaque", () => {
80
+ expect(mergeColorWithExistingAlpha("#123456", "rgb(15, 23, 42)")).toBe("rgb(18, 52, 86)");
81
+ });
82
+ });
@@ -0,0 +1,175 @@
1
+ export interface ParsedColor {
2
+ red: number;
3
+ green: number;
4
+ blue: number;
5
+ alpha: number;
6
+ }
7
+
8
+ export interface HsvColor {
9
+ hue: number;
10
+ saturation: number;
11
+ value: number;
12
+ }
13
+
14
+ function clampChannel(value: number): number {
15
+ return Math.max(0, Math.min(255, Math.round(value)));
16
+ }
17
+
18
+ function clampAlpha(value: number): number {
19
+ return Math.max(0, Math.min(1, value));
20
+ }
21
+
22
+ function toHex(value: number): string {
23
+ return clampChannel(value).toString(16).padStart(2, "0");
24
+ }
25
+
26
+ function formatAlpha(value: number): string {
27
+ return `${Math.round(clampAlpha(value) * 100) / 100}`;
28
+ }
29
+
30
+ export function parseCssColor(value: string): ParsedColor | null {
31
+ const trimmed = value.trim().toLowerCase();
32
+ if (!trimmed) return null;
33
+ if (trimmed === "transparent") {
34
+ return { red: 0, green: 0, blue: 0, alpha: 0 };
35
+ }
36
+
37
+ const shortHex = trimmed.match(/^#([0-9a-f]{3})$/i);
38
+ if (shortHex) {
39
+ const [r, g, b] = shortHex[1].split("");
40
+ return {
41
+ red: Number.parseInt(r + r, 16),
42
+ green: Number.parseInt(g + g, 16),
43
+ blue: Number.parseInt(b + b, 16),
44
+ alpha: 1,
45
+ };
46
+ }
47
+
48
+ const hex = trimmed.match(/^#([0-9a-f]{6})$/i);
49
+ if (hex) {
50
+ return {
51
+ red: Number.parseInt(hex[1].slice(0, 2), 16),
52
+ green: Number.parseInt(hex[1].slice(2, 4), 16),
53
+ blue: Number.parseInt(hex[1].slice(4, 6), 16),
54
+ alpha: 1,
55
+ };
56
+ }
57
+
58
+ const rgba = trimmed.match(
59
+ /^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i,
60
+ );
61
+ if (rgba) {
62
+ return {
63
+ red: clampChannel(Number.parseFloat(rgba[1])),
64
+ green: clampChannel(Number.parseFloat(rgba[2])),
65
+ blue: clampChannel(Number.parseFloat(rgba[3])),
66
+ alpha: clampAlpha(rgba[4] != null ? Number.parseFloat(rgba[4]) : 1),
67
+ };
68
+ }
69
+
70
+ return null;
71
+ }
72
+
73
+ export function toColorPickerValue(value: string): string {
74
+ const parsed = parseCssColor(value);
75
+ if (!parsed) return "#000000";
76
+ return toHexColor(parsed);
77
+ }
78
+
79
+ export function toHexColor(color: Pick<ParsedColor, "red" | "green" | "blue">): string {
80
+ return `#${toHex(color.red)}${toHex(color.green)}${toHex(color.blue)}`;
81
+ }
82
+
83
+ export function formatCssColor(color: ParsedColor): string {
84
+ const red = clampChannel(color.red);
85
+ const green = clampChannel(color.green);
86
+ const blue = clampChannel(color.blue);
87
+ const alpha = clampAlpha(color.alpha);
88
+
89
+ if (alpha >= 1) {
90
+ return `rgb(${red}, ${green}, ${blue})`;
91
+ }
92
+
93
+ return `rgba(${red}, ${green}, ${blue}, ${formatAlpha(alpha)})`;
94
+ }
95
+
96
+ export function rgbToHsv(color: Pick<ParsedColor, "red" | "green" | "blue">): HsvColor {
97
+ const red = clampChannel(color.red) / 255;
98
+ const green = clampChannel(color.green) / 255;
99
+ const blue = clampChannel(color.blue) / 255;
100
+ const max = Math.max(red, green, blue);
101
+ const min = Math.min(red, green, blue);
102
+ const delta = max - min;
103
+
104
+ let hue = 0;
105
+ if (delta !== 0) {
106
+ if (max === red) {
107
+ hue = 60 * (((green - blue) / delta) % 6);
108
+ } else if (max === green) {
109
+ hue = 60 * ((blue - red) / delta + 2);
110
+ } else {
111
+ hue = 60 * ((red - green) / delta + 4);
112
+ }
113
+ }
114
+
115
+ if (hue < 0) hue += 360;
116
+
117
+ return {
118
+ hue,
119
+ saturation: max === 0 ? 0 : delta / max,
120
+ value: max,
121
+ };
122
+ }
123
+
124
+ export function hsvToRgb(color: HsvColor): Pick<ParsedColor, "red" | "green" | "blue"> {
125
+ const hue = (((color.hue % 360) + 360) % 360) / 60;
126
+ const saturation = Math.max(0, Math.min(1, color.saturation));
127
+ const value = Math.max(0, Math.min(1, color.value));
128
+ const chroma = value * saturation;
129
+ const x = chroma * (1 - Math.abs((hue % 2) - 1));
130
+ const m = value - chroma;
131
+
132
+ let red = 0;
133
+ let green = 0;
134
+ let blue = 0;
135
+
136
+ if (hue >= 0 && hue < 1) {
137
+ red = chroma;
138
+ green = x;
139
+ } else if (hue >= 1 && hue < 2) {
140
+ red = x;
141
+ green = chroma;
142
+ } else if (hue >= 2 && hue < 3) {
143
+ green = chroma;
144
+ blue = x;
145
+ } else if (hue >= 3 && hue < 4) {
146
+ green = x;
147
+ blue = chroma;
148
+ } else if (hue >= 4 && hue < 5) {
149
+ red = x;
150
+ blue = chroma;
151
+ } else {
152
+ red = chroma;
153
+ blue = x;
154
+ }
155
+
156
+ return {
157
+ red: clampChannel((red + m) * 255),
158
+ green: clampChannel((green + m) * 255),
159
+ blue: clampChannel((blue + m) * 255),
160
+ };
161
+ }
162
+
163
+ export function mergeColorWithExistingAlpha(nextHex: string, previousValue: string): string {
164
+ const hex = nextHex.trim();
165
+ const match = hex.match(/^#([0-9a-f]{6})$/i);
166
+ if (!match) return previousValue;
167
+
168
+ const previous = parseCssColor(previousValue);
169
+ const red = Number.parseInt(match[1].slice(0, 2), 16);
170
+ const green = Number.parseInt(match[1].slice(2, 4), 16);
171
+ const blue = Number.parseInt(match[1].slice(4, 6), 16);
172
+ const alpha = previous?.alpha ?? 1;
173
+
174
+ return formatCssColor({ red, green, blue, alpha });
175
+ }