@motion-proto/live-tokens 0.66.0 → 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 +132 -0
- package/dist-plugin/{chunk-NRONZ3T5.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 +23 -27
- 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 +64 -33
- package/src/editor/core/sketch/sketchStyleService.ts +3 -0
- package/src/editor/core/sketch/sketchStyles.ts +57 -96
- package/src/editor/docs/content/sketch-mode.md +56 -14
- 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
|
@@ -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 ? ` · ${
|
|
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 =
|
|
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
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const SIZE = { min:
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
289
|
-
<
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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-
|
|
304
|
-
<
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
<
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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-
|
|
1008
|
+
.saved-actions {
|
|
920
1009
|
display: flex;
|
|
921
1010
|
align-items: center;
|
|
922
|
-
|
|
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
|
-
/*
|
|
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.43,
|
|
29
|
+
"maskOutputMax": 0.95,
|
|
30
|
+
"maskOctaves": 2,
|
|
31
|
+
"maskGrain": "fractal",
|
|
32
|
+
"maskPosterize": 2,
|
|
33
|
+
"maskSoftness": 10,
|
|
34
|
+
"jitterX": 6,
|
|
35
|
+
"jitterY": 6,
|
|
36
|
+
"jitterRot": 1.8,
|
|
37
|
+
"jitterScale": 0.1,
|
|
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
|
+
}
|