@motion-proto/live-tokens 0.65.1 → 0.67.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/CHANGELOG.md +184 -0
- package/dist-plugin/{chunk-NDJJORKJ.js → chunk-T4PMCFJN.js} +220 -114
- package/dist-plugin/index.cjs +253 -129
- package/dist-plugin/index.js +27 -9
- package/dist-plugin/migrateData/index.cjs +220 -114
- package/dist-plugin/migrateData/index.js +1 -1
- package/package.json +8 -1
- package/src/editor/bootstrap.ts +12 -0
- package/src/editor/core/productionPulse.ts +1 -1
- package/src/editor/core/sketch/index.ts +78 -21
- package/src/editor/core/sketch/maskField.ts +200 -62
- package/src/editor/core/sketch/sketchLayer.ts +10 -4
- package/src/editor/core/sketch/sketchRegistry.ts +98 -0
- package/src/editor/core/sketch/sketchStore.ts +110 -31
- package/src/editor/core/sketch/sketchStyleService.ts +3 -0
- package/src/editor/core/sketch/sketchStyles.ts +63 -96
- package/src/editor/core/themes/themeInit.ts +4 -13
- package/src/editor/docs/content/sketch-mode.md +82 -13
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/ui/sketch/SketchTab.svelte +219 -74
- package/src/live-tokens/data/sketch-styles/dashed.json +47 -0
- package/src/live-tokens/data/sketch-styles/dry.json +47 -0
- package/src/live-tokens/data/sketch-styles/hatched.json +47 -0
- package/src/live-tokens/data/sketch-styles/marker.json +47 -0
- package/src/live-tokens/data/sketch-styles/napkin.json +47 -0
- package/src/live-tokens/data/sketch-styles/pencil.json +47 -0
- package/src/live-tokens/data/sketch-styles/whiteboard.json +47 -0
- package/src/live-tokens/data/themes/autumn.json +4 -4
- package/src/live-tokens/data/themes/halloween.json +4 -4
- package/src/live-tokens/data/themes/midnight-study.json +4 -4
- package/src/live-tokens/data/themes/ocean.json +4 -4
- package/src/live-tokens/data/themes/royal-velvet.json +4 -4
- package/src/live-tokens/data/themes/sketchy.json +4 -4
- package/src/live-tokens/data/themes/spring-meadow.json +4 -4
- package/src/live-tokens/data/themes/sunset.json +4 -4
- package/src/system/components/Button.svelte +2 -2
- package/src/system/components/IconButton.svelte +2 -2
- package/src/system/styles/fonts.css +6 -6
- package/template/src/main.ts +14 -1
|
@@ -2,9 +2,11 @@ import { derived, get, writable } from 'svelte/store';
|
|
|
2
2
|
import {
|
|
3
3
|
SKETCH_STYLES,
|
|
4
4
|
DEFAULT_SKETCH_STYLE,
|
|
5
|
+
THEME_SKETCH_ID,
|
|
5
6
|
hydrateSketchStyle,
|
|
6
7
|
type SketchStyle,
|
|
7
8
|
} from './sketchStyles';
|
|
9
|
+
import { lookById, replaceRegisteredLooks, sketchLooks } from './sketchRegistry';
|
|
8
10
|
import {
|
|
9
11
|
applySketchLayer,
|
|
10
12
|
hostRoot,
|
|
@@ -101,15 +103,21 @@ function readBaseline(): SketchStyle | null {
|
|
|
101
103
|
return null;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
/** Retired. Saved sketchstyles and shipped ones share one id namespace now, so
|
|
107
|
+
a file named `pencil` replaces the shipped Pencil rather than sitting beside
|
|
108
|
+
it. Stripped on read below; delete a release after that ships. */
|
|
109
|
+
const RETIRED_USER_PREFIX = 'user:';
|
|
107
110
|
|
|
111
|
+
/** Deliberately unvalidated. Looks are registered after this module is
|
|
112
|
+
imported, so an id it has never heard of is the normal case rather than a
|
|
113
|
+
fault: `selectSketchStyle` no-ops on one, and `sketchPick` already reports
|
|
114
|
+
a look nothing names as `adjusted`. Only a browser that has stored nothing
|
|
115
|
+
falls back. */
|
|
108
116
|
function readStyleName(): string {
|
|
109
117
|
try {
|
|
110
118
|
const name = localStorage.getItem(STYLE_NAME_KEY);
|
|
111
|
-
if (name
|
|
112
|
-
return name;
|
|
119
|
+
if (name !== null) {
|
|
120
|
+
return name.startsWith(RETIRED_USER_PREFIX) ? name.slice(RETIRED_USER_PREFIX.length) : name;
|
|
113
121
|
}
|
|
114
122
|
} catch {
|
|
115
123
|
// fall through
|
|
@@ -122,8 +130,9 @@ function readStyleName(): string {
|
|
|
122
130
|
export const sketchEnabled = writable<boolean>(readEnabled());
|
|
123
131
|
export const sketchSettings = writable<SketchStyle>(readSettings());
|
|
124
132
|
/** The sketchstyle the dials started from. It survives dial moves, so the grid
|
|
125
|
-
keeps showing what the current look is closest to
|
|
126
|
-
|
|
133
|
+
keeps showing what the current look is closest to. Any id in the pool, or
|
|
134
|
+
`THEME_SKETCH_ID` for the look the open theme carries; empty only when
|
|
135
|
+
nothing was picked, or the picked file was deleted. */
|
|
127
136
|
export const sketchStyleName = writable<string>(readStyleName());
|
|
128
137
|
|
|
129
138
|
/** The settings as the selected sketchstyle defined them. Kept beside the live
|
|
@@ -134,7 +143,7 @@ export const sketchBaseline = writable<SketchStyle | null>(readBaseline());
|
|
|
134
143
|
|
|
135
144
|
/** Dial-set fields only. `label` and `blurb` name the sketchstyle rather than
|
|
136
145
|
describe the look, and no dial writes them. */
|
|
137
|
-
function sameLook(a: SketchStyle, b: SketchStyle): boolean {
|
|
146
|
+
export function sameLook(a: SketchStyle, b: SketchStyle): boolean {
|
|
138
147
|
return (Object.keys(a) as (keyof SketchStyle)[])
|
|
139
148
|
.filter((k) => k !== 'label' && k !== 'blurb')
|
|
140
149
|
.every((k) => a[k] === b[k]);
|
|
@@ -170,9 +179,9 @@ export const themeSketchStyle = writable<SketchStyle | undefined>(undefined);
|
|
|
170
179
|
|
|
171
180
|
/** Open a theme's sketchstyle: the dials, the on/off state, and the name
|
|
172
181
|
recovered by comparison (RJC 3). Overwrites the live buffer, which
|
|
173
|
-
is what opening a theme means everywhere else (RJC 6).
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
is what opening a theme means everywhere else (RJC 6). The name is recovered
|
|
183
|
+
over the whole pool, so a theme carrying a look a saved file also holds is
|
|
184
|
+
named by that file rather than falling back to `THEME_SKETCH_ID`. */
|
|
176
185
|
export function openThemeSketchStyle(sketchStyle: SketchStyle | undefined): void {
|
|
177
186
|
themeSketchStyle.set(sketchStyle);
|
|
178
187
|
if (!sketchStyle) {
|
|
@@ -181,13 +190,56 @@ export function openThemeSketchStyle(sketchStyle: SketchStyle | undefined): void
|
|
|
181
190
|
sketchStyleName.set('');
|
|
182
191
|
return;
|
|
183
192
|
}
|
|
184
|
-
const matched = (
|
|
193
|
+
const matched = get(sketchLooks).find((look) => sameLook(look.settings, sketchStyle))?.id;
|
|
185
194
|
sketchSettings.set({ ...sketchStyle });
|
|
186
195
|
sketchBaseline.set({ ...sketchStyle });
|
|
187
|
-
sketchStyleName.set(matched ??
|
|
196
|
+
sketchStyleName.set(matched ?? THEME_SKETCH_ID);
|
|
188
197
|
sketchEnabled.set(true);
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Take the sketchstyle a theme carries as this browser's own, unless this
|
|
202
|
+
* browser has already decided for itself.
|
|
203
|
+
*
|
|
204
|
+
* The rule boot has always followed in dev, and the only one a built site has:
|
|
205
|
+
* a visitor who picked a look, or picked None, keeps it, and `themeSketchStyle`
|
|
206
|
+
* still learns what the theme holds so the panel can call the difference
|
|
207
|
+
* unsaved. Both branches set it, so a picker can offer the theme's look as a
|
|
208
|
+
* row either way.
|
|
209
|
+
*
|
|
210
|
+
* Takes the raw field rather than a `SketchStyle`, and hydrates it here: a
|
|
211
|
+
* built site reads its theme JSON straight off disk with no dev server to run
|
|
212
|
+
* `normalizeTheme` over it first, so this is the only place a look stored under
|
|
213
|
+
* a retired dial name gets carried forward. Anything that is not an object is
|
|
214
|
+
* the absent case, which is off (invariant 3).
|
|
215
|
+
*/
|
|
216
|
+
export function seedSketchFromTheme(sketchStyle: unknown): void {
|
|
217
|
+
const style =
|
|
218
|
+
typeof sketchStyle === 'object' && sketchStyle !== null && !Array.isArray(sketchStyle)
|
|
219
|
+
? hydrateSketchStyle(sketchStyle)
|
|
220
|
+
: undefined;
|
|
221
|
+
if (hasPersistedSketchState()) {
|
|
222
|
+
themeSketchStyle.set(style);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
openThemeSketchStyle(style);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Go back to the look the theme carries, after picking something else. The
|
|
229
|
+
theme's is the one look a picker can offer that this module did not ship, so
|
|
230
|
+
it needs a door of its own beside `selectSketchStyle`; `setSketch` gives the
|
|
231
|
+
two the same face. Silent when the theme carries none, the way
|
|
232
|
+
`selectSketchStyle` is for a name it does not know. */
|
|
233
|
+
export function selectThemeSketchStyle(): void {
|
|
234
|
+
const style = get(themeSketchStyle);
|
|
235
|
+
if (!style) return;
|
|
236
|
+
markSketchTouched();
|
|
237
|
+
if (get(sketchEnabled)) liveMovedSinceBake.set(true);
|
|
238
|
+
sketchStyleName.set(THEME_SKETCH_ID);
|
|
239
|
+
sketchBaseline.set({ ...style });
|
|
240
|
+
sketchSettings.set({ ...style });
|
|
241
|
+
}
|
|
242
|
+
|
|
191
243
|
/** The live sketch differs from what the open theme carries. Presence is
|
|
192
244
|
half the comparison: on with dials the theme does not hold, or off while
|
|
193
245
|
the theme holds a layer, are both off the theme. */
|
|
@@ -201,14 +253,17 @@ export const sketchOffLook = derived(
|
|
|
201
253
|
},
|
|
202
254
|
);
|
|
203
255
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
256
|
+
/** Takes any id in the pool: shipped, or registered from a file or a consumer.
|
|
257
|
+
Silent for an id nothing knows, the way it has always been for an unknown
|
|
258
|
+
shipped name. */
|
|
259
|
+
export function selectSketchStyle(id: string): void {
|
|
260
|
+
const look = lookById(id);
|
|
261
|
+
if (!look) return;
|
|
207
262
|
markSketchTouched();
|
|
208
263
|
if (get(sketchEnabled)) liveMovedSinceBake.set(true);
|
|
209
|
-
sketchStyleName.set(
|
|
210
|
-
sketchBaseline.set({ ...
|
|
211
|
-
sketchSettings.set({ ...
|
|
264
|
+
sketchStyleName.set(id);
|
|
265
|
+
sketchBaseline.set({ ...look.settings });
|
|
266
|
+
sketchSettings.set({ ...look.settings });
|
|
212
267
|
}
|
|
213
268
|
|
|
214
269
|
/** Saved sketchstyles, listed from the data tree. Empty until
|
|
@@ -216,17 +271,22 @@ export function selectSketchStyle(name: string): void {
|
|
|
216
271
|
the network. */
|
|
217
272
|
export const savedSketchStyles = writable<SketchStyleMeta[]>([]);
|
|
218
273
|
|
|
274
|
+
/** Lists the files and registers them in one gesture, so the editor's grid and
|
|
275
|
+
a built site's picker read the same pool. Loading every file to list them is
|
|
276
|
+
affordable: a sketchstyle is a few dozen numbers, and the alternative is a
|
|
277
|
+
grid that cannot paint a row until it is picked. */
|
|
219
278
|
export async function refreshSavedSketchStyles(): Promise<void> {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
const files = await listSketchStyles();
|
|
280
|
+
const loaded = await Promise.all(files.map((f) => loadSketchStyle(f.fileName)));
|
|
281
|
+
savedSketchStyles.set(files);
|
|
282
|
+
replaceRegisteredLooks(
|
|
283
|
+
files.map((file, i) => ({
|
|
284
|
+
id: file.fileName,
|
|
285
|
+
label: file.name || file.fileName,
|
|
286
|
+
settings: loaded[i].settings,
|
|
287
|
+
source: file.isPackage ? ('shipped' as const) : ('file' as const),
|
|
288
|
+
})),
|
|
289
|
+
);
|
|
230
290
|
}
|
|
231
291
|
|
|
232
292
|
/** Writes whatever the dials currently say to a named file and selects it, so
|
|
@@ -243,15 +303,34 @@ export async function saveCurrentSketchStyle(name: string): Promise<string> {
|
|
|
243
303
|
await refreshSavedSketchStyles();
|
|
244
304
|
sketchSettings.set(settings);
|
|
245
305
|
sketchBaseline.set({ ...settings });
|
|
246
|
-
sketchStyleName.set(
|
|
306
|
+
sketchStyleName.set(fileName);
|
|
247
307
|
return fileName;
|
|
248
308
|
}
|
|
249
309
|
|
|
310
|
+
/** Overwrite the selected saved sketchstyle. The file name comes from the
|
|
311
|
+
selection rather than from re-slugifying the label, because the two can
|
|
312
|
+
disagree: a file hand-edited to a new display name would otherwise be saved
|
|
313
|
+
beside itself under a fresh slug instead of over itself. The label comes
|
|
314
|
+
from the look for the same reason, so the name the grid shows survives.
|
|
315
|
+
|
|
316
|
+
No `markSketchTouched`: the button only lights once a dial has moved, and
|
|
317
|
+
every dial goes through `updateSketchSettings`, which marks it. */
|
|
318
|
+
export async function saveSelectedSketchStyle(): Promise<void> {
|
|
319
|
+
const look = lookById(get(sketchStyleName));
|
|
320
|
+
if (look?.source !== 'file') throw new Error('No saved sketchstyle is selected');
|
|
321
|
+
const settings = { ...get(sketchSettings) };
|
|
322
|
+
await saveSketchStyle(look.id, look.label, settings);
|
|
323
|
+
await refreshSavedSketchStyles();
|
|
324
|
+
// Re-baselining is what disables the button again and returns the readout
|
|
325
|
+
// from "Modified from X" to the saved blurb.
|
|
326
|
+
sketchBaseline.set(settings);
|
|
327
|
+
}
|
|
328
|
+
|
|
250
329
|
export async function deleteSavedSketchStyle(fileName: string): Promise<void> {
|
|
251
330
|
await deleteSketchStyle(fileName);
|
|
252
331
|
await refreshSavedSketchStyles();
|
|
253
332
|
// The dials keep their values; only the name stops naming a file that exists.
|
|
254
|
-
if (get(sketchStyleName) ===
|
|
333
|
+
if (get(sketchStyleName) === fileName) {
|
|
255
334
|
sketchStyleName.set('');
|
|
256
335
|
sketchBaseline.set(null);
|
|
257
336
|
}
|
|
@@ -12,6 +12,9 @@ export interface SketchStyleMeta {
|
|
|
12
12
|
name: string;
|
|
13
13
|
fileName: string;
|
|
14
14
|
updatedAt: string;
|
|
15
|
+
/** Served from the package rather than this project, so there is no local
|
|
16
|
+
file to write over or delete. Saving over one creates that file. */
|
|
17
|
+
isPackage: boolean;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const BASE = `${API_BASE}/sketch-styles`;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import pencil from '../../../live-tokens/data/sketch-styles/pencil.json';
|
|
2
|
+
import marker from '../../../live-tokens/data/sketch-styles/marker.json';
|
|
3
|
+
import whiteboard from '../../../live-tokens/data/sketch-styles/whiteboard.json';
|
|
4
|
+
import hatched from '../../../live-tokens/data/sketch-styles/hatched.json';
|
|
5
|
+
import dashed from '../../../live-tokens/data/sketch-styles/dashed.json';
|
|
6
|
+
import napkin from '../../../live-tokens/data/sketch-styles/napkin.json';
|
|
7
|
+
import dry from '../../../live-tokens/data/sketch-styles/dry.json';
|
|
8
|
+
|
|
1
9
|
export interface SketchStyle {
|
|
2
10
|
label: string;
|
|
3
11
|
blurb: string;
|
|
@@ -59,8 +67,22 @@ export interface SketchStyle {
|
|
|
59
67
|
hatchInk: number;
|
|
60
68
|
strokeStyle: 'solid' | 'dashed';
|
|
61
69
|
maskOn: boolean;
|
|
62
|
-
/**
|
|
63
|
-
|
|
70
|
+
/** Blob size of the coverage noise, in page px, across and down. Small gives
|
|
71
|
+
speckle, large gives broad patches. */
|
|
72
|
+
maskBlobX: number;
|
|
73
|
+
maskBlobY: number;
|
|
74
|
+
/** Which way the stretch runs, in degrees clockwise from level. Nothing at
|
|
75
|
+
all while the two blob sizes match, since a field the same in every
|
|
76
|
+
direction is the same field turned. The tile only meets itself at the
|
|
77
|
+
angles that land the page's axes back on whole cells, so the dial is
|
|
78
|
+
answered with the nearest of those. */
|
|
79
|
+
maskAngle: number;
|
|
80
|
+
/** Whether the two move together. Unlinked they part, and the field comes out
|
|
81
|
+
stretched: blobs wider than they are tall read as a wash dragged sideways,
|
|
82
|
+
the way ink pulled across a page does. Stored rather than inferred from
|
|
83
|
+
the pair matching, so a look that stretches to exactly square keeps its
|
|
84
|
+
dials apart. */
|
|
85
|
+
maskBlobLinked: boolean;
|
|
64
86
|
/** Output levels on the coverage field, 0 to 1: the palest the fill gets and
|
|
65
87
|
the densest. The whole field is squeezed into the gap, never cut at it, so
|
|
66
88
|
0.4 to 1 is a fill that is never thinner than 40% ink and 0 to 0.8 one
|
|
@@ -119,103 +141,34 @@ export interface SketchStyle {
|
|
|
119
141
|
iconMaskScale: number;
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
pencil: {
|
|
138
|
-
...base, label: 'Pencil',
|
|
139
|
-
blurb: 'Two graphite passes on their own seeds, so the outline disagrees with itself the way a hand coming back round does. Tight grain, little else.',
|
|
140
|
-
fillTravel: 0.75, strokeTravel: 1.25, wobble: 30, roughness: 3, waveform: 1,
|
|
141
|
-
strokeWidth: 1.25, doubleStroke: true, retracePass: 'reseeded', retraceOffset: 1.5, strokeInk: 0.85,
|
|
142
|
-
maskBlob: 40, maskOutputMin: 0.62, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 1, maskSoftness: 0,
|
|
143
|
-
jitterX: 1.5, jitterY: 1.5, jitterRot: 0.35, jitterScale: 0.022,
|
|
144
|
-
cornerSpread: 6, cornerTravel: 4.5,
|
|
145
|
-
pressure: 0.15, pressureMod: 0.3, pooling: 0, iconTravel: 0.75,
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
marker: {
|
|
149
|
-
...base, label: 'Marker',
|
|
150
|
-
blurb: 'Broad translucent nib gone round twice on the same line, so the overlap darkens and the ink pools where it slows.',
|
|
151
|
-
fillTravel: 2, strokeTravel: 1.5, wobble: 56, waveform: 1.4, borderWavelength: 1.3,
|
|
152
|
-
strokeWidth: 4, doubleStroke: true, retracePass: 'copy', strokeInk: 0.52, retraceOffset: 2.2,
|
|
153
|
-
maskBlob: 115, maskOutputMin: 0.42, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 4, maskSoftness: 4,
|
|
154
|
-
jitterX: 3, jitterY: 3, jitterRot: 0.8, jitterScale: 0.045,
|
|
155
|
-
cornerSpread: 10, cornerTravel: 8,
|
|
156
|
-
pressure: 0.2, pressureMod: 0.25, pooling: 2, iconTravel: 1.25, iconMaskScale: 4,
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
whiteboard: {
|
|
160
|
-
...base, label: 'Whiteboard',
|
|
161
|
-
blurb: 'The fattest nib on glass. One long smooth undulation, and a veined mask that streaks the fill like a half-wiped board.',
|
|
162
|
-
fillTravel: 2.5, strokeTravel: 2.5, wobble: 90, roughness: 1, waveform: 1, borderWavelength: 1.5,
|
|
163
|
-
strokeWidth: 5.5, doubleStroke: true, retracePass: 'copy', strokeInk: 0.66, retraceOffset: 3,
|
|
164
|
-
maskGrain: 'turbulence', maskBlob: 180, maskOutputMin: 0.42, maskOutputMax: 1,
|
|
165
|
-
maskOctaves: 1, maskPosterize: 3, maskSoftness: 8,
|
|
166
|
-
jitterX: 4.5, jitterY: 4.5, jitterRot: 1.2, jitterScale: 0.07,
|
|
167
|
-
cornerSpread: 14, cornerTravel: 11,
|
|
168
|
-
pressure: 0.15, pressureMod: 0.15, pooling: 3.5, iconTravel: 1.75, iconMaskScale: 1.5,
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
hatched: {
|
|
172
|
-
...base, label: 'Hatched',
|
|
173
|
-
blurb: 'An etching. The fill is angled shading, the outline a single hard-edged scratch that chatters along its length. No mask: the hatch is the texture.',
|
|
174
|
-
fillTravel: 1.25, strokeTravel: 1.5, wobble: 24, roughness: 3, waveform: 3,
|
|
175
|
-
strokeWidth: 1.5, fillStyle: 'hatched', hatchInk: 0.5, doubleStroke: false,
|
|
176
|
-
maskOn: false, iconMaskOn: false,
|
|
177
|
-
jitterX: 1.5, jitterY: 1.5, jitterRot: 0.4, jitterScale: 0.03,
|
|
178
|
-
cornerSpread: 6, cornerTravel: 6,
|
|
179
|
-
pressure: 0.3, pressureMod: 0.45, pooling: 0.8, iconTravel: 1.25, iconWavelength: 0.5,
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
dashed: {
|
|
183
|
-
...base, label: 'Dashed',
|
|
184
|
-
blurb: 'A drafting outline. One slow drift along the ruler, broken into strokes, with jitter, mask and pressure all off. The clean pole.',
|
|
185
|
-
strokeStyle: 'dashed', strokeTravel: 1, fillTravel: 0.5, wobble: 120, roughness: 1, waveform: 1,
|
|
186
|
-
strokeWidth: 1.5, doubleStroke: false,
|
|
187
|
-
maskOn: false, iconMaskOn: false,
|
|
188
|
-
jitterX: 0, jitterY: 0, jitterRot: 0, jitterScale: 0,
|
|
189
|
-
cornerSpread: 4, cornerTravel: 3,
|
|
190
|
-
pressure: 0, pressureMod: 0, pooling: 0, iconTravel: 0,
|
|
191
|
-
},
|
|
192
|
-
|
|
193
|
-
napkin: {
|
|
194
|
-
...base, label: 'Napkin',
|
|
195
|
-
blurb: 'Ballpoint in a hurry. Everything loose at once: a square wave sends every edge to full travel, and the second pass lands wherever it lands.',
|
|
196
|
-
fillTravel: 3, strokeTravel: 2.25, wobble: 50, roughness: 3, waveform: 2.5,
|
|
197
|
-
strokeWidth: 2.25, doubleStroke: true, retracePass: 'reseeded', retraceOffset: 4, strokeInk: 1,
|
|
198
|
-
maskBlob: 150, maskOutputMin: 0.43, maskOutputMax: 0.95, maskOctaves: 2, maskPosterize: 2, maskSoftness: 10,
|
|
199
|
-
jitterX: 6, jitterY: 6, jitterRot: 1.8, jitterScale: 0.1,
|
|
200
|
-
cornerSpread: 20, cornerTravel: 17,
|
|
201
|
-
pressure: 0.45, pressureMod: 0.6, pooling: 2.5, iconTravel: 2.25, iconMaskScale: 3.6,
|
|
202
|
-
},
|
|
144
|
+
/**
|
|
145
|
+
* The shipped sketchstyles, read from the files the package distributes. The
|
|
146
|
+
* files are the source: a project shadows one by saving a sketchstyle under the
|
|
147
|
+
* same id, the editor restores a shipped look by deleting that file, and
|
|
148
|
+
* `themeFileApi` serves these as the read-only fallback behind the project's own
|
|
149
|
+
* directory. Editing a look here means editing its JSON, which is what the
|
|
150
|
+
* Sketchstyle view already writes.
|
|
151
|
+
*
|
|
152
|
+
* Each file carries every dial, so there is nothing to merge a default into.
|
|
153
|
+
* `sketchStyles.test.ts` pins that: the seven key sets have to match, and a dial
|
|
154
|
+
* added to `SketchStyle` has to reach all seven before the suite goes green.
|
|
155
|
+
*
|
|
156
|
+
* Order is picker order.
|
|
157
|
+
*/
|
|
158
|
+
const SHIPPED_FILES = { pencil, marker, whiteboard, hatched, dashed, napkin, dry };
|
|
203
159
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
fillTravel: 2.25, strokeTravel: 1.75, wobble: 50, waveform: 2, borderWavelength: 0.5,
|
|
208
|
-
strokeWidth: 3.5, doubleStroke: false, strokeInk: 0.4,
|
|
209
|
-
maskBlob: 150, maskOutputMin: 0.24, maskOutputMax: 0.91,
|
|
210
|
-
maskOctaves: 2, maskPosterize: 4, maskSoftness: 1.75,
|
|
211
|
-
jitterX: 5, jitterY: 5, jitterRot: 1.4, jitterScale: 0.08,
|
|
212
|
-
cornerSpread: 16, cornerTravel: 13,
|
|
213
|
-
pressure: 0.4, pressureMod: 0.8, pooling: 1.5, iconTravel: 1.75, iconMaskScale: 3.8,
|
|
214
|
-
},
|
|
215
|
-
};
|
|
160
|
+
export const SKETCH_STYLES: Record<string, SketchStyle> = Object.fromEntries(
|
|
161
|
+
Object.entries(SHIPPED_FILES).map(([id, file]) => [id, file.settings as unknown as SketchStyle]),
|
|
162
|
+
);
|
|
216
163
|
|
|
217
164
|
export const DEFAULT_SKETCH_STYLE = 'marker';
|
|
218
165
|
|
|
166
|
+
/** The id of the look a theme carries, in the same id namespace as the shipped
|
|
167
|
+
sketchstyles so one picker row and one `setSketch` call cover both. Never a
|
|
168
|
+
key of `SKETCH_STYLES`: a shipped style claiming it would shadow the theme's
|
|
169
|
+
own look in every picker. `index.test.ts` pins that. */
|
|
170
|
+
export const THEME_SKETCH_ID = 'theme';
|
|
171
|
+
|
|
219
172
|
/** Reconciled against a full sketchstyle in both directions: a value stored before a
|
|
220
173
|
control existed picks up the default, and a value stored for a control since
|
|
221
174
|
retired is dropped. Without the drop, a stale key survives every spread and
|
|
@@ -233,6 +186,7 @@ export function hydrateSketchStyle(raw: unknown): SketchStyle {
|
|
|
233
186
|
convertCutToLevels(stored as Record<string, unknown>, out);
|
|
234
187
|
carryLevelsToOutput(stored as Record<string, unknown>, out);
|
|
235
188
|
halveSwingDials(stored as Record<string, unknown>, out);
|
|
189
|
+
splitBlobAxes(stored as Record<string, unknown>, out);
|
|
236
190
|
convertCyclesToWavelength(stored as Record<string, unknown>, out);
|
|
237
191
|
convertIconTileToScale(stored as Record<string, unknown>, out);
|
|
238
192
|
restoreDerivedRetrace(stored as Record<string, unknown>, out);
|
|
@@ -280,6 +234,16 @@ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchS
|
|
|
280
234
|
if (typeof layers === 'number') out.roughness = Math.min(3, Math.max(1, layers));
|
|
281
235
|
}
|
|
282
236
|
|
|
237
|
+
/** The blob size was one number for both axes. A look stored before the split
|
|
238
|
+
comes back square, with the two dials linked, which is the look it had. */
|
|
239
|
+
function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyle): void {
|
|
240
|
+
const blob = stored.maskBlob;
|
|
241
|
+
if (typeof blob !== 'number') return;
|
|
242
|
+
out.maskBlobX = blob;
|
|
243
|
+
out.maskBlobY = blob;
|
|
244
|
+
out.maskBlobLinked = true;
|
|
245
|
+
}
|
|
246
|
+
|
|
283
247
|
/** The icon mask tile used to be a px size, which is a unit the glyph it covers
|
|
284
248
|
has no say in: 90px against a 16px icon put a whole glyph inside one patch
|
|
285
249
|
of the field, so it came out either untouched or gone. It is a share of the
|
|
@@ -305,7 +269,10 @@ function convertTiledMask(stored: Record<string, unknown>, out: SketchStyle): vo
|
|
|
305
269
|
const scale = stored.maskScale;
|
|
306
270
|
if (typeof scale !== 'number') return;
|
|
307
271
|
const freq = stored.maskFrequency;
|
|
308
|
-
if (typeof freq === 'number')
|
|
272
|
+
if (typeof freq === 'number') {
|
|
273
|
+
out.maskBlobX = Math.round(scale / (freq * LEGACY_TILE));
|
|
274
|
+
out.maskBlobY = out.maskBlobX;
|
|
275
|
+
}
|
|
309
276
|
const soft = stored.maskSoftness;
|
|
310
277
|
if (typeof soft === 'number') out.maskSoftness = Number(((soft * scale) / LEGACY_TILE).toFixed(1));
|
|
311
278
|
}
|
|
@@ -5,7 +5,7 @@ import { loadFromFile, seedComponentsFromApi } from '../store/editorStore';
|
|
|
5
5
|
import { getActiveComponentConfig, type ComponentSummary } from '../components/componentConfigService';
|
|
6
6
|
import { safeFetch } from '../storage/storage';
|
|
7
7
|
import { API_BASE } from '../storage/apiBase';
|
|
8
|
-
import {
|
|
8
|
+
import { seedSketchFromTheme } from '../sketch/sketchStore';
|
|
9
9
|
|
|
10
10
|
interface ListComponentsDto {
|
|
11
11
|
components: ComponentSummary[];
|
|
@@ -67,16 +67,7 @@ export async function initializeTheme(): Promise<void> {
|
|
|
67
67
|
// A failed fetch is not "the theme carries no sketchstyle": treating null as
|
|
68
68
|
// absent would tell the panel the look is off the theme, or hand a fresh
|
|
69
69
|
// browser a blank buffer, over a fetch that will likely succeed next time.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// the theme holds so unsaved dial work reads as unsaved rather than
|
|
74
|
-
// getting silently overwritten (that overwrite is what Apply is for).
|
|
75
|
-
themeSketchStyle.set(active.sketchStyle);
|
|
76
|
-
} else {
|
|
77
|
-
// Nothing was ever recorded in this browser: the theme's value becomes
|
|
78
|
-
// the live value, the same reconciliation opening a theme performs.
|
|
79
|
-
openThemeSketchStyle(active.sketchStyle);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
70
|
+
// The same call a built site makes (`@motion-proto/live-tokens/sketch`), so
|
|
71
|
+
// one rule decides what a theme's sketchstyle means at boot in both.
|
|
72
|
+
if (active) seedSketchFromTheme(active.sketchStyle);
|
|
82
73
|
}
|
|
@@ -6,7 +6,7 @@ they are drawn with.
|
|
|
6
6
|
|
|
7
7
|
It is an effect layer, not a set of token values. It never touches a token
|
|
8
8
|
itself, so turning it off returns every component to exactly what its tokens
|
|
9
|
-
already say.
|
|
9
|
+
already say.
|
|
10
10
|
|
|
11
11
|
Open the **Sketchstyle** view in the editor and switch **Sketch mode** on. The effect
|
|
12
12
|
applies to the page behind the editor as well as to the preview, so what you see
|
|
@@ -38,10 +38,22 @@ than just a name:
|
|
|
38
38
|
- **Napkin.** Ballpoint in a hurry. Everything loose at once.
|
|
39
39
|
- **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
All seven ship as files, one per look, under
|
|
42
|
+
`src/live-tokens/data/sketch-styles/` in the package. There is no look that
|
|
43
|
+
exists only as code, so every one of them can be read, copied and edited.
|
|
44
|
+
|
|
45
|
+
Pick one, then move whatever you like. **Save As** keeps your dials under a name
|
|
46
|
+
of your own, alongside the shipped seven, as a file under
|
|
47
|
+
`src/live-tokens/data/sketch-styles/` in your project. **Save** writes them back
|
|
48
|
+
over the sketchstyle you have selected, and lights as soon as the dials leave
|
|
49
|
+
it. On one of your own it writes that file. On a shipped one it writes your
|
|
50
|
+
project's own copy under the same name, which takes its place in the list;
|
|
51
|
+
delete that copy and the shipped file behind it comes back. Both are a
|
|
52
|
+
different gesture from saving a theme; see "Where the settings live" below.
|
|
53
|
+
|
|
54
|
+
Your sketchstyles and the shipped ones are one list. A sketchstyle named after
|
|
55
|
+
a shipped one replaces it, keeping its place in the list, so a project that
|
|
56
|
+
wants its own Pencil saves one and every picker shows that one instead.
|
|
45
57
|
|
|
46
58
|
## The dials
|
|
47
59
|
|
|
@@ -50,8 +62,11 @@ saving a theme; see "Where the settings live" below.
|
|
|
50
62
|
few pixels off or runs it through the pen again on its own seed.
|
|
51
63
|
- **Fill.** Solid or hatched, how far the fill's edge travels, and how far each
|
|
52
64
|
instance is offset, rotated and scaled from its neighbours. **Ink coverage**
|
|
53
|
-
thins the fill with a field of blotches: set their size and
|
|
54
|
-
detail, then work the field as a levels control. The
|
|
65
|
+
thins the fill with a field of blotches: set their size across and down and
|
|
66
|
+
how many levels of detail, then work the field as a levels control. The two
|
|
67
|
+
sizes move together under a chain; break it and the blotches stretch, which
|
|
68
|
+
reads as ink dragged along the axis you widened, and a **Rotation** dial joins
|
|
69
|
+
them to point that stretch anywhere you like. The field always runs black
|
|
55
70
|
to white whatever the noise underneath. Steps flattens it into tones, and
|
|
56
71
|
Output squeezes the whole of it into the range the ink covers, from how pale
|
|
57
72
|
it gets at its thinnest to how dense at its fullest. Menus and tooltips are
|
|
@@ -81,14 +96,68 @@ change. The built-in **Motion Proto** theme is read-only, so Save
|
|
|
81
96
|
is disabled there; use **Save As** to fold the dials into a theme of your
|
|
82
97
|
own.
|
|
83
98
|
|
|
84
|
-
**Save
|
|
85
|
-
|
|
86
|
-
can pick from any theme.
|
|
99
|
+
**Save** and **Save As** in the **Sketchstyle** view are a different gesture.
|
|
100
|
+
They write a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a look
|
|
101
|
+
you can pick from any theme. Neither touches the open theme, and neither marks
|
|
87
102
|
the look off the theme.
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
## Shipping the layer
|
|
105
|
+
|
|
106
|
+
The dev server reads the open theme and paints whatever it carries. A built site
|
|
107
|
+
has no server to ask, so it hands the field over itself:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { seedSketchFromTheme } from '@motion-proto/live-tokens/sketch';
|
|
111
|
+
import theme from './live-tokens/data/themes/sketchy.json';
|
|
112
|
+
|
|
113
|
+
seedSketchFromTheme(theme.sketchStyle);
|
|
114
|
+
await bootLiveTokens(App, '#app');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Call it before mounting, so the look is up on the first frame. Pass the field
|
|
118
|
+
raw. A theme written against older dial names is carried forward on the way in,
|
|
119
|
+
the same reconciliation the dev server runs on every theme it reads.
|
|
120
|
+
|
|
121
|
+
Nothing is baked. `tokens.generated.css` still holds token values only, and the
|
|
122
|
+
layer stays JavaScript the page runs, because it builds an SVG filter bank
|
|
123
|
+
rather than a set of custom properties.
|
|
124
|
+
|
|
125
|
+
A visitor who has picked a look of their own keeps it, None included. The theme
|
|
126
|
+
seeds a browser that has decided nothing and never overwrites one that has, so
|
|
127
|
+
calling this on every boot is safe.
|
|
128
|
+
|
|
129
|
+
### Your own sketchstyles
|
|
130
|
+
|
|
131
|
+
The dev server lists the files in `sketch-styles/`. A built site has no server
|
|
132
|
+
to ask, so it hands them over at boot, the way it hands over components:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const files = import.meta.glob<{ name?: string; settings: unknown }>(
|
|
136
|
+
'./live-tokens/data/sketch-styles/*.json',
|
|
137
|
+
{ eager: true, import: 'default' },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await bootLiveTokens(App, '#app', {
|
|
141
|
+
sketchLooks: Object.entries(files).map(([path, file]) => {
|
|
142
|
+
const id = path.split('/').pop()!.replace('.json', '');
|
|
143
|
+
return { id, label: file.name || id, settings: file.settings };
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Projects made with `create` ship this already. The file's slug is the look's
|
|
149
|
+
id, so a sketchstyle picked in the editor keeps working once the site is built.
|
|
150
|
+
|
|
151
|
+
### Building a picker
|
|
152
|
+
|
|
153
|
+
`sketchLooks` is every look on offer, shipped and your own, as a store. Give
|
|
154
|
+
each row `setSketch(look.id)`, and add your own **None** row: off is a state of
|
|
155
|
+
the effect rather than one of the looks.
|
|
156
|
+
|
|
157
|
+
`themeSketchLook` is the theme's own look as one more row. It is null when the
|
|
158
|
+
theme carries none, and null when what it carries is a look already in
|
|
159
|
+
`sketchLooks`, since that row names it. Its id goes to `setSketch` like any
|
|
160
|
+
other, so a visitor who wanders off the theme's look can come back to it.
|
|
92
161
|
|
|
93
162
|
## Drawing your own elements
|
|
94
163
|
|