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