@lvl99dotcom/paintcanvas 0.5.0 → 0.7.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 (120) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  3. package/dist/lib/brushes/configRanges/index.js +31 -1
  4. package/dist/lib/brushes/params/index.d.ts +2 -1
  5. package/dist/lib/brushes/params/index.js +7 -4
  6. package/dist/lib/brushes/types/index.d.ts +8 -0
  7. package/dist/lib/classes/Layer/index.d.ts +10 -0
  8. package/dist/lib/classes/Layer/index.js +15 -1
  9. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  10. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  11. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  12. package/dist/lib/classes/LayerStack/index.js +26 -2
  13. package/dist/lib/classes/PaintCanvas/index.d.ts +36 -5
  14. package/dist/lib/classes/PaintCanvas/index.js +45 -14
  15. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  16. package/dist/lib/classes/PngEncoder/index.js +104 -0
  17. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  18. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  19. package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
  20. package/dist/lib/classes/SettingsAutosave/index.js +2 -1
  21. package/dist/lib/classes/UiSettings/index.d.ts +24 -2
  22. package/dist/lib/classes/UiSettings/index.js +32 -7
  23. package/dist/lib/messages/schema.d.ts +720 -0
  24. package/dist/lib/presets/schema.d.ts +360 -0
  25. package/dist/lib/presets/schema.js +8 -1
  26. package/dist/lib/profiles/index.d.ts +61 -0
  27. package/dist/lib/profiles/index.js +131 -0
  28. package/dist/lib/profiles/schema.d.ts +73 -0
  29. package/dist/lib/profiles/schema.js +53 -0
  30. package/dist/lib/rendering/sample/index.d.ts +11 -0
  31. package/dist/lib/rendering/sample/index.js +36 -5
  32. package/dist/lib/settings/index.d.ts +2 -1
  33. package/dist/lib/settings/index.js +4 -1
  34. package/dist/lib/settings/schema.d.ts +53 -1
  35. package/dist/lib/settings/schema.js +9 -2
  36. package/dist/lib/storage/index.d.ts +26 -0
  37. package/dist/lib/storage/index.js +38 -1
  38. package/dist/lib/validation/validators.generated.js +13061 -3675
  39. package/dist/lib/version/index.d.ts +25 -0
  40. package/dist/lib/version/index.js +35 -0
  41. package/dist/locales/en-GB.js +35 -0
  42. package/dist/locales/en-US.js +35 -0
  43. package/dist/locales/fr.js +35 -0
  44. package/dist/locales/ja.js +35 -0
  45. package/dist/locales/ko.js +35 -0
  46. package/dist/locales/zh-Hans.js +35 -0
  47. package/dist/locales/zh-Hant.js +35 -0
  48. package/dist/package.js +1 -1
  49. package/dist/ui/ask/index.d.ts +48 -0
  50. package/dist/ui/ask/index.js +67 -0
  51. package/dist/ui/base/index.d.ts +8 -1
  52. package/dist/ui/base/index.js +19 -4
  53. package/dist/ui/elements/PaintCanvas/index.d.ts +13 -3
  54. package/dist/ui/elements/PaintCanvas/index.js +25 -6
  55. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  56. package/dist/ui/elements/PcAppSettings/index.d.ts +15 -7
  57. package/dist/ui/elements/PcAppSettings/index.js +53 -62
  58. package/dist/ui/elements/PcBrushConfig/index.d.ts +26 -14
  59. package/dist/ui/elements/PcBrushConfig/index.js +214 -145
  60. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  61. package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
  62. package/dist/ui/elements/PcBrushTest/index.js +15 -9
  63. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  64. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  65. package/dist/ui/elements/PcButton/index.js +18 -10
  66. package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
  67. package/dist/ui/elements/PcButtonMenu/index.js +125 -0
  68. package/dist/ui/elements/PcCanvas/index.d.ts +15 -1
  69. package/dist/ui/elements/PcCanvas/index.js +42 -19
  70. package/dist/ui/elements/PcCanvas/style.js +1 -1
  71. package/dist/ui/elements/PcClipboardTools/index.d.ts +11 -7
  72. package/dist/ui/elements/PcClipboardTools/index.js +30 -29
  73. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  74. package/dist/ui/elements/PcDialog/index.js +102 -51
  75. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  76. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  77. package/dist/ui/elements/PcInputSettings/index.d.ts +2 -1
  78. package/dist/ui/elements/PcInputSettings/index.js +9 -15
  79. package/dist/ui/elements/PcLayerItem/index.d.ts +21 -6
  80. package/dist/ui/elements/PcLayerItem/index.js +53 -26
  81. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  82. package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
  83. package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
  84. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  85. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  86. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  87. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  88. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  89. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  90. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  91. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  92. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  93. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  94. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  95. package/dist/ui/elements/PcSliderRange/index.js +308 -0
  96. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  97. package/dist/ui/elements/PcStats/index.js +29 -13
  98. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +13 -2
  99. package/dist/ui/elements/PcToggleCheckbox/index.js +27 -8
  100. package/dist/ui/elements/PcToolSettings/index.d.ts +1 -0
  101. package/dist/ui/elements/PcToolSettings/index.js +14 -14
  102. package/dist/ui/elements/PcVersion/index.d.ts +12 -0
  103. package/dist/ui/elements/PcVersion/index.js +43 -0
  104. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  105. package/dist/ui/icons/index.d.ts +1 -1
  106. package/dist/ui/icons/index.js +2 -0
  107. package/dist/ui/index.d.ts +9 -2
  108. package/dist/ui/index.js +9 -2
  109. package/dist/ui/layout/index.d.ts +2 -2
  110. package/dist/ui/layout/index.js +2 -2
  111. package/dist/ui/styles/index.d.ts +21 -1
  112. package/dist/ui/styles/index.js +73 -14
  113. package/locales/en-GB.json +35 -0
  114. package/locales/en-US.json +35 -0
  115. package/locales/fr.json +35 -0
  116. package/locales/ja.json +35 -0
  117. package/locales/ko.json +35 -0
  118. package/locales/zh-Hans.json +35 -0
  119. package/locales/zh-Hant.json +35 -0
  120. package/package.json +25 -23
@@ -1,13 +1,16 @@
1
1
  import { nothing } from "lit";
2
+ import { type ConfirmFn, type PromptFn } from "../../ask";
2
3
  import { PcElement } from "../../base";
3
4
  import "../PcBrushTest";
5
+ import "../PcToggleCheckbox";
6
+ import "../PcDialogWindow";
7
+ import "../PcSliderRange";
8
+ import "../PcProfileEditor";
4
9
  import type PaintCanvas from "../../../lib/classes/PaintCanvas";
5
10
  import { SmoothingType, type BrushConfig } from "../../../lib/brushes/types";
11
+ import { type VaryingParam } from "../../../lib/brushes/configRanges";
6
12
  import { type ShapeImage } from "../../../lib/brushes/shapeImage";
7
- /** Asks the user to confirm; replaceable for tests */
8
- export type ConfirmFn = (message: string) => boolean;
9
- /** Asks the user for text; replaceable for tests */
10
- export type PromptFn = (message: string, value: string) => string | null;
13
+ export type { ConfirmFn, PromptFn } from "../../ask";
11
14
  /** The smoothing choices, in the order shown: value, translation key of the name, and icon name */
12
15
  export declare const SMOOTHING_CHOICES: readonly [{
13
16
  readonly value: SmoothingType.Linear;
@@ -26,12 +29,13 @@ export declare const SMOOTHING_CHOICES: readonly [{
26
29
  * The brush (or eraser) configuration window: a testing area, the shape (pressing it picks a built-in shape from the
27
30
  * device's own list, which says "Custom" for an uploaded or pasted one; or upload or paste one), smoothing, width, height,
28
31
  * opacity, rotation, offsets, sample rate, spacing and snapping to the pixel grid. Width, height, opacity, rotation and the offsets can vary with
29
- * pressure, tilt and randomness; when any is on, the setting has a min and max.
32
+ * pressure, tilt and randomness; when any is on, the setting has a min and max, set with a `<pc-slider-range>`.
30
33
  *
31
34
  * Changes stay in the window until "Save" (fires `pc-save` with the settings, to apply to the current tool) or "Add to
32
35
  * presets" (saves them as one of the user's presets, asking before replacing one with the same name, and selects it).
33
- * "Delete preset…" shows when editing one of the user's presets. Fires `pc-close` when closed, and `pc-toast` with a
34
- * message after adding or deleting a preset, or when a shape can't be used. Styling is structural only.
36
+ * "Delete preset…" shows when editing one of the user's presets. It's a shaded `<pc-dialog-window>`, closed with its ⨉
37
+ * button or Escape (not a press outside it, which would lose the changes). Fires `pc-close` when closed, and `pc-toast`
38
+ * with a message after adding or deleting a preset, or when a shape can't be used.
35
39
  *
36
40
  * @example
37
41
  * html`<pc-brush-config .app=${app} kind="brush" .config=${app.brush.preset} @pc-save=${apply}></pc-brush-config>`
@@ -56,6 +60,9 @@ export declare class PcBrushConfig extends PcElement {
56
60
  busy: {
57
61
  state: boolean;
58
62
  };
63
+ editingProfile: {
64
+ state: boolean;
65
+ };
59
66
  translations: {
60
67
  attribute: boolean;
61
68
  };
@@ -79,7 +86,11 @@ export declare class PcBrushConfig extends PcElement {
79
86
  draft?: BrushConfig;
80
87
  /** Whether a preset is being saved or deleted */
81
88
  busy: boolean;
89
+ /** The setting whose profile graph is being edited, if any */
90
+ editingProfile?: VaryingParam;
91
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
82
92
  confirm: ConfirmFn;
93
+ /** Asks the user for text, in a dialog (replaceable for tests) */
83
94
  prompt: PromptFn;
84
95
  /** Turns files into brush shapes (replaceable for tests) */
85
96
  readShape: (file: File) => Promise<ShapeImage>;
@@ -87,10 +98,7 @@ export declare class PcBrushConfig extends PcElement {
87
98
  * Reads a brush shape from the clipboard, or else from image data copied on the canvas (replaceable for tests)
88
99
  */
89
100
  readClipboard: () => Promise<ShapeImage>;
90
- private readonly onKeyDown;
91
101
  constructor();
92
- connectedCallback(): void;
93
- disconnectedCallback(): void;
94
102
  protected willUpdate(changed: Map<PropertyKey, unknown>): void;
95
103
  /** Fire an event that crosses shadow DOM boundaries */
96
104
  private emit;
@@ -121,10 +129,10 @@ export declare class PcBrushConfig extends PcElement {
121
129
  * Delete the preset being edited, after confirming.
122
130
  */
123
131
  deletePreset(): Promise<void>;
124
- /** Render a number input */
125
- private number;
126
- /** Render a range slider */
127
- private slider;
132
+ /**
133
+ * Render a slider for one value (a single `<pc-slider-range>`), the same as the ranges of settings that vary.
134
+ */
135
+ private single;
128
136
  /** Render a setting that can vary: its value (or min and max), and its pressure, tilt and randomise icon toggles */
129
137
  private renderParam;
130
138
  /**
@@ -134,4 +142,8 @@ export declare class PcBrushConfig extends PcElement {
134
142
  /** Render the shape and smoothing row */
135
143
  private renderShape;
136
144
  render(): import("lit").TemplateResult<1> | typeof nothing;
145
+ /** The profile graph editor for the setting being edited, while open */
146
+ private renderProfileEditor;
147
+ /** Profile graphs, when there's no canvas to keep them */
148
+ private readonly profiles;
137
149
  }
@@ -1,10 +1,17 @@
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 "../PcToggleCheckbox/index.js";
8
+ import "../PcDialogWindow/index.js";
9
+ import "../PcSliderRange/index.js";
10
+ import "../PcProfileEditor/index.js";
11
+ import ProfileGraphs from "../../../lib/classes/ProfileGraphs/index.js";
12
+ import { isLinear } from "../../../lib/profiles/index.js";
6
13
  import { SmoothingType } from "../../../lib/brushes/types/index.js";
7
- import { PARAM_LIMITS, VARIATION_SOURCES, readRange, toggleSource, writeRange } from "../../../lib/brushes/configRanges/index.js";
14
+ import { PARAM_LIMITS, VARIATION_SOURCES, configKey, readRange, selectSource, writeRange } from "../../../lib/brushes/configRanges/index.js";
8
15
  import { SHAPE_ERRORS, ShapeImageError, readClipboardShape, readShapeImage } from "../../../lib/brushes/shapeImage/index.js";
9
16
  import { BUILT_IN_SHAPES, builtInShapeId } from "../../../lib/brushes/shapes/index.js";
10
17
  import "../../../lib/brushes/StrokeSampler/index.js";
@@ -36,6 +43,15 @@ var SOURCE_ICONS = {
36
43
  tilt: "tilt",
37
44
  randomise: "randomise"
38
45
  };
46
+ /** Translation keys of the varying settings' names */
47
+ var PARAM_LABELS = {
48
+ width: "paintCanvas.common.width",
49
+ height: "paintCanvas.common.height",
50
+ alpha: "paintCanvas.common.opacity",
51
+ rotation: "paintCanvas.brushConfig.rotation",
52
+ offsetX: "paintCanvas.brushConfig.offsetX",
53
+ offsetY: "paintCanvas.brushConfig.offsetY"
54
+ };
39
55
  /** Translation keys of the variation sources' names */
40
56
  var SOURCE_LABELS = {
41
57
  pressure: "paintCanvas.toolSettings.pressure",
@@ -53,21 +69,17 @@ var SHAPE_ERROR_KEYS = {
53
69
  };
54
70
  /** Largest stamp spacing in pixels */
55
71
  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
72
  /**
62
73
  * The brush (or eraser) configuration window: a testing area, the shape (pressing it picks a built-in shape from the
63
74
  * device's own list, which says "Custom" for an uploaded or pasted one; or upload or paste one), smoothing, width, height,
64
75
  * 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.
76
+ * pressure, tilt and randomness; when any is on, the setting has a min and max, set with a `<pc-slider-range>`.
66
77
  *
67
78
  * Changes stay in the window until "Save" (fires `pc-save` with the settings, to apply to the current tool) or "Add to
68
79
  * 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.
80
+ * "Delete preset…" shows when editing one of the user's presets. It's a shaded `<pc-dialog-window>`, closed with its ⨉
81
+ * button or Escape (not a press outside it, which would lose the changes). Fires `pc-close` when closed, and `pc-toast`
82
+ * with a message after adding or deleting a preset, or when a shape can't be used.
71
83
  *
72
84
  * @example
73
85
  * html`<pc-brush-config .app=${app} kind="brush" .config=${app.brush.preset} @pc-save=${apply}></pc-brush-config>`
@@ -80,7 +92,8 @@ var PcBrushConfig = class extends PcElement {
80
92
  config: { attribute: false },
81
93
  editingId: { type: String },
82
94
  draft: { state: true },
83
- busy: { state: true }
95
+ busy: { state: true },
96
+ editingProfile: { state: true }
84
97
  };
85
98
  static styles = [
86
99
  PcElement.styles,
@@ -89,30 +102,21 @@ var PcBrushConfig = class extends PcElement {
89
102
  unsafeCSS(ICON_CONTROLS_CSS),
90
103
  unsafeCSS(PILLS_CSS)
91
104
  ];
92
- confirm = (message) => window.confirm(message);
93
- prompt = (message, value) => window.prompt(message, value);
105
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
106
+ confirm = (options) => askConfirm(this.renderRoot, options);
107
+ /** Asks the user for text, in a dialog (replaceable for tests) */
108
+ prompt = (options) => askText(this.renderRoot, options);
94
109
  /** Turns files into brush shapes (replaceable for tests) */
95
110
  readShape = readShapeImage;
96
111
  /**
97
112
  * Reads a brush shape from the clipboard, or else from image data copied on the canvas (replaceable for tests)
98
113
  */
99
114
  readClipboard = () => readClipboardShape(navigator.clipboard, {}, async () => this.app?.copiedPng());
100
- onKeyDown = (event) => {
101
- if (event.key === "Escape") this.close();
102
- };
103
115
  constructor() {
104
116
  super();
105
117
  this.kind = "brush";
106
118
  this.busy = false;
107
119
  }
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
120
  willUpdate(changed) {
117
121
  super.willUpdate(changed);
118
122
  if (changed.has("config") && this.config) this.draft = structuredClone(this.config);
@@ -190,10 +194,22 @@ var PcBrushConfig = class extends PcElement {
190
194
  const app = this.app;
191
195
  const draft = this.draft;
192
196
  if (!app || !draft) return;
193
- const name = this.prompt(this.t("paintCanvas.brushConfig.presetName"), draft.name ?? "")?.trim();
197
+ const name = (await this.prompt({
198
+ heading: this.t("paintCanvas.brushConfig.addToPresets"),
199
+ label: this.t("paintCanvas.brushConfig.presetName"),
200
+ value: draft.name ?? "",
201
+ confirm: this.t("paintCanvas.common.save"),
202
+ cancel: this.t("paintCanvas.common.cancel")
203
+ }))?.trim();
194
204
  if (!name) return;
195
205
  const existing = app.customPresets.findOwn(name, app.authorHandle, app.names.unknownAuthor);
196
- if (existing && !this.confirm(this.t("paintCanvas.presets.replaceConfirm", { name }))) return;
206
+ if (existing && !await this.confirm({
207
+ heading: this.t("paintCanvas.presets.replaceHeading"),
208
+ message: this.t("paintCanvas.presets.replaceConfirm", { name }),
209
+ confirm: this.t("paintCanvas.presets.replace"),
210
+ cancel: this.t("paintCanvas.common.cancel"),
211
+ cancelling: true
212
+ })) return;
197
213
  const preset = {
198
214
  ...app.newPreset(draft, name),
199
215
  ...existing ? { id: existing.id } : {}
@@ -224,7 +240,7 @@ var PcBrushConfig = class extends PcElement {
224
240
  const id = this.editingId;
225
241
  if (!app || !id) return;
226
242
  const name = app.customPresets.get(id)?.name ?? "this preset";
227
- if (!this.confirm(this.t("paintCanvas.presets.deleteConfirm", { name }))) return;
243
+ if (!await this.confirm(deletePresetQuestion(this.translations, name))) return;
228
244
  this.busy = true;
229
245
  try {
230
246
  await app.customPresets.remove(id);
@@ -242,32 +258,22 @@ var PcBrushConfig = class extends PcElement {
242
258
  this.busy = false;
243
259
  }
244
260
  }
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"
261
+ /**
262
+ * Render a slider for one value (a single `<pc-slider-range>`), the same as the ranges of settings that vary.
263
+ */
264
+ single(className, label, value, limits, onChange, disabled = false) {
265
+ const changed = (event) => onChange(event.detail.value);
266
+ return html`<pc-slider-range
267
+ single
264
268
  class=${className}
269
+ label=${label}
265
270
  min=${limits.min}
266
271
  max=${limits.max}
272
+ .value=${live(Math.round(value))}
267
273
  ?disabled=${disabled || this.busy}
268
- .value=${String(value)}
269
- @input=${(e) => onChange(e.target.valueAsNumber)}
270
- />`;
274
+ @pc-input=${changed}
275
+ @pc-change=${changed}
276
+ ></pc-slider-range>`;
271
277
  }
272
278
  /** Render a setting that can vary: its value (or min and max), and its pressure, tilt and randomise icon toggles */
273
279
  renderParam(draft, param, label, disabled = false) {
@@ -275,31 +281,61 @@ var PcBrushConfig = class extends PcElement {
275
281
  const limits = PARAM_LIMITS[param];
276
282
  const isOffset = param === "offsetX" || param === "offsetY";
277
283
  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)}`;
284
+ const writeEnds = (event) => write({
285
+ min: event.detail.low,
286
+ max: event.detail.high
287
+ });
288
+ const values = range.varies ? html`<pc-slider-range
289
+ class=${`${param} range`}
290
+ label=${label}
291
+ min=${limits.min}
292
+ max=${limits.max}
293
+ .low=${live(Math.round(range.min))}
294
+ .high=${live(Math.round(range.max))}
295
+ ?disabled=${disabled || this.busy}
296
+ @pc-input=${writeEnds}
297
+ @pc-change=${writeEnds}
298
+ ></pc-slider-range>` : this.single(param, label, range.value, limits, (value) => write({ value }), disabled || isOffset);
299
+ const active = VARIATION_SOURCES.find((source) => range.sources[source]);
300
+ const select = (source) => this.changeParam(param, selectSource(draft, param, source));
283
301
  return html`<div class="row" data-param=${param}>
284
302
  <span class="label">${label}</span>
285
303
  ${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", {
304
+ <fieldset class="sources" ?disabled=${disabled || this.busy}>
305
+ <legend class="visually-hidden">
306
+ ${this.t("paintCanvas.brushConfig.varyWith", { param: label })}
307
+ </legend>
308
+ ${VARIATION_SOURCES.map((source) => html`<label
309
+ class="icon-toggle"
310
+ title=${this.t(SOURCE_LABELS[source])}
311
+ >
312
+ <input
313
+ type="radio"
314
+ name=${`${param}-source`}
315
+ class=${`${param} ${source}`}
316
+ aria-label=${this.t("paintCanvas.brushConfig.variation", {
294
317
  param: label,
295
318
  source: this.t(SOURCE_LABELS[source])
296
319
  })}
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>`)}
320
+ .checked=${live(active === source)}
321
+ @change=${() => select(source)}
322
+ @click=${() => {
323
+ if (active === source) select(void 0);
324
+ }}
325
+ />
326
+ ${iconLabel(this.iconUrl(SOURCE_ICONS[source]), this.t(SOURCE_LABELS[source]))}
327
+ </label>`)}
328
+ </fieldset>
329
+ <button
330
+ type="button"
331
+ class=${`icon-button profile ${param} ${draft[configKey(param)]?.profile ? "configured" : ""}`}
332
+ title=${this.t("paintCanvas.profileEditor.button", { param: label })}
333
+ aria-haspopup="dialog"
334
+ ?disabled=${!active || disabled || this.busy}
335
+ @click=${() => this.editingProfile = param}
336
+ >
337
+ ${iconLabel(this.iconUrl("profileGraph"), this.t("paintCanvas.profileEditor.button", { param: label }))}
338
+ </button>
303
339
  </div>`;
304
340
  }
305
341
  /**
@@ -406,13 +442,17 @@ var PcBrushConfig = class extends PcElement {
406
442
  min: 0,
407
443
  max: MAX_SPACING
408
444
  };
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>
445
+ return html`<pc-dialog-window
446
+ open
447
+ shaded
448
+ dismissible="false"
449
+ label=${this.t(this.kind === "eraser" ? "paintCanvas.brushConfig.eraserTitle" : "paintCanvas.brushConfig.brushTitle")}
450
+ @pc-dismiss=${(event) => {
451
+ event.preventDefault();
452
+ this.close();
453
+ }}
454
+ >
455
+ <label slot="header-start" class="name-field">
416
456
  ${this.t("paintCanvas.brushConfig.name")}
417
457
  <input
418
458
  class="name"
@@ -421,86 +461,115 @@ var PcBrushConfig = class extends PcElement {
421
461
  @input=${(e) => this.change({ name: e.target.value })}
422
462
  />
423
463
  </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}
464
+ <div slot="body" class="settings">
465
+ <pc-brush-test
466
+ .config=${draft}
467
+ .tips=${this.app?.tips}
468
+ ></pc-brush-test>
469
+ ${this.renderShape(draft)}
470
+ ${this.renderParam(draft, "width", this.t(PARAM_LABELS.width))}
471
+ <div class="row">
472
+ <span class="label"></span>
473
+ <label
474
+ class="icon-toggle"
475
+ title=${this.t("paintCanvas.toolSettings.constrainProportions")}
476
+ >
477
+ <input
478
+ type="checkbox"
479
+ class="constrain"
480
+ .checked=${constrained}
481
+ ?disabled=${this.busy}
482
+ @change=${(e) => this.change({ constrainProportions: e.target.checked })}
483
+ />
484
+ ${iconLabel(this.iconUrl(constrained ? "linked" : "unlinked"), this.t("paintCanvas.toolSettings.constrainProportions"))}
485
+ </label>
486
+ </div>
487
+ ${this.renderParam(draft, "height", this.t(PARAM_LABELS.height), constrained)}
488
+ ${this.renderParam(draft, "alpha", this.t(PARAM_LABELS.alpha))}
489
+ ${this.renderParam(draft, "rotation", this.t(PARAM_LABELS.rotation))}
490
+ ${this.renderParam(draft, "offsetX", this.t(PARAM_LABELS.offsetX))}
491
+ ${this.renderParam(draft, "offsetY", this.t(PARAM_LABELS.offsetY))}
492
+ <div class="row">
493
+ <span class="label"
494
+ >${this.t("paintCanvas.brushConfig.sampleRate")}</span
495
+ >
496
+ ${this.single("sample-rate", this.t("paintCanvas.brushConfig.sampleRate"), draft.sampleRate ?? 5, sampleLimits, (sampleRate) => this.change({ sampleRate }))}
497
+ </div>
498
+ <div class="row">
499
+ <span class="label"
500
+ >${this.t("paintCanvas.brushConfig.spacing")}</span
501
+ >
502
+ ${this.single("spacing", this.t("paintCanvas.brushConfig.spacing"), draft.spacing ?? 5, spacingLimits, (spacing) => this.change({ spacing }))}
503
+ </div>
504
+ <pc-toggle-checkbox
505
+ class="snap"
506
+ .checked=${draft.snapToPixelGrid ?? false}
445
507
  ?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">
508
+ @change=${(e) => this.change({ snapToPixelGrid: e.detail.checked })}
509
+ >
510
+ ${this.t("paintCanvas.brushConfig.snapToPixelGrid")}
511
+ </pc-toggle-checkbox>
512
+ </div>
477
513
  ${this.editingId ? html`<button
514
+ slot="footer-start"
478
515
  type="button"
479
- class="delete"
516
+ class="cancel delete"
480
517
  ?disabled=${this.busy}
481
518
  @click=${() => void this.deletePreset()}
482
519
  >
483
520
  ${this.t("paintCanvas.brushConfig.deletePreset")}
484
521
  </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>`;
522
+ <div slot="footer-end" class="actions">
523
+ <button
524
+ type="button"
525
+ class="add affirmative"
526
+ ?disabled=${this.busy}
527
+ @click=${() => void this.addToPresets()}
528
+ >
529
+ ${this.t("paintCanvas.brushConfig.addToPresets")}
530
+ </button>
531
+ <button
532
+ type="button"
533
+ class="save affirmative"
534
+ ?disabled=${this.busy}
535
+ @click=${() => this.save()}
536
+ >
537
+ ${this.t("paintCanvas.common.save")}
538
+ </button>
539
+ </div>
540
+ </pc-dialog-window>
541
+ ${this.renderProfileEditor(draft)}`;
542
+ }
543
+ /** The profile graph editor for the setting being edited, while open */
544
+ renderProfileEditor(draft) {
545
+ const param = this.editingProfile;
546
+ if (!param) return nothing;
547
+ const key = configKey(param);
548
+ const variation = draft[key];
549
+ return html`<pc-profile-editor
550
+ .profiles=${this.app?.profiles ?? this.profiles}
551
+ .points=${variation.profile}
552
+ profile-id=${variation.profileId ?? (variation.profile ? nothing : "linear")}
553
+ label=${this.t(PARAM_LABELS[param])}
554
+ @pc-save=${(event) => {
555
+ event.stopPropagation();
556
+ const { points, profileId } = event.detail;
557
+ const linear = isLinear(points);
558
+ this.changeParam(param, { [key]: {
559
+ ...variation,
560
+ profile: linear ? void 0 : points,
561
+ profileId: linear ? void 0 : profileId
562
+ } });
563
+ this.editingProfile = void 0;
564
+ }}
565
+ @pc-close=${(event) => {
566
+ event.stopPropagation();
567
+ this.editingProfile = void 0;
568
+ }}
569
+ ></pc-profile-editor>`;
503
570
  }
571
+ /** Profile graphs, when there's no canvas to keep them */
572
+ profiles = new ProfileGraphs();
504
573
  };
505
574
  defineElement("pc-brush-config", PcBrushConfig);
506
575
  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 { PcElement } from "../../base";
2
+ import "../PcToggleCheckbox";
2
3
  import type { BrushConfig } from "../../../lib/brushes/types";
3
4
  import type BrushTipCache from "../../../lib/brushes/BrushTipCache";
4
5
  import { type CanvasFactory } from "../../../lib/classes/Layer";
@@ -1,5 +1,7 @@
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";
4
+ import "../PcToggleCheckbox/index.js";
3
5
  import StrokeEngine from "../../../lib/brushes/StrokeEngine/index.js";
4
6
  import Layer, { createOffscreenCanvas } from "../../../lib/classes/Layer/index.js";
5
7
  import { html, unsafeCSS } from "lit";
@@ -38,7 +40,11 @@ var PcBrushTest = class extends PcElement {
38
40
  attribute: "show-points"
39
41
  }
40
42
  };
41
- static styles = [PcElement.styles, unsafeCSS(style_default)];
43
+ static styles = [
44
+ PcElement.styles,
45
+ unsafeCSS(PILLS_CSS),
46
+ unsafeCSS(style_default)
47
+ ];
42
48
  constructor() {
43
49
  super();
44
50
  this.showPoints = false;
@@ -185,17 +191,17 @@ var PcBrushTest = class extends PcElement {
185
191
  @pointercancel=${this.onPointerUp}
186
192
  ></canvas>
187
193
  <div class="actions">
188
- <button type="button" class="clear" @click=${() => this.clear()}>
194
+ <button type="button" class="pill clear" @click=${() => this.clear()}>
189
195
  ${this.t("paintCanvas.common.clear")}
190
196
  </button>
191
- <label class="show-points">
192
- <input
193
- type="checkbox"
194
- .checked=${this.showPoints}
195
- @change=${(e) => this.setShowPoints(e.target.checked)}
196
- />
197
+ <pc-toggle-checkbox
198
+ class="show-points"
199
+ size="s"
200
+ .checked=${this.showPoints}
201
+ @change=${(e) => this.setShowPoints(e.detail.checked)}
202
+ >
197
203
  ${this.t("paintCanvas.brushConfig.showStrokePoints")}
198
- </label>
204
+ </pc-toggle-checkbox>
199
205
  </div>
200
206
  `;
201
207
  }
@@ -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}\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 };