@lvl99dotcom/paintcanvas 0.5.0 → 0.6.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/dist/lib/brushes/configRanges/index.d.ts +17 -0
- package/dist/lib/brushes/configRanges/index.js +31 -1
- package/dist/lib/brushes/params/index.d.ts +2 -1
- package/dist/lib/brushes/params/index.js +7 -4
- package/dist/lib/brushes/types/index.d.ts +8 -0
- package/dist/lib/classes/Layer/index.d.ts +10 -0
- package/dist/lib/classes/Layer/index.js +15 -1
- package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
- package/dist/lib/classes/LayerAutosave/index.js +30 -43
- package/dist/lib/classes/LayerStack/index.d.ts +15 -0
- package/dist/lib/classes/LayerStack/index.js +26 -2
- package/dist/lib/classes/PaintCanvas/index.d.ts +26 -3
- package/dist/lib/classes/PaintCanvas/index.js +39 -12
- package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
- package/dist/lib/classes/PngEncoder/index.js +104 -0
- package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
- package/dist/lib/classes/ProfileGraphs/index.js +148 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
- package/dist/lib/classes/SettingsAutosave/index.js +2 -1
- package/dist/lib/messages/schema.d.ts +720 -0
- package/dist/lib/presets/schema.d.ts +360 -0
- package/dist/lib/presets/schema.js +8 -1
- package/dist/lib/profiles/index.d.ts +61 -0
- package/dist/lib/profiles/index.js +131 -0
- package/dist/lib/profiles/schema.d.ts +73 -0
- package/dist/lib/profiles/schema.js +53 -0
- package/dist/lib/rendering/sample/index.d.ts +11 -0
- package/dist/lib/rendering/sample/index.js +36 -5
- package/dist/lib/settings/index.d.ts +2 -1
- package/dist/lib/settings/index.js +4 -1
- package/dist/lib/settings/schema.d.ts +49 -0
- package/dist/lib/settings/schema.js +6 -0
- package/dist/lib/validation/validators.generated.js +13064 -3696
- package/dist/locales/en-GB.js +30 -0
- package/dist/locales/en-US.js +30 -0
- package/dist/locales/fr.js +30 -0
- package/dist/locales/ja.js +30 -0
- package/dist/locales/ko.js +30 -0
- package/dist/locales/zh-Hans.js +30 -0
- package/dist/locales/zh-Hant.js +30 -0
- package/dist/package.js +1 -1
- package/dist/ui/ask/index.d.ts +48 -0
- package/dist/ui/ask/index.js +67 -0
- package/dist/ui/base/index.d.ts +8 -1
- package/dist/ui/base/index.js +19 -4
- package/dist/ui/elements/PaintCanvas/index.js +3 -1
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
- package/dist/ui/elements/PcAppSettings/index.js +21 -65
- package/dist/ui/elements/PcBrushConfig/index.d.ts +25 -14
- package/dist/ui/elements/PcBrushConfig/index.js +216 -146
- package/dist/ui/elements/PcBrushConfig/style.js +1 -1
- package/dist/ui/elements/PcBrushTest/index.js +7 -2
- package/dist/ui/elements/PcBrushTest/style.js +1 -1
- package/dist/ui/elements/PcButton/index.d.ts +12 -3
- package/dist/ui/elements/PcButton/index.js +18 -10
- package/dist/ui/elements/PcCanvas/index.d.ts +6 -1
- package/dist/ui/elements/PcCanvas/index.js +10 -3
- package/dist/ui/elements/PcCanvas/style.js +1 -1
- package/dist/ui/elements/PcClipboardTools/index.d.ts +5 -4
- package/dist/ui/elements/PcClipboardTools/index.js +12 -5
- package/dist/ui/elements/PcDialog/index.d.ts +46 -10
- package/dist/ui/elements/PcDialog/index.js +102 -51
- package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
- package/dist/ui/elements/PcDialogWindow/index.js +385 -0
- package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
- package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
- package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
- package/dist/ui/elements/PcPanelColours/index.js +2 -2
- package/dist/ui/elements/PcPanelColours/style.js +1 -1
- package/dist/ui/elements/PcPanelLayers/index.js +1 -1
- package/dist/ui/elements/PcPanelLayers/style.js +1 -1
- package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
- package/dist/ui/elements/PcProfileEditor/index.js +259 -0
- package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
- package/dist/ui/elements/PcProfileGraph/index.js +317 -0
- package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
- package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
- package/dist/ui/elements/PcSliderRange/index.js +307 -0
- package/dist/ui/elements/PcStats/index.d.ts +19 -4
- package/dist/ui/elements/PcStats/index.js +29 -13
- package/dist/ui/elements/PcToolSettings/index.js +6 -6
- package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
- package/dist/ui/icons/index.d.ts +1 -1
- package/dist/ui/icons/index.js +2 -0
- package/dist/ui/index.d.ts +6 -1
- package/dist/ui/index.js +6 -1
- package/dist/ui/styles/index.d.ts +21 -1
- package/dist/ui/styles/index.js +73 -14
- package/locales/en-GB.json +30 -0
- package/locales/en-US.json +30 -0
- package/locales/fr.json +30 -0
- package/locales/ja.json +30 -0
- package/locales/ko.json +30 -0
- package/locales/zh-Hans.json +30 -0
- package/locales/zh-Hant.json +30 -0
- package/package.json +25 -23
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { PcElement, defineElement, emit } from "../../base/index.js";
|
|
2
|
+
import { askConfirm, askText } from "../../ask/index.js";
|
|
3
|
+
import "../PcDialogWindow/index.js";
|
|
4
|
+
import "../PcProfileGraph/index.js";
|
|
5
|
+
import "../PcContextMenu/index.js";
|
|
6
|
+
import "../PcButton/index.js";
|
|
7
|
+
import ProfileGraphs from "../../../lib/classes/ProfileGraphs/index.js";
|
|
8
|
+
import { linearPoints, samePoints } from "../../../lib/profiles/index.js";
|
|
9
|
+
import { css, html } from "lit";
|
|
10
|
+
import { live } from "lit/directives/live.js";
|
|
11
|
+
// src/ui/elements/PcProfileEditor/index.ts
|
|
12
|
+
/** The value of the drop-down's "Custom" option */
|
|
13
|
+
var CUSTOM = "";
|
|
14
|
+
/**
|
|
15
|
+
* A window for editing a brush setting's profile graph (see `<pc-profile-graph>`): a drop-down of the presets and the
|
|
16
|
+
* user's own graphs (it says "Custom" once the graph is edited, or for one that isn't saved), a ⋮ menu (New graph,
|
|
17
|
+
* Duplicate…, Rename… and Delete…, the last two for the user's own graphs), the graph, and Save, Reset and Dismiss.
|
|
18
|
+
*
|
|
19
|
+
* - Choosing a graph in the drop-down loads its points.
|
|
20
|
+
* - New graph starts from a straight line, and Duplicate… saves a copy of the current graph under a name ("Custom" to
|
|
21
|
+
* start with); Rename… and Delete… ask first.
|
|
22
|
+
* - Save uses the graph (firing `pc-save` with its points and ID); an edited or unsaved graph is first saved with a name
|
|
23
|
+
* it asks for, asking before replacing another of the user's graphs with that name.
|
|
24
|
+
* - Reset goes back to the graph it opened with; Dismiss, ⨉ and Escape close it (`pc-close`) without changing anything.
|
|
25
|
+
*
|
|
26
|
+
* Graphs are saved in `profiles` (the canvas's `ProfileGraphs`).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* html`<pc-profile-editor .profiles=${app.profiles} .points=${points} profile-id="overweight" label="Width"
|
|
30
|
+
* @pc-save=${(e) => use(e.detail)} @pc-close=${close}></pc-profile-editor>`
|
|
31
|
+
*/
|
|
32
|
+
var PcProfileEditor = class extends PcElement {
|
|
33
|
+
static properties = {
|
|
34
|
+
...PcElement.properties,
|
|
35
|
+
profiles: { attribute: false },
|
|
36
|
+
points: { attribute: false },
|
|
37
|
+
profileId: {
|
|
38
|
+
type: String,
|
|
39
|
+
attribute: "profile-id"
|
|
40
|
+
},
|
|
41
|
+
label: { type: String },
|
|
42
|
+
draft: { state: true },
|
|
43
|
+
sourceId: { state: true }
|
|
44
|
+
};
|
|
45
|
+
static styles = [PcElement.styles, css`
|
|
46
|
+
:host {
|
|
47
|
+
display: contents;
|
|
48
|
+
}
|
|
49
|
+
pc-dialog-window {
|
|
50
|
+
--pc-dialog-width: 340px;
|
|
51
|
+
}
|
|
52
|
+
.choose {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: var(--pc-space-2);
|
|
56
|
+
width: 100%;
|
|
57
|
+
}
|
|
58
|
+
select {
|
|
59
|
+
flex: 1;
|
|
60
|
+
min-width: 0;
|
|
61
|
+
min-height: var(--pc-control-size);
|
|
62
|
+
padding: 0 var(--pc-space-2);
|
|
63
|
+
border: 1px solid var(--pc-border);
|
|
64
|
+
border-radius: var(--pc-radius-small);
|
|
65
|
+
background: var(--pc-surface);
|
|
66
|
+
color: var(--pc-text);
|
|
67
|
+
font: inherit;
|
|
68
|
+
}
|
|
69
|
+
.actions {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
gap: var(--pc-space-2);
|
|
73
|
+
}
|
|
74
|
+
`];
|
|
75
|
+
/** Asks the user to confirm, in a dialog (replaceable for tests) */
|
|
76
|
+
confirm = (options) => askConfirm(this.renderRoot, options);
|
|
77
|
+
/** Asks the user for text, in a dialog (replaceable for tests) */
|
|
78
|
+
prompt = (options) => askText(this.renderRoot, options);
|
|
79
|
+
constructor() {
|
|
80
|
+
super();
|
|
81
|
+
this.profiles = new ProfileGraphs();
|
|
82
|
+
this.label = "";
|
|
83
|
+
this.draft = linearPoints();
|
|
84
|
+
}
|
|
85
|
+
willUpdate(changed) {
|
|
86
|
+
super.willUpdate(changed);
|
|
87
|
+
if (changed.has("points") || changed.has("profileId")) this.reset();
|
|
88
|
+
}
|
|
89
|
+
/** Go back to the graph it opened with */
|
|
90
|
+
reset() {
|
|
91
|
+
this.draft = structuredClone(this.points ?? linearPoints());
|
|
92
|
+
const source = this.profiles.get(this.profileId);
|
|
93
|
+
this.sourceId = source && samePoints(source.points, this.draft) ? source.id : void 0;
|
|
94
|
+
}
|
|
95
|
+
/** Load a graph's points */
|
|
96
|
+
load(id) {
|
|
97
|
+
const graph = this.profiles.get(id);
|
|
98
|
+
if (!graph) return;
|
|
99
|
+
this.draft = structuredClone(graph.points);
|
|
100
|
+
this.sourceId = graph.id;
|
|
101
|
+
}
|
|
102
|
+
/** Whether the graph being edited is one of the user's own, unchanged */
|
|
103
|
+
get ownSource() {
|
|
104
|
+
return this.sourceId !== void 0 && !this.profiles.isPreset(this.sourceId);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ask for a name and save the points as one of the user's graphs, asking before replacing another with that name.
|
|
108
|
+
* Returns the saved graph's ID, or undefined if it wasn't saved.
|
|
109
|
+
*/
|
|
110
|
+
async saveAs(value) {
|
|
111
|
+
const name = (await this.prompt({
|
|
112
|
+
heading: this.t("paintCanvas.profileEditor.saveHeading"),
|
|
113
|
+
label: this.t("paintCanvas.profileEditor.name"),
|
|
114
|
+
value,
|
|
115
|
+
confirm: this.t("paintCanvas.common.save"),
|
|
116
|
+
cancel: this.t("paintCanvas.common.cancel")
|
|
117
|
+
}))?.trim();
|
|
118
|
+
if (!name) return void 0;
|
|
119
|
+
const existing = this.profiles.findByName(name);
|
|
120
|
+
if (existing && !await this.confirm({
|
|
121
|
+
heading: this.t("paintCanvas.profileEditor.replaceHeading"),
|
|
122
|
+
message: this.t("paintCanvas.profileEditor.replaceConfirm", { name }),
|
|
123
|
+
confirm: this.t("paintCanvas.presets.replace"),
|
|
124
|
+
cancel: this.t("paintCanvas.common.cancel"),
|
|
125
|
+
cancelling: true
|
|
126
|
+
})) return void 0;
|
|
127
|
+
const saved = this.profiles.save({
|
|
128
|
+
id: existing?.id,
|
|
129
|
+
name,
|
|
130
|
+
points: this.draft
|
|
131
|
+
});
|
|
132
|
+
if (saved) this.sourceId = saved.id;
|
|
133
|
+
return saved?.id;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Use the graph: save it first when it's edited or unsaved (asking for a name), then fire `pc-save`.
|
|
137
|
+
*/
|
|
138
|
+
async save() {
|
|
139
|
+
const id = this.sourceId ?? await this.saveAs(this.t("paintCanvas.profileEditor.custom"));
|
|
140
|
+
if (!id) return;
|
|
141
|
+
emit(this, "pc-save", {
|
|
142
|
+
points: structuredClone(this.draft),
|
|
143
|
+
profileId: id
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Do one of the ⋮ menu's actions */
|
|
147
|
+
async act(action) {
|
|
148
|
+
const graph = this.profiles.get(this.sourceId);
|
|
149
|
+
if (action === "new") {
|
|
150
|
+
this.draft = linearPoints();
|
|
151
|
+
this.sourceId = void 0;
|
|
152
|
+
} else if (action === "duplicate") await this.saveAs(this.t("paintCanvas.profileEditor.custom"));
|
|
153
|
+
else if (action === "rename" && graph && this.ownSource) {
|
|
154
|
+
const name = await this.prompt({
|
|
155
|
+
heading: this.t("paintCanvas.profileEditor.renameHeading"),
|
|
156
|
+
label: this.t("paintCanvas.profileEditor.name"),
|
|
157
|
+
value: graph.name,
|
|
158
|
+
confirm: this.t("paintCanvas.common.save"),
|
|
159
|
+
cancel: this.t("paintCanvas.common.cancel")
|
|
160
|
+
});
|
|
161
|
+
if (name && this.profiles.rename(graph.id, name)) this.requestUpdate();
|
|
162
|
+
} else if (action === "delete" && graph && this.ownSource) {
|
|
163
|
+
if (await this.confirm({
|
|
164
|
+
heading: this.t("paintCanvas.profileEditor.deleteHeading"),
|
|
165
|
+
message: this.t("paintCanvas.profileEditor.deleteConfirm", { name: graph.name }),
|
|
166
|
+
confirm: this.t("paintCanvas.common.delete"),
|
|
167
|
+
cancel: this.t("paintCanvas.common.cancel"),
|
|
168
|
+
cancelling: true
|
|
169
|
+
}) && this.profiles.remove(graph.id)) this.sourceId = void 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** A graph's name: the presets' in the chosen language */
|
|
173
|
+
graphName(id, name) {
|
|
174
|
+
return this.profiles.isPreset(id) ? this.t(`paintCanvas.profiles.${id}`) : name;
|
|
175
|
+
}
|
|
176
|
+
render() {
|
|
177
|
+
const own = this.ownSource;
|
|
178
|
+
const items = [
|
|
179
|
+
{
|
|
180
|
+
value: "new",
|
|
181
|
+
label: this.t("paintCanvas.profileEditor.new")
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
value: "duplicate",
|
|
185
|
+
label: this.t("paintCanvas.profileEditor.duplicate")
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
value: "rename",
|
|
189
|
+
label: this.t("paintCanvas.profileEditor.rename"),
|
|
190
|
+
disabled: !own
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
value: "delete",
|
|
194
|
+
label: this.t("paintCanvas.profileEditor.delete"),
|
|
195
|
+
disabled: !own,
|
|
196
|
+
danger: true
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
const title = this.t("paintCanvas.profileEditor.title", { param: this.label });
|
|
200
|
+
return html`<pc-dialog-window
|
|
201
|
+
open
|
|
202
|
+
shaded
|
|
203
|
+
label=${title}
|
|
204
|
+
@pc-dismiss=${(event) => {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
emit(this, "pc-close");
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<div slot="header-middle" class="choose">
|
|
210
|
+
<select
|
|
211
|
+
aria-label=${title}
|
|
212
|
+
.value=${live(this.sourceId ?? CUSTOM)}
|
|
213
|
+
@change=${(event) => this.load(event.target.value)}
|
|
214
|
+
>
|
|
215
|
+
${this.sourceId === void 0 ? html`<option value=${CUSTOM}>
|
|
216
|
+
${this.t("paintCanvas.profileEditor.custom")}
|
|
217
|
+
</option>` : ""}
|
|
218
|
+
${this.profiles.all.map((graph) => html`<option
|
|
219
|
+
value=${graph.id}
|
|
220
|
+
.selected=${graph.id === this.sourceId}
|
|
221
|
+
>
|
|
222
|
+
${this.graphName(graph.id, graph.name)}
|
|
223
|
+
</option>`)}
|
|
224
|
+
</select>
|
|
225
|
+
<pc-context-menu
|
|
226
|
+
label=${this.t("paintCanvas.profileEditor.actions")}
|
|
227
|
+
.items=${items}
|
|
228
|
+
@pc-select=${(event) => void this.act(event.detail.value)}
|
|
229
|
+
></pc-context-menu>
|
|
230
|
+
</div>
|
|
231
|
+
<pc-profile-graph
|
|
232
|
+
slot="body"
|
|
233
|
+
label=${title}
|
|
234
|
+
.points=${this.draft}
|
|
235
|
+
@pc-change=${(event) => {
|
|
236
|
+
this.draft = event.detail.points;
|
|
237
|
+
this.sourceId = void 0;
|
|
238
|
+
}}
|
|
239
|
+
></pc-profile-graph>
|
|
240
|
+
<div slot="footer-middle" class="actions">
|
|
241
|
+
<pc-button class="save" @click=${() => void this.save()}>
|
|
242
|
+
${this.t("paintCanvas.common.save")}
|
|
243
|
+
</pc-button>
|
|
244
|
+
<pc-button class="reset" variant="cancel" @click=${() => this.reset()}>
|
|
245
|
+
${this.t("paintCanvas.profileEditor.reset")}
|
|
246
|
+
</pc-button>
|
|
247
|
+
<pc-button
|
|
248
|
+
class="dismiss"
|
|
249
|
+
variant="dismiss"
|
|
250
|
+
@click=${() => emit(this, "pc-close")}
|
|
251
|
+
>
|
|
252
|
+
${this.t("paintCanvas.common.dismiss")}
|
|
253
|
+
</pc-button>
|
|
254
|
+
</div>
|
|
255
|
+
</pc-dialog-window>`;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
defineElement("pc-profile-editor", PcProfileEditor);
|
|
259
|
+
export { PcProfileEditor };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { PcElement } from "../../base";
|
|
2
|
+
import { Interpolation, type ProfilePoint } from "../../../lib/profiles";
|
|
3
|
+
/**
|
|
4
|
+
* The SVG path of a curve through points, drawn as short straight pieces (y is flipped: up is less negative).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* curvePath(linearPoints()); // "M0,10 L0.1,9.9 …"
|
|
8
|
+
*/
|
|
9
|
+
export declare const curvePath: (points: readonly ProfilePoint[]) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Move a point within the grid, keeping it between its neighbours (and the first and last points at the edges), to
|
|
12
|
+
* multiples of 0.1. Returns new points.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* movePoint(points, 1, 4.04, -3); // points[1] at { x: 4, y: -3 }
|
|
16
|
+
*/
|
|
17
|
+
export declare const movePoint: (points: readonly ProfilePoint[], index: number, x: number, y: number) => ProfilePoint[];
|
|
18
|
+
/**
|
|
19
|
+
* A profile graph to edit (see `@lvl99dotcom/paintcanvas/lib/profiles`): a 10 × 10 grid with the curve through its
|
|
20
|
+
* points, from the setting's min at the bottom left to its max at the top right.
|
|
21
|
+
*
|
|
22
|
+
* - Pressing the line adds a point there (smooth), selected.
|
|
23
|
+
* - Pressing a point selects it; dragging moves it (between its neighbours; the first and last stay at the edges).
|
|
24
|
+
* - With a point selected, the arrow keys move it a grid unit, and the buttons below make it a sharp corner (linear)
|
|
25
|
+
* or smooth (bezier cubic), or remove it (−; not the first or last).
|
|
26
|
+
*
|
|
27
|
+
* Points are focusable buttons (Tab moves between them; focusing one selects it). Fires `pc-change` (detail:
|
|
28
|
+
* `{ points }`) whenever the points change, after changing `points` itself.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* html`<pc-profile-graph .points=${points} @pc-change=${(e) => (points = e.detail.points)}></pc-profile-graph>`
|
|
32
|
+
*/
|
|
33
|
+
export declare class PcProfileGraph extends PcElement {
|
|
34
|
+
static properties: {
|
|
35
|
+
points: {
|
|
36
|
+
attribute: boolean;
|
|
37
|
+
};
|
|
38
|
+
selected: {
|
|
39
|
+
type: NumberConstructor;
|
|
40
|
+
};
|
|
41
|
+
label: {
|
|
42
|
+
type: StringConstructor;
|
|
43
|
+
};
|
|
44
|
+
translations: {
|
|
45
|
+
attribute: boolean;
|
|
46
|
+
};
|
|
47
|
+
icons: {
|
|
48
|
+
attribute: boolean;
|
|
49
|
+
};
|
|
50
|
+
theme: {
|
|
51
|
+
type: StringConstructor;
|
|
52
|
+
reflect: boolean;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
static styles: import("lit").CSSResultGroup[];
|
|
56
|
+
/** The points, from left to right */
|
|
57
|
+
points: ProfilePoint[];
|
|
58
|
+
/** The selected point's index, if any */
|
|
59
|
+
selected?: number;
|
|
60
|
+
/** The graph's name for screen readers */
|
|
61
|
+
label: string;
|
|
62
|
+
/** The point being dragged */
|
|
63
|
+
private dragging?;
|
|
64
|
+
constructor();
|
|
65
|
+
/** Change the points and tell listeners */
|
|
66
|
+
private setPoints;
|
|
67
|
+
/** A pointer's position in grid units */
|
|
68
|
+
private gridAt;
|
|
69
|
+
/**
|
|
70
|
+
* Add a point (smooth) where a press on the line is, and select it. Does nothing away from the line or when full.
|
|
71
|
+
*/
|
|
72
|
+
private onGraphDown;
|
|
73
|
+
/** Select a point and start dragging it */
|
|
74
|
+
private startDrag;
|
|
75
|
+
private onMove;
|
|
76
|
+
private onUp;
|
|
77
|
+
/** Move the selected point a grid unit with the arrow keys */
|
|
78
|
+
private onPointKeyDown;
|
|
79
|
+
/** Make the selected point a sharp corner or smooth */
|
|
80
|
+
setInterpolation(i: Interpolation): void;
|
|
81
|
+
/** Remove the selected point (not the first or last) */
|
|
82
|
+
removeSelected(): void;
|
|
83
|
+
render(): import("lit").TemplateResult<1>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { PcElement, defineElement, emit } from "../../base/index.js";
|
|
2
|
+
import { ICON_CONTROLS_CSS } from "../../styles/index.js";
|
|
3
|
+
import { iconLabel } from "../../icons/index.js";
|
|
4
|
+
import { Interpolation, linearPoints, profileY } from "../../../lib/profiles/index.js";
|
|
5
|
+
import { css, html, nothing, svg, unsafeCSS } from "lit";
|
|
6
|
+
// src/ui/elements/PcProfileGraph/index.ts
|
|
7
|
+
/** Space around the grid, in grid units, so points on its edges can be pressed */
|
|
8
|
+
var MARGIN = .6;
|
|
9
|
+
/** How close (in grid units) a press must be to the line to add a point there */
|
|
10
|
+
var LINE_REACH = .6;
|
|
11
|
+
/** Points are placed on multiples of this many grid units when dragged */
|
|
12
|
+
var SNAP = .1;
|
|
13
|
+
/** How many pieces the curve is drawn with */
|
|
14
|
+
var CURVE_STEPS = 100;
|
|
15
|
+
/** A value rounded to `SNAP` and kept within a range */
|
|
16
|
+
var snap = (value, min, max) => Math.min(max, Math.max(min, Math.round(value / SNAP) * SNAP));
|
|
17
|
+
/**
|
|
18
|
+
* The SVG path of a curve through points, drawn as short straight pieces (y is flipped: up is less negative).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* curvePath(linearPoints()); // "M0,10 L0.1,9.9 …"
|
|
22
|
+
*/
|
|
23
|
+
var curvePath = (points) => Array.from({ length: 101 }, (_, step) => {
|
|
24
|
+
const x = step / CURVE_STEPS * 10;
|
|
25
|
+
const y = Math.min(10, Math.max(0, -profileY(points, x)));
|
|
26
|
+
return `${step ? "L" : "M"}${+x.toFixed(3)},${+y.toFixed(3)}`;
|
|
27
|
+
}).join(" ");
|
|
28
|
+
/**
|
|
29
|
+
* Move a point within the grid, keeping it between its neighbours (and the first and last points at the edges), to
|
|
30
|
+
* multiples of 0.1. Returns new points.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* movePoint(points, 1, 4.04, -3); // points[1] at { x: 4, y: -3 }
|
|
34
|
+
*/
|
|
35
|
+
var movePoint = (points, index, x, y) => {
|
|
36
|
+
const last = points.length - 1;
|
|
37
|
+
const before = points[index - 1];
|
|
38
|
+
const after = points[index + 1];
|
|
39
|
+
const nextX = index === 0 ? 0 : index === last ? 10 : snap(x, before.x, after.x);
|
|
40
|
+
return points.map((point, i) => i === index ? {
|
|
41
|
+
...point,
|
|
42
|
+
x: nextX,
|
|
43
|
+
y: snap(y, -10, 0)
|
|
44
|
+
} : point);
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A profile graph to edit (see `@lvl99dotcom/paintcanvas/lib/profiles`): a 10 × 10 grid with the curve through its
|
|
48
|
+
* points, from the setting's min at the bottom left to its max at the top right.
|
|
49
|
+
*
|
|
50
|
+
* - Pressing the line adds a point there (smooth), selected.
|
|
51
|
+
* - Pressing a point selects it; dragging moves it (between its neighbours; the first and last stay at the edges).
|
|
52
|
+
* - With a point selected, the arrow keys move it a grid unit, and the buttons below make it a sharp corner (linear)
|
|
53
|
+
* or smooth (bezier cubic), or remove it (−; not the first or last).
|
|
54
|
+
*
|
|
55
|
+
* Points are focusable buttons (Tab moves between them; focusing one selects it). Fires `pc-change` (detail:
|
|
56
|
+
* `{ points }`) whenever the points change, after changing `points` itself.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* html`<pc-profile-graph .points=${points} @pc-change=${(e) => (points = e.detail.points)}></pc-profile-graph>`
|
|
60
|
+
*/
|
|
61
|
+
var PcProfileGraph = class extends PcElement {
|
|
62
|
+
static properties = {
|
|
63
|
+
...PcElement.properties,
|
|
64
|
+
points: { attribute: false },
|
|
65
|
+
selected: { type: Number },
|
|
66
|
+
label: { type: String }
|
|
67
|
+
};
|
|
68
|
+
static styles = [
|
|
69
|
+
PcElement.styles,
|
|
70
|
+
unsafeCSS(ICON_CONTROLS_CSS),
|
|
71
|
+
css`
|
|
72
|
+
:host {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: var(--pc-space-2);
|
|
77
|
+
color: var(--pc-text);
|
|
78
|
+
font-family: var(--pc-font);
|
|
79
|
+
}
|
|
80
|
+
svg {
|
|
81
|
+
width: 100%;
|
|
82
|
+
max-width: 300px;
|
|
83
|
+
aspect-ratio: 1;
|
|
84
|
+
touch-action: none;
|
|
85
|
+
cursor: crosshair;
|
|
86
|
+
user-select: none;
|
|
87
|
+
}
|
|
88
|
+
.grid {
|
|
89
|
+
stroke: var(--pc-border);
|
|
90
|
+
stroke-width: 0.03;
|
|
91
|
+
}
|
|
92
|
+
.frame {
|
|
93
|
+
fill: var(--pc-surface);
|
|
94
|
+
stroke: var(--pc-text);
|
|
95
|
+
stroke-width: 0.08;
|
|
96
|
+
}
|
|
97
|
+
.diagonal {
|
|
98
|
+
stroke: var(--pc-text-muted);
|
|
99
|
+
stroke-width: 0.03;
|
|
100
|
+
}
|
|
101
|
+
.area {
|
|
102
|
+
fill: var(--pc-surface-active);
|
|
103
|
+
}
|
|
104
|
+
.curve {
|
|
105
|
+
fill: none;
|
|
106
|
+
stroke: var(--pc-text);
|
|
107
|
+
stroke-width: 0.12;
|
|
108
|
+
}
|
|
109
|
+
.point {
|
|
110
|
+
fill: var(--pc-text);
|
|
111
|
+
stroke: var(--pc-surface);
|
|
112
|
+
stroke-width: 0.08;
|
|
113
|
+
cursor: grab;
|
|
114
|
+
outline: none;
|
|
115
|
+
}
|
|
116
|
+
.point.selected {
|
|
117
|
+
fill: var(--pc-accent);
|
|
118
|
+
stroke: var(--pc-text);
|
|
119
|
+
}
|
|
120
|
+
.point:focus-visible {
|
|
121
|
+
stroke: var(--pc-focus);
|
|
122
|
+
stroke-width: 0.15;
|
|
123
|
+
}
|
|
124
|
+
.buttons {
|
|
125
|
+
display: flex;
|
|
126
|
+
gap: var(--pc-space-2);
|
|
127
|
+
}
|
|
128
|
+
button.icon-button {
|
|
129
|
+
min-width: 32px;
|
|
130
|
+
min-height: 32px;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
align-items: center;
|
|
133
|
+
color: var(--pc-text);
|
|
134
|
+
font: inherit;
|
|
135
|
+
font-size: 20px;
|
|
136
|
+
line-height: 1;
|
|
137
|
+
}
|
|
138
|
+
`
|
|
139
|
+
];
|
|
140
|
+
/** The point being dragged */
|
|
141
|
+
dragging;
|
|
142
|
+
constructor() {
|
|
143
|
+
super();
|
|
144
|
+
this.points = linearPoints();
|
|
145
|
+
this.label = "";
|
|
146
|
+
}
|
|
147
|
+
/** Change the points and tell listeners */
|
|
148
|
+
setPoints(points) {
|
|
149
|
+
this.points = points;
|
|
150
|
+
emit(this, "pc-change", { points });
|
|
151
|
+
}
|
|
152
|
+
/** A pointer's position in grid units */
|
|
153
|
+
gridAt(event) {
|
|
154
|
+
const { left, top, width, height } = this.renderRoot.querySelector("svg").getBoundingClientRect();
|
|
155
|
+
const size = 10 + MARGIN * 2;
|
|
156
|
+
return {
|
|
157
|
+
x: (event.clientX - left) / (width || 1) * size - MARGIN,
|
|
158
|
+
y: -((event.clientY - top) / (height || 1) * size - MARGIN)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Add a point (smooth) where a press on the line is, and select it. Does nothing away from the line or when full.
|
|
163
|
+
*/
|
|
164
|
+
onGraphDown(event) {
|
|
165
|
+
if (event.button > 0 || this.points.length >= 32) return;
|
|
166
|
+
const { x, y } = this.gridAt(event);
|
|
167
|
+
if (x <= 0 || x >= 10) return;
|
|
168
|
+
if (Math.abs(profileY(this.points, x) - y) > LINE_REACH) return;
|
|
169
|
+
const index = this.points.findIndex((point) => point.x > x);
|
|
170
|
+
const added = {
|
|
171
|
+
x: snap(x, 0, 10),
|
|
172
|
+
y: snap(profileY(this.points, x), -10, 0),
|
|
173
|
+
i: Interpolation.BezierCubic
|
|
174
|
+
};
|
|
175
|
+
this.setPoints([
|
|
176
|
+
...this.points.slice(0, index),
|
|
177
|
+
added,
|
|
178
|
+
...this.points.slice(index)
|
|
179
|
+
]);
|
|
180
|
+
this.startDrag(event, index);
|
|
181
|
+
}
|
|
182
|
+
/** Select a point and start dragging it */
|
|
183
|
+
startDrag(event, index) {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
event.stopPropagation();
|
|
186
|
+
this.selected = index;
|
|
187
|
+
this.dragging = index;
|
|
188
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
189
|
+
}
|
|
190
|
+
onMove(event) {
|
|
191
|
+
if (this.dragging === void 0) return;
|
|
192
|
+
const { x, y } = this.gridAt(event);
|
|
193
|
+
this.setPoints(movePoint(this.points, this.dragging, x, y));
|
|
194
|
+
}
|
|
195
|
+
onUp() {
|
|
196
|
+
this.dragging = void 0;
|
|
197
|
+
}
|
|
198
|
+
/** Move the selected point a grid unit with the arrow keys */
|
|
199
|
+
onPointKeyDown(event, index) {
|
|
200
|
+
const move = {
|
|
201
|
+
ArrowLeft: [-1, 0],
|
|
202
|
+
ArrowRight: [1, 0],
|
|
203
|
+
ArrowUp: [0, 1],
|
|
204
|
+
ArrowDown: [0, -1]
|
|
205
|
+
}[event.key];
|
|
206
|
+
if (!move) return;
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
const point = this.points[index];
|
|
209
|
+
this.setPoints(movePoint(this.points, index, point.x + move[0], point.y + move[1]));
|
|
210
|
+
}
|
|
211
|
+
/** Make the selected point a sharp corner or smooth */
|
|
212
|
+
setInterpolation(i) {
|
|
213
|
+
const index = this.selected;
|
|
214
|
+
if (index === void 0) return;
|
|
215
|
+
this.setPoints(this.points.map((point, at) => at === index ? {
|
|
216
|
+
...point,
|
|
217
|
+
i
|
|
218
|
+
} : point));
|
|
219
|
+
}
|
|
220
|
+
/** Remove the selected point (not the first or last) */
|
|
221
|
+
removeSelected() {
|
|
222
|
+
const index = this.selected;
|
|
223
|
+
if (!index || index >= this.points.length - 1) return;
|
|
224
|
+
this.selected = void 0;
|
|
225
|
+
this.setPoints(this.points.filter((_, at) => at !== index));
|
|
226
|
+
}
|
|
227
|
+
render() {
|
|
228
|
+
const { points } = this;
|
|
229
|
+
const selected = this.selected === void 0 ? void 0 : points[this.selected];
|
|
230
|
+
const inner = this.selected !== void 0 && this.selected > 0 && this.selected < points.length - 1;
|
|
231
|
+
const curve = curvePath(points);
|
|
232
|
+
const lines = Array.from({ length: 9 }, (_, i) => i + 1);
|
|
233
|
+
const size = 10 + MARGIN * 2;
|
|
234
|
+
return html`<svg
|
|
235
|
+
part="graph"
|
|
236
|
+
viewBox="${-.6} ${-.6} ${size} ${size}"
|
|
237
|
+
role="group"
|
|
238
|
+
aria-label=${this.label || nothing}
|
|
239
|
+
@pointerdown=${this.onGraphDown}
|
|
240
|
+
@pointermove=${this.onMove}
|
|
241
|
+
@pointerup=${this.onUp}
|
|
242
|
+
@pointercancel=${this.onUp}
|
|
243
|
+
>
|
|
244
|
+
<rect
|
|
245
|
+
class="frame"
|
|
246
|
+
x="0"
|
|
247
|
+
y="0"
|
|
248
|
+
width=${10}
|
|
249
|
+
height=${10}
|
|
250
|
+
></rect>
|
|
251
|
+
<path
|
|
252
|
+
class="area"
|
|
253
|
+
d="${curve} L${10},${10} L0,${10} Z"
|
|
254
|
+
></path>
|
|
255
|
+
${lines.map((n) => svg`<line class="grid" x1=${n} y1="0" x2=${n} y2=${10}></line><line class="grid" x1="0" y1=${n} x2=${10} y2=${n}></line>`)}
|
|
256
|
+
<line
|
|
257
|
+
class="diagonal"
|
|
258
|
+
x1="0"
|
|
259
|
+
y1=${10}
|
|
260
|
+
x2=${10}
|
|
261
|
+
y2="0"
|
|
262
|
+
></line>
|
|
263
|
+
<path class="curve" d=${curve}></path>
|
|
264
|
+
${points.map((point, index) => svg`<circle
|
|
265
|
+
class="point ${index === this.selected ? "selected" : ""}"
|
|
266
|
+
cx=${point.x}
|
|
267
|
+
cy=${-point.y}
|
|
268
|
+
r="0.35"
|
|
269
|
+
tabindex="0"
|
|
270
|
+
role="button"
|
|
271
|
+
aria-pressed=${index === this.selected ? "true" : "false"}
|
|
272
|
+
aria-label=${this.t("paintCanvas.profileGraph.point", {
|
|
273
|
+
number: index + 1,
|
|
274
|
+
x: point.x,
|
|
275
|
+
y: point.y
|
|
276
|
+
})}
|
|
277
|
+
@focus=${() => this.selected = index}
|
|
278
|
+
@pointerdown=${(event) => this.startDrag(event, index)}
|
|
279
|
+
@keydown=${(event) => this.onPointKeyDown(event, index)}
|
|
280
|
+
></circle>`)}
|
|
281
|
+
</svg>
|
|
282
|
+
<div class="buttons" part="buttons">
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
class="icon-button remove"
|
|
286
|
+
title=${this.t("paintCanvas.profileGraph.remove")}
|
|
287
|
+
aria-label=${this.t("paintCanvas.profileGraph.remove")}
|
|
288
|
+
?disabled=${!inner}
|
|
289
|
+
@click=${() => this.removeSelected()}
|
|
290
|
+
>
|
|
291
|
+
−
|
|
292
|
+
</button>
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
class="icon-button linear"
|
|
296
|
+
title=${this.t("paintCanvas.profileGraph.linear")}
|
|
297
|
+
aria-pressed=${selected?.i === Interpolation.Linear ? "true" : "false"}
|
|
298
|
+
?disabled=${!selected}
|
|
299
|
+
@click=${() => this.setInterpolation(Interpolation.Linear)}
|
|
300
|
+
>
|
|
301
|
+
${iconLabel(this.iconUrl("smoothingLinear"), this.t("paintCanvas.profileGraph.linear"))}
|
|
302
|
+
</button>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
class="icon-button bezier"
|
|
306
|
+
title=${this.t("paintCanvas.profileGraph.bezier")}
|
|
307
|
+
aria-pressed=${selected?.i === Interpolation.BezierCubic ? "true" : "false"}
|
|
308
|
+
?disabled=${!selected}
|
|
309
|
+
@click=${() => this.setInterpolation(Interpolation.BezierCubic)}
|
|
310
|
+
>
|
|
311
|
+
${iconLabel(this.iconUrl("smoothingBezierCubic"), this.t("paintCanvas.profileGraph.bezier"))}
|
|
312
|
+
</button>
|
|
313
|
+
</div>`;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
defineElement("pc-profile-graph", PcProfileGraph);
|
|
317
|
+
export { PcProfileGraph, curvePath, movePoint };
|