@newtonedev/editor 0.1.6 → 0.1.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.
- package/dist/Editor.d.ts +1 -1
- package/dist/Editor.d.ts.map +1 -1
- package/dist/components/ConfiguratorPanel.d.ts +17 -0
- package/dist/components/ConfiguratorPanel.d.ts.map +1 -0
- package/dist/components/FontPicker.d.ts +4 -2
- package/dist/components/FontPicker.d.ts.map +1 -1
- package/dist/components/PreviewWindow.d.ts +7 -2
- package/dist/components/PreviewWindow.d.ts.map +1 -1
- package/dist/components/PrimaryNav.d.ts +7 -0
- package/dist/components/PrimaryNav.d.ts.map +1 -0
- package/dist/components/Sidebar.d.ts +1 -10
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/TableOfContents.d.ts +2 -1
- package/dist/components/TableOfContents.d.ts.map +1 -1
- package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
- package/dist/components/sections/FontsSection.d.ts +3 -1
- package/dist/components/sections/FontsSection.d.ts.map +1 -1
- package/dist/hooks/useEditorState.d.ts +4 -1
- package/dist/hooks/useEditorState.d.ts.map +1 -1
- package/dist/index.cjs +2484 -2052
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2486 -2055
- package/dist/index.js.map +1 -1
- package/dist/preview/ComponentDetailView.d.ts +7 -1
- package/dist/preview/ComponentDetailView.d.ts.map +1 -1
- package/dist/preview/ComponentRenderer.d.ts +2 -1
- package/dist/preview/ComponentRenderer.d.ts.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/lookupFontMetrics.d.ts +19 -0
- package/dist/utils/lookupFontMetrics.d.ts.map +1 -0
- package/dist/utils/measureFonts.d.ts +18 -0
- package/dist/utils/measureFonts.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/Editor.tsx +53 -10
- package/src/components/ConfiguratorPanel.tsx +77 -0
- package/src/components/FontPicker.tsx +38 -29
- package/src/components/PreviewWindow.tsx +14 -1
- package/src/components/PrimaryNav.tsx +76 -0
- package/src/components/Sidebar.tsx +5 -132
- package/src/components/TableOfContents.tsx +41 -78
- package/src/components/sections/DynamicRangeSection.tsx +2 -225
- package/src/components/sections/FontsSection.tsx +61 -93
- package/src/hooks/useEditorState.ts +68 -17
- package/src/index.ts +2 -0
- package/src/preview/ComponentDetailView.tsx +531 -67
- package/src/preview/ComponentRenderer.tsx +6 -4
- package/src/types.ts +15 -0
- package/src/utils/lookupFontMetrics.ts +52 -0
- package/src/utils/measureFonts.ts +41 -0
|
@@ -1,34 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useTokens, Icon } from "@newtonedev/components";
|
|
3
|
-
import type { ColorMode } from "@newtonedev/components";
|
|
1
|
+
import { useTokens } from "@newtonedev/components";
|
|
4
2
|
import { srgbToHex } from "newtone";
|
|
5
|
-
import type { ColorResult } from "newtone";
|
|
6
|
-
import type { ConfiguratorState } from "@newtonedev/configurator";
|
|
7
|
-
import type { ConfiguratorAction } from "@newtonedev/configurator";
|
|
8
|
-
import {
|
|
9
|
-
ColorsSection,
|
|
10
|
-
DynamicRangeSection,
|
|
11
|
-
IconsSection,
|
|
12
|
-
FontsSection,
|
|
13
|
-
OthersSection,
|
|
14
|
-
} from "./sections";
|
|
15
3
|
import { PresetSelector } from "./PresetSelector";
|
|
16
4
|
import type { Preset } from "../types";
|
|
17
5
|
|
|
18
6
|
const SIDEBAR_WIDTH = 360;
|
|
19
7
|
|
|
20
|
-
const ACCORDION_SECTIONS = [
|
|
21
|
-
{ id: "dynamic-range", label: "Dynamic Range", icon: "contrast" },
|
|
22
|
-
{ id: "colors", label: "Colors", icon: "palette" },
|
|
23
|
-
{ id: "fonts", label: "Fonts", icon: "text_fields" },
|
|
24
|
-
{ id: "icons", label: "Icons", icon: "grid_view" },
|
|
25
|
-
{ id: "others", label: "Others", icon: "tune" },
|
|
26
|
-
] as const;
|
|
27
|
-
|
|
28
8
|
interface SidebarProps {
|
|
29
|
-
readonly state: ConfiguratorState;
|
|
30
|
-
readonly dispatch: (action: ConfiguratorAction) => void;
|
|
31
|
-
readonly previewColors: readonly (readonly ColorResult[])[];
|
|
32
9
|
readonly isDirty: boolean;
|
|
33
10
|
readonly onRevert: () => void;
|
|
34
11
|
readonly presets: readonly Preset[];
|
|
@@ -42,14 +19,9 @@ interface SidebarProps {
|
|
|
42
19
|
presetId: string,
|
|
43
20
|
name: string,
|
|
44
21
|
) => Promise<string>;
|
|
45
|
-
readonly colorMode: ColorMode;
|
|
46
|
-
readonly onColorModeChange: (mode: ColorMode) => void;
|
|
47
22
|
}
|
|
48
23
|
|
|
49
24
|
export function Sidebar({
|
|
50
|
-
state,
|
|
51
|
-
dispatch,
|
|
52
|
-
previewColors,
|
|
53
25
|
isDirty,
|
|
54
26
|
onRevert,
|
|
55
27
|
presets,
|
|
@@ -60,52 +32,11 @@ export function Sidebar({
|
|
|
60
32
|
onRenamePreset,
|
|
61
33
|
onDeletePreset,
|
|
62
34
|
onDuplicatePreset,
|
|
63
|
-
colorMode,
|
|
64
|
-
onColorModeChange,
|
|
65
35
|
}: SidebarProps) {
|
|
66
36
|
const tokens = useTokens();
|
|
67
|
-
const [openSections, setOpenSections] = useState<Set<string>>(
|
|
68
|
-
new Set(["dynamic-range", "colors"]),
|
|
69
|
-
);
|
|
70
|
-
const [hoveredSectionId, setHoveredSectionId] = useState<string | null>(null);
|
|
71
37
|
|
|
72
38
|
const borderColor = srgbToHex(tokens.border.srgb);
|
|
73
39
|
const bgColor = srgbToHex(tokens.background.srgb);
|
|
74
|
-
const hoverBg = `${borderColor}10`;
|
|
75
|
-
|
|
76
|
-
const toggleSection = (id: string) => {
|
|
77
|
-
setOpenSections((prev) => {
|
|
78
|
-
const next = new Set(prev);
|
|
79
|
-
if (next.has(id)) next.delete(id);
|
|
80
|
-
else next.add(id);
|
|
81
|
-
return next;
|
|
82
|
-
});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const renderSectionContent = (sectionId: string) => {
|
|
86
|
-
switch (sectionId) {
|
|
87
|
-
case "dynamic-range":
|
|
88
|
-
return <DynamicRangeSection state={state} dispatch={dispatch} />;
|
|
89
|
-
case "colors":
|
|
90
|
-
return (
|
|
91
|
-
<ColorsSection
|
|
92
|
-
state={state}
|
|
93
|
-
dispatch={dispatch}
|
|
94
|
-
previewColors={previewColors}
|
|
95
|
-
colorMode={colorMode}
|
|
96
|
-
onColorModeChange={onColorModeChange}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
case "icons":
|
|
100
|
-
return <IconsSection state={state} dispatch={dispatch} />;
|
|
101
|
-
case "fonts":
|
|
102
|
-
return <FontsSection state={state} dispatch={dispatch} />;
|
|
103
|
-
case "others":
|
|
104
|
-
return <OthersSection state={state} dispatch={dispatch} />;
|
|
105
|
-
default:
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
40
|
|
|
110
41
|
return (
|
|
111
42
|
<div
|
|
@@ -119,7 +50,7 @@ export function Sidebar({
|
|
|
119
50
|
backgroundColor: bgColor,
|
|
120
51
|
}}
|
|
121
52
|
>
|
|
122
|
-
{/*
|
|
53
|
+
{/* Header */}
|
|
123
54
|
<div
|
|
124
55
|
style={{
|
|
125
56
|
flexShrink: 0,
|
|
@@ -151,74 +82,16 @@ export function Sidebar({
|
|
|
151
82
|
/>
|
|
152
83
|
</div>
|
|
153
84
|
|
|
154
|
-
{/*
|
|
85
|
+
{/* Content area (empty for now) */}
|
|
155
86
|
<div
|
|
156
87
|
style={{
|
|
157
88
|
flex: 1,
|
|
158
89
|
overflowY: "auto",
|
|
159
90
|
overflowX: "hidden",
|
|
160
91
|
}}
|
|
161
|
-
|
|
162
|
-
{ACCORDION_SECTIONS.map((section) => {
|
|
163
|
-
const isOpen = openSections.has(section.id);
|
|
164
|
-
const isHovered = hoveredSectionId === section.id;
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<div key={section.id}>
|
|
168
|
-
<button
|
|
169
|
-
onClick={() => toggleSection(section.id)}
|
|
170
|
-
onMouseEnter={() => setHoveredSectionId(section.id)}
|
|
171
|
-
onMouseLeave={() => setHoveredSectionId(null)}
|
|
172
|
-
aria-expanded={isOpen}
|
|
173
|
-
aria-controls={`section-${section.id}`}
|
|
174
|
-
style={{
|
|
175
|
-
display: "flex",
|
|
176
|
-
alignItems: "center",
|
|
177
|
-
justifyContent: "space-between",
|
|
178
|
-
width: "100%",
|
|
179
|
-
padding: "12px 20px",
|
|
180
|
-
border: "none",
|
|
181
|
-
borderBottom: `1px solid ${borderColor}`,
|
|
182
|
-
background: isHovered ? hoverBg : "none",
|
|
183
|
-
cursor: "pointer",
|
|
184
|
-
fontSize: 14,
|
|
185
|
-
fontWeight: 500,
|
|
186
|
-
color: srgbToHex(tokens.textPrimary.srgb),
|
|
187
|
-
transition: "background-color 100ms ease",
|
|
188
|
-
}}
|
|
189
|
-
>
|
|
190
|
-
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
191
|
-
<Icon name={section.icon} size={16} />
|
|
192
|
-
{section.label}
|
|
193
|
-
</span>
|
|
194
|
-
<Icon
|
|
195
|
-
name="expand_more"
|
|
196
|
-
size={16}
|
|
197
|
-
style={{
|
|
198
|
-
transform: isOpen ? "rotate(180deg)" : "none",
|
|
199
|
-
transition: "transform 150ms ease",
|
|
200
|
-
} as any}
|
|
201
|
-
/>
|
|
202
|
-
</button>
|
|
203
|
-
{isOpen && (
|
|
204
|
-
<div
|
|
205
|
-
id={`section-${section.id}`}
|
|
206
|
-
role="region"
|
|
207
|
-
aria-label={section.label}
|
|
208
|
-
style={{
|
|
209
|
-
padding: "16px 20px",
|
|
210
|
-
borderBottom: `1px solid ${borderColor}`,
|
|
211
|
-
}}
|
|
212
|
-
>
|
|
213
|
-
{renderSectionContent(section.id)}
|
|
214
|
-
</div>
|
|
215
|
-
)}
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
})}
|
|
219
|
-
</div>
|
|
92
|
+
/>
|
|
220
93
|
|
|
221
|
-
{/*
|
|
94
|
+
{/* Footer */}
|
|
222
95
|
<div
|
|
223
96
|
style={{
|
|
224
97
|
flexShrink: 0,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useTokens,
|
|
4
|
-
CATEGORIES,
|
|
5
4
|
getComponentsByCategory,
|
|
6
5
|
} from "@newtonedev/components";
|
|
7
6
|
import { srgbToHex } from "newtone";
|
|
8
7
|
import type { PreviewView } from "../types";
|
|
9
8
|
|
|
10
9
|
interface TableOfContentsProps {
|
|
10
|
+
readonly activeSectionId: string;
|
|
11
11
|
readonly activeView: PreviewView;
|
|
12
12
|
readonly selectedComponentId: string | null;
|
|
13
13
|
readonly onNavigate: (view: PreviewView) => void;
|
|
@@ -16,6 +16,7 @@ interface TableOfContentsProps {
|
|
|
16
16
|
const TOC_WIDTH = 220;
|
|
17
17
|
|
|
18
18
|
export function TableOfContents({
|
|
19
|
+
activeSectionId,
|
|
19
20
|
activeView,
|
|
20
21
|
selectedComponentId,
|
|
21
22
|
onNavigate,
|
|
@@ -26,10 +27,13 @@ export function TableOfContents({
|
|
|
26
27
|
const borderColor = srgbToHex(tokens.border.srgb);
|
|
27
28
|
const activeColor = srgbToHex(tokens.accent.fill.srgb);
|
|
28
29
|
const textPrimary = srgbToHex(tokens.textPrimary.srgb);
|
|
29
|
-
const textSecondary = srgbToHex(tokens.textSecondary.srgb);
|
|
30
30
|
const hoverBg = `${borderColor}20`;
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const components = getComponentsByCategory(activeSectionId);
|
|
33
|
+
|
|
34
|
+
const isOverviewActive =
|
|
35
|
+
activeView.kind === "overview" ||
|
|
36
|
+
(activeView.kind === "category" && activeView.categoryId === activeSectionId);
|
|
33
37
|
|
|
34
38
|
return (
|
|
35
39
|
<nav
|
|
@@ -44,7 +48,7 @@ export function TableOfContents({
|
|
|
44
48
|
}}
|
|
45
49
|
>
|
|
46
50
|
<button
|
|
47
|
-
onClick={() => onNavigate({ kind: "
|
|
51
|
+
onClick={() => onNavigate({ kind: "category", categoryId: activeSectionId })}
|
|
48
52
|
onMouseEnter={() => setHoveredId("overview")}
|
|
49
53
|
onMouseLeave={() => setHoveredId(null)}
|
|
50
54
|
aria-current={isOverviewActive ? "page" : undefined}
|
|
@@ -69,82 +73,41 @@ export function TableOfContents({
|
|
|
69
73
|
Overview
|
|
70
74
|
</button>
|
|
71
75
|
|
|
72
|
-
{
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
{components.map((comp) => {
|
|
77
|
+
const isComponentActive =
|
|
78
|
+
(activeView.kind === "component" &&
|
|
79
|
+
activeView.componentId === comp.id) ||
|
|
80
|
+
selectedComponentId === comp.id;
|
|
77
81
|
|
|
78
82
|
return (
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{category.name}
|
|
108
|
-
</button>
|
|
109
|
-
|
|
110
|
-
{components.map((comp) => {
|
|
111
|
-
const isComponentActive =
|
|
112
|
-
(activeView.kind === "component" &&
|
|
113
|
-
activeView.componentId === comp.id) ||
|
|
114
|
-
selectedComponentId === comp.id;
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<button
|
|
118
|
-
key={comp.id}
|
|
119
|
-
onClick={() =>
|
|
120
|
-
onNavigate({ kind: "component", componentId: comp.id })
|
|
121
|
-
}
|
|
122
|
-
onMouseEnter={() => setHoveredId(comp.id)}
|
|
123
|
-
onMouseLeave={() => setHoveredId(null)}
|
|
124
|
-
aria-current={isComponentActive ? "page" : undefined}
|
|
125
|
-
style={{
|
|
126
|
-
display: "block",
|
|
127
|
-
width: "100%",
|
|
128
|
-
padding: "4px 20px 4px 32px",
|
|
129
|
-
border: "none",
|
|
130
|
-
background: isComponentActive
|
|
131
|
-
? `${activeColor}14`
|
|
132
|
-
: hoveredId === comp.id
|
|
133
|
-
? hoverBg
|
|
134
|
-
: "none",
|
|
135
|
-
cursor: "pointer",
|
|
136
|
-
textAlign: "left",
|
|
137
|
-
fontSize: 13,
|
|
138
|
-
fontWeight: isComponentActive ? 600 : 400,
|
|
139
|
-
color: isComponentActive ? activeColor : textPrimary,
|
|
140
|
-
transition: "background-color 100ms ease",
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
{comp.name}
|
|
144
|
-
</button>
|
|
145
|
-
);
|
|
146
|
-
})}
|
|
147
|
-
</div>
|
|
83
|
+
<button
|
|
84
|
+
key={comp.id}
|
|
85
|
+
onClick={() =>
|
|
86
|
+
onNavigate({ kind: "component", componentId: comp.id })
|
|
87
|
+
}
|
|
88
|
+
onMouseEnter={() => setHoveredId(comp.id)}
|
|
89
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
90
|
+
aria-current={isComponentActive ? "page" : undefined}
|
|
91
|
+
style={{
|
|
92
|
+
display: "block",
|
|
93
|
+
width: "100%",
|
|
94
|
+
padding: "4px 20px",
|
|
95
|
+
border: "none",
|
|
96
|
+
background: isComponentActive
|
|
97
|
+
? `${activeColor}14`
|
|
98
|
+
: hoveredId === comp.id
|
|
99
|
+
? hoverBg
|
|
100
|
+
: "none",
|
|
101
|
+
cursor: "pointer",
|
|
102
|
+
textAlign: "left",
|
|
103
|
+
fontSize: 13,
|
|
104
|
+
fontWeight: isComponentActive ? 600 : 400,
|
|
105
|
+
color: isComponentActive ? activeColor : textPrimary,
|
|
106
|
+
transition: "background-color 100ms ease",
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{comp.name}
|
|
110
|
+
</button>
|
|
148
111
|
);
|
|
149
112
|
})}
|
|
150
113
|
</nav>
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import { useState, useRef, useCallback
|
|
1
|
+
import { useState, useRef, useCallback } from "react";
|
|
2
2
|
import { HueSlider, Select, useTokens } from "@newtonedev/components";
|
|
3
|
-
import {
|
|
4
|
-
srgbToHex,
|
|
5
|
-
resolveLightness,
|
|
6
|
-
findMaxChromaInGamut,
|
|
7
|
-
oklchToSrgb,
|
|
8
|
-
clampSrgb,
|
|
9
|
-
HUE_GRADING_STRENGTH_LOW,
|
|
10
|
-
HUE_GRADING_STRENGTH_MEDIUM,
|
|
11
|
-
HUE_GRADING_STRENGTH_HARD,
|
|
12
|
-
HUE_GRADING_EASING_POWER,
|
|
13
|
-
} from "newtone";
|
|
3
|
+
import { srgbToHex } from "newtone";
|
|
14
4
|
import type { HueGradingStrength } from "newtone";
|
|
15
5
|
import type { ConfiguratorState } from "@newtonedev/configurator";
|
|
16
6
|
import type { ConfiguratorAction } from "@newtonedev/configurator";
|
|
17
|
-
import { traditionalHueToOklch } from "@newtonedev/configurator";
|
|
18
7
|
|
|
19
8
|
const STRENGTH_OPTIONS = [
|
|
20
9
|
{ label: "None", value: "none" },
|
|
@@ -275,215 +264,6 @@ function RangeInput({ display, onCommit, toInternal }: RangeInputProps) {
|
|
|
275
264
|
);
|
|
276
265
|
}
|
|
277
266
|
|
|
278
|
-
// --- Dynamic Range Graph ---
|
|
279
|
-
|
|
280
|
-
const GRAPH_HEIGHT = 80;
|
|
281
|
-
const GRAPH_COLS = 256;
|
|
282
|
-
const GRAPH_ROWS = 64;
|
|
283
|
-
|
|
284
|
-
function strengthToFactor(strength: HueGradingStrength): number {
|
|
285
|
-
switch (strength) {
|
|
286
|
-
case "none":
|
|
287
|
-
return 0;
|
|
288
|
-
case "low":
|
|
289
|
-
return HUE_GRADING_STRENGTH_LOW;
|
|
290
|
-
case "medium":
|
|
291
|
-
return HUE_GRADING_STRENGTH_MEDIUM;
|
|
292
|
-
case "hard":
|
|
293
|
-
return HUE_GRADING_STRENGTH_HARD;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function blendHues(
|
|
298
|
-
lightHue: number,
|
|
299
|
-
darkHue: number,
|
|
300
|
-
wLight: number,
|
|
301
|
-
wDark: number,
|
|
302
|
-
): number {
|
|
303
|
-
const totalW = wLight + wDark;
|
|
304
|
-
if (totalW === 0) return 0;
|
|
305
|
-
const delta = (((darkHue - lightHue + 180) % 360) + 360) % 360 - 180;
|
|
306
|
-
const t = wDark / totalW;
|
|
307
|
-
const result = lightHue + delta * t;
|
|
308
|
-
return ((result % 360) + 360) % 360;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
interface GraphData {
|
|
312
|
-
readonly buffer: Uint8ClampedArray;
|
|
313
|
-
readonly curvePoints: readonly { readonly x: number; readonly y: number }[];
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function computeGraphData(state: ConfiguratorState): GraphData {
|
|
317
|
-
const { dynamicRange, globalHueGrading } = state;
|
|
318
|
-
|
|
319
|
-
const lightActive = globalHueGrading.light.strength !== "none";
|
|
320
|
-
const darkActive = globalHueGrading.dark.strength !== "none";
|
|
321
|
-
const lightOklchHue = traditionalHueToOklch(globalHueGrading.light.hue);
|
|
322
|
-
const darkOklchHue = traditionalHueToOklch(globalHueGrading.dark.hue);
|
|
323
|
-
const lightFactor = strengthToFactor(globalHueGrading.light.strength);
|
|
324
|
-
const darkFactor = strengthToFactor(globalHueGrading.dark.strength);
|
|
325
|
-
|
|
326
|
-
const buffer = new Uint8ClampedArray(GRAPH_COLS * GRAPH_ROWS * 4);
|
|
327
|
-
|
|
328
|
-
for (let col = 0; col < GRAPH_COLS; col++) {
|
|
329
|
-
const nv = 1 - col / (GRAPH_COLS - 1);
|
|
330
|
-
const L = resolveLightness(dynamicRange, nv);
|
|
331
|
-
|
|
332
|
-
// Easing weights for hue blend at top row (assumes hard strength)
|
|
333
|
-
const wLight = lightActive ? Math.pow(nv, HUE_GRADING_EASING_POWER) : 0;
|
|
334
|
-
const wDark = darkActive
|
|
335
|
-
? Math.pow(1 - nv, HUE_GRADING_EASING_POWER)
|
|
336
|
-
: 0;
|
|
337
|
-
const totalW = wLight + wDark;
|
|
338
|
-
|
|
339
|
-
let topHue: number;
|
|
340
|
-
let topChroma: number;
|
|
341
|
-
|
|
342
|
-
if (totalW === 0) {
|
|
343
|
-
topHue = 0;
|
|
344
|
-
topChroma = 0;
|
|
345
|
-
} else {
|
|
346
|
-
if (!lightActive) {
|
|
347
|
-
topHue = darkOklchHue;
|
|
348
|
-
} else if (!darkActive) {
|
|
349
|
-
topHue = lightOklchHue;
|
|
350
|
-
} else {
|
|
351
|
-
topHue = blendHues(lightOklchHue, darkOklchHue, wLight, wDark);
|
|
352
|
-
}
|
|
353
|
-
topChroma =
|
|
354
|
-
findMaxChromaInGamut(L, topHue) * Math.min(totalW, 1);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
for (let row = 0; row < GRAPH_ROWS; row++) {
|
|
358
|
-
const gradingIntensity = row / (GRAPH_ROWS - 1);
|
|
359
|
-
const C = topChroma * gradingIntensity;
|
|
360
|
-
const srgb = clampSrgb(oklchToSrgb({ L, C, h: topHue }));
|
|
361
|
-
|
|
362
|
-
// Canvas Y=0 is top; row=0 is bottom of our graph
|
|
363
|
-
const canvasY = GRAPH_ROWS - 1 - row;
|
|
364
|
-
const idx = (canvasY * GRAPH_COLS + col) * 4;
|
|
365
|
-
buffer[idx] = Math.round(srgb.r * 255);
|
|
366
|
-
buffer[idx + 1] = Math.round(srgb.g * 255);
|
|
367
|
-
buffer[idx + 2] = Math.round(srgb.b * 255);
|
|
368
|
-
buffer[idx + 3] = 255;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 26-step curve points
|
|
373
|
-
const curvePoints: { x: number; y: number }[] = [];
|
|
374
|
-
for (let i = 0; i < 26; i++) {
|
|
375
|
-
const nv = 1 - i / 25;
|
|
376
|
-
const x = (i / 25) * (GRAPH_COLS - 1);
|
|
377
|
-
|
|
378
|
-
const lightContrib =
|
|
379
|
-
Math.pow(nv, HUE_GRADING_EASING_POWER) *
|
|
380
|
-
(lightFactor / HUE_GRADING_STRENGTH_HARD);
|
|
381
|
-
const darkContrib =
|
|
382
|
-
Math.pow(1 - nv, HUE_GRADING_EASING_POWER) *
|
|
383
|
-
(darkFactor / HUE_GRADING_STRENGTH_HARD);
|
|
384
|
-
const y = clamp(lightContrib + darkContrib, 0, 1);
|
|
385
|
-
|
|
386
|
-
curvePoints.push({ x, y });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return { buffer, curvePoints };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
interface DynamicRangeGraphProps {
|
|
393
|
-
readonly state: ConfiguratorState;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function DynamicRangeGraph({ state }: DynamicRangeGraphProps) {
|
|
397
|
-
const tokens = useTokens();
|
|
398
|
-
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
399
|
-
|
|
400
|
-
const graphData = useMemo(
|
|
401
|
-
() => computeGraphData(state),
|
|
402
|
-
[
|
|
403
|
-
state.dynamicRange.lightest,
|
|
404
|
-
state.dynamicRange.darkest,
|
|
405
|
-
state.globalHueGrading.light.strength,
|
|
406
|
-
state.globalHueGrading.light.hue,
|
|
407
|
-
state.globalHueGrading.dark.strength,
|
|
408
|
-
state.globalHueGrading.dark.hue,
|
|
409
|
-
],
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
useEffect(() => {
|
|
413
|
-
const canvas = canvasRef.current;
|
|
414
|
-
if (!canvas) return;
|
|
415
|
-
|
|
416
|
-
canvas.width = GRAPH_COLS;
|
|
417
|
-
canvas.height = GRAPH_ROWS;
|
|
418
|
-
|
|
419
|
-
const ctx = canvas.getContext("2d");
|
|
420
|
-
if (!ctx) return;
|
|
421
|
-
|
|
422
|
-
// Draw gradient from pre-computed buffer
|
|
423
|
-
const imageData = ctx.createImageData(GRAPH_COLS, GRAPH_ROWS);
|
|
424
|
-
imageData.data.set(graphData.buffer);
|
|
425
|
-
ctx.putImageData(imageData, 0, 0);
|
|
426
|
-
|
|
427
|
-
// Draw 26-step curve overlay
|
|
428
|
-
const curveColor = srgbToHex(tokens.accent.fill.srgb);
|
|
429
|
-
const { curvePoints } = graphData;
|
|
430
|
-
|
|
431
|
-
if (curvePoints.length < 2) return;
|
|
432
|
-
|
|
433
|
-
const mapped = curvePoints.map((p) => ({
|
|
434
|
-
cx: p.x,
|
|
435
|
-
cy: (1 - p.y) * (GRAPH_ROWS - 1),
|
|
436
|
-
}));
|
|
437
|
-
|
|
438
|
-
// Smooth Catmull-Rom spline
|
|
439
|
-
ctx.beginPath();
|
|
440
|
-
ctx.strokeStyle = curveColor;
|
|
441
|
-
ctx.lineWidth = 1.5;
|
|
442
|
-
ctx.lineJoin = "round";
|
|
443
|
-
ctx.lineCap = "round";
|
|
444
|
-
|
|
445
|
-
ctx.moveTo(mapped[0].cx, mapped[0].cy);
|
|
446
|
-
for (let i = 0; i < mapped.length - 1; i++) {
|
|
447
|
-
const p0 = mapped[Math.max(0, i - 1)];
|
|
448
|
-
const p1 = mapped[i];
|
|
449
|
-
const p2 = mapped[i + 1];
|
|
450
|
-
const p3 = mapped[Math.min(mapped.length - 1, i + 2)];
|
|
451
|
-
|
|
452
|
-
const cp1x = p1.cx + (p2.cx - p0.cx) / 6;
|
|
453
|
-
const cp1y = p1.cy + (p2.cy - p0.cy) / 6;
|
|
454
|
-
const cp2x = p2.cx - (p3.cx - p1.cx) / 6;
|
|
455
|
-
const cp2y = p2.cy - (p3.cy - p1.cy) / 6;
|
|
456
|
-
|
|
457
|
-
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.cx, p2.cy);
|
|
458
|
-
}
|
|
459
|
-
ctx.stroke();
|
|
460
|
-
|
|
461
|
-
// Draw dots at each of the 26 steps
|
|
462
|
-
ctx.fillStyle = curveColor;
|
|
463
|
-
for (const p of mapped) {
|
|
464
|
-
ctx.beginPath();
|
|
465
|
-
ctx.arc(p.cx, p.cy, 2, 0, Math.PI * 2);
|
|
466
|
-
ctx.fill();
|
|
467
|
-
}
|
|
468
|
-
}, [graphData, tokens]);
|
|
469
|
-
|
|
470
|
-
const borderColor = srgbToHex(tokens.border.srgb);
|
|
471
|
-
|
|
472
|
-
return (
|
|
473
|
-
<canvas
|
|
474
|
-
ref={canvasRef}
|
|
475
|
-
style={{
|
|
476
|
-
width: "100%",
|
|
477
|
-
height: GRAPH_HEIGHT,
|
|
478
|
-
borderRadius: 6,
|
|
479
|
-
border: `1px solid ${borderColor}`,
|
|
480
|
-
display: "block",
|
|
481
|
-
overflow: "hidden",
|
|
482
|
-
}}
|
|
483
|
-
/>
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
267
|
// --- Section ---
|
|
488
268
|
|
|
489
269
|
interface DynamicRangeSectionProps {
|
|
@@ -511,9 +291,6 @@ export function DynamicRangeSection({
|
|
|
511
291
|
|
|
512
292
|
return (
|
|
513
293
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
514
|
-
{/* Dynamic range graph */}
|
|
515
|
-
<DynamicRangeGraph state={state} />
|
|
516
|
-
|
|
517
294
|
{/* Labels above slider */}
|
|
518
295
|
<div
|
|
519
296
|
style={{
|