@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.
- package/LICENSE +201 -0
- package/README.md +131 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/lib/brushes/BrushTipCache/index.d.ts +32 -0
- package/dist/lib/brushes/BrushTipCache/index.js +60 -0
- package/dist/lib/brushes/StrokeEngine/index.d.ts +171 -0
- package/dist/lib/brushes/StrokeEngine/index.js +324 -0
- package/dist/lib/brushes/StrokePath/index.d.ts +53 -0
- package/dist/lib/brushes/StrokePath/index.js +161 -0
- package/dist/lib/brushes/StrokeSampler/index.d.ts +55 -0
- package/dist/lib/brushes/StrokeSampler/index.js +75 -0
- package/dist/lib/brushes/authors/index.d.ts +55 -0
- package/dist/lib/brushes/authors/index.js +60 -0
- package/dist/lib/brushes/configRanges/index.d.ts +52 -0
- package/dist/lib/brushes/configRanges/index.js +206 -0
- package/dist/lib/brushes/index.d.ts +5 -0
- package/dist/lib/brushes/index.js +19 -0
- package/dist/lib/brushes/params/index.d.ts +108 -0
- package/dist/lib/brushes/params/index.js +139 -0
- package/dist/lib/brushes/presetFile/index.d.ts +32 -0
- package/dist/lib/brushes/presetFile/index.js +55 -0
- package/dist/lib/brushes/presets/drawing/brush-pen.js +3 -0
- package/dist/lib/brushes/presets/drawing/felt-pen.js +3 -0
- package/dist/lib/brushes/presets/drawing/index.d.ts +13 -0
- package/dist/lib/brushes/presets/drawing/index.js +88 -0
- package/dist/lib/brushes/presets/drawing/pencil-2b.js +3 -0
- package/dist/lib/brushes/presets/drawing/pencil-hb.js +3 -0
- package/dist/lib/brushes/presets/painting/grass.js +3 -0
- package/dist/lib/brushes/presets/painting/hair.js +3 -0
- package/dist/lib/brushes/presets/painting/index.d.ts +18 -0
- package/dist/lib/brushes/presets/painting/index.js +118 -0
- package/dist/lib/brushes/presets/painting/splatter.js +3 -0
- package/dist/lib/brushes/presets/painting/textured-paint.js +3 -0
- package/dist/lib/brushes/presets/painting/watery-paint.js +3 -0
- package/dist/lib/brushes/presets/round/index.d.ts +9 -0
- package/dist/lib/brushes/presets/round/index.js +44 -0
- package/dist/lib/brushes/presets/round/round.js +3 -0
- package/dist/lib/brushes/presets/round/soft-round.js +3 -0
- package/dist/lib/brushes/presets/sized/index.d.ts +20 -0
- package/dist/lib/brushes/presets/sized/index.js +30 -0
- package/dist/lib/brushes/presets/sketching/index.d.ts +14 -0
- package/dist/lib/brushes/presets/sketching/index.js +107 -0
- package/dist/lib/brushes/presets/square/index.d.ts +7 -0
- package/dist/lib/brushes/presets/square/index.js +22 -0
- package/dist/lib/brushes/presets/square/square.js +3 -0
- package/dist/lib/brushes/shapeImage/index.d.ts +40 -0
- package/dist/lib/brushes/shapeImage/index.js +61 -0
- package/dist/lib/brushes/types/index.d.ts +243 -0
- package/dist/lib/brushes/types/index.js +20 -0
- package/dist/lib/classes/BackgroundWork/index.d.ts +55 -0
- package/dist/lib/classes/BackgroundWork/index.js +97 -0
- package/dist/lib/classes/BrushSettings/index.d.ts +91 -0
- package/dist/lib/classes/BrushSettings/index.js +152 -0
- package/dist/lib/classes/CustomPresets/index.d.ts +53 -0
- package/dist/lib/classes/CustomPresets/index.js +71 -0
- package/dist/lib/classes/ExternalSheets/index.d.ts +64 -0
- package/dist/lib/classes/ExternalSheets/index.js +112 -0
- package/dist/lib/classes/GestureTracker/index.d.ts +62 -0
- package/dist/lib/classes/GestureTracker/index.js +122 -0
- package/dist/lib/classes/History/index.d.ts +83 -0
- package/dist/lib/classes/History/index.js +126 -0
- package/dist/lib/classes/HistoryGuard/index.d.ts +58 -0
- package/dist/lib/classes/HistoryGuard/index.js +97 -0
- package/dist/lib/classes/InputSettings/index.d.ts +74 -0
- package/dist/lib/classes/InputSettings/index.js +101 -0
- package/dist/lib/classes/KeyboardShortcuts/index.d.ts +48 -0
- package/dist/lib/classes/KeyboardShortcuts/index.js +137 -0
- package/dist/lib/classes/Layer/index.d.ts +86 -0
- package/dist/lib/classes/Layer/index.js +111 -0
- package/dist/lib/classes/LayerAutosave/index.d.ts +180 -0
- package/dist/lib/classes/LayerAutosave/index.js +346 -0
- package/dist/lib/classes/LayerStack/index.d.ts +177 -0
- package/dist/lib/classes/LayerStack/index.js +394 -0
- package/dist/lib/classes/PaintCanvas/index.d.ts +537 -0
- package/dist/lib/classes/PaintCanvas/index.js +965 -0
- package/dist/lib/classes/StrokeLog/index.d.ts +149 -0
- package/dist/lib/classes/StrokeLog/index.js +257 -0
- package/dist/lib/classes/Viewport/index.d.ts +108 -0
- package/dist/lib/classes/Viewport/index.js +194 -0
- package/dist/lib/document/index.d.ts +17 -0
- package/dist/lib/document/index.js +25 -0
- package/dist/lib/document/schema.d.ts +132 -0
- package/dist/lib/document/schema.js +110 -0
- package/dist/lib/i18n/index.d.ts +94 -0
- package/dist/lib/i18n/index.js +182 -0
- package/dist/lib/image/index.d.ts +90 -0
- package/dist/lib/image/index.js +147 -0
- package/dist/lib/messages/PatchSender/index.d.ts +75 -0
- package/dist/lib/messages/PatchSender/index.js +125 -0
- package/dist/lib/messages/index.d.ts +26 -0
- package/dist/lib/messages/index.js +59 -0
- package/dist/lib/messages/schema.d.ts +1775 -0
- package/dist/lib/messages/schema.js +192 -0
- package/dist/lib/presets/index.d.ts +32 -0
- package/dist/lib/presets/index.js +35 -0
- package/dist/lib/presets/schema.d.ts +753 -0
- package/dist/lib/presets/schema.js +181 -0
- package/dist/lib/rendering/BrushTip/index.d.ts +63 -0
- package/dist/lib/rendering/BrushTip/index.js +112 -0
- package/dist/lib/rendering/checkerboard/index.d.ts +22 -0
- package/dist/lib/rendering/checkerboard/index.js +31 -0
- package/dist/lib/rendering/compositing/index.d.ts +64 -0
- package/dist/lib/rendering/compositing/index.js +124 -0
- package/dist/lib/rendering/cursor/index.d.ts +14 -0
- package/dist/lib/rendering/cursor/index.js +39 -0
- package/dist/lib/rendering/cursors/index.d.ts +18 -0
- package/dist/lib/rendering/cursors/index.js +23 -0
- package/dist/lib/rendering/maskOutline/index.d.ts +22 -0
- package/dist/lib/rendering/maskOutline/index.js +78 -0
- package/dist/lib/rendering/outline/index.d.ts +36 -0
- package/dist/lib/rendering/outline/index.js +120 -0
- package/dist/lib/rendering/sample/index.d.ts +55 -0
- package/dist/lib/rendering/sample/index.js +83 -0
- package/dist/lib/rendering/screenshot/index.d.ts +67 -0
- package/dist/lib/rendering/screenshot/index.js +96 -0
- package/dist/lib/rendering/transformBox/index.d.ts +10 -0
- package/dist/lib/rendering/transformBox/index.js +57 -0
- package/dist/lib/selection/Lasso/index.d.ts +70 -0
- package/dist/lib/selection/Lasso/index.js +156 -0
- package/dist/lib/selection/Selection/index.d.ts +86 -0
- package/dist/lib/selection/Selection/index.js +228 -0
- package/dist/lib/selection/geometry/index.d.ts +37 -0
- package/dist/lib/selection/geometry/index.js +57 -0
- package/dist/lib/storage/index.d.ts +63 -0
- package/dist/lib/storage/index.js +114 -0
- package/dist/lib/transform/Transformer/index.d.ts +124 -0
- package/dist/lib/transform/Transformer/index.js +286 -0
- package/dist/lib/transform/geometry/index.d.ts +95 -0
- package/dist/lib/transform/geometry/index.js +258 -0
- package/dist/lib/utils/array/index.d.ts +10 -0
- package/dist/lib/utils/array/index.js +21 -0
- package/dist/lib/utils/gestures/index.d.ts +35 -0
- package/dist/lib/utils/gestures/index.js +81 -0
- package/dist/lib/utils/navigation/index.d.ts +51 -0
- package/dist/lib/utils/navigation/index.js +59 -0
- package/dist/lib/utils/random/index.d.ts +18 -0
- package/dist/lib/utils/random/index.js +27 -0
- package/dist/lib/utils/shortcuts/index.d.ts +74 -0
- package/dist/lib/utils/shortcuts/index.js +189 -0
- package/dist/lib/validation/validators.generated.d.ts +27 -0
- package/dist/lib/validation/validators.generated.js +19826 -0
- package/dist/locales/en-GB.js +188 -0
- package/dist/locales/en-US.js +188 -0
- package/dist/locales/fr.js +188 -0
- package/dist/locales/ja.js +188 -0
- package/dist/locales/ko.js +188 -0
- package/dist/locales/zh-Hans.js +188 -0
- package/dist/locales/zh-Hant.js +188 -0
- package/dist/package.js +3 -0
- package/dist/testing/app.d.ts +7 -0
- package/dist/testing/app.js +16 -0
- package/dist/testing/elements.d.ts +42 -0
- package/dist/testing/elements.js +58 -0
- package/dist/testing/fakeCanvas.d.ts +21 -0
- package/dist/testing/fakeCanvas.js +26 -0
- package/dist/testing/layerStore.d.ts +26 -0
- package/dist/testing/layerStore.js +43 -0
- package/dist/testing/locks.d.ts +15 -0
- package/dist/testing/locks.js +41 -0
- package/dist/testing/recordingCanvas.d.ts +14 -0
- package/dist/testing/recordingCanvas.js +28 -0
- package/dist/testing/recordingContext.d.ts +10 -0
- package/dist/testing/recordingContext.js +19 -0
- package/dist/ui/ChangeController/index.d.ts +40 -0
- package/dist/ui/ChangeController/index.js +62 -0
- package/dist/ui/base/index.d.ts +69 -0
- package/dist/ui/base/index.js +94 -0
- package/dist/ui/elements/PaintCanvas/index.d.ts +249 -0
- package/dist/ui/elements/PaintCanvas/index.js +584 -0
- package/dist/ui/elements/PaintCanvas/style.js +3 -0
- package/dist/ui/elements/PcBrushConfig/index.d.ts +130 -0
- package/dist/ui/elements/PcBrushConfig/index.js +463 -0
- package/dist/ui/elements/PcBrushConfig/style.js +3 -0
- package/dist/ui/elements/PcBrushMenu/index.d.ts +121 -0
- package/dist/ui/elements/PcBrushMenu/index.js +362 -0
- package/dist/ui/elements/PcBrushMenu/style.js +3 -0
- package/dist/ui/elements/PcBrushTest/index.d.ts +71 -0
- package/dist/ui/elements/PcBrushTest/index.js +136 -0
- package/dist/ui/elements/PcBrushTest/style.js +3 -0
- package/dist/ui/elements/PcButton/index.d.ts +57 -0
- package/dist/ui/elements/PcButton/index.js +124 -0
- package/dist/ui/elements/PcButtonGroup/index.d.ts +44 -0
- package/dist/ui/elements/PcButtonGroup/index.js +93 -0
- package/dist/ui/elements/PcCanvas/index.d.ts +356 -0
- package/dist/ui/elements/PcCanvas/index.js +1004 -0
- package/dist/ui/elements/PcCanvas/style.js +3 -0
- package/dist/ui/elements/PcCanvasMap/index.d.ts +96 -0
- package/dist/ui/elements/PcCanvasMap/index.js +187 -0
- package/dist/ui/elements/PcCanvasMap/style.js +3 -0
- package/dist/ui/elements/PcContextMenu/index.d.ts +72 -0
- package/dist/ui/elements/PcContextMenu/index.js +148 -0
- package/dist/ui/elements/PcDialog/index.d.ts +63 -0
- package/dist/ui/elements/PcDialog/index.js +114 -0
- package/dist/ui/elements/PcIcon/index.d.ts +36 -0
- package/dist/ui/elements/PcIcon/index.js +43 -0
- package/dist/ui/elements/PcIconToggle/index.d.ts +46 -0
- package/dist/ui/elements/PcIconToggle/index.js +87 -0
- package/dist/ui/elements/PcInfoTip/index.d.ts +40 -0
- package/dist/ui/elements/PcInfoTip/index.js +95 -0
- package/dist/ui/elements/PcInputSettings/index.d.ts +69 -0
- package/dist/ui/elements/PcInputSettings/index.js +150 -0
- package/dist/ui/elements/PcLayerItem/index.d.ts +221 -0
- package/dist/ui/elements/PcLayerItem/index.js +561 -0
- package/dist/ui/elements/PcLayerItem/style.js +3 -0
- package/dist/ui/elements/PcLayersMenu/index.d.ts +108 -0
- package/dist/ui/elements/PcLayersMenu/index.js +208 -0
- package/dist/ui/elements/PcLayersMenu/style.js +3 -0
- package/dist/ui/elements/PcMatch/index.d.ts +31 -0
- package/dist/ui/elements/PcMatch/index.js +58 -0
- package/dist/ui/elements/PcPanel/index.d.ts +43 -0
- package/dist/ui/elements/PcPanel/index.js +51 -0
- package/dist/ui/elements/PcPopover/index.d.ts +48 -0
- package/dist/ui/elements/PcPopover/index.js +86 -0
- package/dist/ui/elements/PcSlider/index.d.ts +57 -0
- package/dist/ui/elements/PcSlider/index.js +88 -0
- package/dist/ui/elements/PcStats/index.d.ts +54 -0
- package/dist/ui/elements/PcStats/index.js +141 -0
- package/dist/ui/elements/PcThemePicker/index.d.ts +56 -0
- package/dist/ui/elements/PcThemePicker/index.js +134 -0
- package/dist/ui/elements/PcToast/index.d.ts +58 -0
- package/dist/ui/elements/PcToast/index.js +110 -0
- package/dist/ui/elements/PcToggleCheckbox/index.d.ts +37 -0
- package/dist/ui/elements/PcToggleCheckbox/index.js +105 -0
- package/dist/ui/elements/PcToolSettings/index.d.ts +128 -0
- package/dist/ui/elements/PcToolSettings/index.js +543 -0
- package/dist/ui/elements/PcToolSettings/style.js +3 -0
- package/dist/ui/elements/PcToolbar/index.d.ts +71 -0
- package/dist/ui/elements/PcToolbar/index.js +190 -0
- package/dist/ui/elements/PcToolbar/style.js +3 -0
- package/dist/ui/icons/images/brush.js +3 -0
- package/dist/ui/icons/images/circle-mask.js +3 -0
- package/dist/ui/icons/images/clipboard.js +3 -0
- package/dist/ui/icons/images/cursor.js +3 -0
- package/dist/ui/icons/images/eraser.js +3 -0
- package/dist/ui/icons/images/eyedropper.js +3 -0
- package/dist/ui/icons/images/flip-horizontal.js +3 -0
- package/dist/ui/icons/images/flip-vertical.js +3 -0
- package/dist/ui/icons/images/focus-pen.js +3 -0
- package/dist/ui/icons/images/freehand-mask.js +3 -0
- package/dist/ui/icons/images/hidden.js +3 -0
- package/dist/ui/icons/images/layers.js +3 -0
- package/dist/ui/icons/images/linked.js +3 -0
- package/dist/ui/icons/images/locked.js +3 -0
- package/dist/ui/icons/images/moon.js +3 -0
- package/dist/ui/icons/images/pan.js +3 -0
- package/dist/ui/icons/images/polygon-mask.js +3 -0
- package/dist/ui/icons/images/pressure.js +3 -0
- package/dist/ui/icons/images/randomise.js +3 -0
- package/dist/ui/icons/images/rectangle-mask.js +3 -0
- package/dist/ui/icons/images/redo.js +3 -0
- package/dist/ui/icons/images/rotate-90-left.js +3 -0
- package/dist/ui/icons/images/rotate-90-right.js +3 -0
- package/dist/ui/icons/images/smoothing-bezier-cubic.js +3 -0
- package/dist/ui/icons/images/smoothing-bezier-quad.js +3 -0
- package/dist/ui/icons/images/smoothing-linear.js +3 -0
- package/dist/ui/icons/images/star.js +3 -0
- package/dist/ui/icons/images/sun.js +3 -0
- package/dist/ui/icons/images/tilt.js +3 -0
- package/dist/ui/icons/images/undo.js +3 -0
- package/dist/ui/icons/images/unlinked.js +3 -0
- package/dist/ui/icons/images/unlocked.js +3 -0
- package/dist/ui/icons/images/visible.js +3 -0
- package/dist/ui/icons/images/zoom-in.js +3 -0
- package/dist/ui/icons/images/zoom-out.js +3 -0
- package/dist/ui/icons/images/zoom.js +3 -0
- package/dist/ui/icons/index.d.ts +42 -0
- package/dist/ui/icons/index.js +130 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/index.js +22 -0
- package/dist/ui/layout/index.d.ts +88 -0
- package/dist/ui/layout/index.js +96 -0
- package/dist/ui/styles/index.d.ts +13 -0
- package/dist/ui/styles/index.js +195 -0
- package/dist/ui/tokens/index.d.ts +29 -0
- package/dist/ui/tokens/index.js +114 -0
- package/locales/en-GB.json +186 -0
- package/locales/en-US.json +186 -0
- package/locales/fr.json +186 -0
- package/locales/ja.json +186 -0
- package/locales/ko.json +186 -0
- package/locales/zh-Hans.json +186 -0
- package/locales/zh-Hant.json +186 -0
- package/package.json +97 -3
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
* The text the UI shows, in the chosen language. Starts in the fallback locale; `useDeviceLocale` switches to the best
|
|
86
|
+
* match for the device, and `setLocale` to any available locale. Locale files load when first used, without reloading
|
|
87
|
+
* the page. Strings missing from a locale come from the fallback locale, then the key itself.
|
|
88
|
+
*
|
|
89
|
+
* Dispatches a `change` event when the locale or its strings change, so the UI can show the new text.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const translations = new Translations({ locales: { "en-US": ..., ja: () => import("./ja.json") } });
|
|
93
|
+
* await translations.useDeviceLocale();
|
|
94
|
+
* translations.t("layers.delete", { name: "Sketch" });
|
|
95
|
+
*/
|
|
96
|
+
var Translations = class extends EventTarget {
|
|
97
|
+
loaders;
|
|
98
|
+
fallback;
|
|
99
|
+
/** Strings of each locale loaded so far, plus any added */
|
|
100
|
+
loaded = /* @__PURE__ */ new Map();
|
|
101
|
+
current;
|
|
102
|
+
/** The locale most recently asked for, so a slow load can't override a later choice */
|
|
103
|
+
requested;
|
|
104
|
+
constructor(options = {}) {
|
|
105
|
+
super();
|
|
106
|
+
this.loaders = options.locales ?? BUILT_IN_LOCALES;
|
|
107
|
+
this.fallback = options.fallback ?? "en-US";
|
|
108
|
+
this.loaded.set(this.fallback, options.fallbackStrings ?? en_US_default);
|
|
109
|
+
this.current = this.fallback;
|
|
110
|
+
}
|
|
111
|
+
/** The locale in use (an IETF BCP 47 tag, e.g. `ja`) */
|
|
112
|
+
get locale() {
|
|
113
|
+
return this.current;
|
|
114
|
+
}
|
|
115
|
+
/** Locales that can be used */
|
|
116
|
+
get locales() {
|
|
117
|
+
return [.../* @__PURE__ */ new Set([this.fallback, ...Object.keys(this.loaders)])];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Switch to the best available locale for the device's languages (`navigator.languages`).
|
|
121
|
+
*/
|
|
122
|
+
useDeviceLocale(languages = typeof navigator === "undefined" ? [] : navigator.languages) {
|
|
123
|
+
return this.setLocale(matchLocale(languages, this.locales, this.fallback));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Switch to a locale (the best available match for the tag), loading its strings if needed. Dispatches `change` once
|
|
127
|
+
* its strings are ready. If loading fails, the current locale stays and this rejects.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* await translations.setLocale("zh-TW"); // uses zh-Hant
|
|
131
|
+
*/
|
|
132
|
+
async setLocale(tag) {
|
|
133
|
+
const locale = matchLocale([tag], this.locales, this.fallback);
|
|
134
|
+
this.requested = locale;
|
|
135
|
+
if (!this.loaded.has(locale)) {
|
|
136
|
+
const module = await this.loaders[locale]();
|
|
137
|
+
this.loaded.set(locale, "default" in module && typeof module.default === "object" ? module.default : module);
|
|
138
|
+
}
|
|
139
|
+
if (this.requested !== locale) return;
|
|
140
|
+
this.current = locale;
|
|
141
|
+
this.dispatchEvent(new Event("change"));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Add or replace strings for a locale (e.g. the implementer's own wording or extra keys). Dispatches `change` if it's
|
|
145
|
+
* the locale in use or the fallback.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* translations.addStrings("en-US", { "toolbar.brush": "Paintbrush" });
|
|
149
|
+
*/
|
|
150
|
+
addStrings(locale, strings) {
|
|
151
|
+
this.loaded.set(locale, {
|
|
152
|
+
...this.loaded.get(locale),
|
|
153
|
+
...strings
|
|
154
|
+
});
|
|
155
|
+
if (locale === this.current || locale === this.fallback) this.dispatchEvent(new Event("change"));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Translate a key, putting values into its `{name}` placeholders. With a numeric `count`, the key's plural form for
|
|
159
|
+
* the locale is used when there is one (e.g. `presets.imported.one` and `presets.imported.other`). Falls back to the
|
|
160
|
+
* fallback locale, then the key.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* translations.t("presets.imported", { count: 3 }); // "Imported 3 brush presets"
|
|
164
|
+
*/
|
|
165
|
+
t(key, values) {
|
|
166
|
+
const count = values?.count;
|
|
167
|
+
const keys = typeof count === "number" ? [
|
|
168
|
+
`${key}.${new Intl.PluralRules(this.current).select(count)}`,
|
|
169
|
+
`${key}.other`,
|
|
170
|
+
key
|
|
171
|
+
] : [key];
|
|
172
|
+
for (const locale of [this.current, this.fallback]) {
|
|
173
|
+
const strings = this.loaded.get(locale);
|
|
174
|
+
const found = keys.find((each) => strings && Object.hasOwn(strings, each));
|
|
175
|
+
if (found) return interpolate(strings[found], values);
|
|
176
|
+
}
|
|
177
|
+
return interpolate(key, values);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/** The translations PaintCanvas's UI uses, unless an element is given others */
|
|
181
|
+
var translations = new Translations();
|
|
182
|
+
export { BUILT_IN_LOCALES, FALLBACK_LOCALE, Translations as default, interpolate, 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
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ImageEncoding, encodeRgbaDeflate } from "../../image/index.js";
|
|
2
|
+
import { drawLayers } from "../../rendering/compositing/index.js";
|
|
3
|
+
import { createPatchMessage } from "../index.js";
|
|
4
|
+
// src/lib/messages/PatchSender/index.ts
|
|
5
|
+
/** How long to wait after a change before sending, so quick changes go together */
|
|
6
|
+
var 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
|
+
var changedArea = (before, after, width, height) => {
|
|
15
|
+
let x1 = width;
|
|
16
|
+
let y1 = height;
|
|
17
|
+
let x2 = -1;
|
|
18
|
+
let y2 = -1;
|
|
19
|
+
for (let y = 0; y < height; y++) {
|
|
20
|
+
const row = y * width;
|
|
21
|
+
for (let x = 0; x < width; x++) {
|
|
22
|
+
if (before[row + x] === after[row + x]) continue;
|
|
23
|
+
if (x < x1) x1 = x;
|
|
24
|
+
if (x > x2) x2 = x;
|
|
25
|
+
if (y < y1) y1 = y;
|
|
26
|
+
y2 = y;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return x2 < 0 ? void 0 : [
|
|
30
|
+
x1,
|
|
31
|
+
y1,
|
|
32
|
+
x2 + 1,
|
|
33
|
+
y2 + 1
|
|
34
|
+
];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Copy a box of RGBA pixels out of a whole image.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* cropRgba(imageData.data, imageData.width, [10, 10, 20, 20]); // 10 × 10 pixels
|
|
41
|
+
*/
|
|
42
|
+
var cropRgba = (rgba, width, [x1, y1, x2, y2]) => {
|
|
43
|
+
const rowBytes = (x2 - x1) * 4;
|
|
44
|
+
const cropped = new Uint8ClampedArray(rowBytes * (y2 - y1));
|
|
45
|
+
for (let y = y1; y < y2; y++) {
|
|
46
|
+
const start = (y * width + x1) * 4;
|
|
47
|
+
cropped.set(rgba.subarray(start, start + rowBytes), (y - y1) * rowBytes);
|
|
48
|
+
}
|
|
49
|
+
return cropped;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Sends the user's drawing as patches. Shortly after any change (and once `whenIdle` runs it), it flattens the
|
|
53
|
+
* 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,
|
|
54
|
+
* erasing, moving pixels, layer settings and order) without each tool reporting what it changed.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const sync = new PatchSender({ width, height, layers: () => canvas.layers.layers, send, createCanvas });
|
|
58
|
+
* app.layers.addEventListener("change", () => sync.schedule());
|
|
59
|
+
*/
|
|
60
|
+
var PatchSender = class {
|
|
61
|
+
options;
|
|
62
|
+
canvas;
|
|
63
|
+
context;
|
|
64
|
+
/** The pixels last sent (all transparent to start with) */
|
|
65
|
+
sent;
|
|
66
|
+
timer;
|
|
67
|
+
/** Whether it has stopped (a sync waiting for `whenIdle` doesn't run) */
|
|
68
|
+
stopped = false;
|
|
69
|
+
/** The sync in progress, so syncs never overlap */
|
|
70
|
+
running = Promise.resolve();
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.options = options;
|
|
73
|
+
this.canvas = options.createCanvas(options.width, options.height);
|
|
74
|
+
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
75
|
+
this.sent = new Uint8ClampedArray(options.width * options.height * 4);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Send the changes soon (changes within the delay go together), once `whenIdle` runs the sync.
|
|
79
|
+
*/
|
|
80
|
+
schedule() {
|
|
81
|
+
clearTimeout(this.timer);
|
|
82
|
+
this.timer = setTimeout(() => {
|
|
83
|
+
const run = () => this.stopped ? void 0 : this.sync().catch(() => void 0);
|
|
84
|
+
if (this.options.whenIdle) this.options.whenIdle(run);
|
|
85
|
+
else run();
|
|
86
|
+
}, this.options.delayMs ?? 200);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Send what changed since the last sync now. Resolves once sent; if sending fails, the changes go with the next sync.
|
|
90
|
+
*/
|
|
91
|
+
sync() {
|
|
92
|
+
clearTimeout(this.timer);
|
|
93
|
+
this.running = this.running.catch(() => void 0).then(() => this.sendChanges());
|
|
94
|
+
return this.running;
|
|
95
|
+
}
|
|
96
|
+
/** Flatten the layers and send the changed area, if any */
|
|
97
|
+
async sendChanges() {
|
|
98
|
+
const { width, height, send } = this.options;
|
|
99
|
+
this.context.clearRect(0, 0, width, height);
|
|
100
|
+
drawLayers(this.context, this.options.layers());
|
|
101
|
+
const current = this.context.getImageData(0, 0, width, height).data;
|
|
102
|
+
const area = changedArea(new Uint32Array(this.sent.buffer), new Uint32Array(current.buffer), width, height);
|
|
103
|
+
if (!area) return;
|
|
104
|
+
const data = await encodeRgbaDeflate(cropRgba(current, width, area), area);
|
|
105
|
+
await send(createPatchMessage(area, ImageEncoding.RgbaDeflate, data));
|
|
106
|
+
this.sent = current;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Count pixels as already sent (e.g. the user's earlier work, restored from a shared drawing's history), so they aren't
|
|
110
|
+
* sent again.
|
|
111
|
+
*/
|
|
112
|
+
markSent(area, rgba) {
|
|
113
|
+
const [x1, y1, x2, y2] = area;
|
|
114
|
+
const rowBytes = (x2 - x1) * 4;
|
|
115
|
+
for (let y = y1; y < y2; y++) this.sent.set(rgba.subarray((y - y1) * rowBytes, (y - y1 + 1) * rowBytes), (y * this.options.width + x1) * 4);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Stop any pending sync.
|
|
119
|
+
*/
|
|
120
|
+
stop() {
|
|
121
|
+
this.stopped = true;
|
|
122
|
+
clearTimeout(this.timer);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
export { PATCH_DELAY_MS, changedArea, cropRgba, PatchSender as default };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BoundingBox, ImageEncoding } from "../image";
|
|
2
|
+
import { type CanvasMessage, type PatchMessage } from "./schema";
|
|
3
|
+
export * from "./schema";
|
|
4
|
+
/** Result of checking a received message */
|
|
5
|
+
export type MessageValidationResult = {
|
|
6
|
+
valid: true;
|
|
7
|
+
value: CanvasMessage;
|
|
8
|
+
} | {
|
|
9
|
+
valid: false;
|
|
10
|
+
errors: string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Make a patch message.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* createPatchMessage([0, 0, 10, 10], ImageEncoding.RgbaDeflate, data);
|
|
17
|
+
*/
|
|
18
|
+
export declare const createPatchMessage: (coords: BoundingBox, encoding: ImageEncoding, data: string) => PatchMessage;
|
|
19
|
+
/**
|
|
20
|
+
* Check a received (untrusted) drawing message: its protocol version, its kind, its schema, and that a patch's area
|
|
21
|
+
* isn't empty.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* validateCanvasMessage(JSON.parse(text)); // { valid: true, value: { protocol: 1, type: "patch", … } }
|
|
25
|
+
*/
|
|
26
|
+
export declare const validateCanvasMessage: (input: unknown) => MessageValidationResult;
|