@lvl99dotcom/paintcanvas 0.0.0-stage → 0.3.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 +131 -0
  86. package/dist/lib/i18n/index.js +277 -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 +181 -0
  144. package/dist/locales/en-US.js +181 -0
  145. package/dist/locales/fr.js +181 -0
  146. package/dist/locales/ja.js +181 -0
  147. package/dist/locales/ko.js +181 -0
  148. package/dist/locales/zh-Hans.js +181 -0
  149. package/dist/locales/zh-Hant.js +181 -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 +465 -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 +154 -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 +144 -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 +549 -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 +179 -0
  278. package/locales/en-US.json +179 -0
  279. package/locales/fr.json +179 -0
  280. package/locales/ja.json +179 -0
  281. package/locales/ko.json +179 -0
  282. package/locales/zh-Hans.json +179 -0
  283. package/locales/zh-Hant.json +179 -0
  284. package/package.json +97 -3
@@ -0,0 +1,277 @@
1
+ import en_US_default from "../../locales/en-US.js";
2
+ // src/lib/i18n/index.ts
3
+ /** The locale used when no other matches, and whose strings fill in any missing from other locales */
4
+ var FALLBACK_LOCALE = "en-US";
5
+ /** Locales whose files come with PaintCanvas (in `locales/`, named by their IETF BCP 47 tag) */
6
+ var BUILT_IN_LOCALES = {
7
+ "en-US": async () => en_US_default,
8
+ "en-GB": () => import("../../locales/en-GB.js"),
9
+ fr: () => import("../../locales/fr.js"),
10
+ ja: () => import("../../locales/ja.js"),
11
+ ko: () => import("../../locales/ko.js"),
12
+ "zh-Hans": () => import("../../locales/zh-Hans.js"),
13
+ "zh-Hant": () => import("../../locales/zh-Hant.js")
14
+ };
15
+ /** Regions whose English is closer to British than American English */
16
+ var BRITISH_ENGLISH_REGIONS = /* @__PURE__ */ new Set([
17
+ "GB",
18
+ "AU",
19
+ "NZ",
20
+ "IE",
21
+ "IN",
22
+ "ZA",
23
+ "SG",
24
+ "HK",
25
+ "MY",
26
+ "PK",
27
+ "NG",
28
+ "KE"
29
+ ]);
30
+ /**
31
+ * The language, script and region of a locale tag, with the likely script and region filled in where known (e.g. `zh-TW`
32
+ * is written in Traditional Chinese, `zh-CN` in Simplified).
33
+ */
34
+ var describe = (tag) => {
35
+ try {
36
+ const locale = new Intl.Locale(tag);
37
+ const likely = locale.maximize();
38
+ return {
39
+ language: locale.language,
40
+ script: likely.script,
41
+ region: locale.region
42
+ };
43
+ } catch {
44
+ return;
45
+ }
46
+ };
47
+ /**
48
+ * The best available locale for the requested ones (e.g. the device's `navigator.languages`, most preferred first):
49
+ * the same tag, then the same language and script (`zh-TW` → `zh-Hant`), then the same language (British regions
50
+ * prefer `en-GB`, other English `en-US`), otherwise `fallback`.
51
+ *
52
+ * @example
53
+ * matchLocale(["zh-TW", "en"], ["en-US", "zh-Hans", "zh-Hant"]); // "zh-Hant"
54
+ * matchLocale(["en-AU"], ["en-US", "en-GB"]); // "en-GB"
55
+ * matchLocale(["fr-FR"], ["en-US", "ja"]); // "en-US"
56
+ */
57
+ var matchLocale = (requested, available, fallback = FALLBACK_LOCALE) => {
58
+ const lower = (tag) => tag.toLowerCase();
59
+ for (const tag of requested) {
60
+ const exact = available.find((each) => lower(each) === lower(tag));
61
+ if (exact) return exact;
62
+ const wanted = describe(tag);
63
+ if (!wanted) continue;
64
+ const sameLanguage = available.filter((each) => describe(each)?.language === wanted.language);
65
+ if (sameLanguage.length === 0) continue;
66
+ const sameScript = sameLanguage.filter((each) => describe(each)?.script === wanted.script);
67
+ const candidates = sameScript.length > 0 ? sameScript : sameLanguage;
68
+ if (wanted.language === "en") {
69
+ const preferred = wanted.region && BRITISH_ENGLISH_REGIONS.has(wanted.region) ? "en-GB" : "en-US";
70
+ const regional = candidates.find((each) => lower(each) === lower(preferred));
71
+ if (regional) return regional;
72
+ }
73
+ return candidates[0];
74
+ }
75
+ return available.includes(fallback) ? fallback : available[0] ?? fallback;
76
+ };
77
+ /**
78
+ * Put values into a string's `{name}` placeholders. Placeholders without a value are left as they are.
79
+ *
80
+ * @example
81
+ * interpolate("Delete \"{name}\"?", { name: "Sketch" }); // 'Delete "Sketch"?'
82
+ */
83
+ var interpolate = (text, values = {}) => text.replace(/\{(\w+)\}/g, (placeholder, name) => Object.hasOwn(values, name) ? String(values[name]) : placeholder);
84
+ /**
85
+ * A locale's language name, written in that language (so people can find their own), with its first letter in
86
+ * capitals. Gives the tag itself if the browser can't name it.
87
+ *
88
+ * @example
89
+ * languageName("ja"); // "日本語"
90
+ * languageName("fr"); // "Français"
91
+ * languageName("en-GB"); // "English (United Kingdom)"
92
+ */
93
+ var languageName = (locale) => {
94
+ try {
95
+ const name = new Intl.DisplayNames([locale], {
96
+ type: "language",
97
+ languageDisplay: "standard",
98
+ fallback: "none"
99
+ }).of(locale);
100
+ return name ? name.charAt(0).toLocaleUpperCase(locale) + name.slice(1) : locale;
101
+ } catch {
102
+ return locale;
103
+ }
104
+ };
105
+ /** A locale loader's strings, whether given directly or as a module's default export */
106
+ var unwrap = (module) => "default" in module && typeof module.default === "object" ? module.default : module;
107
+ /**
108
+ * One set of strings for some locales (e.g. PaintCanvas's, or an app's), each loaded once, when first needed.
109
+ */
110
+ var LocaleSource = class {
111
+ loaders;
112
+ /** Strings of each locale loaded so far */
113
+ strings = /* @__PURE__ */ new Map();
114
+ /** Loads started, so a locale loading for two callers loads once */
115
+ loading = /* @__PURE__ */ new Map();
116
+ constructor(loaders = {}) {
117
+ this.loaders = loaders;
118
+ }
119
+ /**
120
+ * Load a locale's strings, if this has a loader for it and they aren't loaded yet. A failed load rejects, and can be
121
+ * tried again.
122
+ */
123
+ load(locale) {
124
+ const loader = this.loaders[locale];
125
+ if (!loader || this.strings.has(locale)) return Promise.resolve();
126
+ let loading = this.loading.get(locale);
127
+ if (!loading) {
128
+ loading = loader().then((module) => {
129
+ this.strings.set(locale, unwrap(module));
130
+ }, (error) => {
131
+ this.loading.delete(locale);
132
+ throw error;
133
+ });
134
+ this.loading.set(locale, loading);
135
+ }
136
+ return loading;
137
+ }
138
+ };
139
+ /**
140
+ * The text the UI shows, in the chosen language. Starts in the fallback locale; `useDeviceLocale` switches to the best
141
+ * match for the device, and `setLocale` to any available locale. Locale files load when first used, without reloading
142
+ * the page. Strings missing from a locale come from the fallback locale, then the key itself.
143
+ *
144
+ * Other projects can add their own strings and locales with `addLocales` and `addStrings`. A locale's strings from
145
+ * every set load together and are merged in the order the sets were added (PaintCanvas's first), so a key in a later
146
+ * set replaces the same key in an earlier one. Keys are prefixed by project by convention: `paintCanvas.` for
147
+ * PaintCanvas's, and e.g. `liveCanvas.` for an app's.
148
+ *
149
+ * Dispatches a `change` event when the locale or its strings change, so the UI can show the new text.
150
+ *
151
+ * @example
152
+ * const translations = new Translations({ locales: { "en-US": ..., ja: () => import("./ja.json") } });
153
+ * await translations.useDeviceLocale();
154
+ * translations.t("paintCanvas.layers.delete", { name: "Sketch" });
155
+ */
156
+ var Translations = class extends EventTarget {
157
+ fallback;
158
+ /** Sets of strings, in the order they were added */
159
+ sources;
160
+ /** Each locale's strings from every set loaded so far, merged in order */
161
+ merged = /* @__PURE__ */ new Map();
162
+ current;
163
+ /** The locale most recently asked for, so a slow load can't override a later choice */
164
+ requested;
165
+ constructor(options = {}) {
166
+ super();
167
+ this.fallback = options.fallback ?? "en-US";
168
+ const builtIn = new LocaleSource(options.locales ?? BUILT_IN_LOCALES);
169
+ builtIn.strings.set(this.fallback, options.fallbackStrings ?? en_US_default);
170
+ this.sources = [builtIn];
171
+ this.merge(this.fallback);
172
+ this.current = this.fallback;
173
+ }
174
+ /** The locale in use (an IETF BCP 47 tag, e.g. `ja`) */
175
+ get locale() {
176
+ return this.current;
177
+ }
178
+ /** Locales that can be used: the fallback, and every locale a set of strings can load */
179
+ get locales() {
180
+ return [.../* @__PURE__ */ new Set([this.fallback, ...this.sources.flatMap((source) => Object.keys(source.loaders))])];
181
+ }
182
+ /**
183
+ * Switch to the best available locale for the device's languages (`navigator.languages`).
184
+ */
185
+ useDeviceLocale(languages = typeof navigator === "undefined" ? [] : navigator.languages) {
186
+ return this.setLocale(matchLocale(languages, this.locales, this.fallback));
187
+ }
188
+ /**
189
+ * Switch to a locale (the best available match for the tag), loading its strings from every set if needed.
190
+ * Dispatches `change` once they're ready. If loading fails, the current locale stays and this rejects.
191
+ *
192
+ * @example
193
+ * await translations.setLocale("zh-TW"); // uses zh-Hant
194
+ */
195
+ async setLocale(tag) {
196
+ const locale = matchLocale([tag], this.locales, this.fallback);
197
+ this.requested = locale;
198
+ await this.load(locale);
199
+ if (this.requested !== locale) return;
200
+ this.current = locale;
201
+ this.dispatchEvent(new Event("change"));
202
+ }
203
+ /**
204
+ * Add a set of strings for some locales, loaded when first used, after the sets already added (so its keys replace
205
+ * theirs). Its locales become available. The locale in use and the fallback load straight away, then `change` is
206
+ * dispatched if the set has strings for either.
207
+ *
208
+ * @example
209
+ * await translations.addLocales({
210
+ * "en-US": () => import("./locales/en-US.json"),
211
+ * fr: () => import("./locales/fr.json"),
212
+ * });
213
+ * translations.t("liveCanvas.share");
214
+ */
215
+ async addLocales(loaders) {
216
+ this.sources.push(new LocaleSource(loaders));
217
+ const locales = [.../* @__PURE__ */ new Set([this.current, this.fallback])];
218
+ await Promise.all(locales.map((locale) => this.load(locale)));
219
+ if (locales.some((locale) => Object.hasOwn(loaders, locale))) this.dispatchEvent(new Event("change"));
220
+ }
221
+ /**
222
+ * Add or replace strings for a locale (e.g. the implementer's own wording or extra keys), after the sets already
223
+ * added. Dispatches `change` if it's the locale in use or the fallback.
224
+ *
225
+ * @example
226
+ * translations.addStrings("en-US", { "paintCanvas.tool.brush": "Paintbrush" });
227
+ */
228
+ addStrings(locale, strings) {
229
+ const source = new LocaleSource();
230
+ source.strings.set(locale, strings);
231
+ this.sources.push(source);
232
+ this.merge(locale);
233
+ if (locale === this.current || locale === this.fallback) this.dispatchEvent(new Event("change"));
234
+ }
235
+ /**
236
+ * Load a locale's strings from every set, including sets added while loading, then merge them.
237
+ */
238
+ async load(locale) {
239
+ let count;
240
+ do {
241
+ count = this.sources.length;
242
+ await Promise.all(this.sources.map((source) => source.load(locale)));
243
+ } while (count !== this.sources.length);
244
+ this.merge(locale);
245
+ }
246
+ /**
247
+ * Merge a locale's strings loaded so far from every set, in the order the sets were added.
248
+ */
249
+ merge(locale) {
250
+ this.merged.set(locale, Object.assign({}, ...this.sources.map((source) => source.strings.get(locale))));
251
+ }
252
+ /**
253
+ * Translate a key, putting values into its `{name}` placeholders. With a numeric `count`, the key's plural form for
254
+ * the locale is used when there is one (e.g. `paintCanvas.presets.imported.one` and
255
+ * `paintCanvas.presets.imported.other`). Falls back to the fallback locale, then the key.
256
+ *
257
+ * @example
258
+ * translations.t("paintCanvas.presets.imported", { count: 3 }); // "Imported 3 brush presets"
259
+ */
260
+ t(key, values) {
261
+ const count = values?.count;
262
+ const keys = typeof count === "number" ? [
263
+ `${key}.${new Intl.PluralRules(this.current).select(count)}`,
264
+ `${key}.other`,
265
+ key
266
+ ] : [key];
267
+ for (const locale of [this.current, this.fallback]) {
268
+ const strings = this.merged.get(locale);
269
+ const found = keys.find((each) => strings && Object.hasOwn(strings, each));
270
+ if (found) return interpolate(strings[found], values);
271
+ }
272
+ return interpolate(key, values);
273
+ }
274
+ };
275
+ /** The translations PaintCanvas's UI uses, unless an element is given others */
276
+ var translations = new Translations();
277
+ export { BUILT_IN_LOCALES, FALLBACK_LOCALE, Translations as default, interpolate, languageName, matchLocale, translations };
@@ -0,0 +1,90 @@
1
+ /** An area of the canvas as [x1, y1, x2, y2] in pixels (x2 and y2 are exclusive) */
2
+ export type BoundingBox = [number, number, number, number];
3
+ /** How a patch's image data is encoded */
4
+ export declare const ImageEncoding: {
5
+ /** Raw RGBA bytes compressed with deflate (lossless, no PNG overhead) */
6
+ readonly RgbaDeflate: "rgba-deflate";
7
+ /** PNG file bytes (lossless) */
8
+ readonly Png: "png";
9
+ };
10
+ export type ImageEncoding = (typeof ImageEncoding)[keyof typeof ImageEncoding];
11
+ /** Number of bytes per RGBA pixel */
12
+ export declare const BYTES_PER_PIXEL = 4;
13
+ /**
14
+ * Get the width and height of a bounding box.
15
+ *
16
+ * @example
17
+ * boundingBoxSize([10, 10, 30, 20]); // { width: 20, height: 10 }
18
+ */
19
+ export declare const boundingBoxSize: ([x1, y1, x2, y2]: BoundingBox) => {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /**
24
+ * Get the number of raw RGBA bytes needed for a bounding box.
25
+ *
26
+ * @example
27
+ * rgbaByteLength([0, 0, 2, 2]); // 16
28
+ */
29
+ export declare const rgbaByteLength: (coords: BoundingBox) => number;
30
+ /**
31
+ * A quick fingerprint of RGBA pixels (FNV-1a over each pixel), to tell whether they changed without keeping a copy of
32
+ * them. Different pixels give the same fingerprint only very rarely (about 1 in 4 billion).
33
+ *
34
+ * @example
35
+ * const print = pixelFingerprint(layer.context.getImageData(0, 0, layer.width, layer.height).data);
36
+ * if (print !== savedPrint) save(layer);
37
+ */
38
+ export declare const pixelFingerprint: (rgba: Uint8ClampedArray) => number;
39
+ /**
40
+ * Encode bytes as a base64 string.
41
+ *
42
+ * @example
43
+ * bytesToBase64(new Uint8Array([104, 105])); // "aGk="
44
+ */
45
+ export declare const bytesToBase64: (bytes: Uint8Array) => string;
46
+ /**
47
+ * Decode a base64 string to bytes.
48
+ *
49
+ * @example
50
+ * base64ToBytes("aGk="); // Uint8Array [104, 105]
51
+ */
52
+ export declare const base64ToBytes: (base64: string) => Uint8Array<ArrayBuffer>;
53
+ /**
54
+ * Get the decoded byte length of a base64 string without decoding it.
55
+ *
56
+ * @example
57
+ * base64ByteLength("aGk="); // 2
58
+ */
59
+ export declare const base64ByteLength: (base64: string) => number;
60
+ /**
61
+ * Compress bytes with deflate using the built-in `CompressionStream`.
62
+ *
63
+ * @example
64
+ * const compressed = await deflate(rgbaBytes);
65
+ */
66
+ export declare const deflate: (bytes: Uint8Array<ArrayBuffer>) => Promise<Uint8Array<ArrayBuffer>>;
67
+ /**
68
+ * Decompress deflated bytes using the built-in `DecompressionStream`.
69
+ *
70
+ * Stops and throws as soon as the output exceeds `maxBytes`, so malicious data cannot exhaust memory (decompression bomb).
71
+ *
72
+ * @example
73
+ * const rgba = await inflate(compressed, 2000 * 2000 * 4);
74
+ */
75
+ export declare const inflate: (bytes: Uint8Array<ArrayBuffer>, maxBytes: number) => Promise<Uint8Array<ArrayBuffer>>;
76
+ /**
77
+ * Encode raw RGBA pixels for a bounding box as deflated base64 (the `rgba-deflate` image encoding).
78
+ *
79
+ * @example
80
+ * const data = await encodeRgbaDeflate(imageData.data, [0, 0, imageData.width, imageData.height]);
81
+ */
82
+ export declare const encodeRgbaDeflate: (rgba: Uint8Array | Uint8ClampedArray, coords: BoundingBox) => Promise<string>;
83
+ /**
84
+ * Decode `rgba-deflate` base64 data into raw RGBA pixels for a bounding box. Throws if the size does not match exactly.
85
+ *
86
+ * @example
87
+ * const rgba = await decodeRgbaDeflate(message.data, message.coords);
88
+ * const imageData = new ImageData(rgba, width, height);
89
+ */
90
+ export declare const decodeRgbaDeflate: (data: string, coords: BoundingBox) => Promise<Uint8ClampedArray<ArrayBuffer>>;
@@ -0,0 +1,147 @@
1
+ // src/lib/image/index.ts
2
+ /** How a patch's image data is encoded */
3
+ var ImageEncoding = {
4
+ /** Raw RGBA bytes compressed with deflate (lossless, no PNG overhead) */
5
+ RgbaDeflate: "rgba-deflate",
6
+ /** PNG file bytes (lossless) */
7
+ Png: "png"
8
+ };
9
+ /** Number of bytes per RGBA pixel */
10
+ var BYTES_PER_PIXEL = 4;
11
+ /** Size of chunks used when converting bytes to base64, to stay within function argument limits */
12
+ var BASE64_CHUNK_SIZE = 32768;
13
+ /**
14
+ * Get the width and height of a bounding box.
15
+ *
16
+ * @example
17
+ * boundingBoxSize([10, 10, 30, 20]); // { width: 20, height: 10 }
18
+ */
19
+ var boundingBoxSize = ([x1, y1, x2, y2]) => ({
20
+ width: x2 - x1,
21
+ height: y2 - y1
22
+ });
23
+ /**
24
+ * Get the number of raw RGBA bytes needed for a bounding box.
25
+ *
26
+ * @example
27
+ * rgbaByteLength([0, 0, 2, 2]); // 16
28
+ */
29
+ var rgbaByteLength = (coords) => {
30
+ const { width, height } = boundingBoxSize(coords);
31
+ return width * height * 4;
32
+ };
33
+ /**
34
+ * A quick fingerprint of RGBA pixels (FNV-1a over each pixel), to tell whether they changed without keeping a copy of
35
+ * them. Different pixels give the same fingerprint only very rarely (about 1 in 4 billion).
36
+ *
37
+ * @example
38
+ * const print = pixelFingerprint(layer.context.getImageData(0, 0, layer.width, layer.height).data);
39
+ * if (print !== savedPrint) save(layer);
40
+ */
41
+ var pixelFingerprint = (rgba) => {
42
+ const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length / 4);
43
+ let hash = 2166136261;
44
+ for (let i = 0; i < pixels.length; i++) hash = Math.imul(hash ^ pixels[i], 16777619);
45
+ return hash >>> 0;
46
+ };
47
+ /**
48
+ * Encode bytes as a base64 string.
49
+ *
50
+ * @example
51
+ * bytesToBase64(new Uint8Array([104, 105])); // "aGk="
52
+ */
53
+ var bytesToBase64 = (bytes) => {
54
+ let binary = "";
55
+ for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE) binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));
56
+ return btoa(binary);
57
+ };
58
+ /**
59
+ * Decode a base64 string to bytes.
60
+ *
61
+ * @example
62
+ * base64ToBytes("aGk="); // Uint8Array [104, 105]
63
+ */
64
+ var base64ToBytes = (base64) => {
65
+ const binary = atob(base64);
66
+ const bytes = new Uint8Array(binary.length);
67
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
68
+ return bytes;
69
+ };
70
+ /**
71
+ * Get the decoded byte length of a base64 string without decoding it.
72
+ *
73
+ * @example
74
+ * base64ByteLength("aGk="); // 2
75
+ */
76
+ var base64ByteLength = (base64) => {
77
+ const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
78
+ return base64.length / 4 * 3 - padding;
79
+ };
80
+ /** Bytes as a stream (through `Response`, which every runtime streams, unlike some `Blob` implementations) */
81
+ var streamOf = (bytes) => new Response(bytes).body;
82
+ /**
83
+ * Compress bytes with deflate using the built-in `CompressionStream`.
84
+ *
85
+ * @example
86
+ * const compressed = await deflate(rgbaBytes);
87
+ */
88
+ var deflate = async (bytes) => {
89
+ const stream = streamOf(bytes).pipeThrough(new CompressionStream("deflate"));
90
+ return new Uint8Array(await new Response(stream).arrayBuffer());
91
+ };
92
+ /**
93
+ * Decompress deflated bytes using the built-in `DecompressionStream`.
94
+ *
95
+ * Stops and throws as soon as the output exceeds `maxBytes`, so malicious data cannot exhaust memory (decompression bomb).
96
+ *
97
+ * @example
98
+ * const rgba = await inflate(compressed, 2000 * 2000 * 4);
99
+ */
100
+ var inflate = async (bytes, maxBytes) => {
101
+ const reader = streamOf(bytes).pipeThrough(new DecompressionStream("deflate")).getReader();
102
+ const chunks = [];
103
+ let total = 0;
104
+ for (;;) {
105
+ const { done, value } = await reader.read();
106
+ if (done) break;
107
+ total += value.length;
108
+ if (total > maxBytes) {
109
+ await reader.cancel();
110
+ throw new Error(`Decompressed data exceeds ${maxBytes} bytes`);
111
+ }
112
+ chunks.push(value);
113
+ }
114
+ const output = new Uint8Array(total);
115
+ let offset = 0;
116
+ for (const chunk of chunks) {
117
+ output.set(chunk, offset);
118
+ offset += chunk.length;
119
+ }
120
+ return output;
121
+ };
122
+ /**
123
+ * Encode raw RGBA pixels for a bounding box as deflated base64 (the `rgba-deflate` image encoding).
124
+ *
125
+ * @example
126
+ * const data = await encodeRgbaDeflate(imageData.data, [0, 0, imageData.width, imageData.height]);
127
+ */
128
+ var encodeRgbaDeflate = async (rgba, coords) => {
129
+ if (rgba.length !== rgbaByteLength(coords)) throw new Error("RGBA data length does not match bounding box size");
130
+ const bytes = new Uint8Array(rgba.length);
131
+ bytes.set(rgba);
132
+ return bytesToBase64(await deflate(bytes));
133
+ };
134
+ /**
135
+ * Decode `rgba-deflate` base64 data into raw RGBA pixels for a bounding box. Throws if the size does not match exactly.
136
+ *
137
+ * @example
138
+ * const rgba = await decodeRgbaDeflate(message.data, message.coords);
139
+ * const imageData = new ImageData(rgba, width, height);
140
+ */
141
+ var decodeRgbaDeflate = async (data, coords) => {
142
+ const expected = rgbaByteLength(coords);
143
+ const rgba = await inflate(base64ToBytes(data), expected);
144
+ if (rgba.length !== expected) throw new Error("RGBA data length does not match bounding box size");
145
+ return new Uint8ClampedArray(rgba.buffer);
146
+ };
147
+ export { BYTES_PER_PIXEL, ImageEncoding, base64ByteLength, base64ToBytes, boundingBoxSize, bytesToBase64, decodeRgbaDeflate, deflate, encodeRgbaDeflate, inflate, pixelFingerprint, rgbaByteLength };
@@ -0,0 +1,75 @@
1
+ import { type BoundingBox } from "../../image";
2
+ import type { CanvasFactory } from "../../classes/Layer";
3
+ import { type DrawableLayer } from "../../rendering/compositing";
4
+ import { type PatchMessage } from "..";
5
+ /** How long to wait after a change before sending, so quick changes go together */
6
+ export declare const PATCH_DELAY_MS = 200;
7
+ /**
8
+ * The smallest box containing every pixel that differs between two images of the same size (one 32-bit value per
9
+ * pixel), or undefined if they are the same.
10
+ *
11
+ * @example
12
+ * changedArea(before, after, 100, 100); // [10, 20, 31, 45]
13
+ */
14
+ export declare const changedArea: (before: Uint32Array, after: Uint32Array, width: number, height: number) => BoundingBox | undefined;
15
+ /**
16
+ * Copy a box of RGBA pixels out of a whole image.
17
+ *
18
+ * @example
19
+ * cropRgba(imageData.data, imageData.width, [10, 10, 20, 20]); // 10 × 10 pixels
20
+ */
21
+ export declare const cropRgba: (rgba: Uint8ClampedArray, width: number, [x1, y1, x2, y2]: BoundingBox) => Uint8ClampedArray;
22
+ /** Options for sending patches */
23
+ export interface PatchSenderOptions {
24
+ width: number;
25
+ height: number;
26
+ /** The user's own layers, top first */
27
+ layers: () => readonly DrawableLayer[];
28
+ /** Delivers a patch (if it throws or rejects, the changes go with the next patch) */
29
+ send: (message: PatchMessage) => void | Promise<void>;
30
+ createCanvas: CanvasFactory;
31
+ delayMs?: number;
32
+ /** Runs the sync once it's due, e.g. waiting for a pause in drawing (runs it straight away by default) */
33
+ whenIdle?: (run: () => unknown) => void;
34
+ }
35
+ /**
36
+ * Sends the user's drawing as patches. Shortly after any change (and once `whenIdle` runs it), it flattens the
37
+ * user's own layers, compares the result with what it last sent, and sends only the area that changed. This covers every kind of change (strokes,
38
+ * erasing, moving pixels, layer settings and order) without each tool reporting what it changed.
39
+ *
40
+ * @example
41
+ * const sync = new PatchSender({ width, height, layers: () => canvas.layers.layers, send, createCanvas });
42
+ * app.layers.addEventListener("change", () => sync.schedule());
43
+ */
44
+ export default class PatchSender {
45
+ private readonly options;
46
+ private readonly canvas;
47
+ private readonly context;
48
+ /** The pixels last sent (all transparent to start with) */
49
+ private sent;
50
+ private timer?;
51
+ /** Whether it has stopped (a sync waiting for `whenIdle` doesn't run) */
52
+ private stopped;
53
+ /** The sync in progress, so syncs never overlap */
54
+ private running;
55
+ constructor(options: PatchSenderOptions);
56
+ /**
57
+ * Send the changes soon (changes within the delay go together), once `whenIdle` runs the sync.
58
+ */
59
+ schedule(): void;
60
+ /**
61
+ * Send what changed since the last sync now. Resolves once sent; if sending fails, the changes go with the next sync.
62
+ */
63
+ sync(): Promise<void>;
64
+ /** Flatten the layers and send the changed area, if any */
65
+ private sendChanges;
66
+ /**
67
+ * Count pixels as already sent (e.g. the user's earlier work, restored from a shared drawing's history), so they aren't
68
+ * sent again.
69
+ */
70
+ markSent(area: BoundingBox, rgba: Uint8ClampedArray): void;
71
+ /**
72
+ * Stop any pending sync.
73
+ */
74
+ stop(): void;
75
+ }