@pixerate/editor-svelte 0.215.1 → 0.216.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.
- package/dist/brushes/BrushEditor.svelte +1 -0
- package/dist/brushes/BrushLibraryBrowser.svelte +5 -3
- package/dist/paint-editor/BrushPanel.svelte +6 -2
- package/dist/paint-editor/BrushPanel.svelte.d.ts +2 -0
- package/dist/paint-editor/EvolutionPanel.svelte +2 -2
- package/dist/paint-editor/PaintEditor.svelte +13 -7
- package/dist/paint-editor/PaperPanel.svelte +4 -43
- package/dist/paint-editor/SpecialtyGuideCard.svelte +43 -0
- package/dist/paint-editor/SpecialtyGuideCard.svelte.d.ts +10 -0
- package/dist/paint-editor/SpecialtyPanel.svelte +86 -0
- package/dist/paint-editor/SpecialtyPanel.svelte.d.ts +8 -0
- package/dist/paint-editor/index.d.ts +6 -0
- package/dist/paint-editor/index.js +2 -0
- package/package.json +2 -2
|
@@ -158,6 +158,7 @@
|
|
|
158
158
|
{/each}
|
|
159
159
|
</div>
|
|
160
160
|
{/if}
|
|
161
|
+
{#if c.hint}<p class="text-[10px] leading-snug text-slate-400" data-hint={c.path}>{c.hint}</p>{/if}
|
|
161
162
|
{#if onaddsource && (c.path === 'tip.stamps' || c.path === 'tip.grain.source')}
|
|
162
163
|
{@const label = c.path === 'tip.stamps' ? 'Add image tip…' : 'Add grain image…'}
|
|
163
164
|
<label class={`${button} cursor-pointer self-start`}>
|
|
@@ -21,15 +21,17 @@
|
|
|
21
21
|
const groups = $derived(brushLibraryGroups(entries, { text, profile }));
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
<!-- The brush library grouped by medium, then variety, with tools as their own
|
|
24
|
+
<!-- The brush library grouped by medium, then variety, with specialty paint (by kind) and tools as their own groups, and a filter (brushLibraryGroups). -->
|
|
25
25
|
<div class={`flex flex-col gap-1 ${className}`} data-testid="brush-library">
|
|
26
26
|
<input type="search" bind:value={text} placeholder="Find a brush" aria-label="Find a brush" class="rounded bg-slate-800 px-1.5 py-0.5" />
|
|
27
27
|
<div class="flex flex-col gap-1.5" role="listbox" aria-label="Brushes">
|
|
28
28
|
{#each groups as g (g.id)}
|
|
29
|
-
<div role="group" aria-label={g.title} class=
|
|
30
|
-
<span class=
|
|
29
|
+
<div role="group" aria-label={g.title} data-group={g.id} class={`flex flex-col gap-0.5 ${g.id === 'specialty' ? 'mt-1 border-t border-indigo-500/40 pt-1' : ''}`}>
|
|
30
|
+
<span class={`text-[10px] uppercase tracking-wide ${g.id === 'specialty' ? 'text-indigo-300' : 'text-slate-500'}`}>{g.title}</span>
|
|
31
31
|
<div class="grid grid-cols-2 gap-1">
|
|
32
32
|
{#each g.varieties as v (v.variety)}
|
|
33
|
+
<!-- Specialty paint is listed by kind (Ferrofluid, UV-fluorescent…). -->
|
|
34
|
+
{#if g.id === 'specialty'}<span class="col-span-2 text-[10px] text-slate-400">{v.title}</span>{/if}
|
|
33
35
|
{#each v.entries as e (e.brush.id)}
|
|
34
36
|
<button
|
|
35
37
|
type="button"
|
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
import BrushEditor from '../brushes/BrushEditor.svelte';
|
|
7
7
|
import BrushLibraryBrowser from '../brushes/BrushLibraryBrowser.svelte';
|
|
8
8
|
import PaintGradientEditor from './PaintGradientEditor.svelte';
|
|
9
|
+
import SpecialtyGuideCard from './SpecialtyGuideCard.svelte';
|
|
9
10
|
import { attempt, button, chip, paintEditorState, panel } from './paintEditorRune.svelte.js';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
controller: PaintEditorController;
|
|
13
14
|
thumbnailWidth?: number;
|
|
14
15
|
thumbnailHeight?: number;
|
|
16
|
+
/** With it, the specialty guide card has an "Open Specialty panel" button (the paint editor's Specialty tab). */
|
|
17
|
+
onopenspecialtypanel?: () => void;
|
|
15
18
|
class?: string;
|
|
16
19
|
}
|
|
17
|
-
let { controller, thumbnailWidth = 112, thumbnailHeight = 32, class: className = '' }: Props = $props();
|
|
20
|
+
let { controller, thumbnailWidth = 112, thumbnailHeight = 32, onopenspecialtypanel, class: className = '' }: Props = $props();
|
|
18
21
|
const editor = paintEditorState(() => controller);
|
|
19
22
|
const snap = $derived(editor.current);
|
|
20
23
|
const brush = $derived(snap.brush);
|
|
@@ -102,9 +105,10 @@
|
|
|
102
105
|
{/if}
|
|
103
106
|
{/snippet}
|
|
104
107
|
|
|
105
|
-
<!-- The brush library (BrushLibraryBrowser) and the active brush in the BrushEditor, plus the brush gradient, sticker stamps, what the brush carries, import and export. Images picked for an image tip or grain go into the brush library as sources; Import also reads brush packs (.abr, .brush, .brushset). -->
|
|
108
|
+
<!-- The brush library (BrushLibraryBrowser), a guide card for a specialty brush (SpecialtyGuideCard) and the active brush in the BrushEditor, plus the brush gradient, sticker stamps, what the brush carries, import and export. Images picked for an image tip or grain go into the brush library as sources; Import also reads brush packs (.abr, .brush, .brushset). -->
|
|
106
109
|
<div class={`${panel} ${className}`} data-testid="brush-panel">
|
|
107
110
|
<BrushLibraryBrowser {entries} activeId={brush.id} onselect={(id) => controller.setBrush({ id })} thumbnail={(id) => thumbs.get(id) ?? null} {thumbnailWidth} {thumbnailHeight} />
|
|
111
|
+
<SpecialtyGuideCard {controller} {onopenspecialtypanel} />
|
|
108
112
|
<BrushEditor
|
|
109
113
|
brush={spec}
|
|
110
114
|
profile={PAINT_PROFILE}
|
|
@@ -3,6 +3,8 @@ interface Props {
|
|
|
3
3
|
controller: PaintEditorController;
|
|
4
4
|
thumbnailWidth?: number;
|
|
5
5
|
thumbnailHeight?: number;
|
|
6
|
+
/** With it, the specialty guide card has an "Open Specialty panel" button (the paint editor's Specialty tab). */
|
|
7
|
+
onopenspecialtypanel?: () => void;
|
|
6
8
|
class?: string;
|
|
7
9
|
}
|
|
8
10
|
declare const BrushPanel: import("svelte").Component<Props, {}, "">;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
EVOLUTION_SLIDERS,
|
|
5
5
|
exportProposalContactSheetPng,
|
|
6
|
-
|
|
6
|
+
evolutionStrokeRefusal,
|
|
7
7
|
normalizeMutationRates,
|
|
8
8
|
paintProposalGhost,
|
|
9
9
|
proposalGenomeStrokes,
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
let error = $state<string | null>(null);
|
|
46
46
|
const run = (fn: () => unknown) => attempt(fn, (m) => (error = m));
|
|
47
47
|
|
|
48
|
-
const eligibleStrokes = $derived(editor.current.document.strokes.filter((s) => !
|
|
48
|
+
const eligibleStrokes = $derived(editor.current.document.strokes.filter((s) => !evolutionStrokeRefusal(s.brush)));
|
|
49
49
|
const activeStrokeIds = $derived(selectedStrokes.filter((id) => eligibleStrokes.some((s) => s.id === id)));
|
|
50
50
|
const effectiveStrokeIds = $derived(
|
|
51
51
|
activeStrokeIds.length > 0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { PaintPresence, documentToScreen, screenToDocument, type FillRegionOptions, type PaintEditorController, type PaintProposals, type PaintAgentStrokes, type PaintTool } from '@pixerate/editor/paint-editor';
|
|
2
|
+
import { PaintPresence, documentToScreen, screenToDocument, specialtyInPlay, type FillRegionOptions, type PaintEditorController, type PaintProposals, type PaintAgentStrokes, type PaintTool } from '@pixerate/editor/paint-editor';
|
|
3
3
|
import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
|
|
4
4
|
import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
|
|
5
5
|
import PresenceLayer from '../playback/PresenceLayer.svelte';
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
import PaintLayersPanel from './PaintLayersPanel.svelte';
|
|
10
10
|
import PaintToolbar from './PaintToolbar.svelte';
|
|
11
11
|
import PaperPanel from './PaperPanel.svelte';
|
|
12
|
+
import SpecialtyPanel from './SpecialtyPanel.svelte';
|
|
12
13
|
import WetPanel from './WetPanel.svelte';
|
|
13
14
|
import StickerPanel from './StickerPanel.svelte';
|
|
14
15
|
import StrokePanel from './StrokePanel.svelte';
|
|
15
16
|
import PathStrokePanel from './PathStrokePanel.svelte';
|
|
16
17
|
import EvolutionPanel from './EvolutionPanel.svelte';
|
|
17
18
|
import PaintEffectsPanel from './PaintEffectsPanel.svelte';
|
|
18
|
-
import { chip, paintEffectsProvider } from './paintEditorRune.svelte.js';
|
|
19
|
+
import { chip, paintEditorState, paintEffectsProvider } from './paintEditorRune.svelte.js';
|
|
19
20
|
|
|
20
21
|
interface Props {
|
|
21
22
|
controller: PaintEditorController;
|
|
@@ -109,20 +110,24 @@
|
|
|
109
110
|
};
|
|
110
111
|
let tool = $state<PaintTool>('brush');
|
|
111
112
|
let fillOptions = $state<Omit<FillRegionOptions, 'x' | 'y' | 'points'>>({ reference: 'all', tolerance: 32, closeGap: 4, expand: 1, antialias: true });
|
|
112
|
-
let tab = $state<'color' | 'wet' | 'paper' | 'layers' | 'effects'>('color');
|
|
113
|
+
let tab = $state<'color' | 'wet' | 'paper' | 'specialty' | 'layers' | 'effects'>('color');
|
|
113
114
|
const effects = paintEffectsProvider(() => controller);
|
|
114
|
-
const tabs = $derived(effects.current ? (['color', 'wet', 'paper', 'layers', 'effects'] as const) : (['color', 'wet', 'paper', 'layers'] as const));
|
|
115
|
+
const tabs = $derived(effects.current ? (['color', 'wet', 'paper', 'specialty', 'layers', 'effects'] as const) : (['color', 'wet', 'paper', 'specialty', 'layers'] as const));
|
|
116
|
+
const TAB_LABELS = { color: 'Colour', wet: 'Wet', paper: 'Paper', specialty: 'Specialty', layers: 'Layers', effects: 'Effects' } as const;
|
|
117
|
+
// The Specialty tab's indicator: a specialty brush, or the UV blacklight, heat, magnets or a view tilt in play.
|
|
118
|
+
const editorState = paintEditorState(() => controller);
|
|
119
|
+
const specialty = $derived(specialtyInPlay(editorState.current));
|
|
115
120
|
let error = $state<string | null>(null);
|
|
116
121
|
</script>
|
|
117
122
|
|
|
118
|
-
<!-- Toolbar, brush library, the canvas, and colour / wet media / paper / layers. -->
|
|
123
|
+
<!-- Toolbar, brush library, the canvas, and colour / wet media / paper / specialty paint / layers. -->
|
|
119
124
|
<div bind:this={root} class={`relative flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className}`} style={`height: ${typeof height === 'number' ? `${height}px` : height}`} data-testid="paint-editor">
|
|
120
125
|
{#if playing}
|
|
121
126
|
<PresenceLayer presence={agentPresence} viewport={docToRoot} />
|
|
122
127
|
{/if}
|
|
123
128
|
<PaintToolbar {controller} {tool} ontoolchange={(t) => (tool = t)} {fillOptions} onfilloptionschange={(f) => (fillOptions = f)} {fileName} />
|
|
124
129
|
<div class="flex min-h-0 flex-1 gap-2">
|
|
125
|
-
<BrushPanel {controller} class="w-60 shrink-0 overflow-y-auto pr-1" />
|
|
130
|
+
<BrushPanel {controller} onopenspecialtypanel={() => (tab = 'specialty')} class="w-60 shrink-0 overflow-y-auto pr-1" />
|
|
126
131
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
127
132
|
<!-- With playback the editor draws the cursor (over the panels too), so the canvas doesn't. -->
|
|
128
133
|
<PaintCanvas {controller} {tool} {fillOptions} presence={playing ? undefined : presence} {proposals} {agentStrokes} {deviceTilt} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
|
|
@@ -131,13 +136,14 @@
|
|
|
131
136
|
<div class="flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1">
|
|
132
137
|
<div class="flex gap-1" role="tablist" aria-label="Paint panels">
|
|
133
138
|
{#each tabs as t (t)}
|
|
134
|
-
<button type="button" role="tab" aria-selected={tab === t} data-paint-tab={t} class={chip(tab === t)} onclick={() => (tab = t)}
|
|
139
|
+
<button type="button" role="tab" aria-selected={tab === t} data-paint-tab={t} class={chip(tab === t)} onclick={() => (tab = t)} data-active={t === 'specialty' && specialty ? '' : undefined} title={t === 'specialty' && specialty ? 'Specialty paint is in play' : undefined}>{TAB_LABELS[t]}{#if t === 'specialty' && specialty}<span aria-hidden="true" class="ml-1 text-indigo-300">•</span>{/if}</button>
|
|
135
140
|
{/each}
|
|
136
141
|
</div>
|
|
137
142
|
<div class="flex flex-col gap-2" data-paint-panel={tab}>
|
|
138
143
|
{#if tab === 'color'}<ColorPanel {controller} />{/if}
|
|
139
144
|
{#if tab === 'wet'}<WetPanel {controller} />{/if}
|
|
140
145
|
{#if tab === 'paper'}<PaperPanel {controller} />{/if}
|
|
146
|
+
{#if tab === 'specialty'}<SpecialtyPanel {controller} />{/if}
|
|
141
147
|
{#if tab === 'layers'}<PaintLayersPanel {controller} /><StrokePanel {controller} /><PathStrokePanel {controller} /><EvolutionPanel {controller} {proposals} /><StickerPanel {controller} />{/if}
|
|
142
148
|
{#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
|
|
143
149
|
</div>
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { DEFAULT_BACKLIGHT_COLOR, LIGHT_SLIDERS, PAPER_FAMILIES, PAPER_LIBRARY, PAPER_SLIDERS,
|
|
3
|
-
import { untrack } from 'svelte';
|
|
2
|
+
import { DEFAULT_BACKLIGHT_COLOR, LIGHT_SLIDERS, PAPER_FAMILIES, PAPER_LIBRARY, PAPER_SLIDERS, patchPath, readPath, renderPaper, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
4
3
|
import PaintSlider from './PaintSlider.svelte';
|
|
5
4
|
import PixelCanvas from './PixelCanvas.svelte';
|
|
6
|
-
import FerroPanel from './FerroPanel.svelte';
|
|
7
5
|
import TimeMachinePanel from './TimeMachinePanel.svelte';
|
|
8
6
|
import CulturePanel from './CulturePanel.svelte';
|
|
9
|
-
import { attempt, button,
|
|
7
|
+
import { attempt, button, paintEditorState, panel } from './paintEditorRune.svelte.js';
|
|
10
8
|
|
|
11
9
|
interface Props {
|
|
12
10
|
controller: PaintEditorController;
|
|
@@ -22,17 +20,10 @@
|
|
|
22
20
|
void editor.current;
|
|
23
21
|
return controller.getLighting();
|
|
24
22
|
});
|
|
25
|
-
const viewTilt = $derived(editor.current.viewTilt);
|
|
26
|
-
let tiltFollow = $state(false);
|
|
27
|
-
$effect(() => {
|
|
28
|
-
const c = controller;
|
|
29
|
-
const on = tiltFollow;
|
|
30
|
-
return on ? untrack(() => startDeviceTilt(c)) : undefined;
|
|
31
|
-
});
|
|
32
23
|
const previews = new Map(PAPER_LIBRARY.map((p) => [p.preset, { ...SWATCH, data: renderPaper({ ...p, shading: Math.max(0.8, p.shading) }, SWATCH) }]));
|
|
33
24
|
</script>
|
|
34
25
|
|
|
35
|
-
<!-- The paper library by family and the current paper's settings (each change one undo step). -->
|
|
26
|
+
<!-- The paper library by family and the current paper's settings (each change one undo step), and the light for thick paint. The UV blacklight, heat, magnets and view tilt are in SpecialtyPanel. -->
|
|
36
27
|
<div class={`${panel} ${className}`} data-testid="paper-panel">
|
|
37
28
|
{#each PAPER_FAMILIES as family (family)}
|
|
38
29
|
<div>
|
|
@@ -74,38 +65,8 @@
|
|
|
74
65
|
{#each LIGHT_SLIDERS as c (c.path)}
|
|
75
66
|
<PaintSlider control={c} value={readPath(lighting, c.path)} onchange={(v) => run(() => controller.setLighting({ [c.path]: v }))} />
|
|
76
67
|
{/each}
|
|
77
|
-
{
|
|
78
|
-
<PaintSlider control={c} value={readPath(viewTilt, c.path)} onchange={(v) => run(() => controller.setViewTilt({ [c.path]: v }))} />
|
|
79
|
-
{/each}
|
|
80
|
-
<div class="flex flex-wrap items-center gap-1">
|
|
81
|
-
<button
|
|
82
|
-
type="button"
|
|
83
|
-
aria-pressed={tiltFollow}
|
|
84
|
-
class={chip(tiltFollow)}
|
|
85
|
-
onclick={() =>
|
|
86
|
-
run(async () => {
|
|
87
|
-
if (!tiltFollow && !(await requestDeviceTiltPermission())) return;
|
|
88
|
-
tiltFollow = !tiltFollow;
|
|
89
|
-
})}
|
|
90
|
-
>Device tilt</button>
|
|
91
|
-
<button type="button" class={`self-start ${button}`} onclick={() => run(() => controller.flattenRelief())}>Flatten relief</button>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
<div class="flex flex-col gap-1.5" data-testid="uv-heat-panel">
|
|
95
|
-
<span class="font-medium text-slate-300">UV blacklight & heat</span>
|
|
96
|
-
<label class="flex items-center gap-2">
|
|
97
|
-
<input type="checkbox" checked={editor.current.uvLight.on} onchange={(e) => run(() => controller.setUvLight({ on: e.currentTarget.checked }))} /> UV blacklight
|
|
98
|
-
</label>
|
|
99
|
-
{#each UV_LIGHT_SLIDERS as c (c.path)}
|
|
100
|
-
<PaintSlider control={c} value={readPath(editor.current.uvLight, c.path)} onchange={(v) => run(() => controller.setUvLight({ [c.path]: v }))} />
|
|
101
|
-
{/each}
|
|
102
|
-
<div class="flex items-center gap-1.5">
|
|
103
|
-
<button type="button" class={button} onclick={() => run(() => controller.advanceHeat(10))}>Step heat</button>
|
|
104
|
-
<button type="button" class={button} onclick={() => run(() => controller.clearHeat())}>Cool canvas</button>
|
|
105
|
-
{#if editor.current.heat.hotCells > 0}<span class="text-[11px] text-amber-400">{Math.round(editor.current.heat.max * 100)}% warm</span>{/if}
|
|
106
|
-
</div>
|
|
68
|
+
<button type="button" class={`self-start ${button}`} onclick={() => run(() => controller.flattenRelief())}>Flatten relief</button>
|
|
107
69
|
</div>
|
|
108
|
-
<FerroPanel {controller} />
|
|
109
70
|
<TimeMachinePanel {controller} />
|
|
110
71
|
<CulturePanel {controller} />
|
|
111
72
|
{#if error}<p role="alert" class="text-[11px] text-rose-400">{error}</p>{/if}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { brushSpecialtyGuides, runBrushSpecialtyAction, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
3
|
+
import { attempt, button, paintEditorState } from './paintEditorRune.svelte.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
controller: PaintEditorController;
|
|
7
|
+
/** With it, an "Open Specialty panel" button (the paint editor switches to its Specialty tab). */
|
|
8
|
+
onopenspecialtypanel?: () => void;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
let { controller, onopenspecialtypanel, class: className = '' }: Props = $props();
|
|
12
|
+
const editor = paintEditorState(() => controller);
|
|
13
|
+
const guides = $derived(brushSpecialtyGuides(editor.current));
|
|
14
|
+
let error = $state<string | null>(null);
|
|
15
|
+
const run = (fn: () => unknown) => attempt(fn, (m) => (error = m));
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<!-- How to use the active brush's specialty paint: its how-to, the canvas's live status and one-click setup actions. Nothing for a standard brush. -->
|
|
19
|
+
{#if guides.length}
|
|
20
|
+
<div class={`flex flex-col gap-2 rounded-lg border border-indigo-500/40 bg-indigo-950/30 p-2 ${className}`} data-testid="brush-specialty-guide">
|
|
21
|
+
{#each guides as g (g.kind)}
|
|
22
|
+
<div class="flex flex-col gap-1" data-specialty={g.kind}>
|
|
23
|
+
<div class="flex items-center gap-1.5">
|
|
24
|
+
<span class="rounded bg-indigo-600 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-white">Specialty</span>
|
|
25
|
+
<span class="font-medium text-slate-200">{g.title}</span>
|
|
26
|
+
</div>
|
|
27
|
+
<p class="text-[11px] leading-snug text-slate-400">{g.howToUse}</p>
|
|
28
|
+
<p role="status" data-ready={g.ready} class={`text-[11px] ${g.ready ? 'text-emerald-400' : 'text-amber-400'}`}>{g.ready ? '● ' : '○ '}{g.status}</p>
|
|
29
|
+
{#if g.actions.length}
|
|
30
|
+
<div class="flex flex-wrap gap-1">
|
|
31
|
+
{#each g.actions as a (a.id)}
|
|
32
|
+
<button type="button" class={button} data-action={a.id} title={a.tool} onclick={() => run(() => runBrushSpecialtyAction(controller, a))}>{a.label}</button>
|
|
33
|
+
{/each}
|
|
34
|
+
</div>
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
37
|
+
{/each}
|
|
38
|
+
{#if onopenspecialtypanel}
|
|
39
|
+
<button type="button" class={`${button} self-start`} onclick={onopenspecialtypanel}>Open Specialty panel</button>
|
|
40
|
+
{/if}
|
|
41
|
+
{#if error}<p role="alert" class="text-[11px] text-rose-400">{error}</p>{/if}
|
|
42
|
+
</div>
|
|
43
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: PaintEditorController;
|
|
4
|
+
/** With it, an "Open Specialty panel" button (the paint editor switches to its Specialty tab). */
|
|
5
|
+
onopenspecialtypanel?: () => void;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const SpecialtyGuideCard: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type SpecialtyGuideCard = ReturnType<typeof SpecialtyGuideCard>;
|
|
10
|
+
export default SpecialtyGuideCard;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { UV_LIGHT_SLIDERS, VIEW_TILT_SLIDERS, brushSpecialtyGuides, readPath, requestDeviceTiltPermission, startDeviceTilt, warmCanvasHeat, type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import PaintSlider from './PaintSlider.svelte';
|
|
5
|
+
import FerroPanel from './FerroPanel.svelte';
|
|
6
|
+
import SpecialtyGuideCard from './SpecialtyGuideCard.svelte';
|
|
7
|
+
import { attempt, button, chip, paintEditorState, panel } from './paintEditorRune.svelte.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
controller: PaintEditorController;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
let { controller, class: className = '' }: Props = $props();
|
|
14
|
+
const editor = paintEditorState(() => controller);
|
|
15
|
+
const snap = $derived(editor.current);
|
|
16
|
+
let error = $state<string | null>(null);
|
|
17
|
+
const run = (fn: () => unknown) => attempt(fn, (m) => (error = m));
|
|
18
|
+
/** Specialty presets offered as a quick pick while a standard brush is active. */
|
|
19
|
+
const PICKS = ['ferrofluid', 'uv-marker', 'thermo-ink', 'soap-film', 'ghost-wash', 'mutation-wash', 'turing-wash'];
|
|
20
|
+
const guides = $derived(brushSpecialtyGuides(snap));
|
|
21
|
+
const picks = $derived(PICKS.flatMap((id) => snap.brushes.filter((b) => b.id === id)));
|
|
22
|
+
const tilted = $derived(snap.viewTilt.x !== 0 || snap.viewTilt.y !== 0);
|
|
23
|
+
let tiltFollow = $state(false);
|
|
24
|
+
$effect(() => {
|
|
25
|
+
const c = controller;
|
|
26
|
+
const on = tiltFollow;
|
|
27
|
+
return on ? untrack(() => startDeviceTilt(c)) : undefined;
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<!-- The canvas environment for specialty paint: the brush's guide (or a quick pick), the UV blacklight and heat, magnets, and the view tilt. -->
|
|
32
|
+
<div class={`${panel} ${className}`} data-testid="specialty-panel">
|
|
33
|
+
{#if guides.length}
|
|
34
|
+
<SpecialtyGuideCard {controller} />
|
|
35
|
+
{:else}
|
|
36
|
+
<div class="flex flex-col gap-1" data-testid="specialty-picks">
|
|
37
|
+
<span class="text-slate-400">The active brush is a standard one. Pick a specialty brush:</span>
|
|
38
|
+
<div class="flex flex-wrap gap-1">
|
|
39
|
+
{#each picks as b (b.id)}
|
|
40
|
+
<button type="button" class={button} onclick={() => run(() => controller.setBrush({ id: b.id }))}>{b.name}</button>
|
|
41
|
+
{/each}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|
|
45
|
+
<div class="flex flex-col gap-1.5" data-testid="uv-heat-panel">
|
|
46
|
+
<span class="font-medium text-slate-300">UV blacklight & heat</span>
|
|
47
|
+
<p class="text-[11px] leading-snug text-slate-500">UV-fluorescent ink (UV marker) glows in its own colour under the blacklight; invisible UV ink shows only under it. Thermochromic ink (Thermochromic ink preset) shifts toward its hot colour where the canvas is warm.</p>
|
|
48
|
+
<label class="flex items-center gap-2">
|
|
49
|
+
<input type="checkbox" checked={snap.uvLight.on} onchange={(e) => run(() => controller.setUvLight({ on: e.currentTarget.checked }))} /> UV blacklight
|
|
50
|
+
</label>
|
|
51
|
+
{#each UV_LIGHT_SLIDERS as c (c.path)}
|
|
52
|
+
<PaintSlider control={c} value={readPath(snap.uvLight, c.path)} onchange={(v) => run(() => controller.setUvLight({ [c.path]: v }))} />
|
|
53
|
+
{/each}
|
|
54
|
+
<span class="text-[11px] text-slate-400" role="status">{snap.heat.hotCells > 0 ? `Canvas heat: ${Math.round(snap.heat.max * 100)}% at its warmest` : 'Canvas is cool'}</span>
|
|
55
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
56
|
+
<button type="button" class={button} onclick={() => run(() => controller.paintHeat(warmCanvasHeat(snap.document.canvas)))}>Warm canvas</button>
|
|
57
|
+
<button type="button" class={button} disabled={snap.heat.hotCells === 0} onclick={() => run(() => controller.advanceHeat(10))}>Step heat</button>
|
|
58
|
+
<button type="button" class={button} disabled={snap.heat.hotCells === 0} onclick={() => run(() => controller.clearHeat())}>Cool canvas</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex flex-col gap-1.5">
|
|
62
|
+
<p class="text-[11px] leading-snug text-slate-500">Ferrofluid and Ferro ink pool into liquid magnetic paint that magnets pull into 3D Rosensweig spikes.</p>
|
|
63
|
+
<FerroPanel {controller} />
|
|
64
|
+
</div>
|
|
65
|
+
<div class="flex flex-col gap-1.5" data-testid="view-tilt-panel">
|
|
66
|
+
<span class="font-medium text-slate-300">Thin-film iridescence & view tilt</span>
|
|
67
|
+
<p class="text-[11px] leading-snug text-slate-500">Scarab lacquer, Soap film and Pearl glaze shift colour with the viewing angle.</p>
|
|
68
|
+
{#each VIEW_TILT_SLIDERS as c (c.path)}
|
|
69
|
+
<PaintSlider control={c} value={readPath(snap.viewTilt, c.path)} onchange={(v) => run(() => controller.setViewTilt({ [c.path]: v }))} />
|
|
70
|
+
{/each}
|
|
71
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
72
|
+
<button type="button" class={button} disabled={!tilted} onclick={() => run(() => controller.setViewTilt({ x: 0, y: 0 }))}>Reset tilt</button>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
aria-pressed={tiltFollow}
|
|
76
|
+
class={chip(tiltFollow)}
|
|
77
|
+
onclick={() =>
|
|
78
|
+
run(async () => {
|
|
79
|
+
if (!tiltFollow && !(await requestDeviceTiltPermission())) return;
|
|
80
|
+
tiltFollow = !tiltFollow;
|
|
81
|
+
})}
|
|
82
|
+
>Device tilt</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
{#if error}<p role="alert" class="text-[11px] text-rose-400">{error}</p>{/if}
|
|
86
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PaintEditorController } from '@pixerate/editor/paint-editor';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: PaintEditorController;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
declare const SpecialtyPanel: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type SpecialtyPanel = ReturnType<typeof SpecialtyPanel>;
|
|
8
|
+
export default SpecialtyPanel;
|
|
@@ -7,6 +7,8 @@ import type BrushEditorComponent from '../brushes/BrushEditor.svelte';
|
|
|
7
7
|
import type BrushLibraryBrowserComponent from '../brushes/BrushLibraryBrowser.svelte';
|
|
8
8
|
import type PaperPanelComponent from './PaperPanel.svelte';
|
|
9
9
|
import type FerroPanelComponent from './FerroPanel.svelte';
|
|
10
|
+
import type SpecialtyPanelComponent from './SpecialtyPanel.svelte';
|
|
11
|
+
import type SpecialtyGuideCardComponent from './SpecialtyGuideCard.svelte';
|
|
10
12
|
import type TimeMachinePanelComponent from './TimeMachinePanel.svelte';
|
|
11
13
|
import type CulturePanelComponent from './CulturePanel.svelte';
|
|
12
14
|
import type ColorPanelComponent from './ColorPanel.svelte';
|
|
@@ -27,6 +29,8 @@ export { default as BrushEditor } from '../brushes/BrushEditor.svelte';
|
|
|
27
29
|
export { default as BrushLibraryBrowser } from '../brushes/BrushLibraryBrowser.svelte';
|
|
28
30
|
export { default as PaperPanel } from './PaperPanel.svelte';
|
|
29
31
|
export { default as FerroPanel } from './FerroPanel.svelte';
|
|
32
|
+
export { default as SpecialtyPanel } from './SpecialtyPanel.svelte';
|
|
33
|
+
export { default as SpecialtyGuideCard } from './SpecialtyGuideCard.svelte';
|
|
30
34
|
export { default as TimeMachinePanel } from './TimeMachinePanel.svelte';
|
|
31
35
|
export { default as CulturePanel } from './CulturePanel.svelte';
|
|
32
36
|
export { default as ColorPanel } from './ColorPanel.svelte';
|
|
@@ -52,6 +56,8 @@ export type BrushEditorProps = ComponentProps<typeof BrushEditorComponent>;
|
|
|
52
56
|
export type BrushLibraryBrowserProps = ComponentProps<typeof BrushLibraryBrowserComponent>;
|
|
53
57
|
export type PaperPanelProps = ComponentProps<typeof PaperPanelComponent>;
|
|
54
58
|
export type FerroPanelProps = ComponentProps<typeof FerroPanelComponent>;
|
|
59
|
+
export type SpecialtyPanelProps = ComponentProps<typeof SpecialtyPanelComponent>;
|
|
60
|
+
export type SpecialtyGuideCardProps = ComponentProps<typeof SpecialtyGuideCardComponent>;
|
|
55
61
|
export type TimeMachinePanelProps = ComponentProps<typeof TimeMachinePanelComponent>;
|
|
56
62
|
export type CulturePanelProps = ComponentProps<typeof CulturePanelComponent>;
|
|
57
63
|
export type ColorPanelProps = ComponentProps<typeof ColorPanelComponent>;
|
|
@@ -8,6 +8,8 @@ export { default as BrushEditor } from '../brushes/BrushEditor.svelte';
|
|
|
8
8
|
export { default as BrushLibraryBrowser } from '../brushes/BrushLibraryBrowser.svelte';
|
|
9
9
|
export { default as PaperPanel } from './PaperPanel.svelte';
|
|
10
10
|
export { default as FerroPanel } from './FerroPanel.svelte';
|
|
11
|
+
export { default as SpecialtyPanel } from './SpecialtyPanel.svelte';
|
|
12
|
+
export { default as SpecialtyGuideCard } from './SpecialtyGuideCard.svelte';
|
|
11
13
|
export { default as TimeMachinePanel } from './TimeMachinePanel.svelte';
|
|
12
14
|
export { default as CulturePanel } from './CulturePanel.svelte';
|
|
13
15
|
export { default as ColorPanel } from './ColorPanel.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.216.1",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"**/*.css"
|
|
185
185
|
],
|
|
186
186
|
"dependencies": {
|
|
187
|
-
"@pixerate/editor": "0.
|
|
187
|
+
"@pixerate/editor": "0.294.0"
|
|
188
188
|
},
|
|
189
189
|
"peerDependencies": {
|
|
190
190
|
"@dagrejs/dagre": "^1.1.4",
|