@pixerate/editor-svelte 0.79.1 → 0.80.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.
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BLEND_MENU_GROUPS,
|
|
4
|
+
BRUSH_ROTATIONS,
|
|
5
|
+
BRUSH_SLIDERS,
|
|
6
|
+
brushEngineControls,
|
|
7
|
+
brushLoadColor,
|
|
8
|
+
downloadBytes,
|
|
9
|
+
multiColorSliders,
|
|
10
|
+
patchBrushValue,
|
|
11
|
+
patchPath,
|
|
12
|
+
readPath,
|
|
13
|
+
type BlendMode,
|
|
14
|
+
type BrushRotation,
|
|
15
|
+
type MultiColorMode,
|
|
16
|
+
type PaintEditorController,
|
|
17
|
+
} from '@pixerate/editor/paint-editor';
|
|
3
18
|
import { untrack } from 'svelte';
|
|
4
19
|
import PaintSlider from './PaintSlider.svelte';
|
|
5
20
|
import PixelCanvas from './PixelCanvas.svelte';
|
|
@@ -20,15 +35,23 @@
|
|
|
20
35
|
let fileInput = $state<HTMLInputElement | null>(null);
|
|
21
36
|
const run = (fn: () => unknown) => attempt(fn, (m) => (error = m));
|
|
22
37
|
// Thumbnails follow the library and the paper.
|
|
38
|
+
// Separate deriveds keep their identity across unrelated changes, so thumbnails are not redrawn.
|
|
39
|
+
const library = $derived(snap.brushes);
|
|
40
|
+
const paper = $derived(snap.document.paper);
|
|
23
41
|
const thumbs = $derived.by(() => {
|
|
24
|
-
const list =
|
|
25
|
-
void
|
|
42
|
+
const list = library;
|
|
43
|
+
void paper;
|
|
26
44
|
return untrack(() => new Map(list.map((b) => [b.id, controller.brushThumbnail(b.id, { width: thumbnailWidth, height: thumbnailHeight, color: '#26324a' })])));
|
|
27
45
|
});
|
|
46
|
+
const engine = $derived(brushEngineControls(brush));
|
|
47
|
+
const mc = $derived(brush.behavior.multicolor);
|
|
48
|
+
const carried = $derived(brushLoadColor(snap.mixerLoad));
|
|
49
|
+
const mixingValue = $derived(engine?.mixingPath === 'blender.mixing' ? brush.blender.mixing : engine?.mixingPath === 'smudge.mixing' ? brush.smudge.mixing : brush.behavior.mixing);
|
|
50
|
+
const setColors = (colors: string[]) => controller.setBrush({ behavior: { multicolor: { colors } } });
|
|
28
51
|
const slug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'brush';
|
|
29
52
|
</script>
|
|
30
53
|
|
|
31
|
-
<!-- The brush library with thumbnails, the active brush's settings, save, import and export. -->
|
|
54
|
+
<!-- The brush library with thumbnails, the active brush's settings and blend, its engine's controls (blender, smudge, pick-up / mix, mixing, dirty, multi-colour, load), save, import and export. -->
|
|
32
55
|
<div class={`${panel} ${className}`} data-testid="brush-panel">
|
|
33
56
|
<div class="grid grid-cols-2 gap-1" role="listbox" aria-label="Brushes">
|
|
34
57
|
{#each snap.brushes as b (b.id)}
|
|
@@ -63,7 +86,91 @@
|
|
|
63
86
|
<label class="flex items-center gap-2">
|
|
64
87
|
<input type="checkbox" checked={brush.tip.pressureOpacity} onchange={(e) => controller.setBrush({ tip: { pressureOpacity: e.currentTarget.checked } })} /> Pressure → opacity
|
|
65
88
|
</label>
|
|
89
|
+
{#if brush.engine === 'dab'}
|
|
90
|
+
<label class="flex items-center justify-between gap-2">
|
|
91
|
+
<span class="text-slate-400">Blend</span>
|
|
92
|
+
<select aria-label="Brush blend" value={brush.tip.blend} onchange={(e) => controller.setBrush({ tip: { blend: e.currentTarget.value as BlendMode } })} class="rounded bg-slate-800 px-1 py-0.5">
|
|
93
|
+
{#each BLEND_MENU_GROUPS as g (g.label)}
|
|
94
|
+
<optgroup label={g.label}>
|
|
95
|
+
{#each g.modes as m (m.id)}
|
|
96
|
+
<option value={m.id} title={m.description}>{m.label}</option>
|
|
97
|
+
{/each}
|
|
98
|
+
</optgroup>
|
|
99
|
+
{/each}
|
|
100
|
+
</select>
|
|
101
|
+
</label>
|
|
102
|
+
{/if}
|
|
66
103
|
</div>
|
|
104
|
+
{#if engine}
|
|
105
|
+
<div class="flex flex-col gap-1.5" data-testid="brush-engine">
|
|
106
|
+
<span class="font-medium text-slate-300">{engine.title}</span>
|
|
107
|
+
{#each engine.sliders as c (c.path)}
|
|
108
|
+
<PaintSlider control={c} value={readPath(brush, c.path)} onchange={(v) => controller.setBrush(patchPath(brush as never, c.path, v) as never)} />
|
|
109
|
+
{/each}
|
|
110
|
+
{#if engine.mixingPath}
|
|
111
|
+
<label class="flex items-center justify-between gap-2">
|
|
112
|
+
<span class="text-slate-400">Mixing</span>
|
|
113
|
+
<select aria-label="Brush mixing" value={mixingValue} onchange={(e) => controller.setBrush(patchBrushValue(brush, engine.mixingPath!, e.currentTarget.value))} class="rounded bg-slate-800 px-1 py-0.5">
|
|
114
|
+
<option value="pigment">Pigment</option>
|
|
115
|
+
<option value="rgb">RGB</option>
|
|
116
|
+
</select>
|
|
117
|
+
</label>
|
|
118
|
+
{/if}
|
|
119
|
+
{#if engine.dirty}
|
|
120
|
+
<label class="flex items-center gap-2" title="Keep the colour the brush picked up for the next stroke (needs Pick-up above 0)">
|
|
121
|
+
<input type="checkbox" aria-label="Dirty brush" checked={brush.behavior.dirty} onchange={(e) => controller.setBrush({ behavior: { dirty: e.currentTarget.checked } })} /> Dirty brush
|
|
122
|
+
</label>
|
|
123
|
+
{/if}
|
|
124
|
+
{#if engine.multicolor}
|
|
125
|
+
<div class="flex flex-col gap-1.5" data-testid="multicolor">
|
|
126
|
+
<label class="flex items-center justify-between gap-2">
|
|
127
|
+
<span class="text-slate-400">Multi-colour</span>
|
|
128
|
+
<select aria-label="Multi-colour" value={mc.mode} onchange={(e) => controller.setBrush({ behavior: { multicolor: { mode: e.currentTarget.value as MultiColorMode } } })} class="rounded bg-slate-800 px-1 py-0.5">
|
|
129
|
+
<option value="off">Off</option>
|
|
130
|
+
<option value="tip">Across the tip</option>
|
|
131
|
+
<option value="dab">Per dab</option>
|
|
132
|
+
</select>
|
|
133
|
+
</label>
|
|
134
|
+
{#if mc.mode !== 'off'}
|
|
135
|
+
<div class="flex flex-wrap items-center gap-1" aria-label="Multi-colour colours">
|
|
136
|
+
{#each mc.colors as col, i (i)}
|
|
137
|
+
<span class="flex items-center">
|
|
138
|
+
<input type="color" aria-label={`Multi-colour ${i + 1}`} value={col} onchange={(e) => setColors(mc.colors.map((x, j) => (j === i ? e.currentTarget.value : x)))} class="h-5 w-6 rounded bg-transparent" />
|
|
139
|
+
<button type="button" aria-label={`Remove multi-colour ${i + 1}`} class="px-0.5 text-slate-500 hover:text-white" onclick={() => setColors(mc.colors.filter((_, j) => j !== i))}>×</button>
|
|
140
|
+
</span>
|
|
141
|
+
{/each}
|
|
142
|
+
{#if mc.colors.length < 8}
|
|
143
|
+
<button type="button" class={button} aria-label="Add multi-colour" onclick={() => setColors([...(mc.colors.length ? mc.colors : [snap.color, snap.secondaryColor]), snap.color].slice(0, 8))}>+</button>
|
|
144
|
+
{/if}
|
|
145
|
+
{#if mc.colors.length < 2}<span class="text-[10px] text-slate-500">Uses the paint and second colours</span>{/if}
|
|
146
|
+
</div>
|
|
147
|
+
{#if mc.mode === 'dab'}
|
|
148
|
+
<label class="flex items-center justify-between gap-2">
|
|
149
|
+
<span class="text-slate-400">Order</span>
|
|
150
|
+
<select aria-label="Colour order" value={mc.order} onchange={(e) => controller.setBrush({ behavior: { multicolor: { order: e.currentTarget.value as 'sequence' | 'random' } } })} class="rounded bg-slate-800 px-1 py-0.5">
|
|
151
|
+
<option value="sequence">In order</option>
|
|
152
|
+
<option value="random">Random</option>
|
|
153
|
+
</select>
|
|
154
|
+
</label>
|
|
155
|
+
{/if}
|
|
156
|
+
{#each multiColorSliders(mc.mode) as c (c.path)}
|
|
157
|
+
<PaintSlider control={c} value={readPath(brush, c.path)} onchange={(v) => controller.setBrush(patchPath(brush as never, c.path, v) as never)} />
|
|
158
|
+
{/each}
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
{#if engine.load}
|
|
163
|
+
<div class="flex items-center gap-1">
|
|
164
|
+
<span class="text-slate-400">Carries</span>
|
|
165
|
+
<span aria-label="Brush load" title={carried ?? 'Clean'} class="h-4 w-4 rounded ring-1 ring-slate-700" style={`background: ${carried ?? 'transparent'}`}></span>
|
|
166
|
+
{#if brush.engine === 'mixer'}
|
|
167
|
+
<button type="button" class={button} onclick={() => controller.loadMixerBrush()}>Load</button>
|
|
168
|
+
{/if}
|
|
169
|
+
<button type="button" class={button} onclick={() => controller.cleanMixerBrush(brush.engine === 'mixer')}>Clean</button>
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
67
174
|
<div class="flex flex-wrap items-center gap-1">
|
|
68
175
|
<input bind:value={saveName} placeholder="Brush name" aria-label="Brush name" class="w-28 rounded bg-slate-800 px-1.5 py-0.5" />
|
|
69
176
|
<button
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrushPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/paint-editor/BrushPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"BrushPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/paint-editor/BrushPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAcH,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAOvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuKH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
HARMONY_KINDS,
|
|
4
4
|
MIXING_PAD_HEIGHT,
|
|
5
5
|
MIXING_PAD_WIDTH,
|
|
6
|
+
PAD_SAMPLE_SIZES,
|
|
6
7
|
hsvToRgb,
|
|
8
|
+
padSampleRadius,
|
|
7
9
|
hueToWheel,
|
|
8
10
|
parseHexColor,
|
|
9
11
|
renderColorWheel,
|
|
@@ -34,6 +36,9 @@
|
|
|
34
36
|
let wheel = $state<ColorWheel>('ryb');
|
|
35
37
|
let kind = $state<HarmonyKind>('complementary');
|
|
36
38
|
let padTool = $state<'paint' | 'knife' | 'pick'>('paint');
|
|
39
|
+
let sample = $state(3);
|
|
40
|
+
let parts = $state<[number, number]>([1, 1]);
|
|
41
|
+
let padName = $state('');
|
|
37
42
|
let padPoints: Array<{ x: number; y: number; pressure?: number }> | null = null;
|
|
38
43
|
const run = (fn: () => unknown) => attempt(fn, (m) => (error = m));
|
|
39
44
|
const ring = $derived(renderColorWheel(wheelSize, wheel));
|
|
@@ -54,7 +59,7 @@
|
|
|
54
59
|
};
|
|
55
60
|
</script>
|
|
56
61
|
|
|
57
|
-
<!-- Hue wheel with harmonies, saturation and value, colours, harmony chips, swatches, mixing pad. -->
|
|
62
|
+
<!-- Hue wheel with harmonies, saturation and value, colours, harmony chips, swatches, mixing pad (paint, knife, pick with a sample size, measured mixes, kept pads). -->
|
|
58
63
|
<div class={`${panel} ${className}`} data-testid="color-panel">
|
|
59
64
|
<div class="relative self-center" style={`width: ${wheelSize}px; height: ${wheelSize}px`}>
|
|
60
65
|
<div
|
|
@@ -154,7 +159,7 @@
|
|
|
154
159
|
onpointerdown={(e) => {
|
|
155
160
|
const p = padPoint(e);
|
|
156
161
|
if (padTool === 'pick') {
|
|
157
|
-
run(() => controller.mixingPadPick(p.x, p.y, { setColor: true }));
|
|
162
|
+
run(() => controller.mixingPadPick(p.x, p.y, { setColor: true, radius: padSampleRadius(sample) }));
|
|
158
163
|
return;
|
|
159
164
|
}
|
|
160
165
|
padPoints = [p];
|
|
@@ -169,6 +174,60 @@
|
|
|
169
174
|
>
|
|
170
175
|
<PixelCanvas pixels={pad} label="Mixing pad pixels" class="w-full rounded" style={`aspect-ratio: ${MIXING_PAD_WIDTH} / ${MIXING_PAD_HEIGHT}`} />
|
|
171
176
|
</div>
|
|
177
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
178
|
+
<label class="flex items-center gap-1">
|
|
179
|
+
<span class="text-slate-400">Sample</span>
|
|
180
|
+
<select aria-label="Pick sample size" value={sample} onchange={(e) => (sample = Number(e.currentTarget.value))} class="rounded bg-slate-800 px-1 py-0.5">
|
|
181
|
+
{#each PAD_SAMPLE_SIZES as n (n)}
|
|
182
|
+
<option value={n}>{n === 1 ? '1 px' : `${n}×${n}`}</option>
|
|
183
|
+
{/each}
|
|
184
|
+
</select>
|
|
185
|
+
</label>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
188
|
+
<span class="h-4 w-4 rounded ring-1 ring-slate-700" style={`background: ${color}`}></span>
|
|
189
|
+
<input type="number" aria-label="Paint colour parts" min="0" max="20" value={parts[0]} oninput={(e) => (parts = [Number(e.currentTarget.value), parts[1]])} class="w-10 rounded bg-slate-800 px-1 py-0.5" />
|
|
190
|
+
<span class="text-slate-500">:</span>
|
|
191
|
+
<input type="number" aria-label="Second colour parts" min="0" max="20" value={parts[1]} oninput={(e) => (parts = [parts[0], Number(e.currentTarget.value)])} class="w-10 rounded bg-slate-800 px-1 py-0.5" />
|
|
192
|
+
<span class="h-4 w-4 rounded ring-1 ring-slate-700" style={`background: ${snap.secondaryColor}`}></span>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
class={button}
|
|
196
|
+
title="Mix the paint and second colours in these parts on the pad, and use the mix"
|
|
197
|
+
onclick={() =>
|
|
198
|
+
run(() =>
|
|
199
|
+
controller.mixOnPad(
|
|
200
|
+
[
|
|
201
|
+
{ color, parts: parts[0] },
|
|
202
|
+
{ color: snap.secondaryColor, parts: parts[1] },
|
|
203
|
+
],
|
|
204
|
+
{ setColor: true }
|
|
205
|
+
)
|
|
206
|
+
)}>Mix</button
|
|
207
|
+
>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
210
|
+
<input bind:value={padName} placeholder="Pad name" aria-label="Pad name" class="w-24 rounded bg-slate-800 px-1.5 py-0.5" />
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
class={button}
|
|
214
|
+
onclick={() =>
|
|
215
|
+
run(() => {
|
|
216
|
+
controller.saveMixingPad(padName || undefined);
|
|
217
|
+
padName = '';
|
|
218
|
+
})}>Keep pad</button
|
|
219
|
+
>
|
|
220
|
+
</div>
|
|
221
|
+
{#if snap.savedMixingPads.length > 0}
|
|
222
|
+
<ul class="flex flex-col gap-0.5" aria-label="Kept pads">
|
|
223
|
+
{#each snap.savedMixingPads as p (p.id)}
|
|
224
|
+
<li class="flex items-center gap-1">
|
|
225
|
+
<button type="button" class={`${button} flex-1 text-left`} onclick={() => run(() => controller.loadMixingPad(p.id))}>{p.name}</button>
|
|
226
|
+
<button type="button" class={button} aria-label={`Remove ${p.name}`} onclick={() => run(() => controller.removeMixingPad(p.id))}>×</button>
|
|
227
|
+
</li>
|
|
228
|
+
{/each}
|
|
229
|
+
</ul>
|
|
230
|
+
{/if}
|
|
172
231
|
</div>
|
|
173
232
|
{/if}
|
|
174
233
|
{#if error}<p role="alert" class="text-[11px] text-rose-400">{error}</p>{/if}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColorPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/paint-editor/ColorPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"ColorPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/paint-editor/ColorPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAgBH,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAMvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6KH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.1",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"@tiptap/extension-bubble-menu": "^2.11.5",
|
|
140
140
|
"@tiptap/extension-placeholder": "^2.11.5",
|
|
141
141
|
"@tiptap/starter-kit": "^2.11.5",
|
|
142
|
-
"@pixerate/editor": "0.
|
|
142
|
+
"@pixerate/editor": "0.111.0"
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|
|
145
145
|
"@dagrejs/dagre": "^1.1.4",
|