@lvl99dotcom/paintcanvas 0.0.0-stage → 0.2.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 (284) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +131 -2
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +5 -0
  5. package/dist/lib/brushes/BrushTipCache/index.d.ts +32 -0
  6. package/dist/lib/brushes/BrushTipCache/index.js +60 -0
  7. package/dist/lib/brushes/StrokeEngine/index.d.ts +171 -0
  8. package/dist/lib/brushes/StrokeEngine/index.js +324 -0
  9. package/dist/lib/brushes/StrokePath/index.d.ts +53 -0
  10. package/dist/lib/brushes/StrokePath/index.js +161 -0
  11. package/dist/lib/brushes/StrokeSampler/index.d.ts +55 -0
  12. package/dist/lib/brushes/StrokeSampler/index.js +75 -0
  13. package/dist/lib/brushes/authors/index.d.ts +55 -0
  14. package/dist/lib/brushes/authors/index.js +60 -0
  15. package/dist/lib/brushes/configRanges/index.d.ts +52 -0
  16. package/dist/lib/brushes/configRanges/index.js +206 -0
  17. package/dist/lib/brushes/index.d.ts +5 -0
  18. package/dist/lib/brushes/index.js +19 -0
  19. package/dist/lib/brushes/params/index.d.ts +108 -0
  20. package/dist/lib/brushes/params/index.js +139 -0
  21. package/dist/lib/brushes/presetFile/index.d.ts +32 -0
  22. package/dist/lib/brushes/presetFile/index.js +55 -0
  23. package/dist/lib/brushes/presets/drawing/brush-pen.js +3 -0
  24. package/dist/lib/brushes/presets/drawing/felt-pen.js +3 -0
  25. package/dist/lib/brushes/presets/drawing/index.d.ts +13 -0
  26. package/dist/lib/brushes/presets/drawing/index.js +88 -0
  27. package/dist/lib/brushes/presets/drawing/pencil-2b.js +3 -0
  28. package/dist/lib/brushes/presets/drawing/pencil-hb.js +3 -0
  29. package/dist/lib/brushes/presets/painting/grass.js +3 -0
  30. package/dist/lib/brushes/presets/painting/hair.js +3 -0
  31. package/dist/lib/brushes/presets/painting/index.d.ts +18 -0
  32. package/dist/lib/brushes/presets/painting/index.js +118 -0
  33. package/dist/lib/brushes/presets/painting/splatter.js +3 -0
  34. package/dist/lib/brushes/presets/painting/textured-paint.js +3 -0
  35. package/dist/lib/brushes/presets/painting/watery-paint.js +3 -0
  36. package/dist/lib/brushes/presets/round/index.d.ts +9 -0
  37. package/dist/lib/brushes/presets/round/index.js +44 -0
  38. package/dist/lib/brushes/presets/round/round.js +3 -0
  39. package/dist/lib/brushes/presets/round/soft-round.js +3 -0
  40. package/dist/lib/brushes/presets/sized/index.d.ts +20 -0
  41. package/dist/lib/brushes/presets/sized/index.js +30 -0
  42. package/dist/lib/brushes/presets/sketching/index.d.ts +14 -0
  43. package/dist/lib/brushes/presets/sketching/index.js +107 -0
  44. package/dist/lib/brushes/presets/square/index.d.ts +7 -0
  45. package/dist/lib/brushes/presets/square/index.js +22 -0
  46. package/dist/lib/brushes/presets/square/square.js +3 -0
  47. package/dist/lib/brushes/shapeImage/index.d.ts +40 -0
  48. package/dist/lib/brushes/shapeImage/index.js +61 -0
  49. package/dist/lib/brushes/types/index.d.ts +243 -0
  50. package/dist/lib/brushes/types/index.js +20 -0
  51. package/dist/lib/classes/BackgroundWork/index.d.ts +55 -0
  52. package/dist/lib/classes/BackgroundWork/index.js +97 -0
  53. package/dist/lib/classes/BrushSettings/index.d.ts +91 -0
  54. package/dist/lib/classes/BrushSettings/index.js +152 -0
  55. package/dist/lib/classes/CustomPresets/index.d.ts +53 -0
  56. package/dist/lib/classes/CustomPresets/index.js +71 -0
  57. package/dist/lib/classes/ExternalSheets/index.d.ts +64 -0
  58. package/dist/lib/classes/ExternalSheets/index.js +112 -0
  59. package/dist/lib/classes/GestureTracker/index.d.ts +62 -0
  60. package/dist/lib/classes/GestureTracker/index.js +122 -0
  61. package/dist/lib/classes/History/index.d.ts +83 -0
  62. package/dist/lib/classes/History/index.js +126 -0
  63. package/dist/lib/classes/HistoryGuard/index.d.ts +58 -0
  64. package/dist/lib/classes/HistoryGuard/index.js +97 -0
  65. package/dist/lib/classes/InputSettings/index.d.ts +74 -0
  66. package/dist/lib/classes/InputSettings/index.js +101 -0
  67. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +48 -0
  68. package/dist/lib/classes/KeyboardShortcuts/index.js +137 -0
  69. package/dist/lib/classes/Layer/index.d.ts +86 -0
  70. package/dist/lib/classes/Layer/index.js +111 -0
  71. package/dist/lib/classes/LayerAutosave/index.d.ts +180 -0
  72. package/dist/lib/classes/LayerAutosave/index.js +346 -0
  73. package/dist/lib/classes/LayerStack/index.d.ts +177 -0
  74. package/dist/lib/classes/LayerStack/index.js +394 -0
  75. package/dist/lib/classes/PaintCanvas/index.d.ts +537 -0
  76. package/dist/lib/classes/PaintCanvas/index.js +965 -0
  77. package/dist/lib/classes/StrokeLog/index.d.ts +149 -0
  78. package/dist/lib/classes/StrokeLog/index.js +257 -0
  79. package/dist/lib/classes/Viewport/index.d.ts +108 -0
  80. package/dist/lib/classes/Viewport/index.js +194 -0
  81. package/dist/lib/document/index.d.ts +17 -0
  82. package/dist/lib/document/index.js +25 -0
  83. package/dist/lib/document/schema.d.ts +132 -0
  84. package/dist/lib/document/schema.js +110 -0
  85. package/dist/lib/i18n/index.d.ts +94 -0
  86. package/dist/lib/i18n/index.js +182 -0
  87. package/dist/lib/image/index.d.ts +90 -0
  88. package/dist/lib/image/index.js +147 -0
  89. package/dist/lib/messages/PatchSender/index.d.ts +75 -0
  90. package/dist/lib/messages/PatchSender/index.js +125 -0
  91. package/dist/lib/messages/index.d.ts +26 -0
  92. package/dist/lib/messages/index.js +59 -0
  93. package/dist/lib/messages/schema.d.ts +1775 -0
  94. package/dist/lib/messages/schema.js +192 -0
  95. package/dist/lib/presets/index.d.ts +32 -0
  96. package/dist/lib/presets/index.js +35 -0
  97. package/dist/lib/presets/schema.d.ts +753 -0
  98. package/dist/lib/presets/schema.js +181 -0
  99. package/dist/lib/rendering/BrushTip/index.d.ts +63 -0
  100. package/dist/lib/rendering/BrushTip/index.js +112 -0
  101. package/dist/lib/rendering/checkerboard/index.d.ts +22 -0
  102. package/dist/lib/rendering/checkerboard/index.js +31 -0
  103. package/dist/lib/rendering/compositing/index.d.ts +64 -0
  104. package/dist/lib/rendering/compositing/index.js +124 -0
  105. package/dist/lib/rendering/cursor/index.d.ts +14 -0
  106. package/dist/lib/rendering/cursor/index.js +39 -0
  107. package/dist/lib/rendering/cursors/index.d.ts +18 -0
  108. package/dist/lib/rendering/cursors/index.js +23 -0
  109. package/dist/lib/rendering/maskOutline/index.d.ts +22 -0
  110. package/dist/lib/rendering/maskOutline/index.js +78 -0
  111. package/dist/lib/rendering/outline/index.d.ts +36 -0
  112. package/dist/lib/rendering/outline/index.js +120 -0
  113. package/dist/lib/rendering/sample/index.d.ts +55 -0
  114. package/dist/lib/rendering/sample/index.js +83 -0
  115. package/dist/lib/rendering/screenshot/index.d.ts +67 -0
  116. package/dist/lib/rendering/screenshot/index.js +96 -0
  117. package/dist/lib/rendering/transformBox/index.d.ts +10 -0
  118. package/dist/lib/rendering/transformBox/index.js +57 -0
  119. package/dist/lib/selection/Lasso/index.d.ts +70 -0
  120. package/dist/lib/selection/Lasso/index.js +156 -0
  121. package/dist/lib/selection/Selection/index.d.ts +86 -0
  122. package/dist/lib/selection/Selection/index.js +228 -0
  123. package/dist/lib/selection/geometry/index.d.ts +37 -0
  124. package/dist/lib/selection/geometry/index.js +57 -0
  125. package/dist/lib/storage/index.d.ts +63 -0
  126. package/dist/lib/storage/index.js +114 -0
  127. package/dist/lib/transform/Transformer/index.d.ts +124 -0
  128. package/dist/lib/transform/Transformer/index.js +286 -0
  129. package/dist/lib/transform/geometry/index.d.ts +95 -0
  130. package/dist/lib/transform/geometry/index.js +258 -0
  131. package/dist/lib/utils/array/index.d.ts +10 -0
  132. package/dist/lib/utils/array/index.js +21 -0
  133. package/dist/lib/utils/gestures/index.d.ts +35 -0
  134. package/dist/lib/utils/gestures/index.js +81 -0
  135. package/dist/lib/utils/navigation/index.d.ts +51 -0
  136. package/dist/lib/utils/navigation/index.js +59 -0
  137. package/dist/lib/utils/random/index.d.ts +18 -0
  138. package/dist/lib/utils/random/index.js +27 -0
  139. package/dist/lib/utils/shortcuts/index.d.ts +74 -0
  140. package/dist/lib/utils/shortcuts/index.js +189 -0
  141. package/dist/lib/validation/validators.generated.d.ts +27 -0
  142. package/dist/lib/validation/validators.generated.js +19826 -0
  143. package/dist/locales/en-GB.js +188 -0
  144. package/dist/locales/en-US.js +188 -0
  145. package/dist/locales/fr.js +188 -0
  146. package/dist/locales/ja.js +188 -0
  147. package/dist/locales/ko.js +188 -0
  148. package/dist/locales/zh-Hans.js +188 -0
  149. package/dist/locales/zh-Hant.js +188 -0
  150. package/dist/package.js +3 -0
  151. package/dist/testing/app.d.ts +7 -0
  152. package/dist/testing/app.js +16 -0
  153. package/dist/testing/elements.d.ts +42 -0
  154. package/dist/testing/elements.js +58 -0
  155. package/dist/testing/fakeCanvas.d.ts +21 -0
  156. package/dist/testing/fakeCanvas.js +26 -0
  157. package/dist/testing/layerStore.d.ts +26 -0
  158. package/dist/testing/layerStore.js +43 -0
  159. package/dist/testing/locks.d.ts +15 -0
  160. package/dist/testing/locks.js +41 -0
  161. package/dist/testing/recordingCanvas.d.ts +14 -0
  162. package/dist/testing/recordingCanvas.js +28 -0
  163. package/dist/testing/recordingContext.d.ts +10 -0
  164. package/dist/testing/recordingContext.js +19 -0
  165. package/dist/ui/ChangeController/index.d.ts +40 -0
  166. package/dist/ui/ChangeController/index.js +62 -0
  167. package/dist/ui/base/index.d.ts +69 -0
  168. package/dist/ui/base/index.js +94 -0
  169. package/dist/ui/elements/PaintCanvas/index.d.ts +249 -0
  170. package/dist/ui/elements/PaintCanvas/index.js +584 -0
  171. package/dist/ui/elements/PaintCanvas/style.js +3 -0
  172. package/dist/ui/elements/PcBrushConfig/index.d.ts +130 -0
  173. package/dist/ui/elements/PcBrushConfig/index.js +463 -0
  174. package/dist/ui/elements/PcBrushConfig/style.js +3 -0
  175. package/dist/ui/elements/PcBrushMenu/index.d.ts +121 -0
  176. package/dist/ui/elements/PcBrushMenu/index.js +362 -0
  177. package/dist/ui/elements/PcBrushMenu/style.js +3 -0
  178. package/dist/ui/elements/PcBrushTest/index.d.ts +71 -0
  179. package/dist/ui/elements/PcBrushTest/index.js +136 -0
  180. package/dist/ui/elements/PcBrushTest/style.js +3 -0
  181. package/dist/ui/elements/PcButton/index.d.ts +57 -0
  182. package/dist/ui/elements/PcButton/index.js +124 -0
  183. package/dist/ui/elements/PcButtonGroup/index.d.ts +44 -0
  184. package/dist/ui/elements/PcButtonGroup/index.js +93 -0
  185. package/dist/ui/elements/PcCanvas/index.d.ts +356 -0
  186. package/dist/ui/elements/PcCanvas/index.js +1004 -0
  187. package/dist/ui/elements/PcCanvas/style.js +3 -0
  188. package/dist/ui/elements/PcCanvasMap/index.d.ts +96 -0
  189. package/dist/ui/elements/PcCanvasMap/index.js +187 -0
  190. package/dist/ui/elements/PcCanvasMap/style.js +3 -0
  191. package/dist/ui/elements/PcContextMenu/index.d.ts +72 -0
  192. package/dist/ui/elements/PcContextMenu/index.js +148 -0
  193. package/dist/ui/elements/PcDialog/index.d.ts +63 -0
  194. package/dist/ui/elements/PcDialog/index.js +114 -0
  195. package/dist/ui/elements/PcIcon/index.d.ts +36 -0
  196. package/dist/ui/elements/PcIcon/index.js +43 -0
  197. package/dist/ui/elements/PcIconToggle/index.d.ts +46 -0
  198. package/dist/ui/elements/PcIconToggle/index.js +87 -0
  199. package/dist/ui/elements/PcInfoTip/index.d.ts +40 -0
  200. package/dist/ui/elements/PcInfoTip/index.js +95 -0
  201. package/dist/ui/elements/PcInputSettings/index.d.ts +69 -0
  202. package/dist/ui/elements/PcInputSettings/index.js +150 -0
  203. package/dist/ui/elements/PcLayerItem/index.d.ts +221 -0
  204. package/dist/ui/elements/PcLayerItem/index.js +561 -0
  205. package/dist/ui/elements/PcLayerItem/style.js +3 -0
  206. package/dist/ui/elements/PcLayersMenu/index.d.ts +108 -0
  207. package/dist/ui/elements/PcLayersMenu/index.js +208 -0
  208. package/dist/ui/elements/PcLayersMenu/style.js +3 -0
  209. package/dist/ui/elements/PcMatch/index.d.ts +31 -0
  210. package/dist/ui/elements/PcMatch/index.js +58 -0
  211. package/dist/ui/elements/PcPanel/index.d.ts +43 -0
  212. package/dist/ui/elements/PcPanel/index.js +51 -0
  213. package/dist/ui/elements/PcPopover/index.d.ts +48 -0
  214. package/dist/ui/elements/PcPopover/index.js +86 -0
  215. package/dist/ui/elements/PcSlider/index.d.ts +57 -0
  216. package/dist/ui/elements/PcSlider/index.js +88 -0
  217. package/dist/ui/elements/PcStats/index.d.ts +54 -0
  218. package/dist/ui/elements/PcStats/index.js +141 -0
  219. package/dist/ui/elements/PcThemePicker/index.d.ts +56 -0
  220. package/dist/ui/elements/PcThemePicker/index.js +134 -0
  221. package/dist/ui/elements/PcToast/index.d.ts +58 -0
  222. package/dist/ui/elements/PcToast/index.js +110 -0
  223. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +37 -0
  224. package/dist/ui/elements/PcToggleCheckbox/index.js +105 -0
  225. package/dist/ui/elements/PcToolSettings/index.d.ts +128 -0
  226. package/dist/ui/elements/PcToolSettings/index.js +543 -0
  227. package/dist/ui/elements/PcToolSettings/style.js +3 -0
  228. package/dist/ui/elements/PcToolbar/index.d.ts +71 -0
  229. package/dist/ui/elements/PcToolbar/index.js +190 -0
  230. package/dist/ui/elements/PcToolbar/style.js +3 -0
  231. package/dist/ui/icons/images/brush.js +3 -0
  232. package/dist/ui/icons/images/circle-mask.js +3 -0
  233. package/dist/ui/icons/images/clipboard.js +3 -0
  234. package/dist/ui/icons/images/cursor.js +3 -0
  235. package/dist/ui/icons/images/eraser.js +3 -0
  236. package/dist/ui/icons/images/eyedropper.js +3 -0
  237. package/dist/ui/icons/images/flip-horizontal.js +3 -0
  238. package/dist/ui/icons/images/flip-vertical.js +3 -0
  239. package/dist/ui/icons/images/focus-pen.js +3 -0
  240. package/dist/ui/icons/images/freehand-mask.js +3 -0
  241. package/dist/ui/icons/images/hidden.js +3 -0
  242. package/dist/ui/icons/images/layers.js +3 -0
  243. package/dist/ui/icons/images/linked.js +3 -0
  244. package/dist/ui/icons/images/locked.js +3 -0
  245. package/dist/ui/icons/images/moon.js +3 -0
  246. package/dist/ui/icons/images/pan.js +3 -0
  247. package/dist/ui/icons/images/polygon-mask.js +3 -0
  248. package/dist/ui/icons/images/pressure.js +3 -0
  249. package/dist/ui/icons/images/randomise.js +3 -0
  250. package/dist/ui/icons/images/rectangle-mask.js +3 -0
  251. package/dist/ui/icons/images/redo.js +3 -0
  252. package/dist/ui/icons/images/rotate-90-left.js +3 -0
  253. package/dist/ui/icons/images/rotate-90-right.js +3 -0
  254. package/dist/ui/icons/images/smoothing-bezier-cubic.js +3 -0
  255. package/dist/ui/icons/images/smoothing-bezier-quad.js +3 -0
  256. package/dist/ui/icons/images/smoothing-linear.js +3 -0
  257. package/dist/ui/icons/images/star.js +3 -0
  258. package/dist/ui/icons/images/sun.js +3 -0
  259. package/dist/ui/icons/images/tilt.js +3 -0
  260. package/dist/ui/icons/images/undo.js +3 -0
  261. package/dist/ui/icons/images/unlinked.js +3 -0
  262. package/dist/ui/icons/images/unlocked.js +3 -0
  263. package/dist/ui/icons/images/visible.js +3 -0
  264. package/dist/ui/icons/images/zoom-in.js +3 -0
  265. package/dist/ui/icons/images/zoom-out.js +3 -0
  266. package/dist/ui/icons/images/zoom.js +3 -0
  267. package/dist/ui/icons/index.d.ts +42 -0
  268. package/dist/ui/icons/index.js +130 -0
  269. package/dist/ui/index.d.ts +21 -0
  270. package/dist/ui/index.js +22 -0
  271. package/dist/ui/layout/index.d.ts +88 -0
  272. package/dist/ui/layout/index.js +96 -0
  273. package/dist/ui/styles/index.d.ts +13 -0
  274. package/dist/ui/styles/index.js +195 -0
  275. package/dist/ui/tokens/index.d.ts +29 -0
  276. package/dist/ui/tokens/index.js +114 -0
  277. package/locales/en-GB.json +186 -0
  278. package/locales/en-US.json +186 -0
  279. package/locales/fr.json +186 -0
  280. package/locales/ja.json +186 -0
  281. package/locales/ko.json +186 -0
  282. package/locales/zh-Hans.json +186 -0
  283. package/locales/zh-Hant.json +186 -0
  284. package/package.json +97 -3
@@ -0,0 +1,346 @@
1
+ import { pixelFingerprint } from "../../image/index.js";
2
+ import "../../storage/index.js";
3
+ // src/lib/classes/LayerAutosave/index.ts
4
+ /** How long to wait after the layers change before saving, so quick changes are saved together (milliseconds) */
5
+ var SAVE_DELAY_MS = 500;
6
+ /** Name of the save in the canvas's background work */
7
+ var SAVE_WORK = "save";
8
+ /** Event dispatched when loading or saving fails (detail: the error) */
9
+ var AUTOSAVE_ERROR_EVENT = "error";
10
+ /** Where autosaving is up to */
11
+ var AutosaveState = {
12
+ /** Waiting to draw (another window may have the drawing) or loading the saved drawing */
13
+ Starting: "starting",
14
+ /** Another window has the drawing; this one doesn't save (see `takeOver`) */
15
+ Elsewhere: "elsewhere",
16
+ /** The saved drawing is shown, and changes are saved */
17
+ Ready: "ready",
18
+ /** Not saving any more (see `stop`) */
19
+ Stopped: "stopped"
20
+ };
21
+ /**
22
+ * Encode a canvas as a PNG.
23
+ */
24
+ var encodePng = (canvas) => {
25
+ canvas.getContext("2d");
26
+ return canvas.convertToBlob({ type: "image/png" });
27
+ };
28
+ /**
29
+ * Whether a saved drawing has the expected shape and an image for each layer.
30
+ */
31
+ var isComplete = (drawing, images) => drawing.version === 1 && drawing.layers.length > 0 && drawing.layers.every((layer) => images.has(layer.id)) && drawing.layers.some((layer) => layer.id === drawing.activeLayer);
32
+ /**
33
+ * Keeps a canvas's layers in a store, so the drawing survives reloading the page: restores the saved drawing when it
34
+ * starts, then saves soon after the layers change (once drawing pauses), and straight away when the page is hidden.
35
+ * Only layers whose pixels changed are encoded and saved again; a quick fingerprint of each layer's pixels tells
36
+ * which.
37
+ *
38
+ * Only one window draws on a drawing at a time: the first to start has it, and others wait in the `elsewhere` state
39
+ * (the UI blocks drawing there). When the page is shown again it tries again, and `takeOver` takes the drawing from
40
+ * the other window, which then waits instead. Without Web Locks, every window draws.
41
+ *
42
+ * If the saved drawing can't be loaded or restored, autosaving stops (`stopped`) so the drawing isn't saved over; clear
43
+ * the store to start again. Dispatches `change` when its state or `storedBytes` changes, and `error` (detail: the
44
+ * error) when loading or saving fails. Undo history isn't saved.
45
+ *
46
+ * @example
47
+ * const autosave = new LayerAutosave(canvas, new IndexedDbLayerStore("my-drawing"));
48
+ * autosave.start();
49
+ * autosave.addEventListener("change", () => (blocked.hidden = autosave.state !== AutosaveState.Elsewhere));
50
+ */
51
+ var LayerAutosave = class extends EventTarget {
52
+ /** Where the layers are kept */
53
+ store;
54
+ /** The last loading or saving error, until a save succeeds */
55
+ error;
56
+ currentState = AutosaveState.Starting;
57
+ canvas;
58
+ locks;
59
+ document;
60
+ encode;
61
+ decode;
62
+ delayMs;
63
+ /** The layers being watched for changes */
64
+ watched;
65
+ /** Fingerprints of each layer's pixels as last saved, by layer ID */
66
+ prints = /* @__PURE__ */ new Map();
67
+ /** Size in bytes of each layer's image as last saved or restored, by layer ID */
68
+ sizes = /* @__PURE__ */ new Map();
69
+ /** Layers whose pixels changed since the last save */
70
+ changed = /* @__PURE__ */ new Set();
71
+ /** Whether any layer may have changed since the last save (not only those in `changed`) */
72
+ checkAll = false;
73
+ timer;
74
+ /** The save being written, if any */
75
+ saving;
76
+ /** Whether to save again once the current save finishes */
77
+ again = false;
78
+ /** Lets go of the drawing's lock, while held */
79
+ release;
80
+ onLayersChange = () => {
81
+ const id = this.watched?.changedLayerId;
82
+ if (id) this.changed.add(id);
83
+ else this.checkAll = true;
84
+ this.schedule();
85
+ };
86
+ onCanvasChange = () => {
87
+ if (this.canvas.layers === this.watched) return;
88
+ this.watch();
89
+ this.onLayersChange();
90
+ };
91
+ onVisibilityChange = (event) => {
92
+ if (event.target.visibilityState === "hidden") this.save();
93
+ else if (this.currentState === AutosaveState.Elsewhere) this.claim(false);
94
+ };
95
+ constructor(canvas, store, options = {}) {
96
+ super();
97
+ this.canvas = canvas;
98
+ this.store = store;
99
+ this.locks = options.locks === void 0 ? globalThis.navigator?.locks ?? null : options.locks;
100
+ this.document = options.document === void 0 ? globalThis.document ?? null : options.document;
101
+ this.encode = options.encode ?? encodePng;
102
+ this.decode = options.decode ?? ((image) => createImageBitmap(image));
103
+ this.delayMs = options.delayMs ?? 500;
104
+ }
105
+ /** Where autosaving is up to (see `AutosaveState`) */
106
+ get state() {
107
+ return this.currentState;
108
+ }
109
+ /** Total size in bytes of the layer images kept in the store (as last saved or restored) */
110
+ get storedBytes() {
111
+ let total = 0;
112
+ for (const size of this.sizes.values()) total += size;
113
+ return total;
114
+ }
115
+ /** Name of the Web Lock that only one window holds per drawing */
116
+ get lockName() {
117
+ return `paintcanvas:${this.store.name}`;
118
+ }
119
+ /**
120
+ * Start: wait for the drawing (unless another window has it), restore it, then save changes.
121
+ */
122
+ start() {
123
+ this.document?.addEventListener("visibilitychange", this.onVisibilityChange);
124
+ this.claim(false);
125
+ }
126
+ /**
127
+ * Take the drawing from the window that has it: that window stops saving, and this one restores the drawing as last
128
+ * saved and saves its changes. Does nothing unless another window has it.
129
+ */
130
+ takeOver() {
131
+ if (this.currentState === AutosaveState.Elsewhere) this.claim(true);
132
+ }
133
+ /**
134
+ * Stop saving and let other windows have the drawing, after saving any changes not saved yet.
135
+ */
136
+ async stop() {
137
+ if (this.currentState === AutosaveState.Stopped) return;
138
+ this.document?.removeEventListener("visibilitychange", this.onVisibilityChange);
139
+ await this.save();
140
+ this.unwatch();
141
+ this.setState(AutosaveState.Stopped);
142
+ this.letGo();
143
+ }
144
+ /**
145
+ * Save the changes not saved yet now, without waiting. While a save is being written, another follows it.
146
+ */
147
+ save() {
148
+ clearTimeout(this.timer);
149
+ this.timer = void 0;
150
+ if (this.currentState !== AutosaveState.Ready) return Promise.resolve();
151
+ if (this.saving) {
152
+ this.again = true;
153
+ return this.saving;
154
+ }
155
+ this.saving = this.write().finally(() => {
156
+ this.saving = void 0;
157
+ if (!this.again) return;
158
+ this.again = false;
159
+ this.save();
160
+ });
161
+ return this.saving;
162
+ }
163
+ /**
164
+ * Ask for the drawing's lock, if there are locks: only if no other window has it, or taking it from the one that
165
+ * does. Without locks, start straight away.
166
+ */
167
+ claim(steal) {
168
+ if (!this.locks) {
169
+ this.begin();
170
+ return;
171
+ }
172
+ this.locks.request(this.lockName, steal ? { steal: true } : { ifAvailable: true }, (lock) => {
173
+ if (this.currentState === AutosaveState.Stopped) return void 0;
174
+ if (!lock) {
175
+ this.setState(AutosaveState.Elsewhere);
176
+ return;
177
+ }
178
+ return new Promise((release) => {
179
+ this.release = release;
180
+ this.begin();
181
+ });
182
+ }).catch((error) => {
183
+ if (error?.name === "AbortError") this.lostLock();
184
+ else this.begin();
185
+ });
186
+ }
187
+ /**
188
+ * Restore the saved drawing (if any), then watch for changes; with nothing saved, the current drawing is saved. If
189
+ * the saved drawing can't be loaded or restored (e.g. it's from another version, or an image can't be decoded),
190
+ * autosaving stops instead, so it isn't saved over.
191
+ */
192
+ async begin() {
193
+ if (this.currentState === AutosaveState.Stopped) return;
194
+ this.setState(AutosaveState.Starting);
195
+ let restored;
196
+ try {
197
+ const saved = await this.store.load();
198
+ restored = saved !== void 0 && await this.restore(saved);
199
+ } catch (error) {
200
+ this.fail(error);
201
+ this.stop();
202
+ return;
203
+ }
204
+ if (this.currentState !== AutosaveState.Starting) return;
205
+ this.prints = /* @__PURE__ */ new Map();
206
+ this.watch();
207
+ if (restored) this.remember();
208
+ this.setState(AutosaveState.Ready);
209
+ if (!restored) this.onLayersChange();
210
+ }
211
+ /**
212
+ * Draw a saved drawing onto the canvas. Returns false if autosaving stopped or another window took the drawing
213
+ * meanwhile. Throws if the drawing is incomplete or from another version, or the canvas can't take it.
214
+ */
215
+ async restore({ drawing, images }) {
216
+ if (!isComplete(drawing, images)) throw new Error("The saved drawing is incomplete or from another version");
217
+ const decoded = await Promise.all(drawing.layers.map((layer) => this.decode(images.get(layer.id))));
218
+ if (this.currentState !== AutosaveState.Starting) return false;
219
+ this.unwatch();
220
+ this.canvas.restoreLayers(drawing.width, drawing.height, drawing.layers, decoded, drawing.activeLayer);
221
+ this.sizes = new Map(drawing.layers.map((layer) => [layer.id, images.get(layer.id).size]));
222
+ return true;
223
+ }
224
+ /**
225
+ * Remember each layer's pixels as saved (just restored).
226
+ */
227
+ remember() {
228
+ for (const layer of this.canvas.layers.layers) this.prints.set(layer.id, this.fingerprint(layer.id));
229
+ this.changed.clear();
230
+ this.checkAll = false;
231
+ }
232
+ /**
233
+ * Save the drawing, encoding only the layers whose pixels changed since they were last saved.
234
+ */
235
+ async write() {
236
+ const stack = this.canvas.layers;
237
+ const { checkAll, changed } = this;
238
+ this.checkAll = false;
239
+ this.changed = /* @__PURE__ */ new Set();
240
+ const prints = /* @__PURE__ */ new Map();
241
+ try {
242
+ const encoding = [];
243
+ for (const layer of stack.layers) {
244
+ if (!!this.prints.has(layer.id) && !checkAll && !changed.has(layer.id)) continue;
245
+ const print = this.fingerprint(layer.id);
246
+ if (print === this.prints.get(layer.id)) continue;
247
+ prints.set(layer.id, print);
248
+ encoding.push(this.encode(layer.canvas).then((image) => [layer.id, image]));
249
+ }
250
+ const drawing = {
251
+ version: 1,
252
+ width: stack.width,
253
+ height: stack.height,
254
+ layers: stack.layers.map((layer) => layer.toConfig()),
255
+ activeLayer: stack.active.id
256
+ };
257
+ const images = new Map(await Promise.all(encoding));
258
+ if (this.currentState !== AutosaveState.Ready) return;
259
+ await this.store.save(drawing, images);
260
+ const kept = new Set(drawing.layers.map((layer) => layer.id));
261
+ for (const id of this.prints.keys()) if (!kept.has(id)) this.prints.delete(id);
262
+ for (const [id, print] of prints) this.prints.set(id, print);
263
+ this.error = void 0;
264
+ this.rememberSizes(kept, images);
265
+ } catch (error) {
266
+ for (const id of [...changed, ...prints.keys()]) this.changed.add(id);
267
+ this.checkAll ||= checkAll;
268
+ this.fail(error);
269
+ }
270
+ }
271
+ /**
272
+ * Remember the sizes of the layer images just saved, forgetting layers that are gone, and tell listeners if the total
273
+ * changed.
274
+ */
275
+ rememberSizes(kept, images) {
276
+ const before = this.storedBytes;
277
+ for (const id of this.sizes.keys()) if (!kept.has(id)) this.sizes.delete(id);
278
+ for (const [id, image] of images) this.sizes.set(id, image.size);
279
+ if (this.storedBytes !== before) this.dispatchEvent(new Event("change"));
280
+ }
281
+ /**
282
+ * A fingerprint of a layer's pixels.
283
+ */
284
+ fingerprint(id) {
285
+ const layer = this.canvas.layers.get(id);
286
+ return pixelFingerprint(layer.context.getImageData(0, 0, layer.width, layer.height).data);
287
+ }
288
+ /**
289
+ * Save once the layers have stopped changing for a moment and drawing pauses.
290
+ */
291
+ schedule() {
292
+ clearTimeout(this.timer);
293
+ this.timer = setTimeout(() => this.canvas.background.run(SAVE_WORK, () => void this.save()), this.delayMs);
294
+ }
295
+ /**
296
+ * Watch the canvas and its current layers for changes.
297
+ */
298
+ watch() {
299
+ this.unwatch();
300
+ this.watched = this.canvas.layers;
301
+ this.watched.addEventListener("change", this.onLayersChange);
302
+ this.canvas.addEventListener("change", this.onCanvasChange);
303
+ }
304
+ /**
305
+ * Stop watching for changes, forgetting a save that hasn't started.
306
+ */
307
+ unwatch() {
308
+ this.watched?.removeEventListener("change", this.onLayersChange);
309
+ this.watched = void 0;
310
+ this.canvas.removeEventListener("change", this.onCanvasChange);
311
+ clearTimeout(this.timer);
312
+ this.timer = void 0;
313
+ this.canvas.background.cancel(SAVE_WORK);
314
+ }
315
+ /**
316
+ * Another window took the drawing: stop saving and wait.
317
+ */
318
+ lostLock() {
319
+ this.release = void 0;
320
+ this.unwatch();
321
+ this.setState(AutosaveState.Elsewhere);
322
+ }
323
+ /**
324
+ * Let other windows have the drawing.
325
+ */
326
+ letGo() {
327
+ this.release?.();
328
+ this.release = void 0;
329
+ }
330
+ /**
331
+ * Remember an error and tell listeners.
332
+ */
333
+ fail(error) {
334
+ this.error = error;
335
+ this.dispatchEvent(new CustomEvent(AUTOSAVE_ERROR_EVENT, { detail: error }));
336
+ }
337
+ /**
338
+ * Change state and tell listeners.
339
+ */
340
+ setState(state) {
341
+ if (state === this.currentState) return;
342
+ this.currentState = state;
343
+ this.dispatchEvent(new Event("change"));
344
+ }
345
+ };
346
+ export { AUTOSAVE_ERROR_EVENT, AutosaveState, SAVE_DELAY_MS, SAVE_WORK, LayerAutosave as default };
@@ -0,0 +1,177 @@
1
+ import Layer, { type CanvasFactory, type LayerConfig } from "../Layer";
2
+ import History from "../History";
3
+ import type { Rect } from "../Viewport";
4
+ /** Most layers a stack can have unless another limit is given */
5
+ export declare const DEFAULT_MAX_LAYERS = 5;
6
+ /**
7
+ * Manages one user's ordered list of layers. Index 0 is the top-most layer.
8
+ *
9
+ * Dispatches a `change` event whenever layers or the active layer change.
10
+ *
11
+ * The user's actions are recorded in `history` so they can be undone: creating, duplicating, deleting and moving layers,
12
+ * changing a layer's blend mode or opacity, and changes to image data (only the changed area).
13
+ *
14
+ * @example
15
+ * const stack = new LayerStack(2000, 2000);
16
+ * stack.create(); // "Layer 2", placed above and selected
17
+ * stack.addEventListener("change", render);
18
+ */
19
+ export default class LayerStack extends EventTarget {
20
+ /** Canvas width in pixels */
21
+ readonly width: number;
22
+ /** Canvas height in pixels */
23
+ readonly height: number;
24
+ /** The user's undoable actions on these layers */
25
+ readonly history: History;
26
+ private items;
27
+ private activeId;
28
+ private createdCount;
29
+ /** The layer the change being announced is about, when only its pixels changed (see `changedLayerId`) */
30
+ private onlyChanged?;
31
+ private readonly createCanvas;
32
+ /** Most layers the stack can have */
33
+ readonly maxLayers: number;
34
+ constructor(width: number, height: number, createCanvas?: CanvasFactory, maxLayers?: number);
35
+ /** Layers from top to bottom */
36
+ get layers(): readonly Layer[];
37
+ /** The selected layer that drawing applies to */
38
+ get active(): Layer;
39
+ /** Whether another layer can be created */
40
+ get canCreate(): boolean;
41
+ /**
42
+ * Get a layer by ID. Throws if not found.
43
+ */
44
+ get(id: string): Layer;
45
+ /**
46
+ * Get a layer's index (0 = top). Throws if not found.
47
+ */
48
+ indexOf(id: string): number;
49
+ /**
50
+ * Select a layer.
51
+ */
52
+ select(id: string): void;
53
+ /**
54
+ * Create an empty layer above the active layer and select it.
55
+ */
56
+ create(): Layer;
57
+ /**
58
+ * Duplicate a layer (settings and image data) above the active layer and select it.
59
+ */
60
+ duplicate(id: string): Layer;
61
+ /**
62
+ * Replace all layers with new, empty ones (e.g. to restore a saved drawing into), top first, and select the top one.
63
+ * Returns the new layers.
64
+ *
65
+ * @example
66
+ * const [top, bottom] = stack.replace([{ name: "Sketch" }, { name: "Background", opacity: 0.5 }]);
67
+ */
68
+ replace(configs: readonly LayerConfig[]): readonly Layer[];
69
+ /**
70
+ * While a `change` event is being dispatched: the layer it's about, when only that layer's image data changed (from
71
+ * `markChanged`, e.g. a brush stroke); undefined when anything may have changed.
72
+ */
73
+ get changedLayerId(): string | undefined;
74
+ /**
75
+ * Tell listeners a layer's image data changed (e.g. after a brush stroke), so thumbnails and the map redraw.
76
+ */
77
+ markChanged(id: string): void;
78
+ /**
79
+ * Remove all image data from a layer, as one undoable action.
80
+ */
81
+ clear(id: string): void;
82
+ /**
83
+ * Record a layer's image data in an area just before it changes, so the change can be undone. Pass `before` when the
84
+ * layer has already changed (a copy of the area's image data from before the change, the size of the area).
85
+ *
86
+ * @example
87
+ * stack.recordImage(layer.id, { x: 10, y: 10, width: 50, height: 20 });
88
+ * layer.context.fillRect(10, 10, 50, 20);
89
+ * stack.markChanged(layer.id);
90
+ */
91
+ recordImage(id: string, area: Rect, before?: OffscreenCanvas): void;
92
+ /**
93
+ * Change a layer's image data within an area as one undoable action, then tell listeners.
94
+ *
95
+ * @example
96
+ * stack.changeImage(layer.id, area, (ctx) => ctx.clearRect(area.x, area.y, area.width, area.height));
97
+ */
98
+ changeImage(id: string, area: Rect, change: (ctx: OffscreenCanvasRenderingContext2D) => void): void;
99
+ /**
100
+ * Whether a layer can be merged into the layer under it: there is one, and both are visible.
101
+ */
102
+ canMergeDown(id: string): boolean;
103
+ /**
104
+ * Merge a layer into the layer under it as one undoable action, making the two one layer: its pixels are drawn onto
105
+ * the layer under it as they look (with its opacity and blend mode, whatever the alpha lock), and it is deleted. The
106
+ * merged layer keeps the lower layer's name and settings, and is selected. Does nothing unless `canMergeDown`.
107
+ *
108
+ * @example
109
+ * stack.mergeDown(stack.active.id);
110
+ */
111
+ mergeDown(id: string): void;
112
+ /**
113
+ * Delete a layer. The last remaining layer cannot be deleted. If the active layer is deleted, the layer below it (or above, if it was the bottom) is selected.
114
+ */
115
+ remove(id: string): void;
116
+ /**
117
+ * Move a layer to a new index (0 = top). Layers in between shift to fill the gap.
118
+ */
119
+ move(id: string, toIndex: number): void;
120
+ /**
121
+ * Move a layer up one place.
122
+ */
123
+ moveUp(id: string): void;
124
+ /**
125
+ * Move a layer down one place.
126
+ */
127
+ moveDown(id: string): void;
128
+ /**
129
+ * Move a layer to the top.
130
+ */
131
+ moveToTop(id: string): void;
132
+ /**
133
+ * Move a layer to the bottom.
134
+ */
135
+ moveToBottom(id: string): void;
136
+ /**
137
+ * Rename a layer.
138
+ */
139
+ rename(id: string, name: string): void;
140
+ /**
141
+ * Change a layer's settings (anything except ID and name). Changes to blend mode and opacity can be undone; visibility
142
+ * and alpha lock cannot.
143
+ */
144
+ update(id: string, changes: Partial<Pick<Layer, "blendMode" | "opacity" | "visible" | "alphaLock">>): void;
145
+ /**
146
+ * Move a layer to an index and select it.
147
+ */
148
+ private moveAndSelect;
149
+ /**
150
+ * An undoable change of image data: undo and redo both swap the stored pixels with the layer's, then select the layer.
151
+ */
152
+ private imageEntry;
153
+ /**
154
+ * Create a layer with the stack's size and canvas factory.
155
+ */
156
+ private newLayer;
157
+ /**
158
+ * Get the next default layer name, e.g. "Layer 2".
159
+ */
160
+ private nextDefaultName;
161
+ /**
162
+ * Throw if the layer limit has been reached.
163
+ */
164
+ private assertCanCreate;
165
+ /**
166
+ * Insert a layer directly above the active layer and select it, as one undoable action. Undo selects `previousId`.
167
+ */
168
+ private insertAboveActive;
169
+ /**
170
+ * Insert a layer at an index (0 = top) and select it.
171
+ */
172
+ private insertAt;
173
+ /**
174
+ * Dispatch a `change` event.
175
+ */
176
+ private notify;
177
+ }