@nice2dev/ui-graphics 1.3.14 → 1.3.16
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/CHANGELOG.md +13 -0
- package/dist/cjs/animation/AnimatedPerson.js +470 -143
- package/dist/cjs/animation/AnimationEditor.js +289 -101
- package/dist/cjs/animation/Audience.js +83 -61
- package/dist/cjs/animation/BodyRenderer.js +69 -28
- package/dist/cjs/animation/animationHelper.js +281 -288
- package/dist/cjs/animation/characterTypes.js +200 -203
- package/dist/cjs/animation/choreoDSL.js +205 -219
- package/dist/cjs/animation/choreography.js +4 -32
- package/dist/cjs/animation/karaokeIntegration.js +68 -77
- package/dist/cjs/animation/lottieExport.js +108 -114
- package/dist/cjs/animation/motionPath.js +225 -248
- package/dist/cjs/animation/physics.js +227 -254
- package/dist/cjs/animation/rig/RigPlayer.js +53 -56
- package/dist/cjs/animation/rig/RiggedBody.js +71 -26
- package/dist/cjs/animation/rig/choreoRigBridge.js +72 -77
- package/dist/cjs/animation/rig/ik.js +13 -18
- package/dist/cjs/animation/rig/poseOps.js +34 -41
- package/dist/cjs/animation/rig/presets.js +64 -72
- package/dist/cjs/animation/rig/rigMath.js +25 -32
- package/dist/cjs/animation/rig/rigTypes.js +59 -63
- package/dist/cjs/animation/shapes/arms.js +31 -13
- package/dist/cjs/animation/shapes/eyes.js +129 -47
- package/dist/cjs/animation/shapes/hair.js +72 -45
- package/dist/cjs/animation/shapes/heads.js +131 -57
- package/dist/cjs/animation/shapes/headwear.js +47 -38
- package/dist/cjs/animation/shapes/legs.js +31 -13
- package/dist/cjs/animation/shapes/mouths.js +120 -44
- package/dist/cjs/animation/shapes/noses.js +80 -25
- package/dist/cjs/animation/shapes/outfits.js +101 -39
- package/dist/cjs/animation/shapes/torsos.js +15 -11
- package/dist/cjs/animation/spineExport.js +103 -121
- package/dist/cjs/core/LocalUI.js +108 -43
- package/dist/cjs/core/NiceBrushEngine.js +758 -552
- package/dist/cjs/core/collaboration.js +249 -251
- package/dist/cjs/core/colorBlindness.js +100 -115
- package/dist/cjs/core/dragDrop.js +155 -158
- package/dist/cjs/core/editorAsk.js +51 -41
- package/dist/cjs/core/fixes.js +140 -171
- package/dist/cjs/core/gridGuides.js +166 -201
- package/dist/cjs/core/historyVisual.js +156 -135
- package/dist/cjs/core/i18n.js +13 -14
- package/dist/cjs/core/integrations.js +238 -249
- package/dist/cjs/core/minimap.js +149 -151
- package/dist/cjs/core/plugins.js +98 -113
- package/dist/cjs/core/rtl.js +71 -83
- package/dist/cjs/core/shortcuts.js +196 -225
- package/dist/cjs/core/theme.js +142 -149
- package/dist/cjs/core/yjsCollaboration.js +232 -293
- package/dist/cjs/design/design-tools.js +12 -22
- package/dist/cjs/font/NiceFontEditor.js +877 -316
- package/dist/cjs/font/fontFormatUtils.js +388 -448
- package/dist/cjs/game/GameAsset2dEditor.js +1201 -392
- package/dist/cjs/game/GameAsset2dEditor.module.css.js +146 -5
- package/dist/cjs/game/gameAssetTypes.js +7 -16
- package/dist/cjs/game/gameAssetUtils.js +390 -413
- package/dist/cjs/game/useGameAssetEditor.js +820 -763
- package/dist/cjs/index.js +81 -84
- package/dist/cjs/nice2dev-ui-graphics.css +1 -1
- package/dist/cjs/node_modules/lib0/array.js +8 -70
- package/dist/cjs/node_modules/lib0/binary.js +3 -20
- package/dist/cjs/node_modules/lib0/broadcastchannel.js +24 -86
- package/dist/cjs/node_modules/lib0/buffer.js +15 -82
- package/dist/cjs/node_modules/lib0/conditions.js +3 -16
- package/dist/cjs/node_modules/lib0/decoding.js +108 -287
- package/dist/cjs/node_modules/lib0/dom.js +14 -44
- package/dist/cjs/node_modules/lib0/encoding.js +68 -418
- package/dist/cjs/node_modules/lib0/environment.js +26 -92
- package/dist/cjs/node_modules/lib0/error.js +5 -29
- package/dist/cjs/node_modules/lib0/function.js +27 -66
- package/dist/cjs/node_modules/lib0/iterator.js +8 -33
- package/dist/cjs/node_modules/lib0/logging.common.js +12 -22
- package/dist/cjs/node_modules/lib0/logging.js +32 -71
- package/dist/cjs/node_modules/lib0/map.js +16 -86
- package/dist/cjs/node_modules/lib0/math.js +3 -32
- package/dist/cjs/node_modules/lib0/number.js +4 -15
- package/dist/cjs/node_modules/lib0/object.js +14 -88
- package/dist/cjs/node_modules/lib0/observable.js +38 -78
- package/dist/cjs/node_modules/lib0/pair.js +3 -20
- package/dist/cjs/node_modules/lib0/prng.js +8 -77
- package/dist/cjs/node_modules/lib0/promise.js +6 -30
- package/dist/cjs/node_modules/lib0/random.js +8 -23
- package/dist/cjs/node_modules/lib0/schema.js +313 -687
- package/dist/cjs/node_modules/lib0/set.js +3 -10
- package/dist/cjs/node_modules/lib0/storage.js +20 -55
- package/dist/cjs/node_modules/lib0/string.js +17 -72
- package/dist/cjs/node_modules/lib0/symbol.js +2 -12
- package/dist/cjs/node_modules/lib0/time.js +2 -15
- package/dist/cjs/node_modules/lib0/trait/equality.js +3 -28
- package/dist/cjs/node_modules/lib0/url.js +4 -18
- package/dist/cjs/node_modules/lib0/webcrypto.js +2 -5
- package/dist/cjs/node_modules/y-protocols/auth.js +5 -19
- package/dist/cjs/node_modules/y-protocols/awareness.js +53 -116
- package/dist/cjs/node_modules/y-protocols/sync.js +16 -103
- package/dist/cjs/node_modules/y-websocket/src/y-websocket.js +77 -219
- package/dist/cjs/node_modules/yjs/dist/yjs.js +2128 -4868
- package/dist/cjs/photo/FilterThumb.js +37 -25
- package/dist/cjs/photo/NiceAITools.js +679 -467
- package/dist/cjs/photo/PhotoEditor.js +34 -20
- package/dist/cjs/photo/PhotoEditor.module.css.js +296 -5
- package/dist/cjs/photo/PhotoEditorCanvas.js +250 -91
- package/dist/cjs/photo/PhotoEditorLeftPanel.js +1086 -224
- package/dist/cjs/photo/PhotoEditorToolbar.js +135 -19
- package/dist/cjs/photo/photoEditorActions.js +89 -72
- package/dist/cjs/photo/photoEditorTypes.js +29 -32
- package/dist/cjs/photo/photoFilters.js +959 -985
- package/dist/cjs/photo/photoOverlays.js +899 -945
- package/dist/cjs/photo/photoSelection.js +427 -499
- package/dist/cjs/photo/usePhotoEditor.js +739 -741
- package/dist/cjs/pixel/HSVPicker.js +191 -175
- package/dist/cjs/pixel/PixelEditor.js +159 -48
- package/dist/cjs/pixel/PixelEditor.module.css.js +209 -5
- package/dist/cjs/pixel/PixelEditorCanvas.js +29 -16
- package/dist/cjs/pixel/PixelEditorMenuBar.js +160 -162
- package/dist/cjs/pixel/PixelEditorRightPanel.js +377 -45
- package/dist/cjs/pixel/PixelEditorStatusBar.js +47 -20
- package/dist/cjs/pixel/PixelEditorTimeline.js +106 -23
- package/dist/cjs/pixel/PixelEditorToolbar.js +99 -21
- package/dist/cjs/pixel/SpriteGeneratorPanel.js +483 -303
- package/dist/cjs/pixel/animalGeneratorPresets.js +776 -601
- package/dist/cjs/pixel/asepriteFormat.js +511 -560
- package/dist/cjs/pixel/buildingGeneratorPresets.js +693 -520
- package/dist/cjs/pixel/characterGeneratorPresets.js +644 -544
- package/dist/cjs/pixel/pixel-pro.js +52 -63
- package/dist/cjs/pixel/pixelEditorExports.js +388 -428
- package/dist/cjs/pixel/pixelEditorTypes.js +91 -108
- package/dist/cjs/pixel/pixelEditorUtils.js +519 -561
- package/dist/cjs/pixel/spriteGeneratorCore.js +206 -257
- package/dist/cjs/pixel/usePixelEditor.js +1321 -1343
- package/dist/cjs/texture/Nice3DTexturePainter.js +707 -440
- package/dist/cjs/ui/NiceUIDesigner.js +954 -527
- package/dist/cjs/vector/NiceSvgToCode.js +586 -446
- package/dist/cjs/vector/VectorEditor.js +290 -117
- package/dist/cjs/vector/VectorEditor.module.css.js +86 -5
- package/dist/cjs/vector/VectorEditorMenuBar.js +333 -275
- package/dist/cjs/vector/VectorEditorRightPanel.js +1094 -371
- package/dist/cjs/vector/VectorEditorShapeRenderer.js +198 -60
- package/dist/cjs/vector/VectorEditorStatusBar.js +39 -13
- package/dist/cjs/vector/useVectorEditor.js +848 -889
- package/dist/cjs/vector/vectorAnimation.js +110 -137
- package/dist/cjs/vector/vectorBooleanOps.js +398 -485
- package/dist/cjs/vector/vectorEditorExport.js +201 -203
- package/dist/cjs/vector/vectorEditorImport.js +173 -189
- package/dist/cjs/vector/vectorEditorTypes.js +42 -49
- package/dist/cjs/vector/vectorGradients.js +151 -174
- package/dist/cjs/vector/vectorIconExport.js +52 -59
- package/dist/cjs/vector/vectorIconImport.js +169 -219
- package/dist/esm/animation/AnimatedPerson.js +470 -141
- package/dist/esm/animation/AnimationEditor.js +290 -100
- package/dist/esm/animation/Audience.js +83 -59
- package/dist/esm/animation/BodyRenderer.js +71 -25
- package/dist/esm/animation/animationHelper.js +284 -287
- package/dist/esm/animation/characterTypes.js +204 -202
- package/dist/esm/animation/choreoDSL.js +212 -218
- package/dist/esm/animation/choreography.js +8 -31
- package/dist/esm/animation/karaokeIntegration.js +74 -76
- package/dist/esm/animation/lottieExport.js +112 -113
- package/dist/esm/animation/motionPath.js +235 -247
- package/dist/esm/animation/physics.js +235 -253
- package/dist/esm/animation/rig/RigPlayer.js +53 -54
- package/dist/esm/animation/rig/RiggedBody.js +71 -24
- package/dist/esm/animation/rig/choreoRigBridge.js +74 -76
- package/dist/esm/animation/rig/ik.js +14 -17
- package/dist/esm/animation/rig/poseOps.js +37 -40
- package/dist/esm/animation/rig/presets.js +70 -71
- package/dist/esm/animation/rig/rigMath.js +28 -31
- package/dist/esm/animation/rig/rigTypes.js +62 -62
- package/dist/esm/animation/shapes/arms.js +32 -12
- package/dist/esm/animation/shapes/eyes.js +135 -46
- package/dist/esm/animation/shapes/hair.js +80 -44
- package/dist/esm/animation/shapes/heads.js +140 -56
- package/dist/esm/animation/shapes/headwear.js +54 -37
- package/dist/esm/animation/shapes/legs.js +32 -12
- package/dist/esm/animation/shapes/mouths.js +127 -43
- package/dist/esm/animation/shapes/noses.js +87 -24
- package/dist/esm/animation/shapes/outfits.js +106 -38
- package/dist/esm/animation/shapes/torsos.js +16 -10
- package/dist/esm/animation/spineExport.js +106 -120
- package/dist/esm/core/LocalUI.js +111 -42
- package/dist/esm/core/NiceBrushEngine.js +761 -551
- package/dist/esm/core/collaboration.js +255 -250
- package/dist/esm/core/colorBlindness.js +106 -114
- package/dist/esm/core/dragDrop.js +157 -157
- package/dist/esm/core/editorAsk.js +53 -40
- package/dist/esm/core/fixes.js +145 -170
- package/dist/esm/core/gridGuides.js +178 -200
- package/dist/esm/core/historyVisual.js +158 -134
- package/dist/esm/core/i18n.js +15 -13
- package/dist/esm/core/integrations.js +244 -248
- package/dist/esm/core/minimap.js +151 -150
- package/dist/esm/core/plugins.js +102 -112
- package/dist/esm/core/rtl.js +77 -82
- package/dist/esm/core/shortcuts.js +200 -224
- package/dist/esm/core/theme.js +150 -148
- package/dist/esm/core/yjsCollaboration.js +240 -292
- package/dist/esm/design/design-tools.js +13 -21
- package/dist/esm/font/NiceFontEditor.js +880 -315
- package/dist/esm/font/fontFormatUtils.js +394 -447
- package/dist/esm/game/GameAsset2dEditor.js +1202 -391
- package/dist/esm/game/GameAsset2dEditor.module.css.js +146 -3
- package/dist/esm/game/gameAssetTypes.js +8 -15
- package/dist/esm/game/gameAssetUtils.js +408 -412
- package/dist/esm/game/useGameAssetEditor.js +821 -762
- package/dist/esm/index.js +332 -69
- package/dist/esm/nice2dev-ui-graphics.css +1 -1
- package/dist/esm/node_modules/lib0/array.js +15 -69
- package/dist/esm/node_modules/lib0/binary.js +14 -19
- package/dist/esm/node_modules/lib0/broadcastchannel.js +27 -85
- package/dist/esm/node_modules/lib0/buffer.js +20 -80
- package/dist/esm/node_modules/lib0/conditions.js +4 -15
- package/dist/esm/node_modules/lib0/decoding.js +128 -286
- package/dist/esm/node_modules/lib0/dom.js +20 -43
- package/dist/esm/node_modules/lib0/encoding.js +90 -415
- package/dist/esm/node_modules/lib0/environment.js +33 -91
- package/dist/esm/node_modules/lib0/error.js +8 -28
- package/dist/esm/node_modules/lib0/function.js +31 -65
- package/dist/esm/node_modules/lib0/iterator.js +11 -32
- package/dist/esm/node_modules/lib0/logging.common.js +22 -21
- package/dist/esm/node_modules/lib0/logging.js +44 -70
- package/dist/esm/node_modules/lib0/map.js +21 -85
- package/dist/esm/node_modules/lib0/math.js +9 -31
- package/dist/esm/node_modules/lib0/number.js +7 -14
- package/dist/esm/node_modules/lib0/object.js +26 -87
- package/dist/esm/node_modules/lib0/observable.js +40 -77
- package/dist/esm/node_modules/lib0/pair.js +5 -19
- package/dist/esm/node_modules/lib0/prng.js +15 -76
- package/dist/esm/node_modules/lib0/promise.js +7 -29
- package/dist/esm/node_modules/lib0/random.js +10 -22
- package/dist/esm/node_modules/lib0/schema.js +367 -686
- package/dist/esm/node_modules/lib0/set.js +4 -9
- package/dist/esm/node_modules/lib0/storage.js +23 -54
- package/dist/esm/node_modules/lib0/string.js +26 -71
- package/dist/esm/node_modules/lib0/symbol.js +3 -11
- package/dist/esm/node_modules/lib0/time.js +3 -14
- package/dist/esm/node_modules/lib0/trait/equality.js +5 -27
- package/dist/esm/node_modules/lib0/url.js +5 -17
- package/dist/esm/node_modules/lib0/webcrypto.js +3 -4
- package/dist/esm/node_modules/y-protocols/auth.js +7 -18
- package/dist/esm/node_modules/y-protocols/awareness.js +58 -115
- package/dist/esm/node_modules/y-protocols/sync.js +26 -102
- package/dist/esm/node_modules/y-websocket/src/y-websocket.js +82 -218
- package/dist/esm/node_modules/yjs/dist/yjs.js +2263 -4899
- package/dist/esm/photo/FilterThumb.js +37 -23
- package/dist/esm/photo/NiceAITools.js +685 -466
- package/dist/esm/photo/PhotoEditor.js +34 -18
- package/dist/esm/photo/PhotoEditor.module.css.js +296 -3
- package/dist/esm/photo/PhotoEditorCanvas.js +250 -89
- package/dist/esm/photo/PhotoEditorLeftPanel.js +1086 -222
- package/dist/esm/photo/PhotoEditorToolbar.js +135 -17
- package/dist/esm/photo/photoEditorActions.js +93 -71
- package/dist/esm/photo/photoEditorTypes.js +32 -31
- package/dist/esm/photo/photoFilters.js +967 -984
- package/dist/esm/photo/photoOverlays.js +913 -944
- package/dist/esm/photo/photoSelection.js +438 -498
- package/dist/esm/photo/usePhotoEditor.js +740 -740
- package/dist/esm/pixel/HSVPicker.js +192 -174
- package/dist/esm/pixel/PixelEditor.js +160 -47
- package/dist/esm/pixel/PixelEditor.module.css.js +209 -3
- package/dist/esm/pixel/PixelEditorCanvas.js +29 -14
- package/dist/esm/pixel/PixelEditorMenuBar.js +160 -160
- package/dist/esm/pixel/PixelEditorRightPanel.js +377 -43
- package/dist/esm/pixel/PixelEditorStatusBar.js +47 -18
- package/dist/esm/pixel/PixelEditorTimeline.js +106 -21
- package/dist/esm/pixel/PixelEditorToolbar.js +99 -19
- package/dist/esm/pixel/SpriteGeneratorPanel.js +483 -301
- package/dist/esm/pixel/animalGeneratorPresets.js +782 -600
- package/dist/esm/pixel/asepriteFormat.js +517 -559
- package/dist/esm/pixel/buildingGeneratorPresets.js +699 -519
- package/dist/esm/pixel/characterGeneratorPresets.js +650 -543
- package/dist/esm/pixel/pixel-pro.js +53 -62
- package/dist/esm/pixel/pixelEditorExports.js +397 -427
- package/dist/esm/pixel/pixelEditorTypes.js +103 -107
- package/dist/esm/pixel/pixelEditorUtils.js +551 -560
- package/dist/esm/pixel/spriteGeneratorCore.js +223 -256
- package/dist/esm/pixel/usePixelEditor.js +1322 -1342
- package/dist/esm/texture/Nice3DTexturePainter.js +709 -439
- package/dist/esm/ui/NiceUIDesigner.js +957 -526
- package/dist/esm/vector/NiceSvgToCode.js +589 -445
- package/dist/esm/vector/VectorEditor.js +291 -116
- package/dist/esm/vector/VectorEditor.module.css.js +86 -3
- package/dist/esm/vector/VectorEditorMenuBar.js +333 -273
- package/dist/esm/vector/VectorEditorRightPanel.js +1094 -369
- package/dist/esm/vector/VectorEditorShapeRenderer.js +199 -59
- package/dist/esm/vector/VectorEditorStatusBar.js +39 -11
- package/dist/esm/vector/useVectorEditor.js +849 -888
- package/dist/esm/vector/vectorAnimation.js +115 -136
- package/dist/esm/vector/vectorBooleanOps.js +401 -484
- package/dist/esm/vector/vectorEditorExport.js +207 -202
- package/dist/esm/vector/vectorEditorImport.js +174 -188
- package/dist/esm/vector/vectorEditorTypes.js +50 -48
- package/dist/esm/vector/vectorGradients.js +158 -173
- package/dist/esm/vector/vectorIconExport.js +53 -58
- package/dist/esm/vector/vectorIconImport.js +171 -218
- package/package.json +9 -15
|
@@ -1,660 +1,618 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require(
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* pixelEditorUtils.ts — Drawing algorithms for the pixel editor.
|
|
7
|
-
* Bresenham line, midpoint ellipse, flood fill, selection, brush shapes,
|
|
8
|
-
* pressure-aware blending, dithering, gradient, symmetry etc.
|
|
9
|
-
*/
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
require("@nice2dev/ui-core");
|
|
10
4
|
const DEFAULT_BRUSH = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
size: 1,
|
|
6
|
+
shape: "square",
|
|
7
|
+
opacity: 100,
|
|
8
|
+
flow: 100,
|
|
9
|
+
spacing: 25,
|
|
10
|
+
pressureSize: true,
|
|
11
|
+
pressureOpacity: false,
|
|
12
|
+
hardness: 100
|
|
19
13
|
};
|
|
20
|
-
/** Read a pixel from ImageData at (x,y). Returns [r,g,b,a]. */
|
|
21
14
|
function getPixel(data, x, y) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
|
|
16
|
+
return [0, 0, 0, 0];
|
|
17
|
+
}
|
|
18
|
+
const i = (y * data.width + x) * 4;
|
|
19
|
+
return [data.data[i], data.data[i + 1], data.data[i + 2], data.data[i + 3]];
|
|
27
20
|
}
|
|
28
|
-
/** Set a pixel in ImageData at (x,y). */
|
|
29
21
|
function setPixel(data, x, y, color) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const i = (y * data.width + x) * 4;
|
|
26
|
+
data.data[i] = color[0];
|
|
27
|
+
data.data[i + 1] = color[1];
|
|
28
|
+
data.data[i + 2] = color[2];
|
|
29
|
+
data.data[i + 3] = color[3];
|
|
38
30
|
}
|
|
39
|
-
/** Blend a pixel onto ImageData at (x,y) with alpha compositing. */
|
|
40
31
|
function blendPixel(data, x, y, color, strength = 1) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
32
|
+
if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const i = (y * data.width + x) * 4;
|
|
36
|
+
const sa = color[3] / 255 * strength;
|
|
37
|
+
if (sa <= 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const da = data.data[i + 3] / 255;
|
|
41
|
+
const oa = sa + da * (1 - sa);
|
|
42
|
+
if (oa <= 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
data.data[i] = Math.round((color[0] * sa + data.data[i] * da * (1 - sa)) / oa);
|
|
46
|
+
data.data[i + 1] = Math.round((color[1] * sa + data.data[i + 1] * da * (1 - sa)) / oa);
|
|
47
|
+
data.data[i + 2] = Math.round((color[2] * sa + data.data[i + 2] * da * (1 - sa)) / oa);
|
|
48
|
+
data.data[i + 3] = Math.round(oa * 255);
|
|
58
49
|
}
|
|
59
|
-
/** Generate brush mask: returns array of {dx, dy, strength} offsets. */
|
|
60
50
|
function getBrushMask(cfg, pressure = 0.5) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
case 'diamond':
|
|
82
|
-
inBrush = Math.abs(dx) + Math.abs(dy) <= half;
|
|
83
|
-
dist = half > 0 ? (Math.abs(dx) + Math.abs(dy)) / half : 0;
|
|
84
|
-
break;
|
|
85
|
-
case 'spray':
|
|
86
|
-
inBrush = Math.random() < 0.3 && Math.sqrt(dx * dx + dy * dy) <= half + 0.5;
|
|
87
|
-
dist = 0;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
if (!inBrush) {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
// hardness: 100 = hard edge, 0 = soft falloff
|
|
94
|
-
const hardFactor = cfg.hardness / 100;
|
|
95
|
-
const edge = dist > hardFactor ? Math.max(0, 1 - (dist - hardFactor) / (1 - hardFactor + 0.001)) : 1;
|
|
96
|
-
mask.push({ dx, dy, strength: edge * flowMul * opaMul });
|
|
51
|
+
const sz = cfg.pressureSize ? Math.max(1, Math.round(cfg.size * pressure)) : cfg.size;
|
|
52
|
+
const half = Math.floor(sz / 2);
|
|
53
|
+
const flowMul = cfg.flow / 100;
|
|
54
|
+
const opaMul = cfg.opacity / 100 * (cfg.pressureOpacity ? pressure : 1);
|
|
55
|
+
const mask = [];
|
|
56
|
+
for (let dy = -half; dy < sz - half; dy++) {
|
|
57
|
+
for (let dx = -half; dx < sz - half; dx++) {
|
|
58
|
+
let inBrush = false;
|
|
59
|
+
let dist = 0;
|
|
60
|
+
switch (cfg.shape) {
|
|
61
|
+
case "square":
|
|
62
|
+
inBrush = true;
|
|
63
|
+
dist = Math.max(Math.abs(dx), Math.abs(dy)) / Math.max(half, 0.5);
|
|
64
|
+
break;
|
|
65
|
+
case "circle": {
|
|
66
|
+
const r = Math.sqrt(dx * dx + dy * dy);
|
|
67
|
+
inBrush = r <= half + 0.5;
|
|
68
|
+
dist = half > 0 ? r / half : 0;
|
|
69
|
+
break;
|
|
97
70
|
}
|
|
71
|
+
case "diamond":
|
|
72
|
+
inBrush = Math.abs(dx) + Math.abs(dy) <= half;
|
|
73
|
+
dist = half > 0 ? (Math.abs(dx) + Math.abs(dy)) / half : 0;
|
|
74
|
+
break;
|
|
75
|
+
case "spray":
|
|
76
|
+
inBrush = Math.random() < 0.3 && Math.sqrt(dx * dx + dy * dy) <= half + 0.5;
|
|
77
|
+
dist = 0;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (!inBrush) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const hardFactor = cfg.hardness / 100;
|
|
84
|
+
const edge = dist > hardFactor ? Math.max(0, 1 - (dist - hardFactor) / (1 - hardFactor + 1e-3)) : 1;
|
|
85
|
+
mask.push({ dx, dy, strength: edge * flowMul * opaMul });
|
|
98
86
|
}
|
|
99
|
-
|
|
87
|
+
}
|
|
88
|
+
return mask;
|
|
100
89
|
}
|
|
101
|
-
/** Apply brush stamp at (x,y) with all brush settings. */
|
|
102
90
|
function applyBrushStamp(data, x, y, color, cfg, pressure = 0.5, erase = false) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
blendPixel(data, gx, gy, color, strength);
|
|
116
|
-
}
|
|
91
|
+
const mask = getBrushMask(cfg, pressure);
|
|
92
|
+
for (const { dx, dy, strength } of mask) {
|
|
93
|
+
const gx = x + dx, gy = y + dy;
|
|
94
|
+
if (gx < 0 || gy < 0 || gx >= data.width || gy >= data.height) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (erase) {
|
|
98
|
+
const cur = getPixel(data, gx, gy);
|
|
99
|
+
const newA = Math.max(0, cur[3] - Math.round(255 * strength));
|
|
100
|
+
setPixel(data, gx, gy, [cur[0], cur[1], cur[2], newA]);
|
|
101
|
+
} else {
|
|
102
|
+
blendPixel(data, gx, gy, color, strength);
|
|
117
103
|
}
|
|
104
|
+
}
|
|
118
105
|
}
|
|
119
|
-
/** Get symmetry points for a coordinate. */
|
|
120
106
|
function getSymmetryPoints(x, y, w, h, mode) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
107
|
+
const pts = [{ x, y }];
|
|
108
|
+
switch (mode) {
|
|
109
|
+
case "h":
|
|
110
|
+
pts.push({ x: w - 1 - x, y });
|
|
111
|
+
break;
|
|
112
|
+
case "v":
|
|
113
|
+
pts.push({ x, y: h - 1 - y });
|
|
114
|
+
break;
|
|
115
|
+
case "both":
|
|
116
|
+
pts.push({ x: w - 1 - x, y }, { x, y: h - 1 - y }, { x: w - 1 - x, y: h - 1 - y });
|
|
117
|
+
break;
|
|
118
|
+
case "radial4": {
|
|
119
|
+
const cx = w / 2, cy = h / 2;
|
|
120
|
+
const dx = x - cx, dy = y - cy;
|
|
121
|
+
pts.push(
|
|
122
|
+
{ x: Math.round(cx - dy), y: Math.round(cy + dx) },
|
|
123
|
+
{ x: Math.round(cx - dx), y: Math.round(cy - dy) },
|
|
124
|
+
{ x: Math.round(cx + dy), y: Math.round(cy - dx) }
|
|
125
|
+
);
|
|
126
|
+
break;
|
|
138
127
|
}
|
|
139
|
-
|
|
128
|
+
}
|
|
129
|
+
return pts;
|
|
140
130
|
}
|
|
141
|
-
/** Draw a linear gradient between two points on ImageData. */
|
|
142
131
|
function drawGradient(data, x0, y0, x1, y1, color1, color2, sel) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for (let
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
setPixel(data, px, py, c);
|
|
161
|
-
}
|
|
132
|
+
const dx = x1 - x0, dy = y1 - y0;
|
|
133
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
134
|
+
if (len === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const sx = sel?.x ?? 0, sy = sel?.y ?? 0;
|
|
138
|
+
const sw = sel?.w ?? data.width, sh = sel?.h ?? data.height;
|
|
139
|
+
for (let py = sy; py < sy + sh; py++) {
|
|
140
|
+
for (let px = sx; px < sx + sw; px++) {
|
|
141
|
+
const t = Math.max(0, Math.min(1, ((px - x0) * dx + (py - y0) * dy) / (len * len)));
|
|
142
|
+
const c = [
|
|
143
|
+
Math.round(color1[0] + (color2[0] - color1[0]) * t),
|
|
144
|
+
Math.round(color1[1] + (color2[1] - color1[1]) * t),
|
|
145
|
+
Math.round(color1[2] + (color2[2] - color1[2]) * t),
|
|
146
|
+
Math.round(color1[3] + (color2[3] - color1[3]) * t)
|
|
147
|
+
];
|
|
148
|
+
setPixel(data, px, py, c);
|
|
162
149
|
}
|
|
150
|
+
}
|
|
163
151
|
}
|
|
164
|
-
/** Ordered dithering 4x4 Bayer matrix. */
|
|
165
152
|
const BAYER4 = [
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
153
|
+
[0, 8, 2, 10],
|
|
154
|
+
[12, 4, 14, 6],
|
|
155
|
+
[3, 11, 1, 9],
|
|
156
|
+
[15, 7, 13, 5]
|
|
170
157
|
];
|
|
171
|
-
/** Apply dithered brush stamp. */
|
|
172
158
|
function applyDitherStamp(data, x, y, color, size, pressure = 0.5) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
159
|
+
const half = Math.floor(size / 2);
|
|
160
|
+
const threshold = pressure * 16;
|
|
161
|
+
for (let dy = -half; dy < size - half; dy++) {
|
|
162
|
+
for (let dx = -half; dx < size - half; dx++) {
|
|
163
|
+
const gx = x + dx, gy = y + dy;
|
|
164
|
+
if (gx < 0 || gy < 0 || gx >= data.width || gy >= data.height) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const bayer = BAYER4[(gy % 4 + 4) % 4][(gx % 4 + 4) % 4];
|
|
168
|
+
if (bayer < threshold) {
|
|
169
|
+
setPixel(data, gx, gy, color);
|
|
170
|
+
}
|
|
186
171
|
}
|
|
172
|
+
}
|
|
187
173
|
}
|
|
188
|
-
/** Copy pixels from a selection region. */
|
|
189
174
|
function copySelection(data, sel) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
175
|
+
const out = new ImageData(sel.w, sel.h);
|
|
176
|
+
for (let y = 0; y < sel.h; y++) {
|
|
177
|
+
for (let x = 0; x < sel.w; x++) {
|
|
178
|
+
const px = getPixel(data, sel.x + x, sel.y + y);
|
|
179
|
+
setPixel(out, x, y, px);
|
|
196
180
|
}
|
|
197
|
-
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
198
183
|
}
|
|
199
|
-
/** Paste ImageData at position. */
|
|
200
184
|
function pasteAt(dst, src, ox, oy) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
185
|
+
for (let y = 0; y < src.height; y++) {
|
|
186
|
+
for (let x = 0; x < src.width; x++) {
|
|
187
|
+
const px = getPixel(src, x, y);
|
|
188
|
+
if (px[3] > 0) {
|
|
189
|
+
blendPixel(dst, ox + x, oy + y, px, 1);
|
|
190
|
+
}
|
|
208
191
|
}
|
|
192
|
+
}
|
|
209
193
|
}
|
|
210
|
-
/** Clear pixels in selection region. */
|
|
211
194
|
function clearSelection(data, sel) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
195
|
+
for (let y = sel.y; y < sel.y + sel.h; y++) {
|
|
196
|
+
for (let x = sel.x; x < sel.x + sel.w; x++) {
|
|
197
|
+
setPixel(data, x, y, [0, 0, 0, 0]);
|
|
216
198
|
}
|
|
199
|
+
}
|
|
217
200
|
}
|
|
218
|
-
/** Bresenham line — calls callback for each pixel along the line. */
|
|
219
201
|
function bresenhamLine(x0, y0, x1, y1, cb) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
202
|
+
const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0);
|
|
203
|
+
const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
|
|
204
|
+
let err = dx + dy;
|
|
205
|
+
for (; ; ) {
|
|
206
|
+
cb(x0, y0);
|
|
207
|
+
if (x0 === x1 && y0 === y1) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
const e2 = 2 * err;
|
|
211
|
+
if (e2 >= dy) {
|
|
212
|
+
err += dy;
|
|
213
|
+
x0 += sx;
|
|
214
|
+
}
|
|
215
|
+
if (e2 <= dx) {
|
|
216
|
+
err += dx;
|
|
217
|
+
y0 += sy;
|
|
237
218
|
}
|
|
219
|
+
}
|
|
238
220
|
}
|
|
239
|
-
/** Flood fill — fills connected region of same color with new color. */
|
|
240
221
|
function floodFill(data, startX, startY, fillColor) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
222
|
+
const w = data.width, h = data.height;
|
|
223
|
+
if (startX < 0 || startY < 0 || startX >= w || startY >= h) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const target = getPixel(data, startX, startY);
|
|
227
|
+
if (colorsEqual(target, fillColor)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const stack = [startX, startY];
|
|
231
|
+
const visited = new Uint8Array(w * h);
|
|
232
|
+
while (stack.length > 0) {
|
|
233
|
+
const y = stack.pop();
|
|
234
|
+
const x = stack.pop();
|
|
235
|
+
const key = y * w + x;
|
|
236
|
+
if (x < 0 || y < 0 || x >= w || y >= h) {
|
|
237
|
+
continue;
|
|
244
238
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return;
|
|
239
|
+
if (visited[key]) {
|
|
240
|
+
continue;
|
|
248
241
|
}
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const x = stack.pop();
|
|
254
|
-
const key = y * w + x;
|
|
255
|
-
if (x < 0 || y < 0 || x >= w || y >= h) {
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (visited[key]) {
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
visited[key] = 1;
|
|
262
|
-
const current = getPixel(data, x, y);
|
|
263
|
-
if (!colorsEqual(current, target)) {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
setPixel(data, x, y, fillColor);
|
|
267
|
-
stack.push(x - 1, y, x + 1, y, x, y - 1, x, y + 1);
|
|
242
|
+
visited[key] = 1;
|
|
243
|
+
const current = getPixel(data, x, y);
|
|
244
|
+
if (!colorsEqual(current, target)) {
|
|
245
|
+
continue;
|
|
268
246
|
}
|
|
247
|
+
setPixel(data, x, y, fillColor);
|
|
248
|
+
stack.push(x - 1, y, x + 1, y, x, y - 1, x, y + 1);
|
|
249
|
+
}
|
|
269
250
|
}
|
|
270
|
-
/** Check if two RGBA colors are equal (with small tolerance for alpha). */
|
|
271
251
|
function colorsEqual(a, b, tolerance = 0) {
|
|
272
|
-
|
|
273
|
-
Math.abs(a[1] - b[1]) <= tolerance &&
|
|
274
|
-
Math.abs(a[2] - b[2]) <= tolerance &&
|
|
275
|
-
Math.abs(a[3] - b[3]) <= tolerance);
|
|
252
|
+
return Math.abs(a[0] - b[0]) <= tolerance && Math.abs(a[1] - b[1]) <= tolerance && Math.abs(a[2] - b[2]) <= tolerance && Math.abs(a[3] - b[3]) <= tolerance;
|
|
276
253
|
}
|
|
277
|
-
/** Parse hex color string to RGBA. */
|
|
278
254
|
function hexToRGBA(hex, alpha = 255) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
255
|
+
const h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
256
|
+
return [
|
|
257
|
+
parseInt(h.slice(0, 2), 16) || 0,
|
|
258
|
+
parseInt(h.slice(2, 4), 16) || 0,
|
|
259
|
+
parseInt(h.slice(4, 6), 16) || 0,
|
|
260
|
+
alpha
|
|
261
|
+
];
|
|
286
262
|
}
|
|
287
|
-
/** Convert RGBA to hex string. */
|
|
288
263
|
function rgbaToHex(c) {
|
|
289
|
-
|
|
264
|
+
return "#" + [c[0], c[1], c[2]].map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
290
265
|
}
|
|
291
|
-
/** Create blank ImageData. */
|
|
292
266
|
function createBlankImageData(width, height) {
|
|
293
|
-
|
|
267
|
+
return new ImageData(width, height);
|
|
294
268
|
}
|
|
295
|
-
/** Clone ImageData. */
|
|
296
269
|
function cloneImageData(src) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
270
|
+
const dst = new ImageData(src.width, src.height);
|
|
271
|
+
dst.data.set(src.data);
|
|
272
|
+
return dst;
|
|
300
273
|
}
|
|
301
|
-
/** Flip ImageData horizontally. */
|
|
302
274
|
function flipH(src) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
275
|
+
const dst = new ImageData(src.width, src.height);
|
|
276
|
+
for (let y = 0; y < src.height; y++) {
|
|
277
|
+
for (let x = 0; x < src.width; x++) {
|
|
278
|
+
const si = (y * src.width + x) * 4;
|
|
279
|
+
const di = (y * src.width + (src.width - 1 - x)) * 4;
|
|
280
|
+
dst.data[di] = src.data[si];
|
|
281
|
+
dst.data[di + 1] = src.data[si + 1];
|
|
282
|
+
dst.data[di + 2] = src.data[si + 2];
|
|
283
|
+
dst.data[di + 3] = src.data[si + 3];
|
|
313
284
|
}
|
|
314
|
-
|
|
285
|
+
}
|
|
286
|
+
return dst;
|
|
315
287
|
}
|
|
316
|
-
/** Flip ImageData vertically. */
|
|
317
288
|
function flipV(src) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
289
|
+
const dst = new ImageData(src.width, src.height);
|
|
290
|
+
for (let y = 0; y < src.height; y++) {
|
|
291
|
+
for (let x = 0; x < src.width; x++) {
|
|
292
|
+
const si = (y * src.width + x) * 4;
|
|
293
|
+
const di = ((src.height - 1 - y) * src.width + x) * 4;
|
|
294
|
+
dst.data[di] = src.data[si];
|
|
295
|
+
dst.data[di + 1] = src.data[si + 1];
|
|
296
|
+
dst.data[di + 2] = src.data[si + 2];
|
|
297
|
+
dst.data[di + 3] = src.data[si + 3];
|
|
328
298
|
}
|
|
329
|
-
|
|
299
|
+
}
|
|
300
|
+
return dst;
|
|
330
301
|
}
|
|
331
|
-
/** Rotate ImageData 90° clockwise. Returns new ImageData with swapped dimensions. */
|
|
332
302
|
function rotateCW(src) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
303
|
+
const dst = new ImageData(src.height, src.width);
|
|
304
|
+
for (let y = 0; y < src.height; y++) {
|
|
305
|
+
for (let x = 0; x < src.width; x++) {
|
|
306
|
+
const si = (y * src.width + x) * 4;
|
|
307
|
+
const di = (x * dst.width + (dst.width - 1 - y)) * 4;
|
|
308
|
+
dst.data[di] = src.data[si];
|
|
309
|
+
dst.data[di + 1] = src.data[si + 1];
|
|
310
|
+
dst.data[di + 2] = src.data[si + 2];
|
|
311
|
+
dst.data[di + 3] = src.data[si + 3];
|
|
343
312
|
}
|
|
344
|
-
|
|
313
|
+
}
|
|
314
|
+
return dst;
|
|
345
315
|
}
|
|
346
|
-
// Bayer matrices for ordered dithering
|
|
347
316
|
const BAYER_2x2 = [
|
|
348
|
-
|
|
349
|
-
|
|
317
|
+
[0, 2],
|
|
318
|
+
[3, 1]
|
|
350
319
|
];
|
|
351
320
|
const BAYER_8x8 = [
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
321
|
+
[0, 32, 8, 40, 2, 34, 10, 42],
|
|
322
|
+
[48, 16, 56, 24, 50, 18, 58, 26],
|
|
323
|
+
[12, 44, 4, 36, 14, 46, 6, 38],
|
|
324
|
+
[60, 28, 52, 20, 62, 30, 54, 22],
|
|
325
|
+
[3, 35, 11, 43, 1, 33, 9, 41],
|
|
326
|
+
[51, 19, 59, 27, 49, 17, 57, 25],
|
|
327
|
+
[15, 47, 7, 39, 13, 45, 5, 37],
|
|
328
|
+
[63, 31, 55, 23, 61, 29, 53, 21]
|
|
360
329
|
];
|
|
361
|
-
/** Find closest color in palette */
|
|
362
330
|
function findClosestPaletteColor(r, g, b, palette) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
331
|
+
let minDist = Infinity;
|
|
332
|
+
let closest = palette[0];
|
|
333
|
+
for (const color of palette) {
|
|
334
|
+
const dr = r - color[0];
|
|
335
|
+
const dg = g - color[1];
|
|
336
|
+
const db = b - color[2];
|
|
337
|
+
const dist = dr * dr + dg * dg + db * db;
|
|
338
|
+
if (dist < minDist) {
|
|
339
|
+
minDist = dist;
|
|
340
|
+
closest = color;
|
|
374
341
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
function ditherOrdered(data, palette, matrix = BAYER4, strength = 1
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
342
|
+
}
|
|
343
|
+
return closest;
|
|
344
|
+
}
|
|
345
|
+
function ditherOrdered(data, palette, matrix = BAYER4, strength = 1) {
|
|
346
|
+
const result = cloneImageData(data);
|
|
347
|
+
const matrixSize = matrix.length;
|
|
348
|
+
const maxMatrixValue = matrixSize * matrixSize;
|
|
349
|
+
for (let y = 0; y < data.height; y++) {
|
|
350
|
+
for (let x = 0; x < data.width; x++) {
|
|
351
|
+
const i = (y * data.width + x) * 4;
|
|
352
|
+
const a = data.data[i + 3];
|
|
353
|
+
if (a < 128) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const threshold = (matrix[y % matrixSize][x % matrixSize] / maxMatrixValue - 0.5) * strength * 64;
|
|
357
|
+
const r = Math.max(0, Math.min(255, data.data[i] + threshold));
|
|
358
|
+
const g = Math.max(0, Math.min(255, data.data[i + 1] + threshold));
|
|
359
|
+
const b = Math.max(0, Math.min(255, data.data[i + 2] + threshold));
|
|
360
|
+
const closest = findClosestPaletteColor(r, g, b, palette);
|
|
361
|
+
result.data[i] = closest[0];
|
|
362
|
+
result.data[i + 1] = closest[1];
|
|
363
|
+
result.data[i + 2] = closest[2];
|
|
398
364
|
}
|
|
399
|
-
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
400
367
|
}
|
|
401
|
-
/** Floyd-Steinberg error diffusion dithering */
|
|
402
368
|
function ditherFloydSteinberg(data, palette) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
errors[ni + 2] += errB * factor;
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
distribute(1, 0, 7 / 16);
|
|
434
|
-
distribute(-1, 1, 3 / 16);
|
|
435
|
-
distribute(0, 1, 5 / 16);
|
|
436
|
-
distribute(1, 1, 1 / 16);
|
|
369
|
+
const result = cloneImageData(data);
|
|
370
|
+
const errors = new Float32Array(data.width * data.height * 3);
|
|
371
|
+
for (let y = 0; y < data.height; y++) {
|
|
372
|
+
for (let x = 0; x < data.width; x++) {
|
|
373
|
+
const idx = y * data.width + x;
|
|
374
|
+
const i = idx * 4;
|
|
375
|
+
const ei = idx * 3;
|
|
376
|
+
if (data.data[i + 3] < 128) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
|
|
380
|
+
const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
|
|
381
|
+
const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
|
|
382
|
+
const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
|
|
383
|
+
result.data[i] = newColor[0];
|
|
384
|
+
result.data[i + 1] = newColor[1];
|
|
385
|
+
result.data[i + 2] = newColor[2];
|
|
386
|
+
const errR = oldR - newColor[0];
|
|
387
|
+
const errG = oldG - newColor[1];
|
|
388
|
+
const errB = oldB - newColor[2];
|
|
389
|
+
const distribute = (dx, dy, factor) => {
|
|
390
|
+
const nx = x + dx, ny = y + dy;
|
|
391
|
+
if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
|
|
392
|
+
const ni = (ny * data.width + nx) * 3;
|
|
393
|
+
errors[ni] += errR * factor;
|
|
394
|
+
errors[ni + 1] += errG * factor;
|
|
395
|
+
errors[ni + 2] += errB * factor;
|
|
437
396
|
}
|
|
397
|
+
};
|
|
398
|
+
distribute(1, 0, 7 / 16);
|
|
399
|
+
distribute(-1, 1, 3 / 16);
|
|
400
|
+
distribute(0, 1, 5 / 16);
|
|
401
|
+
distribute(1, 1, 1 / 16);
|
|
438
402
|
}
|
|
439
|
-
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
440
405
|
}
|
|
441
|
-
/** Jarvis-Judice-Ninke dithering (larger error diffusion kernel) */
|
|
442
406
|
function ditherJarvisJudiceNinke(data, palette) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
// JJN kernel: /48
|
|
473
|
-
distribute(1, 0, 7 / 48);
|
|
474
|
-
distribute(2, 0, 5 / 48);
|
|
475
|
-
distribute(-2, 1, 3 / 48);
|
|
476
|
-
distribute(-1, 1, 5 / 48);
|
|
477
|
-
distribute(0, 1, 7 / 48);
|
|
478
|
-
distribute(1, 1, 5 / 48);
|
|
479
|
-
distribute(2, 1, 3 / 48);
|
|
480
|
-
distribute(-2, 2, 1 / 48);
|
|
481
|
-
distribute(-1, 2, 3 / 48);
|
|
482
|
-
distribute(0, 2, 5 / 48);
|
|
483
|
-
distribute(1, 2, 3 / 48);
|
|
484
|
-
distribute(2, 2, 1 / 48);
|
|
407
|
+
const result = cloneImageData(data);
|
|
408
|
+
const errors = new Float32Array(data.width * data.height * 3);
|
|
409
|
+
for (let y = 0; y < data.height; y++) {
|
|
410
|
+
for (let x = 0; x < data.width; x++) {
|
|
411
|
+
const idx = y * data.width + x;
|
|
412
|
+
const i = idx * 4;
|
|
413
|
+
const ei = idx * 3;
|
|
414
|
+
if (data.data[i + 3] < 128) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
|
|
418
|
+
const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
|
|
419
|
+
const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
|
|
420
|
+
const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
|
|
421
|
+
result.data[i] = newColor[0];
|
|
422
|
+
result.data[i + 1] = newColor[1];
|
|
423
|
+
result.data[i + 2] = newColor[2];
|
|
424
|
+
const errR = oldR - newColor[0];
|
|
425
|
+
const errG = oldG - newColor[1];
|
|
426
|
+
const errB = oldB - newColor[2];
|
|
427
|
+
const distribute = (dx, dy, factor) => {
|
|
428
|
+
const nx = x + dx, ny = y + dy;
|
|
429
|
+
if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
|
|
430
|
+
const ni = (ny * data.width + nx) * 3;
|
|
431
|
+
errors[ni] += errR * factor;
|
|
432
|
+
errors[ni + 1] += errG * factor;
|
|
433
|
+
errors[ni + 2] += errB * factor;
|
|
485
434
|
}
|
|
435
|
+
};
|
|
436
|
+
distribute(1, 0, 7 / 48);
|
|
437
|
+
distribute(2, 0, 5 / 48);
|
|
438
|
+
distribute(-2, 1, 3 / 48);
|
|
439
|
+
distribute(-1, 1, 5 / 48);
|
|
440
|
+
distribute(0, 1, 7 / 48);
|
|
441
|
+
distribute(1, 1, 5 / 48);
|
|
442
|
+
distribute(2, 1, 3 / 48);
|
|
443
|
+
distribute(-2, 2, 1 / 48);
|
|
444
|
+
distribute(-1, 2, 3 / 48);
|
|
445
|
+
distribute(0, 2, 5 / 48);
|
|
446
|
+
distribute(1, 2, 3 / 48);
|
|
447
|
+
distribute(2, 2, 1 / 48);
|
|
486
448
|
}
|
|
487
|
-
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
488
451
|
}
|
|
489
|
-
/** Atkinson dithering (preserves details better) */
|
|
490
452
|
function ditherAtkinson(data, palette) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
errors[ni + 2] += errB;
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
distribute(1, 0);
|
|
522
|
-
distribute(2, 0);
|
|
523
|
-
distribute(-1, 1);
|
|
524
|
-
distribute(0, 1);
|
|
525
|
-
distribute(1, 1);
|
|
526
|
-
distribute(0, 2);
|
|
453
|
+
const result = cloneImageData(data);
|
|
454
|
+
const errors = new Float32Array(data.width * data.height * 3);
|
|
455
|
+
for (let y = 0; y < data.height; y++) {
|
|
456
|
+
for (let x = 0; x < data.width; x++) {
|
|
457
|
+
const idx = y * data.width + x;
|
|
458
|
+
const i = idx * 4;
|
|
459
|
+
const ei = idx * 3;
|
|
460
|
+
if (data.data[i + 3] < 128) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
|
|
464
|
+
const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
|
|
465
|
+
const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
|
|
466
|
+
const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
|
|
467
|
+
result.data[i] = newColor[0];
|
|
468
|
+
result.data[i + 1] = newColor[1];
|
|
469
|
+
result.data[i + 2] = newColor[2];
|
|
470
|
+
const errR = (oldR - newColor[0]) / 8;
|
|
471
|
+
const errG = (oldG - newColor[1]) / 8;
|
|
472
|
+
const errB = (oldB - newColor[2]) / 8;
|
|
473
|
+
const distribute = (dx, dy) => {
|
|
474
|
+
const nx = x + dx, ny = y + dy;
|
|
475
|
+
if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
|
|
476
|
+
const ni = (ny * data.width + nx) * 3;
|
|
477
|
+
errors[ni] += errR;
|
|
478
|
+
errors[ni + 1] += errG;
|
|
479
|
+
errors[ni + 2] += errB;
|
|
527
480
|
}
|
|
481
|
+
};
|
|
482
|
+
distribute(1, 0);
|
|
483
|
+
distribute(2, 0);
|
|
484
|
+
distribute(-1, 1);
|
|
485
|
+
distribute(0, 1);
|
|
486
|
+
distribute(1, 1);
|
|
487
|
+
distribute(0, 2);
|
|
528
488
|
}
|
|
529
|
-
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
530
491
|
}
|
|
531
|
-
/** Random dithering */
|
|
532
492
|
function ditherRandom(data, palette, strength = 32) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
493
|
+
const result = cloneImageData(data);
|
|
494
|
+
for (let y = 0; y < data.height; y++) {
|
|
495
|
+
for (let x = 0; x < data.width; x++) {
|
|
496
|
+
const i = (y * data.width + x) * 4;
|
|
497
|
+
if (data.data[i + 3] < 128) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const noise = (Math.random() - 0.5) * strength * 2;
|
|
501
|
+
const r = Math.max(0, Math.min(255, data.data[i] + noise));
|
|
502
|
+
const g = Math.max(0, Math.min(255, data.data[i + 1] + noise));
|
|
503
|
+
const b = Math.max(0, Math.min(255, data.data[i + 2] + noise));
|
|
504
|
+
const closest = findClosestPaletteColor(r, g, b, palette);
|
|
505
|
+
result.data[i] = closest[0];
|
|
506
|
+
result.data[i + 1] = closest[1];
|
|
507
|
+
result.data[i + 2] = closest[2];
|
|
549
508
|
}
|
|
550
|
-
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
551
511
|
}
|
|
552
|
-
/** Apply dithering with specified algorithm */
|
|
553
512
|
function applyDithering(data, palette, algorithm) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
513
|
+
switch (algorithm) {
|
|
514
|
+
case "none":
|
|
515
|
+
return ditherOrdered(data, palette, [[0]], 0);
|
|
516
|
+
case "ordered-2x2":
|
|
517
|
+
return ditherOrdered(data, palette, BAYER_2x2);
|
|
518
|
+
case "ordered-4x4":
|
|
519
|
+
return ditherOrdered(data, palette, BAYER4);
|
|
520
|
+
case "ordered-8x8":
|
|
521
|
+
return ditherOrdered(data, palette, BAYER_8x8);
|
|
522
|
+
case "floyd-steinberg":
|
|
523
|
+
return ditherFloydSteinberg(data, palette);
|
|
524
|
+
case "jarvis-judice-ninke":
|
|
525
|
+
return ditherJarvisJudiceNinke(data, palette);
|
|
526
|
+
case "atkinson":
|
|
527
|
+
return ditherAtkinson(data, palette);
|
|
528
|
+
case "random":
|
|
529
|
+
return ditherRandom(data, palette);
|
|
530
|
+
default:
|
|
531
|
+
return ditherFloydSteinberg(data, palette);
|
|
532
|
+
}
|
|
575
533
|
}
|
|
576
|
-
/** Generate palette from image using median cut algorithm */
|
|
577
534
|
function extractPalette(data, maxColors) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
colors.push([data.data[i], data.data[i + 1], data.data[i + 2]]);
|
|
583
|
-
}
|
|
535
|
+
const colors = [];
|
|
536
|
+
for (let i = 0; i < data.data.length; i += 4) {
|
|
537
|
+
if (data.data[i + 3] >= 128) {
|
|
538
|
+
colors.push([data.data[i], data.data[i + 1], data.data[i + 2]]);
|
|
584
539
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return [[r, g, b]];
|
|
596
|
-
}
|
|
597
|
-
// Find channel with largest range
|
|
598
|
-
const ranges = [0, 1, 2].map((ch) => {
|
|
599
|
-
const vals = colors.map((c) => c[ch]);
|
|
600
|
-
return Math.max(...vals) - Math.min(...vals);
|
|
601
|
-
});
|
|
602
|
-
const channel = ranges.indexOf(Math.max(...ranges));
|
|
603
|
-
// Sort by channel and split
|
|
604
|
-
colors.sort((a, b) => a[channel] - b[channel]);
|
|
605
|
-
const mid = Math.floor(colors.length / 2);
|
|
606
|
-
return [
|
|
607
|
-
...medianCut(colors.slice(0, mid), depth - 1),
|
|
608
|
-
...medianCut(colors.slice(mid), depth - 1),
|
|
609
|
-
];
|
|
540
|
+
}
|
|
541
|
+
if (colors.length === 0) {
|
|
542
|
+
return [[0, 0, 0, 255]];
|
|
543
|
+
}
|
|
544
|
+
function medianCut(colors2, depth2) {
|
|
545
|
+
if (depth2 === 0 || colors2.length <= 1) {
|
|
546
|
+
const r = Math.round(colors2.reduce((s, c) => s + c[0], 0) / colors2.length);
|
|
547
|
+
const g = Math.round(colors2.reduce((s, c) => s + c[1], 0) / colors2.length);
|
|
548
|
+
const b = Math.round(colors2.reduce((s, c) => s + c[2], 0) / colors2.length);
|
|
549
|
+
return [[r, g, b]];
|
|
610
550
|
}
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
551
|
+
const ranges = [0, 1, 2].map((ch) => {
|
|
552
|
+
const vals = colors2.map((c) => c[ch]);
|
|
553
|
+
return Math.max(...vals) - Math.min(...vals);
|
|
554
|
+
});
|
|
555
|
+
const channel = ranges.indexOf(Math.max(...ranges));
|
|
556
|
+
colors2.sort((a, b) => a[channel] - b[channel]);
|
|
557
|
+
const mid = Math.floor(colors2.length / 2);
|
|
558
|
+
return [
|
|
559
|
+
...medianCut(colors2.slice(0, mid), depth2 - 1),
|
|
560
|
+
...medianCut(colors2.slice(mid), depth2 - 1)
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
const depth = Math.ceil(Math.log2(maxColors));
|
|
564
|
+
const palette = medianCut(colors, depth).slice(0, maxColors);
|
|
565
|
+
return palette.map((c) => [c[0], c[1], c[2], 255]);
|
|
614
566
|
}
|
|
615
|
-
/** Convert hex palette to RGBA array */
|
|
616
567
|
function hexPaletteToRGBA(hexColors) {
|
|
617
|
-
|
|
568
|
+
return hexColors.map((hex) => hexToRGBA(hex));
|
|
618
569
|
}
|
|
619
|
-
/** Create reference overlay from file */
|
|
620
570
|
async function createReferenceOverlay(file, targetWidth, targetHeight) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
const img = new Image();
|
|
573
|
+
img.onload = () => {
|
|
574
|
+
const scale = Math.min(
|
|
575
|
+
targetWidth / img.width,
|
|
576
|
+
targetHeight / img.height,
|
|
577
|
+
1
|
|
578
|
+
// Don't scale up
|
|
579
|
+
);
|
|
580
|
+
const canvas = document.createElement("canvas");
|
|
581
|
+
canvas.width = Math.round(img.width * scale);
|
|
582
|
+
canvas.height = Math.round(img.height * scale);
|
|
583
|
+
const ctx = canvas.getContext("2d");
|
|
584
|
+
ctx.imageSmoothingEnabled = false;
|
|
585
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
586
|
+
resolve({
|
|
587
|
+
image: ctx.getImageData(0, 0, canvas.width, canvas.height),
|
|
588
|
+
x: 0,
|
|
589
|
+
y: 0,
|
|
590
|
+
opacity: 0.4,
|
|
591
|
+
scale: 1,
|
|
592
|
+
locked: false
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
img.onerror = reject;
|
|
596
|
+
img.src = URL.createObjectURL(file);
|
|
597
|
+
});
|
|
644
598
|
}
|
|
645
|
-
/** Render reference overlay onto canvas */
|
|
646
599
|
function renderReferenceOverlay(ctx, ref, zoom, panX, panY) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
600
|
+
ctx.save();
|
|
601
|
+
ctx.globalAlpha = ref.opacity;
|
|
602
|
+
const tempCanvas = document.createElement("canvas");
|
|
603
|
+
tempCanvas.width = ref.image.width;
|
|
604
|
+
tempCanvas.height = ref.image.height;
|
|
605
|
+
tempCanvas.getContext("2d").putImageData(ref.image, 0, 0);
|
|
606
|
+
ctx.imageSmoothingEnabled = false;
|
|
607
|
+
ctx.drawImage(
|
|
608
|
+
tempCanvas,
|
|
609
|
+
(ref.x + panX) * zoom,
|
|
610
|
+
(ref.y + panY) * zoom,
|
|
611
|
+
ref.image.width * ref.scale * zoom,
|
|
612
|
+
ref.image.height * ref.scale * zoom
|
|
613
|
+
);
|
|
614
|
+
ctx.restore();
|
|
615
|
+
}
|
|
658
616
|
exports.DEFAULT_BRUSH = DEFAULT_BRUSH;
|
|
659
617
|
exports.applyBrushStamp = applyBrushStamp;
|
|
660
618
|
exports.applyDitherStamp = applyDitherStamp;
|