@motion-proto/live-tokens 0.1.0
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/README.md +41 -0
- package/dist-plugin/index.cjs +444 -0
- package/dist-plugin/index.d.cts +12 -0
- package/dist-plugin/index.d.ts +12 -0
- package/dist-plugin/index.js +407 -0
- package/package.json +86 -0
- package/src/components/Badge.svelte +82 -0
- package/src/components/Button.svelte +333 -0
- package/src/components/Card.svelte +83 -0
- package/src/components/CollapsibleSection.svelte +82 -0
- package/src/components/DetailNav.svelte +78 -0
- package/src/components/Dialog.svelte +269 -0
- package/src/components/InlineEditActions.svelte +73 -0
- package/src/components/Notification.svelte +308 -0
- package/src/components/ProgressBar.svelte +99 -0
- package/src/components/RadioButton.svelte +87 -0
- package/src/components/SectionDivider.svelte +121 -0
- package/src/components/TabBar.svelte +92 -0
- package/src/components/Toggle.svelte +86 -0
- package/src/components/Tooltip.svelte +64 -0
- package/src/lib/ColumnsOverlay.svelte +120 -0
- package/src/lib/LiveEditorOverlay.svelte +467 -0
- package/src/lib/columnsOverlay.ts +26 -0
- package/src/lib/cssVarSync.ts +72 -0
- package/src/lib/editorConfig.ts +9 -0
- package/src/lib/editorConfigStore.ts +14 -0
- package/src/lib/index.ts +51 -0
- package/src/lib/oklch.ts +129 -0
- package/src/lib/pageSource.ts +6 -0
- package/src/lib/tokenInit.ts +29 -0
- package/src/lib/tokenService.ts +144 -0
- package/src/lib/tokenTypes.ts +45 -0
- package/src/pages/Admin.svelte +100 -0
- package/src/pages/ShowcasePage.svelte +146 -0
- package/src/showcase/BackupBrowser.svelte +617 -0
- package/src/showcase/BezierCurveEditor.svelte +648 -0
- package/src/showcase/ColorEditPanel.svelte +498 -0
- package/src/showcase/ComponentsTab.svelte +107 -0
- package/src/showcase/EditorDialog.svelte +137 -0
- package/src/showcase/PaletteEditor.svelte +2579 -0
- package/src/showcase/PaletteSelector.svelte +627 -0
- package/src/showcase/SurfacesTab.svelte +409 -0
- package/src/showcase/TextTab.svelte +205 -0
- package/src/showcase/TokenFileManager.svelte +683 -0
- package/src/showcase/TokenMap.svelte +54 -0
- package/src/showcase/VariablesTab.svelte +2657 -0
- package/src/showcase/VisualsTab.svelte +233 -0
- package/src/showcase/curveEngine.ts +190 -0
- package/src/showcase/demos/BadgeDemo.svelte +58 -0
- package/src/showcase/demos/CardDemo.svelte +52 -0
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +194 -0
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +56 -0
- package/src/showcase/demos/DialogDemo.svelte +42 -0
- package/src/showcase/demos/InlineEditActionsDemo.svelte +27 -0
- package/src/showcase/demos/NotificationDemo.svelte +149 -0
- package/src/showcase/demos/ProgressBarDemo.svelte +56 -0
- package/src/showcase/demos/RadioButtonDemo.svelte +58 -0
- package/src/showcase/demos/SectionDividerDemo.svelte +79 -0
- package/src/showcase/demos/StandardButtonsDemo.svelte +457 -0
- package/src/showcase/demos/TabBarDemo.svelte +60 -0
- package/src/showcase/demos/TooltipDemo.svelte +54 -0
- package/src/showcase/editor.css +93 -0
- package/src/showcase/index.ts +17 -0
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +97 -0
- package/src/styles/fonts/Domine/README.txt +66 -0
- package/src/styles/fonts.css +18 -0
- package/src/styles/form-controls.css +190 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
type CurveAnchor, type CurveConfig,
|
|
4
|
+
CURVE_H, CURVE_PAD_Y, CURVE_Y_PAD,
|
|
5
|
+
isCornerAnchor, curveXToSvg, curveYToSvg, svgToX, svgToY,
|
|
6
|
+
evalBezier, buildCurvePath, curveTemplates,
|
|
7
|
+
serializeCurve, deserializeCurve,
|
|
8
|
+
} from './curveEngine';
|
|
9
|
+
|
|
10
|
+
export let anchors: CurveAnchor[];
|
|
11
|
+
export let cfg: CurveConfig;
|
|
12
|
+
export let stepCount: number;
|
|
13
|
+
export let padX: number = 0;
|
|
14
|
+
export let offset: number = 0;
|
|
15
|
+
export let defaultAnchors: CurveAnchor[] | null = null;
|
|
16
|
+
export let lockedAnchorIndex: number | null = null;
|
|
17
|
+
export let onAnchorsChange: (anchors: CurveAnchor[]) => void = () => {};
|
|
18
|
+
export let onOffsetChange: (offset: number) => void = () => {};
|
|
19
|
+
|
|
20
|
+
function resetToDefault() {
|
|
21
|
+
if (!defaultAnchors) return;
|
|
22
|
+
onAnchorsChange(defaultAnchors.map(a => ({ ...a })));
|
|
23
|
+
onOffsetChange(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CURVE_W_DEFAULT = 720;
|
|
27
|
+
let svgEl: SVGSVGElement | null = null;
|
|
28
|
+
let dims = CURVE_W_DEFAULT;
|
|
29
|
+
let shiftActive = false;
|
|
30
|
+
|
|
31
|
+
const clipId = `curve-clip-${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
+
|
|
33
|
+
$: w = dims;
|
|
34
|
+
$: offsetPx = -(offset / ((cfg.yMax - cfg.yMin) * (1 + 2 * CURVE_Y_PAD))) * (CURVE_H - 2 * CURVE_PAD_Y);
|
|
35
|
+
|
|
36
|
+
function stepToX(index: number): number {
|
|
37
|
+
return stepCount > 1 ? (index / (stepCount - 1)) * 100 : 50;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dynamicViewBox(node: SVGSVGElement) {
|
|
41
|
+
const update = () => {
|
|
42
|
+
const cw = node.clientWidth;
|
|
43
|
+
const ch = node.clientHeight;
|
|
44
|
+
if (cw > 0 && ch > 0) {
|
|
45
|
+
const newW = CURVE_H * (cw / ch);
|
|
46
|
+
if (Math.abs(dims - newW) > 0.5) {
|
|
47
|
+
dims = newW;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const ro = new ResizeObserver(update);
|
|
52
|
+
ro.observe(node);
|
|
53
|
+
update();
|
|
54
|
+
return { destroy() { ro.disconnect(); } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function svgCoords(e: MouseEvent | PointerEvent): { x: number; y: number } {
|
|
58
|
+
if (!svgEl) return { x: 0, y: 0 };
|
|
59
|
+
const rect = svgEl.getBoundingClientRect();
|
|
60
|
+
return {
|
|
61
|
+
x: (e.clientX - rect.left) * (w / rect.width),
|
|
62
|
+
y: (e.clientY - rect.top) * (CURVE_H / rect.height),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Anchor drag ---
|
|
67
|
+
|
|
68
|
+
type DragTarget = {
|
|
69
|
+
kind: 'anchor' | 'handleIn' | 'handleOut';
|
|
70
|
+
index: number;
|
|
71
|
+
breakHandle: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let drag: DragTarget | null = null;
|
|
75
|
+
|
|
76
|
+
function handlePointerDown(e: PointerEvent, target: Omit<DragTarget, 'breakHandle'>) {
|
|
77
|
+
if (e.altKey && target.kind === 'anchor') {
|
|
78
|
+
if (target.index === lockedAnchorIndex) return; // can't delete locked anchor
|
|
79
|
+
e.stopPropagation();
|
|
80
|
+
removePoint(target.index);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const isHandle = target.kind !== 'anchor';
|
|
84
|
+
const a = anchors[target.index];
|
|
85
|
+
const alreadyBroken = isHandle && a && (Math.abs(a.inDx + a.outDx) > 0.01 || Math.abs(a.inDy + a.outDy) > 0.01);
|
|
86
|
+
drag = { ...target, breakHandle: isHandle && (e.altKey || alreadyBroken) };
|
|
87
|
+
(e.currentTarget as SVGElement).setPointerCapture(e.pointerId);
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handlePointerMove(e: PointerEvent) {
|
|
93
|
+
if (!drag) return;
|
|
94
|
+
const { kind, index: idx, breakHandle } = drag;
|
|
95
|
+
const { x, y } = svgCoords(e);
|
|
96
|
+
const newX = svgToX(x, w, padX);
|
|
97
|
+
const newY = Math.round(svgToY(y, cfg));
|
|
98
|
+
const updated = [...anchors];
|
|
99
|
+
|
|
100
|
+
if (kind === 'anchor') {
|
|
101
|
+
if (idx === lockedAnchorIndex) return; // locked: no x/y dragging
|
|
102
|
+
let ax = Math.round(newX);
|
|
103
|
+
if (idx === 0) ax = 0;
|
|
104
|
+
else if (idx === updated.length - 1) ax = 100;
|
|
105
|
+
else ax = Math.max(updated[idx - 1].x + 1, Math.min(updated[idx + 1].x - 1, ax));
|
|
106
|
+
updated[idx] = { ...updated[idx], x: ax, y: newY };
|
|
107
|
+
} else if (kind === 'handleOut') {
|
|
108
|
+
const a = updated[idx];
|
|
109
|
+
const dx = newX - a.x, dy = newY - a.y;
|
|
110
|
+
updated[idx] = breakHandle
|
|
111
|
+
? { ...a, outDx: dx, outDy: dy }
|
|
112
|
+
: { ...a, outDx: dx, outDy: dy, inDx: -dx, inDy: -dy };
|
|
113
|
+
} else {
|
|
114
|
+
const a = updated[idx];
|
|
115
|
+
const dx = newX - a.x, dy = newY - a.y;
|
|
116
|
+
updated[idx] = breakHandle
|
|
117
|
+
? { ...a, inDx: dx, inDy: dy }
|
|
118
|
+
: { ...a, inDx: dx, inDy: dy, outDx: -dx, outDy: -dy };
|
|
119
|
+
}
|
|
120
|
+
onAnchorsChange(updated);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handlePointerUp() {
|
|
124
|
+
drag = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Insert point ---
|
|
128
|
+
|
|
129
|
+
function insertPointOnPath(e: MouseEvent) {
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
const { x } = svgCoords(e);
|
|
132
|
+
const clickX = svgToX(x, w, padX);
|
|
133
|
+
|
|
134
|
+
if (anchors.some(p => Math.abs(p.x - clickX) < 4)) return;
|
|
135
|
+
|
|
136
|
+
let seg = 0;
|
|
137
|
+
for (let i = 0; i < anchors.length - 1; i++) {
|
|
138
|
+
if (clickX >= anchors[i].x && clickX <= anchors[i + 1].x) { seg = i; break; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const a0 = anchors[seg], a1 = anchors[seg + 1];
|
|
142
|
+
const p0x = a0.x, p0y = a0.y;
|
|
143
|
+
const c0x = a0.x + a0.outDx, c0y = a0.y + a0.outDy;
|
|
144
|
+
const c1x = a1.x + a1.inDx, c1y = a1.y + a1.inDy;
|
|
145
|
+
const p1x = a1.x, p1y = a1.y;
|
|
146
|
+
|
|
147
|
+
let lo = 0, hi = 1;
|
|
148
|
+
for (let i = 0; i < 20; i++) {
|
|
149
|
+
const mid = (lo + hi) / 2;
|
|
150
|
+
if (evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, mid).x < clickX) lo = mid; else hi = mid;
|
|
151
|
+
}
|
|
152
|
+
const t = (lo + hi) / 2;
|
|
153
|
+
|
|
154
|
+
const q0x = p0x + t * (c0x - p0x), q0y = p0y + t * (c0y - p0y);
|
|
155
|
+
const q1x = c0x + t * (c1x - c0x), q1y = c0y + t * (c1y - c0y);
|
|
156
|
+
const q2x = c1x + t * (p1x - c1x), q2y = c1y + t * (p1y - c1y);
|
|
157
|
+
const r0x = q0x + t * (q1x - q0x), r0y = q0y + t * (q1y - q0y);
|
|
158
|
+
const r1x = q1x + t * (q2x - q1x), r1y = q1y + t * (q2y - q1y);
|
|
159
|
+
const mx = r0x + t * (r1x - r0x), my = r0y + t * (r1y - r0y);
|
|
160
|
+
|
|
161
|
+
const updated = [...anchors];
|
|
162
|
+
updated[seg] = { ...updated[seg], outDx: q0x - p0x, outDy: q0y - p0y };
|
|
163
|
+
updated[seg + 1] = { ...updated[seg + 1], inDx: q2x - p1x, inDy: q2y - p1y };
|
|
164
|
+
updated.splice(seg + 1, 0, {
|
|
165
|
+
x: Math.round(mx), y: Math.round(my),
|
|
166
|
+
inDx: r0x - mx, inDy: r0y - my, outDx: r1x - mx, outDy: r1y - my,
|
|
167
|
+
});
|
|
168
|
+
onAnchorsChange(updated);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toggleAnchorSmooth(index: number) {
|
|
172
|
+
const a = anchors[index];
|
|
173
|
+
const updated = [...anchors];
|
|
174
|
+
if (isCornerAnchor(a)) {
|
|
175
|
+
updated[index] = { ...a, inDx: -15, inDy: 0, outDx: 15, outDy: 0 };
|
|
176
|
+
} else {
|
|
177
|
+
updated[index] = { ...a, inDx: 0, inDy: 0, outDx: 0, outDy: 0 };
|
|
178
|
+
}
|
|
179
|
+
onAnchorsChange(updated);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function removePoint(index: number) {
|
|
183
|
+
if (anchors.length <= 2) return;
|
|
184
|
+
onAnchorsChange(anchors.filter((_, i) => i !== index));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function applyTemplate(tpl: typeof curveTemplates[0]) {
|
|
188
|
+
onAnchorsChange(tpl.anchors(cfg));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Shift (vertical offset) drag ---
|
|
192
|
+
|
|
193
|
+
let shiftDrag: { startClientY: number; startOffset: number; pxPerUnit: number } | null = null;
|
|
194
|
+
|
|
195
|
+
function handleShiftPointerDown(e: PointerEvent) {
|
|
196
|
+
if (!svgEl) return;
|
|
197
|
+
const rect = svgEl.getBoundingClientRect();
|
|
198
|
+
const pxPerUnit = rect.height / ((cfg.yMax - cfg.yMin) * (1 + 2 * CURVE_Y_PAD));
|
|
199
|
+
shiftDrag = {
|
|
200
|
+
startClientY: e.clientY,
|
|
201
|
+
startOffset: offset,
|
|
202
|
+
pxPerUnit,
|
|
203
|
+
};
|
|
204
|
+
(e.currentTarget as SVGElement).setPointerCapture(e.pointerId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function handleShiftPointerMove(e: PointerEvent) {
|
|
208
|
+
if (!shiftDrag) return;
|
|
209
|
+
const deltaY = shiftDrag.startClientY - e.clientY;
|
|
210
|
+
const deltaValue = deltaY / shiftDrag.pxPerUnit;
|
|
211
|
+
onOffsetChange(Math.round(shiftDrag.startOffset + deltaValue));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleShiftPointerUp() {
|
|
215
|
+
shiftDrag = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Clipboard (system clipboard for cross-editor paste) ---
|
|
219
|
+
|
|
220
|
+
async function copyToClipboard() {
|
|
221
|
+
try {
|
|
222
|
+
await navigator.clipboard.writeText(serializeCurve(anchors, offset));
|
|
223
|
+
} catch {
|
|
224
|
+
// clipboard write failed silently
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function pasteFromClipboard() {
|
|
229
|
+
try {
|
|
230
|
+
const text = await navigator.clipboard.readText();
|
|
231
|
+
const data = deserializeCurve(text);
|
|
232
|
+
if (!data) return;
|
|
233
|
+
onAnchorsChange(data.anchors.map(a => ({ ...a })));
|
|
234
|
+
onOffsetChange(data.offset);
|
|
235
|
+
} catch {
|
|
236
|
+
// clipboard read failed or permission denied
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
</script>
|
|
240
|
+
|
|
241
|
+
<div class="curve-panel">
|
|
242
|
+
<div class="curve-panel-header">
|
|
243
|
+
<span class="curve-panel-label">{cfg.label}</span>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="curve-container" style="padding-inline: calc(50% / {stepCount})">
|
|
246
|
+
<svg
|
|
247
|
+
bind:this={svgEl}
|
|
248
|
+
class="curve-svg"
|
|
249
|
+
viewBox="0 0 {w} {CURVE_H}"
|
|
250
|
+
use:dynamicViewBox
|
|
251
|
+
>
|
|
252
|
+
<defs>
|
|
253
|
+
<clipPath id={clipId}>
|
|
254
|
+
<rect x="0" y="0" width={w} height={CURVE_H} />
|
|
255
|
+
</clipPath>
|
|
256
|
+
</defs>
|
|
257
|
+
|
|
258
|
+
<!-- Out-of-range shading -->
|
|
259
|
+
<rect x="0" y="0" width={w} height={curveYToSvg(cfg.yMax, cfg)} class="curve-out-of-range" />
|
|
260
|
+
<rect x="0" y={curveYToSvg(cfg.yMin, cfg)} width={w} height={CURVE_H - curveYToSvg(cfg.yMin, cfg)} class="curve-out-of-range" />
|
|
261
|
+
|
|
262
|
+
<!-- Step divider lines -->
|
|
263
|
+
{#each Array(stepCount) as _, si}
|
|
264
|
+
<line
|
|
265
|
+
x1={curveXToSvg(stepToX(si), w, padX)}
|
|
266
|
+
y1={CURVE_PAD_Y}
|
|
267
|
+
x2={curveXToSvg(stepToX(si), w, padX)}
|
|
268
|
+
y2={CURVE_H - CURVE_PAD_Y}
|
|
269
|
+
class="curve-step-line"
|
|
270
|
+
/>
|
|
271
|
+
{/each}
|
|
272
|
+
|
|
273
|
+
<!-- Horizontal grid lines -->
|
|
274
|
+
{#each cfg.gridLines as gl}
|
|
275
|
+
<line x1={padX} y1={curveYToSvg(gl, cfg)} x2={w - padX} y2={curveYToSvg(gl, cfg)} class="curve-grid" />
|
|
276
|
+
{/each}
|
|
277
|
+
{#each cfg.dashedLines as dl}
|
|
278
|
+
<line x1={padX} y1={curveYToSvg(dl, cfg)} x2={w - padX} y2={curveYToSvg(dl, cfg)} class="curve-grid dashed" />
|
|
279
|
+
{/each}
|
|
280
|
+
|
|
281
|
+
<!-- Curve content group — offset vertically, clipped -->
|
|
282
|
+
<g transform="translate(0,{offsetPx})" clip-path="url(#{clipId})">
|
|
283
|
+
{#if shiftActive}
|
|
284
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
285
|
+
<rect
|
|
286
|
+
x="0" y={-CURVE_H} width={w} height={CURVE_H * 3}
|
|
287
|
+
class="shift-overlay"
|
|
288
|
+
on:pointerdown={handleShiftPointerDown}
|
|
289
|
+
on:pointermove={handleShiftPointerMove}
|
|
290
|
+
on:pointerup={handleShiftPointerUp}
|
|
291
|
+
/>
|
|
292
|
+
{:else}
|
|
293
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
294
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
295
|
+
<path
|
|
296
|
+
d={buildCurvePath(anchors, cfg, w, padX)}
|
|
297
|
+
class="curve-hit"
|
|
298
|
+
on:click={insertPointOnPath}
|
|
299
|
+
/>
|
|
300
|
+
{/if}
|
|
301
|
+
|
|
302
|
+
<!-- Visible curve path -->
|
|
303
|
+
<path d={buildCurvePath(anchors, cfg, w, padX)} class="curve-line" />
|
|
304
|
+
|
|
305
|
+
<!-- Bezier tangent handles -->
|
|
306
|
+
{#if !shiftActive}
|
|
307
|
+
{#each anchors as pt, i}
|
|
308
|
+
{#if i > 0 && !isCornerAnchor(pt)}
|
|
309
|
+
<line
|
|
310
|
+
x1={curveXToSvg(pt.x, w, padX)} y1={curveYToSvg(pt.y, cfg)}
|
|
311
|
+
x2={curveXToSvg(pt.x + pt.inDx, w, padX)} y2={curveYToSvg(pt.y + pt.inDy, cfg)}
|
|
312
|
+
class="handle-line"
|
|
313
|
+
/>
|
|
314
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
315
|
+
<circle
|
|
316
|
+
cx={curveXToSvg(pt.x + pt.inDx, w, padX)} cy={curveYToSvg(pt.y + pt.inDy, cfg)}
|
|
317
|
+
r="3.5" class="handle-grip"
|
|
318
|
+
on:pointerdown={(e) => handlePointerDown(e, { kind: 'handleIn', index: i })}
|
|
319
|
+
on:pointermove={handlePointerMove}
|
|
320
|
+
on:pointerup={handlePointerUp}
|
|
321
|
+
/>
|
|
322
|
+
{/if}
|
|
323
|
+
{#if i < anchors.length - 1 && !isCornerAnchor(pt)}
|
|
324
|
+
<line
|
|
325
|
+
x1={curveXToSvg(pt.x, w, padX)} y1={curveYToSvg(pt.y, cfg)}
|
|
326
|
+
x2={curveXToSvg(pt.x + pt.outDx, w, padX)} y2={curveYToSvg(pt.y + pt.outDy, cfg)}
|
|
327
|
+
class="handle-line"
|
|
328
|
+
/>
|
|
329
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
330
|
+
<circle
|
|
331
|
+
cx={curveXToSvg(pt.x + pt.outDx, w, padX)} cy={curveYToSvg(pt.y + pt.outDy, cfg)}
|
|
332
|
+
r="3.5" class="handle-grip"
|
|
333
|
+
on:pointerdown={(e) => handlePointerDown(e, { kind: 'handleOut', index: i })}
|
|
334
|
+
on:pointermove={handlePointerMove}
|
|
335
|
+
on:pointerup={handlePointerUp}
|
|
336
|
+
/>
|
|
337
|
+
{/if}
|
|
338
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
339
|
+
{#if i === lockedAnchorIndex}
|
|
340
|
+
<path
|
|
341
|
+
d="M{curveXToSvg(pt.x, w, padX)},{curveYToSvg(pt.y, cfg) - 6} l5,6 l-5,6 l-5,-6 Z"
|
|
342
|
+
class="curve-handle locked"
|
|
343
|
+
/>
|
|
344
|
+
{:else if isCornerAnchor(pt)}
|
|
345
|
+
<rect
|
|
346
|
+
x={curveXToSvg(pt.x, w, padX) - 4} y={curveYToSvg(pt.y, cfg) - 4}
|
|
347
|
+
width="8" height="8"
|
|
348
|
+
class="curve-handle corner"
|
|
349
|
+
on:pointerdown={(e) => handlePointerDown(e, { kind: 'anchor', index: i })}
|
|
350
|
+
on:pointermove={handlePointerMove}
|
|
351
|
+
on:pointerup={handlePointerUp}
|
|
352
|
+
on:dblclick|stopPropagation={() => toggleAnchorSmooth(i)}
|
|
353
|
+
/>
|
|
354
|
+
{:else}
|
|
355
|
+
<circle
|
|
356
|
+
cx={curveXToSvg(pt.x, w, padX)} cy={curveYToSvg(pt.y, cfg)}
|
|
357
|
+
r="5" class="curve-handle"
|
|
358
|
+
on:pointerdown={(e) => handlePointerDown(e, { kind: 'anchor', index: i })}
|
|
359
|
+
on:pointermove={handlePointerMove}
|
|
360
|
+
on:pointerup={handlePointerUp}
|
|
361
|
+
on:dblclick|stopPropagation={() => toggleAnchorSmooth(i)}
|
|
362
|
+
/>
|
|
363
|
+
{/if}
|
|
364
|
+
{/each}
|
|
365
|
+
{/if}
|
|
366
|
+
</g>
|
|
367
|
+
</svg>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="curve-toolbar">
|
|
370
|
+
<div class="curve-toolbar-left">
|
|
371
|
+
<button
|
|
372
|
+
class="curve-tool-btn"
|
|
373
|
+
class:active={shiftActive}
|
|
374
|
+
type="button"
|
|
375
|
+
title="Vertical offset"
|
|
376
|
+
on:click={() => shiftActive = !shiftActive}
|
|
377
|
+
>
|
|
378
|
+
<svg viewBox="0 0 12 20" class="curve-tool-icon">
|
|
379
|
+
<path d="M6,2 L10,7 L7,7 L7,13 L10,13 L6,18 L2,13 L5,13 L5,7 L2,7 Z" />
|
|
380
|
+
</svg>
|
|
381
|
+
<span>Offset{offset !== 0 ? ` ${offset > 0 ? '+' : ''}${offset}` : ''}</span>
|
|
382
|
+
</button>
|
|
383
|
+
<span class="curve-hint">&x2325;-click to remove point</span>
|
|
384
|
+
<button class="curve-tool-btn" type="button" title="Copy curve" on:click={copyToClipboard}>Copy</button>
|
|
385
|
+
<button class="curve-tool-btn" type="button" title="Paste curve" on:click={pasteFromClipboard}>Paste</button>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="curve-templates">
|
|
388
|
+
{#each curveTemplates as tpl}
|
|
389
|
+
<button
|
|
390
|
+
class="curve-template-btn"
|
|
391
|
+
type="button"
|
|
392
|
+
title={tpl.name}
|
|
393
|
+
on:click={() => applyTemplate(tpl)}
|
|
394
|
+
>
|
|
395
|
+
<svg viewBox="0 0 20 12" class="curve-template-icon">
|
|
396
|
+
<path d={tpl.icon} />
|
|
397
|
+
</svg>
|
|
398
|
+
</button>
|
|
399
|
+
{/each}
|
|
400
|
+
{#if defaultAnchors}
|
|
401
|
+
<button
|
|
402
|
+
class="curve-tool-btn"
|
|
403
|
+
type="button"
|
|
404
|
+
title="Reset to default"
|
|
405
|
+
on:click={resetToDefault}
|
|
406
|
+
>Reset</button>
|
|
407
|
+
{/if}
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<style>
|
|
413
|
+
.curve-panel {
|
|
414
|
+
display: flex;
|
|
415
|
+
flex-direction: column;
|
|
416
|
+
gap: var(--space-4);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.curve-panel-header {
|
|
420
|
+
display: flex;
|
|
421
|
+
align-items: center;
|
|
422
|
+
justify-content: space-between;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.curve-panel-label {
|
|
426
|
+
font-size: var(--font-md);
|
|
427
|
+
font-weight: var(--font-weight-semibold);
|
|
428
|
+
color: var(--ui-text-tertiary);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.curve-container {
|
|
432
|
+
width: 100%;
|
|
433
|
+
height: 250px;
|
|
434
|
+
box-sizing: border-box;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.curve-svg {
|
|
438
|
+
width: 100%;
|
|
439
|
+
height: 100%;
|
|
440
|
+
background: transparent;
|
|
441
|
+
border: 1px solid var(--ui-border-subtle);
|
|
442
|
+
border-radius: var(--radius-sm);
|
|
443
|
+
cursor: crosshair;
|
|
444
|
+
display: block;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.curve-out-of-range {
|
|
448
|
+
fill: white;
|
|
449
|
+
opacity: 0.04;
|
|
450
|
+
pointer-events: none;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.curve-grid {
|
|
454
|
+
stroke: var(--ui-border-faint);
|
|
455
|
+
stroke-width: 0.5;
|
|
456
|
+
vector-effect: non-scaling-stroke;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.curve-grid.dashed {
|
|
460
|
+
stroke-dasharray: 3 3;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.curve-step-line {
|
|
464
|
+
stroke: var(--ui-border-faint);
|
|
465
|
+
stroke-width: 0.5;
|
|
466
|
+
vector-effect: non-scaling-stroke;
|
|
467
|
+
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.shift-overlay {
|
|
471
|
+
fill: transparent;
|
|
472
|
+
cursor: ns-resize;
|
|
473
|
+
touch-action: none;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.curve-hit {
|
|
477
|
+
fill: none;
|
|
478
|
+
stroke: transparent;
|
|
479
|
+
stroke-width: 12;
|
|
480
|
+
cursor: copy;
|
|
481
|
+
pointer-events: stroke;
|
|
482
|
+
vector-effect: non-scaling-stroke;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.curve-line {
|
|
486
|
+
fill: none;
|
|
487
|
+
stroke: var(--ui-text-secondary);
|
|
488
|
+
stroke-width: 1.5;
|
|
489
|
+
pointer-events: none;
|
|
490
|
+
vector-effect: non-scaling-stroke;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.handle-line {
|
|
494
|
+
stroke: var(--ui-text-muted);
|
|
495
|
+
stroke-width: 0.75;
|
|
496
|
+
pointer-events: none;
|
|
497
|
+
vector-effect: non-scaling-stroke;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.handle-grip {
|
|
501
|
+
fill: var(--ui-surface-high);
|
|
502
|
+
stroke: var(--ui-text-tertiary);
|
|
503
|
+
stroke-width: 1.5px;
|
|
504
|
+
paint-order: stroke fill;
|
|
505
|
+
cursor: grab;
|
|
506
|
+
touch-action: none;
|
|
507
|
+
vector-effect: non-scaling-stroke;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.handle-grip:hover {
|
|
511
|
+
fill: var(--ui-text-accent);
|
|
512
|
+
stroke: var(--ui-text-primary);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.handle-grip:active {
|
|
516
|
+
cursor: grabbing;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.curve-handle {
|
|
520
|
+
fill: var(--ui-surface-highest);
|
|
521
|
+
stroke: var(--ui-text-primary);
|
|
522
|
+
stroke-width: 2px;
|
|
523
|
+
paint-order: stroke fill;
|
|
524
|
+
cursor: grab;
|
|
525
|
+
touch-action: none;
|
|
526
|
+
vector-effect: non-scaling-stroke;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.curve-handle:hover {
|
|
530
|
+
fill: var(--ui-text-accent);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.curve-handle.corner {
|
|
534
|
+
rx: 1;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.curve-handle:active {
|
|
538
|
+
cursor: grabbing;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.curve-handle.locked {
|
|
542
|
+
fill: var(--ui-toggle);
|
|
543
|
+
stroke: none;
|
|
544
|
+
cursor: default;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.curve-toolbar {
|
|
548
|
+
display: flex;
|
|
549
|
+
align-items: center;
|
|
550
|
+
justify-content: space-between;
|
|
551
|
+
flex-wrap: wrap;
|
|
552
|
+
gap: var(--space-2);
|
|
553
|
+
padding-top: var(--space-2);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.curve-toolbar-left {
|
|
557
|
+
display: flex;
|
|
558
|
+
align-items: center;
|
|
559
|
+
gap: var(--space-4);
|
|
560
|
+
flex-wrap: wrap;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.curve-tool-btn {
|
|
564
|
+
display: flex;
|
|
565
|
+
align-items: center;
|
|
566
|
+
gap: var(--space-4);
|
|
567
|
+
padding: var(--space-2) var(--space-6);
|
|
568
|
+
border: 1px solid var(--ui-border-subtle);
|
|
569
|
+
border-radius: var(--radius-sm);
|
|
570
|
+
background: var(--ui-surface-lowest);
|
|
571
|
+
cursor: pointer;
|
|
572
|
+
color: var(--ui-text-muted);
|
|
573
|
+
font-size: var(--font-md);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.curve-tool-btn:hover {
|
|
577
|
+
border-color: var(--ui-border-medium);
|
|
578
|
+
color: var(--ui-text-secondary);
|
|
579
|
+
background: var(--ui-surface-high);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.curve-tool-btn.active {
|
|
583
|
+
border-color: var(--ui-border-medium);
|
|
584
|
+
background: var(--ui-surface-highest);
|
|
585
|
+
color: var(--ui-text-primary);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.curve-tool-btn:disabled {
|
|
589
|
+
opacity: 0.35;
|
|
590
|
+
cursor: default;
|
|
591
|
+
pointer-events: none;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.curve-tool-icon {
|
|
595
|
+
width: 0.625rem;
|
|
596
|
+
height: 1rem;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.curve-tool-icon path {
|
|
600
|
+
fill: currentColor;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.curve-hint {
|
|
604
|
+
font-size: var(--font-md);
|
|
605
|
+
color: var(--ui-text-muted);
|
|
606
|
+
opacity: 0.6;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.curve-templates {
|
|
610
|
+
display: flex;
|
|
611
|
+
gap: var(--space-2);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.curve-template-btn {
|
|
615
|
+
display: flex;
|
|
616
|
+
align-items: center;
|
|
617
|
+
justify-content: center;
|
|
618
|
+
width: 1.5rem;
|
|
619
|
+
height: 1rem;
|
|
620
|
+
padding: 0;
|
|
621
|
+
border: 1px solid var(--ui-border-subtle);
|
|
622
|
+
border-radius: var(--radius-sm);
|
|
623
|
+
background: var(--ui-surface-lowest);
|
|
624
|
+
cursor: pointer;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.curve-template-btn:hover {
|
|
628
|
+
border-color: var(--ui-border-medium);
|
|
629
|
+
background: var(--ui-surface-high);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.curve-template-icon {
|
|
633
|
+
width: 100%;
|
|
634
|
+
height: 100%;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.curve-template-icon path {
|
|
638
|
+
fill: none;
|
|
639
|
+
stroke: var(--ui-text-tertiary);
|
|
640
|
+
stroke-width: 1.5;
|
|
641
|
+
stroke-linecap: round;
|
|
642
|
+
stroke-linejoin: round;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.curve-template-btn:hover .curve-template-icon path {
|
|
646
|
+
stroke: var(--ui-text-primary);
|
|
647
|
+
}
|
|
648
|
+
</style>
|