@motion-proto/live-tokens 0.66.0 → 0.67.1

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 (39) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +67 -9
  3. package/dist-plugin/{chunk-NRONZ3T5.js → chunk-J2JT4UEA.js} +225 -119
  4. package/dist-plugin/index.cjs +258 -134
  5. package/dist-plugin/index.js +27 -9
  6. package/dist-plugin/migrateData/index.cjs +225 -119
  7. package/dist-plugin/migrateData/index.js +1 -1
  8. package/package.json +8 -1
  9. package/src/editor/bootstrap.ts +12 -0
  10. package/src/editor/core/productionPulse.ts +1 -1
  11. package/src/editor/core/sketch/index.ts +23 -27
  12. package/src/editor/core/sketch/maskField.ts +200 -62
  13. package/src/editor/core/sketch/sketchLayer.ts +10 -4
  14. package/src/editor/core/sketch/sketchRegistry.ts +98 -0
  15. package/src/editor/core/sketch/sketchStore.ts +64 -33
  16. package/src/editor/core/sketch/sketchStyleService.ts +3 -0
  17. package/src/editor/core/sketch/sketchStyles.ts +57 -96
  18. package/src/editor/docs/content/sketch-mode.md +56 -14
  19. package/src/editor/docs/content.generated.ts +1 -1
  20. package/src/editor/ui/sketch/SketchTab.svelte +219 -74
  21. package/src/live-tokens/data/sketch-styles/dashed.json +47 -0
  22. package/src/live-tokens/data/sketch-styles/dry.json +47 -0
  23. package/src/live-tokens/data/sketch-styles/hatched.json +47 -0
  24. package/src/live-tokens/data/sketch-styles/marker.json +47 -0
  25. package/src/live-tokens/data/sketch-styles/napkin.json +47 -0
  26. package/src/live-tokens/data/sketch-styles/pencil.json +47 -0
  27. package/src/live-tokens/data/sketch-styles/whiteboard.json +47 -0
  28. package/src/live-tokens/data/themes/autumn.json +4 -4
  29. package/src/live-tokens/data/themes/halloween.json +4 -4
  30. package/src/live-tokens/data/themes/midnight-study.json +4 -4
  31. package/src/live-tokens/data/themes/ocean.json +4 -4
  32. package/src/live-tokens/data/themes/royal-velvet.json +4 -4
  33. package/src/live-tokens/data/themes/sketchy.json +4 -4
  34. package/src/live-tokens/data/themes/spring-meadow.json +4 -4
  35. package/src/live-tokens/data/themes/sunset.json +4 -4
  36. package/src/system/components/Button.svelte +2 -2
  37. package/src/system/components/IconButton.svelte +2 -2
  38. package/src/system/styles/fonts.css +6 -6
  39. package/template/src/main.ts +14 -1
@@ -4,13 +4,12 @@
4
4
  import UISegmentedControl from '../UISegmentedControl.svelte';
5
5
  import SketchDial from './SketchDial.svelte';
6
6
  import SketchRange from './SketchRange.svelte';
7
- import { buildMaskUri } from '../../core/sketch/maskField';
7
+ import { buildMaskUri, maskLattice } from '../../core/sketch/maskField';
8
8
  import SketchPreview from './SketchPreview.svelte';
9
9
  import { onMount } from 'svelte';
10
10
  import UIPillButton from '../UIPillButton.svelte';
11
11
  import UIReveal, { REVEAL_MS } from '../UIReveal.svelte';
12
12
  import { scrollSectionIntoView } from '../scrollSection';
13
- import { SKETCH_STYLES } from '../../core/sketch/sketchStyles';
14
13
  import { openThemeSlug } from '../../core/store/editorConfigStore';
15
14
  import {
16
15
  sketchEnabled,
@@ -21,13 +20,12 @@
21
20
  sketchSettings,
22
21
  selectSketchStyle,
23
22
  updateSketchSettings,
24
- savedSketchStyles,
25
23
  refreshSavedSketchStyles,
26
- selectSavedSketchStyle,
27
24
  saveCurrentSketchStyle,
25
+ saveSelectedSketchStyle,
28
26
  deleteSavedSketchStyle,
29
- USER_STYLE_PREFIX,
30
27
  } from '../../core/sketch/sketchStore';
28
+ import { sketchLooks } from '../../core/sketch/sketchRegistry';
31
29
 
32
30
  interface Props {
33
31
  /** The rail's last jump. A fresh object per click, not a bare id, so
@@ -100,6 +98,13 @@
100
98
 
101
99
  const layers = (n: number) => (n === 1 ? '1 layer' : `${n} layers`);
102
100
 
101
+ /** One number while the blob dials are linked, both once they are not: a
102
+ stretched field is the thing you would open the section to check. */
103
+ const blobSummary = () =>
104
+ s.maskBlobX === s.maskBlobY
105
+ ? `${s.maskBlobX}px`
106
+ : `${s.maskBlobX}×${s.maskBlobY}px${blobAngle === 0 ? '' : ` at ${blobAngle}°`}`;
107
+
103
108
  /** Trigger-row summaries: the settings you would open the section to check. */
104
109
  let summary = $derived({
105
110
  border: `${s.strokeWidth}px · ${s.strokeStyle} · ${passLabel(s.doubleStroke)}`
@@ -107,7 +112,7 @@
107
112
  + (s.borderWavelength === 1 ? '' : ` · ${against(s.borderWavelength)} wave`)
108
113
  + (s.strokeInk < 1 ? ` · ${Math.round(s.strokeInk * 100)}% ink` : ''),
109
114
  fill: `${s.fillStyle} · ${s.jitterX === 0 && s.jitterY === 0 && s.jitterRot === 0 ? 'aligned' : `±${s.jitterX}, ${s.jitterY}px`}`
110
- + (s.maskOn ? ` · ${s.maskBlob}px · ${layers(s.maskOctaves)}` : ' · no mask'),
115
+ + (s.maskOn ? ` · ${blobSummary()} · ${layers(s.maskOctaves)}` : ' · no mask'),
111
116
  shape: s.cornerSpread === 0 && s.cornerTravel === 0
112
117
  ? 'true to the theme'
113
118
  : `±${s.cornerSpread}px corners · ${px(s.cornerTravel)} travel`,
@@ -130,6 +135,34 @@
130
135
  : { name: s.label, blurb: s.blurb || 'Your saved sketchstyle.' },
131
136
  );
132
137
 
138
+ let selectedLook = $derived($sketchLooks.find((l) => l.id === $sketchStyleName));
139
+ /** Offered whenever the dials have moved off the sketchstyle they name. A
140
+ look backed by a project file is written over. A shipped one's file lives
141
+ in the package, so saving it writes this project's own under the same
142
+ name, which is how a project comes to own its Pencil: the id is the same,
143
+ so the new file takes the shipped one's place in the grid rather than
144
+ sitting beside it. Nothing is lost either way, since deleting that file
145
+ uncovers the packaged one again. */
146
+ let canSave = $derived(!!selectedLook && $sketchDirty);
147
+ let saveTitle = $derived(
148
+ !selectedLook
149
+ ? 'Pick a sketchstyle to save over'
150
+ : !$sketchDirty
151
+ ? 'No changes to save'
152
+ : selectedLook.source === 'file'
153
+ ? `Save over ${selectedLook.label}`
154
+ : `Keep these dials as this project's ${selectedLook.label}`,
155
+ );
156
+
157
+ function saveInPlace() {
158
+ if (!selectedLook) return;
159
+ runStyleAction(
160
+ selectedLook.source === 'file'
161
+ ? saveSelectedSketchStyle()
162
+ : saveCurrentSketchStyle(selectedLook.label),
163
+ );
164
+ }
165
+
133
166
  let naming = $state(false);
134
167
  let draftName = $state('');
135
168
  let styleError = $state('');
@@ -141,7 +174,7 @@
141
174
  });
142
175
 
143
176
  function startNaming() {
144
- draftName = s.label && $sketchStyleName.startsWith(USER_STYLE_PREFIX) ? s.label : '';
177
+ draftName = '';
145
178
  styleError = '';
146
179
  naming = true;
147
180
  }
@@ -194,11 +227,11 @@
194
227
 
195
228
  // The mask's dials are the stages the field is built in, in the order they
196
229
  // run: make the noise, level it, soften it.
197
- /** Blobs much bigger than a component stop reading as coverage: one patch
198
- spans a whole button, so it is either untouched or gone entirely. The
199
- ceiling is a few blobs across a card, which is where it still reads as
200
- uneven ink. */
201
- const SIZE = { min: 20, max: 250 };
230
+ /** The field is painted at whatever the dials say now, rather than fitted to
231
+ a fixed tile, so the travel is only bounded by what still reads: speckle
232
+ finer than 8px is a texture rather than coverage, and a blob past 600px
233
+ covers a whole card in one patch, which the stretched end wants. */
234
+ const SIZE = { min: 8, max: 600 };
202
235
  const BLUR_MAX = 64;
203
236
  /** Squared response. A blur only reads as a different blur while it is small,
204
237
  so a linear walk to 64px would spend a tenth of the dial on everything
@@ -220,9 +253,36 @@
220
253
  const logVal = (t: number, r: { min: number; max: number }) =>
221
254
  r.min * Math.pow(r.max / r.min, Math.min(1, Math.max(0, t)));
222
255
 
223
- let scalePos = $derived(logPos(s.maskBlob, SIZE));
224
- const setScale = (v: number) =>
225
- updateSketchSettings({ maskBlob: Math.round(logVal(v / 100, SIZE) / 5) * 5 });
256
+ let scaleXPos = $derived(logPos(s.maskBlobX, SIZE));
257
+ let scaleYPos = $derived(logPos(s.maskBlobY, SIZE));
258
+ /** Rounded to a whole px, and to fives above a hundred, where a single px is
259
+ under half a percent of the blob and only makes the readout twitch. */
260
+ const scaleVal = (t: number) => {
261
+ const px = logVal(t / 100, SIZE);
262
+ return px < 100 ? Math.round(px) : Math.round(px / 5) * 5;
263
+ };
264
+ const setScaleX = (v: number) => {
265
+ const maskBlobX = scaleVal(v);
266
+ updateSketchSettings(s.maskBlobLinked ? { maskBlobX, maskBlobY: maskBlobX } : { maskBlobX });
267
+ };
268
+ const setScaleY = (v: number) => {
269
+ const maskBlobY = scaleVal(v);
270
+ updateSketchSettings(s.maskBlobLinked ? { maskBlobX: maskBlobY, maskBlobY } : { maskBlobY });
271
+ };
272
+ /** The turn the tile can actually hold, which is not always the one asked
273
+ for: only the angles that land the page's axes back on whole noise cells
274
+ leave a tile that meets itself. */
275
+ let blobAngle = $derived(Math.round(maskLattice(s).angle));
276
+
277
+ /** Linking again squares the field off on the width, which is the axis the
278
+ pair reads from left to right. Leaving them apart would make the chain a
279
+ claim the dials contradict. */
280
+ const toggleBlobLink = () =>
281
+ updateSketchSettings(
282
+ s.maskBlobLinked
283
+ ? { maskBlobLinked: false }
284
+ : { maskBlobLinked: true, maskBlobY: s.maskBlobX },
285
+ );
226
286
  let outputMin = $derived(Math.round(s.maskOutputMin * 100));
227
287
  let outputMax = $derived(Math.round(s.maskOutputMax * 100));
228
288
  let stepPos = $derived(Math.max(0, STEP_TONES.indexOf(s.maskPosterize)));
@@ -285,25 +345,49 @@
285
345
  </div>
286
346
 
287
347
  <div class="presets">
288
- {#each Object.entries(SKETCH_STYLES) as [name, style] (name)}
289
- <label class="preset">
290
- <input
291
- type="radio"
292
- name={STYLE_RADIO_GROUP}
293
- value={name}
294
- checked={$sketchStyleName === name}
295
- onchange={() => selectSketchStyle(name)}
296
- />
297
- <span class="preset-name">{style.label}</span>
298
- </label>
348
+ {#each $sketchLooks as look (look.id)}
349
+ <div class="saved-item">
350
+ <label class="preset">
351
+ <input
352
+ type="radio"
353
+ name={STYLE_RADIO_GROUP}
354
+ value={look.id}
355
+ checked={$sketchStyleName === look.id}
356
+ onchange={() => selectSketchStyle(look.id)}
357
+ />
358
+ <span class="preset-name">{look.label}</span>
359
+ </label>
360
+ {#if look.source === 'file'}
361
+ <button
362
+ type="button"
363
+ class="saved-delete"
364
+ title="Delete {look.label}"
365
+ aria-label="Delete {look.label}"
366
+ onclick={() => runStyleAction(deleteSavedSketchStyle(look.id))}
367
+ ><i class="fas fa-xmark"></i></button>
368
+ {/if}
369
+ </div>
299
370
  {/each}
300
371
  </div>
301
372
 
302
373
  <div class="saved">
303
- <div class="saved-head">
304
- <span class="band-label">Saved sketchstyles</span>
305
- <UIPillButton size="compact" icon="fa-floppy-disk" onclick={startNaming}>
306
- Save as sketchstyle…
374
+ <div class="saved-actions">
375
+ <UIPillButton
376
+ size="compact"
377
+ icon="fa-floppy-disk"
378
+ disabled={!canSave}
379
+ title={saveTitle}
380
+ onclick={saveInPlace}
381
+ >
382
+ Save
383
+ </UIPillButton>
384
+ <UIPillButton
385
+ size="compact"
386
+ icon="fa-plus"
387
+ title="Keep these dials under a name of your own"
388
+ onclick={startNaming}
389
+ >
390
+ Save As
307
391
  </UIPillButton>
308
392
  </div>
309
393
 
@@ -322,32 +406,6 @@
322
406
  </form>
323
407
  {/if}
324
408
 
325
- {#if $savedSketchStyles.length > 0}
326
- <div class="presets">
327
- {#each $savedSketchStyles as saved (saved.fileName)}
328
- <div class="saved-item">
329
- <label class="preset">
330
- <input
331
- type="radio"
332
- name={STYLE_RADIO_GROUP}
333
- value={USER_STYLE_PREFIX + saved.fileName}
334
- checked={$sketchStyleName === USER_STYLE_PREFIX + saved.fileName}
335
- onchange={() => runStyleAction(selectSavedSketchStyle(saved.fileName))}
336
- />
337
- <span class="preset-name">{saved.name}</span>
338
- </label>
339
- <button
340
- type="button"
341
- class="saved-delete"
342
- title="Delete {saved.name}"
343
- aria-label="Delete {saved.name}"
344
- onclick={() => runStyleAction(deleteSavedSketchStyle(saved.fileName))}
345
- ><i class="fas fa-xmark"></i></button>
346
- </div>
347
- {/each}
348
- </div>
349
- {/if}
350
-
351
409
  {#if styleError}
352
410
  <p class="saved-error">{styleError}</p>
353
411
  {/if}
@@ -577,13 +635,44 @@
577
635
  onchange={(v) => updateSketchSettings({ maskGrain: v as 'fractal' | 'turbulence' })}
578
636
  />
579
637
  </div>
580
- <SketchDial
581
- label="Scale" value={scalePos} min={0} max={100} step={1}
582
- readout={`${s.maskBlob}px`}
583
- ends={['speckle', 'patches']}
584
- hint="Blob size on the page. Small reads as speckle, large as patches."
585
- onchange={setScale}
586
- />
638
+ <div class="pair" class:linked={s.maskBlobLinked}>
639
+ <button
640
+ type="button"
641
+ class="pair-link"
642
+ aria-pressed={s.maskBlobLinked}
643
+ aria-label={s.maskBlobLinked ? 'Unlink the two scales' : 'Link the two scales'}
644
+ title={s.maskBlobLinked
645
+ ? 'Both axes move together. Click to scale them on their own.'
646
+ : 'Each axis on its own. Click to move them together.'}
647
+ onclick={toggleBlobLink}
648
+ >
649
+ <i class="fas" class:fa-link={s.maskBlobLinked} class:fa-link-slash={!s.maskBlobLinked}
650
+ aria-hidden="true"></i>
651
+ </button>
652
+ <div class="pair-dials">
653
+ <SketchDial
654
+ label="Scale across" value={scaleXPos} min={0} max={100} step={1}
655
+ readout={`${s.maskBlobX}px`}
656
+ hint="Blob width on the page. Small reads as speckle, large as patches."
657
+ onchange={setScaleX}
658
+ />
659
+ <SketchDial
660
+ label="Scale down" value={scaleYPos} min={0} max={100} step={1}
661
+ readout={`${s.maskBlobY}px`}
662
+ ends={['speckle', 'patches']}
663
+ hint="Blob height. Unlink the two to stretch the field: wide and short reads as ink dragged sideways."
664
+ onchange={setScaleY}
665
+ />
666
+ </div>
667
+ </div>
668
+ {#if !s.maskBlobLinked}
669
+ <SketchDial
670
+ label="Rotation" value={s.maskAngle} min={0} max={180} step={1}
671
+ readout={`${blobAngle}°`}
672
+ hint="Which way the stretch runs. The tile has to meet itself, so the turn lands on the nearest angle that does."
673
+ onchange={(v) => updateSketchSettings({ maskAngle: v })}
674
+ />
675
+ {/if}
587
676
  <SketchDial
588
677
  label="Detail" value={s.maskOctaves} min={1} max={4} step={1}
589
678
  readout={layers(s.maskOctaves)}
@@ -916,20 +1005,13 @@
916
1005
  border-top: 1px solid var(--ui-border-low);
917
1006
  }
918
1007
 
919
- .saved-head {
1008
+ .saved-actions {
920
1009
  display: flex;
921
1010
  align-items: center;
922
- justify-content: space-between;
923
- gap: var(--ui-space-8);
1011
+ gap: var(--ui-space-6);
924
1012
  min-height: 1.75rem;
925
1013
  }
926
1014
 
927
- .band-label {
928
- font-size: var(--ui-font-size-xs);
929
- font-weight: var(--ui-font-weight-semibold);
930
- color: var(--ui-text-tertiary);
931
- }
932
-
933
1015
  .save-row {
934
1016
  display: flex;
935
1017
  align-items: center;
@@ -953,8 +1035,8 @@
953
1035
  outline-offset: 1px;
954
1036
  }
955
1037
 
956
- /* The row is one control with a delete affordance, so the border lives here
957
- and the inner .preset label drops its own. */
1038
+ /* A row is one control, with a delete affordance on the ones this project
1039
+ owns, so the border lives here and the inner .preset label drops its own. */
958
1040
  .saved-item {
959
1041
  display: flex;
960
1042
  align-items: stretch;
@@ -1155,6 +1237,69 @@
1155
1237
  background: var(--ui-border-high);
1156
1238
  }
1157
1239
 
1240
+ /* Two dials under one chain: the bracket is drawn on the gutter, and the
1241
+ button sits on top of it with a punched-out background so the line reads as
1242
+ running behind the glyph rather than into it. */
1243
+ .pair {
1244
+ display: grid;
1245
+ grid-template-columns: var(--ui-space-24) 1fr;
1246
+ gap: var(--ui-space-10);
1247
+ }
1248
+
1249
+ .pair-dials {
1250
+ display: grid;
1251
+ gap: var(--ui-space-12);
1252
+ }
1253
+
1254
+ .pair-link {
1255
+ position: relative;
1256
+ align-self: center;
1257
+ justify-self: stretch;
1258
+ display: flex;
1259
+ align-items: center;
1260
+ justify-content: center;
1261
+ padding: 0;
1262
+ height: 100%;
1263
+ background: none;
1264
+ border: none;
1265
+ color: var(--ui-text-tertiary);
1266
+ cursor: pointer;
1267
+ }
1268
+
1269
+ /* The bracket. Right-hand side open, so it reaches for the dials it holds. */
1270
+ .pair-link::before {
1271
+ content: '';
1272
+ position: absolute;
1273
+ inset: var(--ui-space-8) 0 var(--ui-space-8) var(--ui-space-4);
1274
+ border: 1px solid var(--ui-border);
1275
+ border-right: none;
1276
+ border-radius: var(--ui-radius-sm) 0 0 var(--ui-radius-sm);
1277
+ }
1278
+
1279
+ .pair-link i {
1280
+ position: relative;
1281
+ padding: var(--ui-space-4) 0;
1282
+ background: var(--ui-surface-low);
1283
+ font-size: var(--ui-font-size-sm);
1284
+ }
1285
+
1286
+ .pair-link:hover,
1287
+ .pair-link:focus-visible {
1288
+ color: var(--ui-text-primary);
1289
+ }
1290
+
1291
+ /* Broken chain, broken bracket: the two dials are no longer held together. */
1292
+ .pair:not(.linked) .pair-link::before {
1293
+ border-color: var(--ui-border-low);
1294
+ border-style: dashed;
1295
+ }
1296
+
1297
+ .pair-link:focus-visible {
1298
+ outline: 2px solid var(--ui-highlight);
1299
+ outline-offset: 2px;
1300
+ border-radius: var(--ui-radius-sm);
1301
+ }
1302
+
1158
1303
  .phase h5 {
1159
1304
  margin: var(--ui-space-8) 0 0;
1160
1305
  font-size: var(--ui-font-size-xs);
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "Dashed",
3
+ "settings": {
4
+ "label": "Dashed",
5
+ "blurb": "A drafting outline. One slow drift along the ruler, broken into strokes, with jitter, mask and pressure all off. The clean pole.",
6
+ "fillTravel": 0.5,
7
+ "strokeTravel": 1,
8
+ "wobble": 120,
9
+ "roughness": 1,
10
+ "borderWavelength": 1,
11
+ "waveform": 1,
12
+ "strokeWidth": 1.5,
13
+ "doubleStroke": false,
14
+ "retraceOffset": 1.2,
15
+ "retracePass": "copy",
16
+ "fillStyle": "solid",
17
+ "hatchInk": 0.4,
18
+ "strokeStyle": "dashed",
19
+ "pressure": 0,
20
+ "pressureMod": 0,
21
+ "pooling": 0,
22
+ "strokeInk": 1,
23
+ "maskOn": false,
24
+ "maskBlobX": 100,
25
+ "maskBlobY": 100,
26
+ "maskBlobLinked": true,
27
+ "maskAngle": 0,
28
+ "maskOutputMin": 0.45,
29
+ "maskOutputMax": 1,
30
+ "maskOctaves": 2,
31
+ "maskGrain": "fractal",
32
+ "maskPosterize": 1,
33
+ "maskSoftness": 1.5,
34
+ "jitterX": 0,
35
+ "jitterY": 0,
36
+ "jitterRot": 0,
37
+ "jitterScale": 0,
38
+ "cornerSpread": 4,
39
+ "cornerTravel": 3,
40
+ "iconTravel": 0,
41
+ "iconWavelength": 0.625,
42
+ "iconMaskOn": false,
43
+ "iconMaskScale": 1
44
+ },
45
+ "updatedAt": "2026-08-28T15:47:36.934Z",
46
+ "createdAt": "2026-08-28T15:47:36.934Z"
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "Dry marker",
3
+ "settings": {
4
+ "label": "Dry marker",
5
+ "blurb": "Ink that ran out. One scratchy pass that breaks up along its length, over a fill the mask has worn nearly through in patches.",
6
+ "fillTravel": 2.25,
7
+ "strokeTravel": 1.75,
8
+ "wobble": 50,
9
+ "roughness": 3,
10
+ "borderWavelength": 0.5,
11
+ "waveform": 2,
12
+ "strokeWidth": 3.5,
13
+ "doubleStroke": false,
14
+ "retraceOffset": 1.2,
15
+ "retracePass": "copy",
16
+ "fillStyle": "solid",
17
+ "hatchInk": 0.4,
18
+ "strokeStyle": "solid",
19
+ "pressure": 0.4,
20
+ "pressureMod": 0.8,
21
+ "pooling": 1.5,
22
+ "strokeInk": 0.4,
23
+ "maskOn": true,
24
+ "maskBlobX": 150,
25
+ "maskBlobY": 150,
26
+ "maskBlobLinked": true,
27
+ "maskAngle": 0,
28
+ "maskOutputMin": 0.24,
29
+ "maskOutputMax": 0.91,
30
+ "maskOctaves": 2,
31
+ "maskGrain": "fractal",
32
+ "maskPosterize": 4,
33
+ "maskSoftness": 1.75,
34
+ "jitterX": 5,
35
+ "jitterY": 5,
36
+ "jitterRot": 1.4,
37
+ "jitterScale": 0.08,
38
+ "cornerSpread": 16,
39
+ "cornerTravel": 13,
40
+ "iconTravel": 1.75,
41
+ "iconWavelength": 0.625,
42
+ "iconMaskOn": true,
43
+ "iconMaskScale": 3.8
44
+ },
45
+ "updatedAt": "2026-08-28T15:47:36.934Z",
46
+ "createdAt": "2026-08-28T15:47:36.934Z"
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "Hatched",
3
+ "settings": {
4
+ "label": "Hatched",
5
+ "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.",
6
+ "fillTravel": 1.25,
7
+ "strokeTravel": 1.5,
8
+ "wobble": 24,
9
+ "roughness": 3,
10
+ "borderWavelength": 1,
11
+ "waveform": 3,
12
+ "strokeWidth": 1.5,
13
+ "doubleStroke": false,
14
+ "retraceOffset": 1.2,
15
+ "retracePass": "copy",
16
+ "fillStyle": "hatched",
17
+ "hatchInk": 0.5,
18
+ "strokeStyle": "solid",
19
+ "pressure": 0.3,
20
+ "pressureMod": 0.45,
21
+ "pooling": 0.8,
22
+ "strokeInk": 1,
23
+ "maskOn": false,
24
+ "maskBlobX": 100,
25
+ "maskBlobY": 100,
26
+ "maskBlobLinked": true,
27
+ "maskAngle": 0,
28
+ "maskOutputMin": 0.45,
29
+ "maskOutputMax": 1,
30
+ "maskOctaves": 2,
31
+ "maskGrain": "fractal",
32
+ "maskPosterize": 1,
33
+ "maskSoftness": 1.5,
34
+ "jitterX": 1.5,
35
+ "jitterY": 1.5,
36
+ "jitterRot": 0.4,
37
+ "jitterScale": 0.03,
38
+ "cornerSpread": 6,
39
+ "cornerTravel": 6,
40
+ "iconTravel": 1.25,
41
+ "iconWavelength": 0.5,
42
+ "iconMaskOn": false,
43
+ "iconMaskScale": 1
44
+ },
45
+ "updatedAt": "2026-08-28T15:47:36.934Z",
46
+ "createdAt": "2026-08-28T15:47:36.934Z"
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "Marker",
3
+ "settings": {
4
+ "label": "Marker",
5
+ "blurb": "Broad translucent nib gone round twice on the same line, so the overlap darkens and the ink pools where it slows.",
6
+ "fillTravel": 2,
7
+ "strokeTravel": 1.5,
8
+ "wobble": 56,
9
+ "roughness": 3,
10
+ "borderWavelength": 1.3,
11
+ "waveform": 1.4,
12
+ "strokeWidth": 4,
13
+ "doubleStroke": true,
14
+ "retraceOffset": 2.2,
15
+ "retracePass": "copy",
16
+ "fillStyle": "solid",
17
+ "hatchInk": 0.4,
18
+ "strokeStyle": "solid",
19
+ "pressure": 0.2,
20
+ "pressureMod": 0.25,
21
+ "pooling": 2,
22
+ "strokeInk": 0.52,
23
+ "maskOn": true,
24
+ "maskBlobX": 230,
25
+ "maskBlobY": 230,
26
+ "maskBlobLinked": true,
27
+ "maskAngle": 0,
28
+ "maskOutputMin": 0.76,
29
+ "maskOutputMax": 1,
30
+ "maskOctaves": 3,
31
+ "maskGrain": "fractal",
32
+ "maskPosterize": 4,
33
+ "maskSoftness": 0,
34
+ "jitterX": 3,
35
+ "jitterY": 3,
36
+ "jitterRot": 0.8,
37
+ "jitterScale": 0.045,
38
+ "cornerSpread": 10,
39
+ "cornerTravel": 8,
40
+ "iconTravel": 1.25,
41
+ "iconWavelength": 0.625,
42
+ "iconMaskOn": true,
43
+ "iconMaskScale": 4
44
+ },
45
+ "updatedAt": "2026-08-28T14:45:04.324Z",
46
+ "createdAt": "2026-08-28T14:45:04.324Z"
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "Napkin",
3
+ "settings": {
4
+ "label": "Napkin",
5
+ "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.",
6
+ "fillTravel": 3,
7
+ "strokeTravel": 2.25,
8
+ "wobble": 50,
9
+ "roughness": 3,
10
+ "borderWavelength": 1,
11
+ "waveform": 2.5,
12
+ "strokeWidth": 2.25,
13
+ "doubleStroke": true,
14
+ "retraceOffset": 4,
15
+ "retracePass": "reseeded",
16
+ "fillStyle": "solid",
17
+ "hatchInk": 0.4,
18
+ "strokeStyle": "solid",
19
+ "pressure": 0.45,
20
+ "pressureMod": 0.6,
21
+ "pooling": 2.5,
22
+ "strokeInk": 1,
23
+ "maskOn": true,
24
+ "maskBlobX": 150,
25
+ "maskBlobY": 150,
26
+ "maskBlobLinked": true,
27
+ "maskAngle": 0,
28
+ "maskOutputMin": 0.62,
29
+ "maskOutputMax": 1,
30
+ "maskOctaves": 2,
31
+ "maskGrain": "fractal",
32
+ "maskPosterize": 2,
33
+ "maskSoftness": 5,
34
+ "jitterX": 6,
35
+ "jitterY": 6,
36
+ "jitterRot": 1.8,
37
+ "jitterScale": 0.075,
38
+ "cornerSpread": 20,
39
+ "cornerTravel": 17,
40
+ "iconTravel": 2.25,
41
+ "iconWavelength": 0.625,
42
+ "iconMaskOn": true,
43
+ "iconMaskScale": 3.6
44
+ },
45
+ "updatedAt": "2026-08-28T15:47:36.934Z",
46
+ "createdAt": "2026-08-28T15:47:36.934Z"
47
+ }