@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.
Files changed (97) hide show
  1. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  2. package/dist/lib/brushes/configRanges/index.js +31 -1
  3. package/dist/lib/brushes/params/index.d.ts +2 -1
  4. package/dist/lib/brushes/params/index.js +7 -4
  5. package/dist/lib/brushes/types/index.d.ts +8 -0
  6. package/dist/lib/classes/Layer/index.d.ts +10 -0
  7. package/dist/lib/classes/Layer/index.js +15 -1
  8. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  9. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  10. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  11. package/dist/lib/classes/LayerStack/index.js +26 -2
  12. package/dist/lib/classes/PaintCanvas/index.d.ts +26 -3
  13. package/dist/lib/classes/PaintCanvas/index.js +39 -12
  14. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  15. package/dist/lib/classes/PngEncoder/index.js +104 -0
  16. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  17. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  18. package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
  19. package/dist/lib/classes/SettingsAutosave/index.js +2 -1
  20. package/dist/lib/messages/schema.d.ts +720 -0
  21. package/dist/lib/presets/schema.d.ts +360 -0
  22. package/dist/lib/presets/schema.js +8 -1
  23. package/dist/lib/profiles/index.d.ts +61 -0
  24. package/dist/lib/profiles/index.js +131 -0
  25. package/dist/lib/profiles/schema.d.ts +73 -0
  26. package/dist/lib/profiles/schema.js +53 -0
  27. package/dist/lib/rendering/sample/index.d.ts +11 -0
  28. package/dist/lib/rendering/sample/index.js +36 -5
  29. package/dist/lib/settings/index.d.ts +2 -1
  30. package/dist/lib/settings/index.js +4 -1
  31. package/dist/lib/settings/schema.d.ts +49 -0
  32. package/dist/lib/settings/schema.js +6 -0
  33. package/dist/lib/validation/validators.generated.js +13064 -3696
  34. package/dist/locales/en-GB.js +30 -0
  35. package/dist/locales/en-US.js +30 -0
  36. package/dist/locales/fr.js +30 -0
  37. package/dist/locales/ja.js +30 -0
  38. package/dist/locales/ko.js +30 -0
  39. package/dist/locales/zh-Hans.js +30 -0
  40. package/dist/locales/zh-Hant.js +30 -0
  41. package/dist/package.js +1 -1
  42. package/dist/ui/ask/index.d.ts +48 -0
  43. package/dist/ui/ask/index.js +67 -0
  44. package/dist/ui/base/index.d.ts +8 -1
  45. package/dist/ui/base/index.js +19 -4
  46. package/dist/ui/elements/PaintCanvas/index.js +3 -1
  47. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  48. package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
  49. package/dist/ui/elements/PcAppSettings/index.js +21 -65
  50. package/dist/ui/elements/PcBrushConfig/index.d.ts +25 -14
  51. package/dist/ui/elements/PcBrushConfig/index.js +216 -146
  52. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  53. package/dist/ui/elements/PcBrushTest/index.js +7 -2
  54. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  55. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  56. package/dist/ui/elements/PcButton/index.js +18 -10
  57. package/dist/ui/elements/PcCanvas/index.d.ts +6 -1
  58. package/dist/ui/elements/PcCanvas/index.js +10 -3
  59. package/dist/ui/elements/PcCanvas/style.js +1 -1
  60. package/dist/ui/elements/PcClipboardTools/index.d.ts +5 -4
  61. package/dist/ui/elements/PcClipboardTools/index.js +12 -5
  62. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  63. package/dist/ui/elements/PcDialog/index.js +102 -51
  64. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  65. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  66. package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
  67. package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
  68. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  69. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  70. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  71. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  72. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  73. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  74. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  75. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  76. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  77. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  78. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  79. package/dist/ui/elements/PcSliderRange/index.js +307 -0
  80. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  81. package/dist/ui/elements/PcStats/index.js +29 -13
  82. package/dist/ui/elements/PcToolSettings/index.js +6 -6
  83. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  84. package/dist/ui/icons/index.d.ts +1 -1
  85. package/dist/ui/icons/index.js +2 -0
  86. package/dist/ui/index.d.ts +6 -1
  87. package/dist/ui/index.js +6 -1
  88. package/dist/ui/styles/index.d.ts +21 -1
  89. package/dist/ui/styles/index.js +73 -14
  90. package/locales/en-GB.json +30 -0
  91. package/locales/en-US.json +30 -0
  92. package/locales/fr.json +30 -0
  93. package/locales/ja.json +30 -0
  94. package/locales/ko.json +30 -0
  95. package/locales/zh-Hans.json +30 -0
  96. package/locales/zh-Hant.json +30 -0
  97. package/package.json +25 -23
@@ -1,10 +1,16 @@
1
1
  import style_default from "./style.js";
2
2
  import { ICON_CONTROLS_CSS, PILLS_CSS, WINDOWS_CSS } from "../../styles/index.js";
3
3
  import { iconLabel } from "../../icons/index.js";
4
+ import { askConfirm, askText, deletePresetQuestion } from "../../ask/index.js";
4
5
  import { PcElement, defineElement } from "../../base/index.js";
5
6
  import "../PcBrushTest/index.js";
7
+ import "../PcDialogWindow/index.js";
8
+ import "../PcSliderRange/index.js";
9
+ import "../PcProfileEditor/index.js";
10
+ import ProfileGraphs from "../../../lib/classes/ProfileGraphs/index.js";
11
+ import { isLinear } from "../../../lib/profiles/index.js";
6
12
  import { SmoothingType } from "../../../lib/brushes/types/index.js";
7
- import { PARAM_LIMITS, VARIATION_SOURCES, readRange, toggleSource, writeRange } from "../../../lib/brushes/configRanges/index.js";
13
+ import { PARAM_LIMITS, VARIATION_SOURCES, configKey, readRange, selectSource, writeRange } from "../../../lib/brushes/configRanges/index.js";
8
14
  import { SHAPE_ERRORS, ShapeImageError, readClipboardShape, readShapeImage } from "../../../lib/brushes/shapeImage/index.js";
9
15
  import { BUILT_IN_SHAPES, builtInShapeId } from "../../../lib/brushes/shapes/index.js";
10
16
  import "../../../lib/brushes/StrokeSampler/index.js";
@@ -36,6 +42,15 @@ var SOURCE_ICONS = {
36
42
  tilt: "tilt",
37
43
  randomise: "randomise"
38
44
  };
45
+ /** Translation keys of the varying settings' names */
46
+ var PARAM_LABELS = {
47
+ width: "paintCanvas.common.width",
48
+ height: "paintCanvas.common.height",
49
+ alpha: "paintCanvas.common.opacity",
50
+ rotation: "paintCanvas.brushConfig.rotation",
51
+ offsetX: "paintCanvas.brushConfig.offsetX",
52
+ offsetY: "paintCanvas.brushConfig.offsetY"
53
+ };
39
54
  /** Translation keys of the variation sources' names */
40
55
  var SOURCE_LABELS = {
41
56
  pressure: "paintCanvas.toolSettings.pressure",
@@ -53,21 +68,17 @@ var SHAPE_ERROR_KEYS = {
53
68
  };
54
69
  /** Largest stamp spacing in pixels */
55
70
  var MAX_SPACING = 200;
56
- /** Read a number input, or undefined if it isn't a number */
57
- var numberValue = (event) => {
58
- const value = event.target.valueAsNumber;
59
- return Number.isFinite(value) ? value : void 0;
60
- };
61
71
  /**
62
72
  * The brush (or eraser) configuration window: a testing area, the shape (pressing it picks a built-in shape from the
63
73
  * device's own list, which says "Custom" for an uploaded or pasted one; or upload or paste one), smoothing, width, height,
64
74
  * opacity, rotation, offsets, sample rate, spacing and snapping to the pixel grid. Width, height, opacity, rotation and the offsets can vary with
65
- * pressure, tilt and randomness; when any is on, the setting has a min and max.
75
+ * pressure, tilt and randomness; when any is on, the setting has a min and max, set with a `<pc-slider-range>`.
66
76
  *
67
77
  * Changes stay in the window until "Save" (fires `pc-save` with the settings, to apply to the current tool) or "Add to
68
78
  * presets" (saves them as one of the user's presets, asking before replacing one with the same name, and selects it).
69
- * "Delete preset…" shows when editing one of the user's presets. Fires `pc-close` when closed, and `pc-toast` with a
70
- * message after adding or deleting a preset, or when a shape can't be used. Styling is structural only.
79
+ * "Delete preset…" shows when editing one of the user's presets. It's a shaded `<pc-dialog-window>`, closed with its ⨉
80
+ * button or Escape (not a press outside it, which would lose the changes). Fires `pc-close` when closed, and `pc-toast`
81
+ * with a message after adding or deleting a preset, or when a shape can't be used.
71
82
  *
72
83
  * @example
73
84
  * html`<pc-brush-config .app=${app} kind="brush" .config=${app.brush.preset} @pc-save=${apply}></pc-brush-config>`
@@ -80,7 +91,8 @@ var PcBrushConfig = class extends PcElement {
80
91
  config: { attribute: false },
81
92
  editingId: { type: String },
82
93
  draft: { state: true },
83
- busy: { state: true }
94
+ busy: { state: true },
95
+ editingProfile: { state: true }
84
96
  };
85
97
  static styles = [
86
98
  PcElement.styles,
@@ -89,30 +101,21 @@ var PcBrushConfig = class extends PcElement {
89
101
  unsafeCSS(ICON_CONTROLS_CSS),
90
102
  unsafeCSS(PILLS_CSS)
91
103
  ];
92
- confirm = (message) => window.confirm(message);
93
- prompt = (message, value) => window.prompt(message, value);
104
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
105
+ confirm = (options) => askConfirm(this.renderRoot, options);
106
+ /** Asks the user for text, in a dialog (replaceable for tests) */
107
+ prompt = (options) => askText(this.renderRoot, options);
94
108
  /** Turns files into brush shapes (replaceable for tests) */
95
109
  readShape = readShapeImage;
96
110
  /**
97
111
  * Reads a brush shape from the clipboard, or else from image data copied on the canvas (replaceable for tests)
98
112
  */
99
113
  readClipboard = () => readClipboardShape(navigator.clipboard, {}, async () => this.app?.copiedPng());
100
- onKeyDown = (event) => {
101
- if (event.key === "Escape") this.close();
102
- };
103
114
  constructor() {
104
115
  super();
105
116
  this.kind = "brush";
106
117
  this.busy = false;
107
118
  }
108
- connectedCallback() {
109
- super.connectedCallback();
110
- window.addEventListener("keydown", this.onKeyDown);
111
- }
112
- disconnectedCallback() {
113
- super.disconnectedCallback();
114
- window.removeEventListener("keydown", this.onKeyDown);
115
- }
116
119
  willUpdate(changed) {
117
120
  super.willUpdate(changed);
118
121
  if (changed.has("config") && this.config) this.draft = structuredClone(this.config);
@@ -190,10 +193,22 @@ var PcBrushConfig = class extends PcElement {
190
193
  const app = this.app;
191
194
  const draft = this.draft;
192
195
  if (!app || !draft) return;
193
- const name = this.prompt(this.t("paintCanvas.brushConfig.presetName"), draft.name ?? "")?.trim();
196
+ const name = (await this.prompt({
197
+ heading: this.t("paintCanvas.brushConfig.addToPresets"),
198
+ label: this.t("paintCanvas.brushConfig.presetName"),
199
+ value: draft.name ?? "",
200
+ confirm: this.t("paintCanvas.common.save"),
201
+ cancel: this.t("paintCanvas.common.cancel")
202
+ }))?.trim();
194
203
  if (!name) return;
195
204
  const existing = app.customPresets.findOwn(name, app.authorHandle, app.names.unknownAuthor);
196
- if (existing && !this.confirm(this.t("paintCanvas.presets.replaceConfirm", { name }))) return;
205
+ if (existing && !await this.confirm({
206
+ heading: this.t("paintCanvas.presets.replaceHeading"),
207
+ message: this.t("paintCanvas.presets.replaceConfirm", { name }),
208
+ confirm: this.t("paintCanvas.presets.replace"),
209
+ cancel: this.t("paintCanvas.common.cancel"),
210
+ cancelling: true
211
+ })) return;
197
212
  const preset = {
198
213
  ...app.newPreset(draft, name),
199
214
  ...existing ? { id: existing.id } : {}
@@ -224,7 +239,7 @@ var PcBrushConfig = class extends PcElement {
224
239
  const id = this.editingId;
225
240
  if (!app || !id) return;
226
241
  const name = app.customPresets.get(id)?.name ?? "this preset";
227
- if (!this.confirm(this.t("paintCanvas.presets.deleteConfirm", { name }))) return;
242
+ if (!await this.confirm(deletePresetQuestion(this.translations, name))) return;
228
243
  this.busy = true;
229
244
  try {
230
245
  await app.customPresets.remove(id);
@@ -242,32 +257,22 @@ var PcBrushConfig = class extends PcElement {
242
257
  this.busy = false;
243
258
  }
244
259
  }
245
- /** Render a number input */
246
- number(className, value, limits, onChange, disabled = false) {
247
- return html`<input
248
- type="number"
249
- class=${className}
250
- min=${limits.min}
251
- max=${limits.max}
252
- ?disabled=${disabled || this.busy}
253
- .value=${String(Math.round(value))}
254
- @change=${(e) => {
255
- const next = numberValue(e);
256
- if (next !== void 0) onChange(next);
257
- }}
258
- />`;
259
- }
260
- /** Render a range slider */
261
- slider(className, value, limits, onChange, disabled = false) {
262
- return html`<input
263
- type="range"
260
+ /**
261
+ * Render a slider for one value (a single `<pc-slider-range>`), the same as the ranges of settings that vary.
262
+ */
263
+ single(className, label, value, limits, onChange, disabled = false) {
264
+ const changed = (event) => onChange(event.detail.value);
265
+ return html`<pc-slider-range
266
+ single
264
267
  class=${className}
268
+ label=${label}
265
269
  min=${limits.min}
266
270
  max=${limits.max}
271
+ .value=${live(Math.round(value))}
267
272
  ?disabled=${disabled || this.busy}
268
- .value=${String(value)}
269
- @input=${(e) => onChange(e.target.valueAsNumber)}
270
- />`;
273
+ @pc-input=${changed}
274
+ @pc-change=${changed}
275
+ ></pc-slider-range>`;
271
276
  }
272
277
  /** Render a setting that can vary: its value (or min and max), and its pressure, tilt and randomise icon toggles */
273
278
  renderParam(draft, param, label, disabled = false) {
@@ -275,31 +280,61 @@ var PcBrushConfig = class extends PcElement {
275
280
  const limits = PARAM_LIMITS[param];
276
281
  const isOffset = param === "offsetX" || param === "offsetY";
277
282
  const write = (values) => this.changeParam(param, writeRange(draft, param, values));
278
- const values = range.varies ? html`${this.number(`${param} min`, range.min, limits, (min) => write({ min }), disabled)}
279
- ${this.slider(`${param} min`, range.min, limits, (min) => write({ min }), disabled)}
280
- ${this.slider(`${param} max`, range.max, limits, (max) => write({ max }), disabled)}
281
- ${this.number(`${param} max`, range.max, limits, (max) => write({ max }), disabled)}` : html`${this.number(param, range.value, limits, (value) => write({ value }), disabled || isOffset)}
282
- ${this.slider(param, range.value, limits, (value) => write({ value }), disabled || isOffset)}`;
283
+ const writeEnds = (event) => write({
284
+ min: event.detail.low,
285
+ max: event.detail.high
286
+ });
287
+ const values = range.varies ? html`<pc-slider-range
288
+ class=${`${param} range`}
289
+ label=${label}
290
+ min=${limits.min}
291
+ max=${limits.max}
292
+ .low=${live(Math.round(range.min))}
293
+ .high=${live(Math.round(range.max))}
294
+ ?disabled=${disabled || this.busy}
295
+ @pc-input=${writeEnds}
296
+ @pc-change=${writeEnds}
297
+ ></pc-slider-range>` : this.single(param, label, range.value, limits, (value) => write({ value }), disabled || isOffset);
298
+ const active = VARIATION_SOURCES.find((source) => range.sources[source]);
299
+ const select = (source) => this.changeParam(param, selectSource(draft, param, source));
283
300
  return html`<div class="row" data-param=${param}>
284
301
  <span class="label">${label}</span>
285
302
  ${values}
286
- ${VARIATION_SOURCES.map((source) => html`<label
287
- class="icon-toggle"
288
- title=${this.t(SOURCE_LABELS[source])}
289
- >
290
- <input
291
- type="checkbox"
292
- class=${`${param} ${source}`}
293
- aria-label=${this.t("paintCanvas.brushConfig.variation", {
303
+ <fieldset class="sources" ?disabled=${disabled || this.busy}>
304
+ <legend class="visually-hidden">
305
+ ${this.t("paintCanvas.brushConfig.varyWith", { param: label })}
306
+ </legend>
307
+ ${VARIATION_SOURCES.map((source) => html`<label
308
+ class="icon-toggle"
309
+ title=${this.t(SOURCE_LABELS[source])}
310
+ >
311
+ <input
312
+ type="radio"
313
+ name=${`${param}-source`}
314
+ class=${`${param} ${source}`}
315
+ aria-label=${this.t("paintCanvas.brushConfig.variation", {
294
316
  param: label,
295
317
  source: this.t(SOURCE_LABELS[source])
296
318
  })}
297
- .checked=${range.sources[source]}
298
- ?disabled=${disabled || this.busy}
299
- @change=${(e) => this.changeParam(param, toggleSource(draft, param, source, e.target.checked))}
300
- />
301
- ${iconLabel(this.iconUrl(SOURCE_ICONS[source]), this.t(SOURCE_LABELS[source]))}
302
- </label>`)}
319
+ .checked=${live(active === source)}
320
+ @change=${() => select(source)}
321
+ @click=${() => {
322
+ if (active === source) select(void 0);
323
+ }}
324
+ />
325
+ ${iconLabel(this.iconUrl(SOURCE_ICONS[source]), this.t(SOURCE_LABELS[source]))}
326
+ </label>`)}
327
+ </fieldset>
328
+ <button
329
+ type="button"
330
+ class=${`icon-button profile ${param} ${draft[configKey(param)]?.profile ? "configured" : ""}`}
331
+ title=${this.t("paintCanvas.profileEditor.button", { param: label })}
332
+ aria-haspopup="dialog"
333
+ ?disabled=${!active || disabled || this.busy}
334
+ @click=${() => this.editingProfile = param}
335
+ >
336
+ ${iconLabel(this.iconUrl("profileGraph"), this.t("paintCanvas.profileEditor.button", { param: label }))}
337
+ </button>
303
338
  </div>`;
304
339
  }
305
340
  /**
@@ -406,13 +441,17 @@ var PcBrushConfig = class extends PcElement {
406
441
  min: 0,
407
442
  max: MAX_SPACING
408
443
  };
409
- return html`<div
410
- role="dialog"
411
- aria-modal="true"
412
- aria-label=${this.t(this.kind === "eraser" ? "paintCanvas.brushConfig.eraserTitle" : "paintCanvas.brushConfig.brushTitle")}
413
- >
414
- <div class="row header">
415
- <label>
444
+ return html`<pc-dialog-window
445
+ open
446
+ shaded
447
+ dismissible="false"
448
+ label=${this.t(this.kind === "eraser" ? "paintCanvas.brushConfig.eraserTitle" : "paintCanvas.brushConfig.brushTitle")}
449
+ @pc-dismiss=${(event) => {
450
+ event.preventDefault();
451
+ this.close();
452
+ }}
453
+ >
454
+ <label slot="header-start" class="name-field">
416
455
  ${this.t("paintCanvas.brushConfig.name")}
417
456
  <input
418
457
  class="name"
@@ -421,86 +460,117 @@ var PcBrushConfig = class extends PcElement {
421
460
  @input=${(e) => this.change({ name: e.target.value })}
422
461
  />
423
462
  </label>
424
- <button
425
- type="button"
426
- class="close dismiss"
427
- @click=${() => this.close()}
428
- >
429
- ${this.t("paintCanvas.common.dismiss")}
430
- </button>
431
- </div>
432
- <pc-brush-test .config=${draft} .tips=${this.app?.tips}></pc-brush-test>
433
- ${this.renderShape(draft)}
434
- ${this.renderParam(draft, "width", this.t("paintCanvas.common.width"))}
435
- <div class="row">
436
- <span class="label"></span>
437
- <label
438
- class="icon-toggle"
439
- title=${this.t("paintCanvas.toolSettings.constrainProportions")}
440
- >
441
- <input
442
- type="checkbox"
443
- class="constrain"
444
- .checked=${constrained}
445
- ?disabled=${this.busy}
446
- @change=${(e) => this.change({ constrainProportions: e.target.checked })}
447
- />
448
- ${iconLabel(this.iconUrl(constrained ? "linked" : "unlinked"), this.t("paintCanvas.toolSettings.constrainProportions"))}
449
- </label>
450
- </div>
451
- ${this.renderParam(draft, "height", this.t("paintCanvas.common.height"), constrained)}
452
- ${this.renderParam(draft, "alpha", this.t("paintCanvas.common.opacity"))}
453
- ${this.renderParam(draft, "rotation", this.t("paintCanvas.brushConfig.rotation"))}
454
- ${this.renderParam(draft, "offsetX", this.t("paintCanvas.brushConfig.offsetX"))}
455
- ${this.renderParam(draft, "offsetY", this.t("paintCanvas.brushConfig.offsetY"))}
456
- <div class="row">
457
- <span class="label"
458
- >${this.t("paintCanvas.brushConfig.sampleRate")}</span
459
- >
460
- ${this.number("sample-rate", draft.sampleRate ?? 5, sampleLimits, (sampleRate) => this.change({ sampleRate }))}
461
- ${this.slider("sample-rate", draft.sampleRate ?? 5, sampleLimits, (sampleRate) => this.change({ sampleRate }))}
462
- <span class="label">${this.t("paintCanvas.brushConfig.spacing")}</span>
463
- ${this.number("spacing", draft.spacing ?? 5, spacingLimits, (spacing) => this.change({ spacing }))}
464
- ${this.slider("spacing", draft.spacing ?? 5, spacingLimits, (spacing) => this.change({ spacing }))}
465
- </div>
466
- <label>
467
- <input
468
- type="checkbox"
469
- class="snap"
470
- .checked=${draft.snapToPixelGrid ?? false}
471
- ?disabled=${this.busy}
472
- @change=${(e) => this.change({ snapToPixelGrid: e.target.checked })}
473
- />
474
- ${this.t("paintCanvas.brushConfig.snapToPixelGrid")}
475
- </label>
476
- <div class="row">
463
+ <div slot="body" class="settings">
464
+ <pc-brush-test
465
+ .config=${draft}
466
+ .tips=${this.app?.tips}
467
+ ></pc-brush-test>
468
+ ${this.renderShape(draft)}
469
+ ${this.renderParam(draft, "width", this.t(PARAM_LABELS.width))}
470
+ <div class="row">
471
+ <span class="label"></span>
472
+ <label
473
+ class="icon-toggle"
474
+ title=${this.t("paintCanvas.toolSettings.constrainProportions")}
475
+ >
476
+ <input
477
+ type="checkbox"
478
+ class="constrain"
479
+ .checked=${constrained}
480
+ ?disabled=${this.busy}
481
+ @change=${(e) => this.change({ constrainProportions: e.target.checked })}
482
+ />
483
+ ${iconLabel(this.iconUrl(constrained ? "linked" : "unlinked"), this.t("paintCanvas.toolSettings.constrainProportions"))}
484
+ </label>
485
+ </div>
486
+ ${this.renderParam(draft, "height", this.t(PARAM_LABELS.height), constrained)}
487
+ ${this.renderParam(draft, "alpha", this.t(PARAM_LABELS.alpha))}
488
+ ${this.renderParam(draft, "rotation", this.t(PARAM_LABELS.rotation))}
489
+ ${this.renderParam(draft, "offsetX", this.t(PARAM_LABELS.offsetX))}
490
+ ${this.renderParam(draft, "offsetY", this.t(PARAM_LABELS.offsetY))}
491
+ <div class="row">
492
+ <span class="label"
493
+ >${this.t("paintCanvas.brushConfig.sampleRate")}</span
494
+ >
495
+ ${this.single("sample-rate", this.t("paintCanvas.brushConfig.sampleRate"), draft.sampleRate ?? 5, sampleLimits, (sampleRate) => this.change({ sampleRate }))}
496
+ </div>
497
+ <div class="row">
498
+ <span class="label"
499
+ >${this.t("paintCanvas.brushConfig.spacing")}</span
500
+ >
501
+ ${this.single("spacing", this.t("paintCanvas.brushConfig.spacing"), draft.spacing ?? 5, spacingLimits, (spacing) => this.change({ spacing }))}
502
+ </div>
503
+ <label>
504
+ <input
505
+ type="checkbox"
506
+ class="snap"
507
+ .checked=${draft.snapToPixelGrid ?? false}
508
+ ?disabled=${this.busy}
509
+ @change=${(e) => this.change({ snapToPixelGrid: e.target.checked })}
510
+ />
511
+ ${this.t("paintCanvas.brushConfig.snapToPixelGrid")}
512
+ </label>
513
+ </div>
477
514
  ${this.editingId ? html`<button
515
+ slot="footer-start"
478
516
  type="button"
479
- class="delete"
517
+ class="cancel delete"
480
518
  ?disabled=${this.busy}
481
519
  @click=${() => void this.deletePreset()}
482
520
  >
483
521
  ${this.t("paintCanvas.brushConfig.deletePreset")}
484
522
  </button>` : nothing}
485
- <button
486
- type="button"
487
- class="add affirmative"
488
- ?disabled=${this.busy}
489
- @click=${() => void this.addToPresets()}
490
- >
491
- ${this.t("paintCanvas.brushConfig.addToPresets")}
492
- </button>
493
- <button
494
- type="button"
495
- class="save affirmative"
496
- ?disabled=${this.busy}
497
- @click=${() => this.save()}
498
- >
499
- ${this.t("paintCanvas.common.save")}
500
- </button>
501
- </div>
502
- </div>`;
523
+ <div slot="footer-end" class="actions">
524
+ <button
525
+ type="button"
526
+ class="add affirmative"
527
+ ?disabled=${this.busy}
528
+ @click=${() => void this.addToPresets()}
529
+ >
530
+ ${this.t("paintCanvas.brushConfig.addToPresets")}
531
+ </button>
532
+ <button
533
+ type="button"
534
+ class="save affirmative"
535
+ ?disabled=${this.busy}
536
+ @click=${() => this.save()}
537
+ >
538
+ ${this.t("paintCanvas.common.save")}
539
+ </button>
540
+ </div>
541
+ </pc-dialog-window>
542
+ ${this.renderProfileEditor(draft)}`;
543
+ }
544
+ /** The profile graph editor for the setting being edited, while open */
545
+ renderProfileEditor(draft) {
546
+ const param = this.editingProfile;
547
+ if (!param) return nothing;
548
+ const key = configKey(param);
549
+ const variation = draft[key];
550
+ return html`<pc-profile-editor
551
+ .profiles=${this.app?.profiles ?? this.profiles}
552
+ .points=${variation.profile}
553
+ profile-id=${variation.profileId ?? (variation.profile ? nothing : "linear")}
554
+ label=${this.t(PARAM_LABELS[param])}
555
+ @pc-save=${(event) => {
556
+ event.stopPropagation();
557
+ const { points, profileId } = event.detail;
558
+ const linear = isLinear(points);
559
+ this.changeParam(param, { [key]: {
560
+ ...variation,
561
+ profile: linear ? void 0 : points,
562
+ profileId: linear ? void 0 : profileId
563
+ } });
564
+ this.editingProfile = void 0;
565
+ }}
566
+ @pc-close=${(event) => {
567
+ event.stopPropagation();
568
+ this.editingProfile = void 0;
569
+ }}
570
+ ></pc-profile-editor>`;
503
571
  }
572
+ /** Profile graphs, when there's no canvas to keep them */
573
+ profiles = new ProfileGraphs();
504
574
  };
505
575
  defineElement("pc-brush-config", PcBrushConfig);
506
576
  export { PcBrushConfig, SMOOTHING_CHOICES };
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcBrushConfig/style.css?inline
2
- var style_default = ":host {\n position: fixed;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--pc-backdrop);\n}\n[role=\"dialog\"] {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: min(560px, 95vw);\n max-height: 95vh;\n overflow: auto;\n padding: 8px;\n background: var(--pc-surface);\n}\n.row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n}\n.row > .label {\n width: 6em;\n}\ninput[type=\"number\"] {\n width: 4.5em;\n}\ninput[type=\"range\"] {\n flex: 1;\n min-width: 60px;\n}\n.shape-preview {\n display: inline-block;\n width: 40px;\n height: 40px;\n background: var(--pc-text);\n border-radius: 50%;\n}\n.shape-preview.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n}\n/* The name, with \"Close\" on the right */\n.header .close {\n margin-left: auto;\n}\n/* The shape (preview, Upload and Paste) and the smoothing icons on one row */\n.shape-row {\n flex-wrap: nowrap;\n}\n/* The shape, with an invisible list over it: pressing the shape opens the device's own list of shapes */\n.shape-picker {\n position: relative;\n display: inline-flex;\n flex: none;\n border-radius: var(--pc-radius-small);\n}\n.shape-picker:hover {\n background: var(--pc-surface-hover);\n}\n.shape-picker:focus-within {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.shape-select {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\nfieldset.smoothing {\n display: flex;\n align-items: center;\n gap: 4px;\n margin: 0 0 0 auto;\n padding: 0;\n border: 0;\n}\nfieldset.smoothing legend {\n float: left;\n margin-right: 4px;\n padding: 0;\n}\n";
2
+ var style_default = ":host {\n display: contents;\n}\npc-dialog-window {\n --pc-dialog-width: 560px;\n}\n.settings {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.name-field {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.actions {\n display: flex;\n gap: 6px;\n}\n.row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n}\n.row > .label {\n width: 6em;\n}\ninput[type=\"number\"] {\n width: 4.5em;\n}\ninput[type=\"range\"] {\n flex: 1;\n min-width: 60px;\n}\npc-slider-range {\n flex: 1;\n min-width: 200px;\n}\n.shape-preview {\n display: inline-block;\n width: 40px;\n height: 40px;\n background: var(--pc-text);\n border-radius: 50%;\n}\n.shape-preview.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n}\n/* The shape (preview, Upload and Paste) and the smoothing icons on one row */\n.shape-row {\n flex-wrap: nowrap;\n}\n/* The shape, with an invisible list over it: pressing the shape opens the device's own list of shapes */\n.shape-picker {\n position: relative;\n display: inline-flex;\n flex: none;\n border-radius: var(--pc-radius-small);\n}\n.shape-picker:hover {\n background: var(--pc-surface-hover);\n}\n.shape-picker:focus-within {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.shape-select {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\nfieldset.smoothing {\n display: flex;\n align-items: center;\n gap: 4px;\n margin: 0 0 0 auto;\n padding: 0;\n border: 0;\n}\nfieldset.smoothing legend {\n float: left;\n margin-right: 4px;\n padding: 0;\n}\n/* Pressure, tilt and randomise: one at a time, side by side */\nfieldset.sources {\n display: flex;\n gap: 4px;\n margin: 0;\n padding: 0;\n border: 0;\n}\n/* A setting with a profile graph: its button is purple */\nbutton.icon-button.profile.configured {\n border-color: var(--pc-accent);\n background: var(--pc-accent);\n}\n";
3
3
  export { style_default as default };
@@ -1,4 +1,5 @@
1
1
  import style_default from "./style.js";
2
+ import { PILLS_CSS } from "../../styles/index.js";
2
3
  import { PcElement, defineElement } from "../../base/index.js";
3
4
  import StrokeEngine from "../../../lib/brushes/StrokeEngine/index.js";
4
5
  import Layer, { createOffscreenCanvas } from "../../../lib/classes/Layer/index.js";
@@ -38,7 +39,11 @@ var PcBrushTest = class extends PcElement {
38
39
  attribute: "show-points"
39
40
  }
40
41
  };
41
- static styles = [PcElement.styles, unsafeCSS(style_default)];
42
+ static styles = [
43
+ PcElement.styles,
44
+ unsafeCSS(PILLS_CSS),
45
+ unsafeCSS(style_default)
46
+ ];
42
47
  constructor() {
43
48
  super();
44
49
  this.showPoints = false;
@@ -185,7 +190,7 @@ var PcBrushTest = class extends PcElement {
185
190
  @pointercancel=${this.onPointerUp}
186
191
  ></canvas>
187
192
  <div class="actions">
188
- <button type="button" class="clear" @click=${() => this.clear()}>
193
+ <button type="button" class="pill clear" @click=${() => this.clear()}>
189
194
  ${this.t("paintCanvas.common.clear")}
190
195
  </button>
191
196
  <label class="show-points">
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcBrushTest/style.css?inline
2
- var style_default = ":host {\n display: block;\n}\ncanvas {\n display: block;\n width: 100%;\n background: #fff;\n border: 1px solid var(--pc-border);\n touch-action: none;\n}\n/* \"Clear\", then \"Show stroke points\" */\n.actions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 12px;\n margin-top: 6px;\n}\n.show-points {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n cursor: pointer;\n}\n/* \"Clear\": a red pill */\nbutton.clear {\n padding: 4px 16px;\n border: 0;\n border-radius: 999px;\n background: var(--pc-red);\n color: #ffffff;\n font: inherit;\n font-weight: bold;\n cursor: pointer;\n}\nbutton.clear:hover {\n filter: brightness(1.1);\n}\nbutton.clear:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 2px;\n}\n";
2
+ var style_default = ":host {\n display: block;\n}\ncanvas {\n display: block;\n width: 100%;\n background: #fff;\n border: 1px solid var(--pc-border);\n touch-action: none;\n}\n/* \"Clear\", then \"Show stroke points\" */\n.actions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 12px;\n margin-top: 6px;\n}\n.show-points {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n cursor: pointer;\n}\n/* \"Clear\": a red pill */\nbutton.pill.clear {\n background: var(--pc-red);\n color: #ffffff;\n}\n";
3
3
  export { style_default as default };
@@ -1,14 +1,17 @@
1
1
  import { PcElement } from "../../base";
2
+ import { type PillSize } from "../../styles";
2
3
  import "../PcIcon";
3
4
  /** How a button looks */
4
5
  export type ButtonVariant = "accent" | "cancel" | "dismiss" | "pill" | "icon";
5
6
  /**
6
- * A button: affirmative (`accent`, the default), cancelling (`cancel`), a quiet text button (`dismiss`), a small pill
7
- * (`pill`), or a boxed icon button (`icon`, which needs a `label` for screen readers). Its text goes in its content.
8
- * Clicks reach listeners on the element; a disabled button sends none.
7
+ * A button: affirmative (`accent`, the default), cancelling (`cancel`), a quiet text button (`dismiss`), a pill (`pill`,
8
+ * the same as `accent`), or a boxed icon button (`icon`, which needs a `label` for screen readers). Its text goes in its
9
+ * content. All but icon buttons are pills, of the `size` given (`pill` by default; see `PILL_SIZE_STYLES`). Clicks
10
+ * reach listeners on the element; a disabled button sends none.
9
11
  *
10
12
  * @example
11
13
  * html`<pc-button variant="cancel" @click=${close}>Cancel</pc-button>`
14
+ * html`<pc-button size="pill-l">Start</pc-button>`
12
15
  * html`<pc-button variant="icon" icon="undo" label="Undo"></pc-button>`
13
16
  */
14
17
  export declare class PcButton extends PcElement {
@@ -17,6 +20,10 @@ export declare class PcButton extends PcElement {
17
20
  type: StringConstructor;
18
21
  reflect: boolean;
19
22
  };
23
+ size: {
24
+ type: StringConstructor;
25
+ reflect: boolean;
26
+ };
20
27
  icon: {
21
28
  type: StringConstructor;
22
29
  };
@@ -43,6 +50,8 @@ export declare class PcButton extends PcElement {
43
50
  };
44
51
  static styles: import("lit").CSSResultGroup[];
45
52
  variant: ButtonVariant;
53
+ /** How big the pill is (not for icon buttons) */
54
+ size: PillSize;
46
55
  /** Name of an icon to show before the text (or alone, for an icon button) */
47
56
  icon?: string;
48
57
  /** The button's name for screen readers, when its content doesn't say it (e.g. an icon button) */
@@ -1,14 +1,19 @@
1
1
  import { PcElement, defineElement } from "../../base/index.js";
2
+ import { PILL_SIZES, pillSizeDeclarations } from "../../styles/index.js";
2
3
  import "../PcIcon/index.js";
3
- import { css, html, nothing } from "lit";
4
+ import { css, html, nothing, unsafeCSS } from "lit";
4
5
  // src/ui/elements/PcButton/index.ts
6
+ /** Each pill size, for the buttons that are pills (all but icon buttons) */
7
+ var SIZE_STYLES = unsafeCSS(PILL_SIZES.map((size) => `:host([size="${size}"]) button { ${pillSizeDeclarations(size)} }`).join("\n"));
5
8
  /**
6
- * A button: affirmative (`accent`, the default), cancelling (`cancel`), a quiet text button (`dismiss`), a small pill
7
- * (`pill`), or a boxed icon button (`icon`, which needs a `label` for screen readers). Its text goes in its content.
8
- * Clicks reach listeners on the element; a disabled button sends none.
9
+ * A button: affirmative (`accent`, the default), cancelling (`cancel`), a quiet text button (`dismiss`), a pill (`pill`,
10
+ * the same as `accent`), or a boxed icon button (`icon`, which needs a `label` for screen readers). Its text goes in its
11
+ * content. All but icon buttons are pills, of the `size` given (`pill` by default; see `PILL_SIZE_STYLES`). Clicks
12
+ * reach listeners on the element; a disabled button sends none.
9
13
  *
10
14
  * @example
11
15
  * html`<pc-button variant="cancel" @click=${close}>Cancel</pc-button>`
16
+ * html`<pc-button size="pill-l">Start</pc-button>`
12
17
  * html`<pc-button variant="icon" icon="undo" label="Undo"></pc-button>`
13
18
  */
14
19
  var PcButton = class extends PcElement {
@@ -18,6 +23,10 @@ var PcButton = class extends PcElement {
18
23
  type: String,
19
24
  reflect: true
20
25
  },
26
+ size: {
27
+ type: String,
28
+ reflect: true
29
+ },
21
30
  icon: { type: String },
22
31
  label: { type: String },
23
32
  disabled: {
@@ -38,9 +47,8 @@ var PcButton = class extends PcElement {
38
47
  flex: 1;
39
48
  align-items: center;
40
49
  justify-content: center;
50
+ box-sizing: border-box;
41
51
  gap: var(--pc-space-1);
42
- min-height: var(--pc-control-size);
43
- padding: var(--pc-space-1) var(--pc-space-3);
44
52
  border: 0;
45
53
  /* A joined button group gives each button its corners */
46
54
  border-radius: var(--pc-group-radius, var(--pc-radius-pill));
@@ -48,10 +56,11 @@ var PcButton = class extends PcElement {
48
56
  color: var(--pc-accent-text);
49
57
  font: inherit;
50
58
  font-family: var(--pc-font);
51
- font-size: var(--pc-font-size);
59
+ ${unsafeCSS(pillSizeDeclarations("pill"))}
52
60
  font-weight: bold;
53
61
  cursor: pointer;
54
62
  }
63
+ ${SIZE_STYLES}
55
64
  button:hover:not(:disabled) {
56
65
  filter: brightness(1.15);
57
66
  }
@@ -76,12 +85,10 @@ var PcButton = class extends PcElement {
76
85
  filter: none;
77
86
  text-decoration: underline;
78
87
  }
79
- :host([variant="pill"]) button {
80
- min-height: 30px;
81
- }
82
88
  :host([variant="icon"]) button {
83
89
  min-height: 0;
84
90
  padding: 2px;
91
+ line-height: normal;
85
92
  border: 1px solid var(--pc-border);
86
93
  border-radius: var(--pc-group-radius, var(--pc-radius-small));
87
94
  background: var(--pc-surface);
@@ -101,6 +108,7 @@ var PcButton = class extends PcElement {
101
108
  constructor() {
102
109
  super();
103
110
  this.variant = "accent";
111
+ this.size = "pill";
104
112
  this.disabled = false;
105
113
  }
106
114
  /** Move keyboard focus to the button */