@weasel-js/dither 0.0.0-stage → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/index.d.ts +102 -0
- package/dist/index.js +279 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/dither
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Turns pixels into 1-bit ink: on the CPU for the pixels an export produces,
|
|
4
|
+
and as a renderer effect that shows the dither live on the canvas.
|
|
5
|
+
|
|
6
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
7
|
+
scene-graph canvas kit for React. See the
|
|
8
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/dither).
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @weasel-js/dither
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Export
|
|
15
|
+
|
|
16
|
+
`ditherPixels(image, { method, threshold, background, protect })` takes
|
|
17
|
+
straight RGBA (what `renderSceneToPixels` returns) and answers one byte per
|
|
18
|
+
pixel, 1 for ink. `method` is `'threshold'`, `'bayer'`, `'floyd-steinberg'`
|
|
19
|
+
or `'atkinson'`. Transparency composites over `background` (white by default)
|
|
20
|
+
before gray is taken. `maskToRgba` paints the result in two colors.
|
|
21
|
+
|
|
22
|
+
## Live
|
|
23
|
+
|
|
24
|
+
`dither({ method, grid, ink, blank })` is an `Effect[]` for a group or a
|
|
25
|
+
layer group: threshold or Bayer, since error diffusion cannot run in a
|
|
26
|
+
shader. On the screen grid each device pixel is a dot. On an output grid, a
|
|
27
|
+
printer's or a 1-bit panel's, each dot is one dot of the target, so the
|
|
28
|
+
canvas shows what the export will print at any zoom:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const grid = { pitch: 72 / 180, origin: { x: 0, y: 0 } }; // 180 dpi, in points
|
|
32
|
+
const layerGroup = {
|
|
33
|
+
id: 'print',
|
|
34
|
+
layers: ['scene'],
|
|
35
|
+
effects: (view, dims) => dither({ method: 'bayer', grid: gridOnScreen(grid, view, dims.dpr) }),
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The live result matches the export exactly when a dot is a whole number of
|
|
40
|
+
device pixels, up to 4, and the grid's origin lands on a whole pixel. A dot
|
|
41
|
+
smaller than a device pixel cannot be shown, so each pixel quantizes alone.
|
|
42
|
+
|
|
43
|
+
## Keeping things out of the dither
|
|
44
|
+
|
|
45
|
+
Put the dither on the groups that have tone (a photo) and end the page with a
|
|
46
|
+
`threshold` pass, and a barcode or a line of type stays crisp with nothing to
|
|
47
|
+
protect. Where tone must be dithered over such artwork, protect it:
|
|
48
|
+
`protectRects` maps items to rects on the grid, `protectMask` turns rects
|
|
49
|
+
into the mask `ditherPixels` takes, and `protectTexture` uploads that mask
|
|
50
|
+
for `dither`, which needs an output grid to read it. Inside it pixels quantize
|
|
51
|
+
at plain threshold, and error diffusion neither enters nor leaves it.
|
|
52
|
+
|
|
53
|
+
## Whole-dot marks
|
|
54
|
+
|
|
55
|
+
Plain threshold still gives each edge to whichever dot it covers more than
|
|
56
|
+
half of, so two bars of one module can print a dot apart and a hairline can
|
|
57
|
+
vanish. Keep the marks on the dots in the document instead, with core's
|
|
58
|
+
quantize rule:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { OUTPUT_GRID_KEY, createScene, quantizeSize } from '@weasel-js/core';
|
|
62
|
+
import { alignToGrid, snapToPitch } from '@weasel-js/geom';
|
|
63
|
+
|
|
64
|
+
const scene = createScene({
|
|
65
|
+
systemLayers,
|
|
66
|
+
doc: { [OUTPUT_GRID_KEY]: { pitch: 72 / 180, origin: { x: 0, y: 0 } } },
|
|
67
|
+
quantize: {
|
|
68
|
+
// What the document holds: here a rule's thickness in whole dots, never under one.
|
|
69
|
+
stored: ({ data, pose }, grid, anchor) => data.mark === 'rule'
|
|
70
|
+
? { pose: quantizeSize(pose, pose.width, snapToPitch(pose.height, grid.pitch), anchor) }
|
|
71
|
+
: null,
|
|
72
|
+
// Where it is drawn: its corner on a dot. Picking keeps the stored pose.
|
|
73
|
+
drawn: ({ data, pose }, grid) => (data.mark ? alignToGrid(pose, grid) : pose),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The grid is a document value, `kit:outputGrid` in `scene.doc`, so setting it
|
|
79
|
+
is an undoable edit, and the stored values re-snap inside that same edit
|
|
80
|
+
(2026-11-03). `stored` runs on every committed write, whatever made it (a
|
|
81
|
+
resize, typed W/H, align's match size, a paste, a data edit such as a stroke
|
|
82
|
+
width), on a document as it is built or loaded, and on the live preview of a
|
|
83
|
+
resize, a move or an insert, so a release lands where its preview showed. It answers data as well as a pose, since a stroke 1.5 dots wide
|
|
84
|
+
prints as ragged as a barcode module (2026-11-03). Which values are stored
|
|
85
|
+
and which only drawn is the rule's choice, per field (2026-11-03): a size that
|
|
86
|
+
must still read true after the grid changes belongs in `stored`, so W and H
|
|
87
|
+
show what prints; a position that moves with the grid's origin belongs in
|
|
88
|
+
`drawn`.
|
|
89
|
+
|
|
90
|
+
A barcode stores its module as a whole number of dots, and its width as that
|
|
91
|
+
module times its modules: every module then prints alike wherever the code
|
|
92
|
+
sits, and its W is the width it prints, which can be well off the width it was
|
|
93
|
+
asked for when the module rounds down. The site's dither demo does this.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Rect, OutputGrid, View } from '@weasel-js/geom';
|
|
2
|
+
import { TextureHandle, Effect } from '@weasel-js/renderer';
|
|
3
|
+
|
|
4
|
+
/** How gray becomes ink: a plain cutoff, an ordered 4×4 Bayer matrix, or one
|
|
5
|
+
* of two error-diffusion kernels (CPU only). */
|
|
6
|
+
type DitherMethod = 'threshold' | 'bayer' | 'floyd-steinberg' | 'atkinson';
|
|
7
|
+
/** 0–255 per channel. */
|
|
8
|
+
type Rgb = readonly [number, number, number];
|
|
9
|
+
/** 0–255 per channel, straight alpha. */
|
|
10
|
+
type Rgba = readonly [number, number, number, number];
|
|
11
|
+
/** Top-down, straight (non-premultiplied) RGBA, 4 bytes per pixel: what
|
|
12
|
+
* `renderSceneToPixels` returns, and what `ImageData` holds. */
|
|
13
|
+
interface RgbaImage {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
data: Uint8ClampedArray | Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
/** How `ditherPixels` quantizes an image. */
|
|
19
|
+
interface DitherOptions {
|
|
20
|
+
/** Default `'threshold'`. */
|
|
21
|
+
method?: DitherMethod;
|
|
22
|
+
/** Gray (0–255) below it is ink. Default 128. */
|
|
23
|
+
threshold?: number;
|
|
24
|
+
/** What transparency composites over before gray is taken. Default white. */
|
|
25
|
+
background?: Rgb;
|
|
26
|
+
/** One byte per pixel, nonzero = protected: quantized at plain `threshold`,
|
|
27
|
+
* and error diffusion neither enters nor leaves it. */
|
|
28
|
+
protect?: Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `image` quantized to ink: one byte per pixel, 1 = ink. */
|
|
32
|
+
declare function ditherPixels(image: RgbaImage, options?: DitherOptions): Uint8Array;
|
|
33
|
+
|
|
34
|
+
/** An ink mask as straight RGBA: `ink` where it is set, `blank` elsewhere. */
|
|
35
|
+
declare function maskToRgba(mask: Uint8Array, width: number, height: number, colors?: {
|
|
36
|
+
ink?: Rgba;
|
|
37
|
+
blank?: Rgba;
|
|
38
|
+
}): Uint8ClampedArray<ArrayBuffer>;
|
|
39
|
+
|
|
40
|
+
/** A per-pixel protect mask (1 = protected) from rects in pixel space. Rects
|
|
41
|
+
* round outward to whole pixels and clip to the image. */
|
|
42
|
+
declare function protectMask(rects: Iterable<Rect>, width: number, height: number): Uint8Array;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Rects to protect, in dot space (one unit per dot, from the grid's origin),
|
|
46
|
+
* the pixel space an export rendered at the grid's resolution has.
|
|
47
|
+
* `protectedRect` answers an item's rect in document units, which may be
|
|
48
|
+
* larger than the item (a barcode's quiet zone), or false or null to leave
|
|
49
|
+
* it to the dither.
|
|
50
|
+
*/
|
|
51
|
+
declare function protectRects<T>(items: Iterable<T>, protectedRect: (item: T) => Rect | false | null, grid: OutputGrid): Rect[];
|
|
52
|
+
|
|
53
|
+
/** A dot grid in device pixels of the drawing buffer, from its top left. */
|
|
54
|
+
interface DeviceGrid {
|
|
55
|
+
pitch: readonly [number, number];
|
|
56
|
+
origin: readonly [number, number];
|
|
57
|
+
}
|
|
58
|
+
/** `grid` where `view` at `dpr` puts it. In a layer group's effect thunk,
|
|
59
|
+
* `dpr` is `dims.dpr`; in a node's draw, `ctx.pixelScale / view.scale.x`.
|
|
60
|
+
* A flipped axis gives a negative pitch, which keeps dots numbered in
|
|
61
|
+
* document order: dot 0 still sits at the grid's document origin. */
|
|
62
|
+
declare function gridOnScreen(grid: OutputGrid, view: View, dpr: number): DeviceGrid;
|
|
63
|
+
|
|
64
|
+
/** How the `dither` effect quantizes what its group drew. */
|
|
65
|
+
interface GpuDitherOptions {
|
|
66
|
+
/** Default `'threshold'`. Error diffusion runs on the CPU only. */
|
|
67
|
+
method?: 'threshold' | 'bayer';
|
|
68
|
+
/** Gray (0–255) below it is ink. Default 128. */
|
|
69
|
+
threshold?: number;
|
|
70
|
+
/** What transparency composites over before gray is taken. Default white. */
|
|
71
|
+
background?: Rgb;
|
|
72
|
+
/** Default opaque black. */
|
|
73
|
+
ink?: Rgba;
|
|
74
|
+
/** Default transparent. */
|
|
75
|
+
blank?: Rgba;
|
|
76
|
+
/** `'screen'` (one dot per device pixel, the default), or an output grid
|
|
77
|
+
* placed with `gridOnScreen`. */
|
|
78
|
+
grid?: 'screen' | DeviceGrid;
|
|
79
|
+
/** A protect mask in dot space, from `protectTexture`. Needs an output grid. */
|
|
80
|
+
protect?: {
|
|
81
|
+
texture: TextureHandle;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Threshold or Bayer dithering as one effect pass. */
|
|
87
|
+
declare function dither(options?: GpuDitherOptions): Effect[];
|
|
88
|
+
|
|
89
|
+
/** A protect mask uploaded for `dither`, one texel per dot, with its size. */
|
|
90
|
+
interface ProtectTexture {
|
|
91
|
+
texture: TextureHandle;
|
|
92
|
+
width: number;
|
|
93
|
+
height: number;
|
|
94
|
+
/** Unregister the texture and free its bitmap. */
|
|
95
|
+
dispose(): void;
|
|
96
|
+
}
|
|
97
|
+
/** A protect mask (from `protectMask`, in dot space) as a texture `dither`
|
|
98
|
+
* can read. The caller disposes it when the mask changes or the view goes. */
|
|
99
|
+
declare function protectTexture(mask: Uint8Array, width: number, height: number): Promise<ProtectTexture>;
|
|
100
|
+
|
|
101
|
+
export { dither, ditherPixels, gridOnScreen, maskToRgba, protectMask, protectRects, protectTexture };
|
|
102
|
+
export type { DeviceGrid, DitherMethod, DitherOptions, GpuDitherOptions, ProtectTexture, Rgb, Rgba, RgbaImage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { registerEffect, registerTexture, unregisterTexture } from '@weasel-js/renderer';
|
|
2
|
+
|
|
3
|
+
// src/diffuse.ts
|
|
4
|
+
var FLOYD_STEINBERG = [[1, 0, 7], [-1, 1, 3], [0, 1, 5], [1, 1, 1]];
|
|
5
|
+
var ATKINSON = [[1, 0, 1], [2, 0, 1], [-1, 1, 1], [0, 1, 1], [1, 1, 1], [0, 2, 1]];
|
|
6
|
+
function diffuse(gray, width, height, threshold, kernel, denominator, protect) {
|
|
7
|
+
const mask = new Uint8Array(width * height);
|
|
8
|
+
for (let y = 0; y < height; y++) {
|
|
9
|
+
for (let x = 0; x < width; x++) {
|
|
10
|
+
const i = y * width + x;
|
|
11
|
+
const old = gray[i];
|
|
12
|
+
const ink = old < threshold;
|
|
13
|
+
mask[i] = ink ? 1 : 0;
|
|
14
|
+
if (protect?.[i]) continue;
|
|
15
|
+
const err = old - (ink ? 0 : 255);
|
|
16
|
+
for (const [dx, dy, w] of kernel) {
|
|
17
|
+
const nx = x + dx;
|
|
18
|
+
const ny = y + dy;
|
|
19
|
+
if (nx < 0 || nx >= width || ny >= height) continue;
|
|
20
|
+
const ni = ny * width + nx;
|
|
21
|
+
if (protect?.[ni]) continue;
|
|
22
|
+
gray[ni] = gray[ni] + err * w / denominator;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return mask;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/shared.ts
|
|
30
|
+
var BAYER_4X4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
|
|
31
|
+
var LUMA = [0.299, 0.587, 0.114];
|
|
32
|
+
var DEFAULT_THRESHOLD = 128;
|
|
33
|
+
var WHITE = [255, 255, 255];
|
|
34
|
+
function bayerCutoff(x, y, threshold) {
|
|
35
|
+
const m = BAYER_4X4[(y % 4 + 4) % 4 * 4 + (x % 4 + 4) % 4];
|
|
36
|
+
return (m + 0.5) * 16 + (threshold - DEFAULT_THRESHOLD);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/gray.ts
|
|
40
|
+
function toGray(image, background = WHITE) {
|
|
41
|
+
const n = image.width * image.height;
|
|
42
|
+
const gray = new Float32Array(n);
|
|
43
|
+
const d = image.data;
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
const a = d[i * 4 + 3] / 255;
|
|
46
|
+
const r = d[i * 4] * a + background[0] * (1 - a);
|
|
47
|
+
const g = d[i * 4 + 1] * a + background[1] * (1 - a);
|
|
48
|
+
const b = d[i * 4 + 2] * a + background[2] * (1 - a);
|
|
49
|
+
gray[i] = LUMA[0] * r + LUMA[1] * g + LUMA[2] * b;
|
|
50
|
+
}
|
|
51
|
+
return gray;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/ditherPixels.ts
|
|
55
|
+
function ditherPixels(image, options = {}) {
|
|
56
|
+
const { method = "threshold", threshold = DEFAULT_THRESHOLD, background = WHITE, protect } = options;
|
|
57
|
+
const { width, height } = image;
|
|
58
|
+
if (image.data.length !== width * height * 4) {
|
|
59
|
+
throw new RangeError(`ditherPixels: data holds ${image.data.length} bytes, a ${width}\xD7${height} image needs ${width * height * 4}`);
|
|
60
|
+
}
|
|
61
|
+
if (protect && protect.length !== width * height) {
|
|
62
|
+
throw new RangeError(`ditherPixels: protect holds ${protect.length} pixels, the image ${width * height}`);
|
|
63
|
+
}
|
|
64
|
+
const gray = toGray(image, background);
|
|
65
|
+
switch (method) {
|
|
66
|
+
case "threshold": {
|
|
67
|
+
const mask = new Uint8Array(width * height);
|
|
68
|
+
for (let i = 0; i < mask.length; i++) mask[i] = gray[i] < threshold ? 1 : 0;
|
|
69
|
+
return mask;
|
|
70
|
+
}
|
|
71
|
+
case "bayer": {
|
|
72
|
+
const mask = new Uint8Array(width * height);
|
|
73
|
+
for (let y = 0; y < height; y++) {
|
|
74
|
+
for (let x = 0; x < width; x++) {
|
|
75
|
+
const i = y * width + x;
|
|
76
|
+
const cutoff = protect?.[i] ? threshold : bayerCutoff(x, y, threshold);
|
|
77
|
+
mask[i] = gray[i] < cutoff ? 1 : 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return mask;
|
|
81
|
+
}
|
|
82
|
+
case "floyd-steinberg":
|
|
83
|
+
return diffuse(gray, width, height, threshold, FLOYD_STEINBERG, 16, protect ?? null);
|
|
84
|
+
case "atkinson":
|
|
85
|
+
return diffuse(gray, width, height, threshold, ATKINSON, 8, protect ?? null);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/maskToRgba.ts
|
|
90
|
+
var INK = [0, 0, 0, 255];
|
|
91
|
+
var BLANK = [0, 0, 0, 0];
|
|
92
|
+
function maskToRgba(mask, width, height, colors = {}) {
|
|
93
|
+
if (mask.length !== width * height) {
|
|
94
|
+
throw new RangeError(`maskToRgba: mask holds ${mask.length} pixels, ${width}\xD7${height} needs ${width * height}`);
|
|
95
|
+
}
|
|
96
|
+
const ink = colors.ink ?? INK;
|
|
97
|
+
const blank = colors.blank ?? BLANK;
|
|
98
|
+
const out = new Uint8ClampedArray(width * height * 4);
|
|
99
|
+
for (let i = 0; i < width * height; i++) out.set(mask[i] ? ink : blank, i * 4);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/protectMask.ts
|
|
104
|
+
function protectMask(rects, width, height) {
|
|
105
|
+
const mask = new Uint8Array(width * height);
|
|
106
|
+
for (const r of rects) {
|
|
107
|
+
const x0 = Math.max(0, Math.floor(r.x));
|
|
108
|
+
const y0 = Math.max(0, Math.floor(r.y));
|
|
109
|
+
const x1 = Math.min(width, Math.ceil(r.x + r.width));
|
|
110
|
+
const y1 = Math.min(height, Math.ceil(r.y + r.height));
|
|
111
|
+
for (let y = y0; y < y1; y++) mask.fill(1, y * width + x0, y * width + Math.max(x0, x1));
|
|
112
|
+
}
|
|
113
|
+
return mask;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/protectRects.ts
|
|
117
|
+
function protectRects(items, protectedRect, grid) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
const r = protectedRect(item);
|
|
121
|
+
if (!r) continue;
|
|
122
|
+
out.push({
|
|
123
|
+
x: (r.x - grid.origin.x) / grid.pitch,
|
|
124
|
+
y: (r.y - grid.origin.y) / grid.pitch,
|
|
125
|
+
width: r.width / grid.pitch,
|
|
126
|
+
height: r.height / grid.pitch
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/gpu/frag.ts
|
|
133
|
+
var list = (xs) => xs.map((x) => x.toFixed(4)).join(", ");
|
|
134
|
+
var DITHER_FRAG = (
|
|
135
|
+
/* glsl */
|
|
136
|
+
`#version 300 es
|
|
137
|
+
precision highp float;
|
|
138
|
+
in vec2 v_uv;
|
|
139
|
+
uniform sampler2D u_source;
|
|
140
|
+
uniform vec2 u_resolution;
|
|
141
|
+
uniform vec2 u_windowOrigin;
|
|
142
|
+
uniform float u_method; // 0 threshold, 1 bayer
|
|
143
|
+
uniform float u_threshold; // 0-255
|
|
144
|
+
uniform vec3 u_background; // 0-1
|
|
145
|
+
uniform vec4 u_ink; // premultiplied 0-1
|
|
146
|
+
uniform vec4 u_blank; // premultiplied 0-1
|
|
147
|
+
uniform vec2 u_pitch; // device px per dot
|
|
148
|
+
uniform vec2 u_gridOrigin; // device px, top-down
|
|
149
|
+
uniform sampler2D u_protect; // one texel per dot, red > 0.5 = protected
|
|
150
|
+
uniform vec2 u_protectSize; // dots; 0 = none
|
|
151
|
+
out vec4 outColor;
|
|
152
|
+
|
|
153
|
+
const float BAYER[16] = float[16](${list(BAYER_4X4)});
|
|
154
|
+
const vec3 LUMA = vec3(${list(LUMA)});
|
|
155
|
+
|
|
156
|
+
float grayOf(vec4 p) {
|
|
157
|
+
return dot(p.rgb + u_background * (1.0 - p.a), LUMA) * 255.0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
vec2 uvOf(vec2 devicePx) {
|
|
161
|
+
vec2 local = (devicePx - u_windowOrigin) / u_resolution;
|
|
162
|
+
return vec2(local.x, 1.0 - local.y);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
void main() {
|
|
166
|
+
vec2 px = u_windowOrigin + vec2(v_uv.x, 1.0 - v_uv.y) * u_resolution;
|
|
167
|
+
vec2 dotIndex = floor((px - u_gridOrigin) / u_pitch);
|
|
168
|
+
vec2 size = abs(u_pitch);
|
|
169
|
+
bool fine = size.x < 1.0 || size.y < 1.0;
|
|
170
|
+
|
|
171
|
+
float gray;
|
|
172
|
+
if (fine) {
|
|
173
|
+
gray = grayOf(texture(u_source, v_uv));
|
|
174
|
+
} else {
|
|
175
|
+
// A negative pitch (a flipped axis) puts start on the dot's far edge and
|
|
176
|
+
// steps the taps back across it.
|
|
177
|
+
vec2 start = u_gridOrigin + dotIndex * u_pitch;
|
|
178
|
+
vec2 taps = clamp(ceil(size), vec2(1.0), vec2(4.0));
|
|
179
|
+
vec4 sum = vec4(0.0);
|
|
180
|
+
for (int j = 0; j < 4; j++) {
|
|
181
|
+
if (float(j) >= taps.y) break;
|
|
182
|
+
for (int i = 0; i < 4; i++) {
|
|
183
|
+
if (float(i) >= taps.x) break;
|
|
184
|
+
vec2 uv = uvOf(start + (vec2(float(i), float(j)) + 0.5) * u_pitch / taps);
|
|
185
|
+
// Past the buffer nothing was drawn: the tap is transparent.
|
|
186
|
+
if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) continue;
|
|
187
|
+
sum += texture(u_source, uv);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
gray = grayOf(sum / (taps.x * taps.y));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
bool protectedDot = u_protectSize.x > 0.0
|
|
194
|
+
&& all(greaterThanEqual(dotIndex, vec2(0.0))) && all(lessThan(dotIndex, u_protectSize))
|
|
195
|
+
&& texelFetch(u_protect, ivec2(dotIndex), 0).r > 0.5;
|
|
196
|
+
|
|
197
|
+
float cutoff = u_threshold;
|
|
198
|
+
if (u_method > 0.5 && !protectedDot) {
|
|
199
|
+
vec2 cell = mod(fine ? floor(px) : dotIndex, 4.0);
|
|
200
|
+
cutoff = (BAYER[int(cell.y) * 4 + int(cell.x)] + 0.5) * 16.0 + (u_threshold - 128.0);
|
|
201
|
+
}
|
|
202
|
+
outColor = gray < cutoff ? u_ink : u_blank;
|
|
203
|
+
}
|
|
204
|
+
`
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// src/gpu/dither.ts
|
|
208
|
+
var PROGRAM = registerEffect("weasel:dither", DITHER_FRAG);
|
|
209
|
+
var INK2 = [0, 0, 0, 255];
|
|
210
|
+
var BLANK2 = [0, 0, 0, 0];
|
|
211
|
+
var SCREEN = { pitch: [1, 1], origin: [0, 0] };
|
|
212
|
+
var premultiplied = ([r, g, b, a]) => {
|
|
213
|
+
const k = a / 255;
|
|
214
|
+
return [r / 255 * k, g / 255 * k, b / 255 * k, k];
|
|
215
|
+
};
|
|
216
|
+
function dither(options = {}) {
|
|
217
|
+
const method = options.method ?? "threshold";
|
|
218
|
+
if (method !== "threshold" && method !== "bayer") {
|
|
219
|
+
throw new Error(`dither: '${String(method)}' is error diffusion, which runs on the CPU only (ditherPixels)`);
|
|
220
|
+
}
|
|
221
|
+
const grid = options.grid === void 0 || options.grid === "screen" ? SCREEN : options.grid;
|
|
222
|
+
if (options.protect && grid === SCREEN) {
|
|
223
|
+
throw new Error("dither: a protect mask is in dots, so it needs an output grid, not the screen");
|
|
224
|
+
}
|
|
225
|
+
const background = options.background ?? WHITE;
|
|
226
|
+
const reach = grid === SCREEN ? 0 : Math.ceil(Math.max(Math.abs(grid.pitch[0]), Math.abs(grid.pitch[1])));
|
|
227
|
+
return [{
|
|
228
|
+
program: PROGRAM,
|
|
229
|
+
reach,
|
|
230
|
+
uniforms: {
|
|
231
|
+
u_method: method === "bayer" ? 1 : 0,
|
|
232
|
+
u_threshold: options.threshold ?? DEFAULT_THRESHOLD,
|
|
233
|
+
u_background: [background[0] / 255, background[1] / 255, background[2] / 255],
|
|
234
|
+
u_ink: premultiplied(options.ink ?? INK2),
|
|
235
|
+
u_blank: premultiplied(options.blank ?? BLANK2),
|
|
236
|
+
u_pitch: [grid.pitch[0], grid.pitch[1]],
|
|
237
|
+
u_gridOrigin: [grid.origin[0], grid.origin[1]],
|
|
238
|
+
...options.protect ? { u_protect: options.protect.texture, u_protectSize: [options.protect.width, options.protect.height] } : { u_protectSize: [0, 0] }
|
|
239
|
+
}
|
|
240
|
+
}];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/gpu/gridOnScreen.ts
|
|
244
|
+
function gridOnScreen(grid, view, dpr) {
|
|
245
|
+
const sx = view.scale.x * dpr;
|
|
246
|
+
const sy = view.scale.y * dpr;
|
|
247
|
+
return {
|
|
248
|
+
pitch: [grid.pitch * sx, grid.pitch * sy],
|
|
249
|
+
origin: [(grid.origin.x - view.x) * sx, (grid.origin.y - view.y) * sy]
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async function protectTexture(mask, width, height) {
|
|
253
|
+
if (mask.length !== width * height) {
|
|
254
|
+
throw new RangeError(`protectTexture: mask holds ${mask.length} dots, ${width}\xD7${height} needs ${width * height}`);
|
|
255
|
+
}
|
|
256
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
257
|
+
for (let i = 0; i < width * height; i++) {
|
|
258
|
+
const v = mask[i] ? 255 : 0;
|
|
259
|
+
data[i * 4] = v;
|
|
260
|
+
data[i * 4 + 1] = v;
|
|
261
|
+
data[i * 4 + 2] = v;
|
|
262
|
+
data[i * 4 + 3] = 255;
|
|
263
|
+
}
|
|
264
|
+
const bitmap = await createImageBitmap(new ImageData(data, width, height));
|
|
265
|
+
const texture = registerTexture(bitmap);
|
|
266
|
+
return {
|
|
267
|
+
texture,
|
|
268
|
+
width,
|
|
269
|
+
height,
|
|
270
|
+
dispose() {
|
|
271
|
+
unregisterTexture(texture);
|
|
272
|
+
bitmap.close();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { dither, ditherPixels, gridOnScreen, maskToRgba, protectMask, protectRects, protectTexture };
|
|
278
|
+
//# sourceMappingURL=index.js.map
|
|
279
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/diffuse.ts","../src/shared.ts","../src/gray.ts","../src/ditherPixels.ts","../src/maskToRgba.ts","../src/protectMask.ts","../src/protectRects.ts","../src/gpu/frag.ts","../src/gpu/dither.ts","../src/gpu/gridOnScreen.ts","../src/gpu/protectTexture.ts"],"names":["INK","BLANK"],"mappings":"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","file":"index.js","sourcesContent":["/** Where a pixel's quantization error goes: offset and weight numerator. */\nexport type Kernel = readonly (readonly [dx: number, dy: number, weight: number])[];\n\nexport const FLOYD_STEINBERG: Kernel = [[1, 0, 7], [-1, 1, 3], [0, 1, 5], [1, 1, 1]];\n\n/** Spreads 6/8 of the error, which keeps highlights and shadows clean. */\nexport const ATKINSON: Kernel = [[1, 0, 1], [2, 0, 1], [-1, 1, 1], [0, 1, 1], [1, 1, 1], [0, 2, 1]];\n\n/** Error diffusion over `gray`, which it consumes. A protected pixel quantizes\n * at `threshold` against a value nothing diffused into, and sends no error. */\nexport function diffuse(\n gray: Float32Array,\n width: number,\n height: number,\n threshold: number,\n kernel: Kernel,\n denominator: number,\n protect: Uint8Array | null,\n): Uint8Array {\n const mask = new Uint8Array(width * height);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const i = y * width + x;\n const old = gray[i]!;\n const ink = old < threshold;\n mask[i] = ink ? 1 : 0;\n if (protect?.[i]) continue;\n const err = old - (ink ? 0 : 255);\n for (const [dx, dy, w] of kernel) {\n const nx = x + dx;\n const ny = y + dy;\n if (nx < 0 || nx >= width || ny >= height) continue;\n const ni = ny * width + nx;\n if (protect?.[ni]) continue;\n gray[ni] = gray[ni]! + (err * w) / denominator;\n }\n }\n }\n return mask;\n}\n","import type { Rgb } from './types';\n\n/** The classic 4×4 Bayer index matrix, row-major. The GPU shader embeds this\n * same array, so the two paths cannot drift. */\nexport const BAYER_4X4: readonly number[] = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];\n\n/** Rec. 601 luma weights. */\nexport const LUMA: readonly [number, number, number] = [0.299, 0.587, 0.114];\n\nexport const DEFAULT_THRESHOLD = 128;\n\nexport const WHITE: Rgb = [255, 255, 255];\n\n/** The gray an ordered dither compares against at dot (x, y): below it is ink. */\nexport function bayerCutoff(x: number, y: number, threshold: number): number {\n const m = BAYER_4X4[(((y % 4) + 4) % 4) * 4 + (((x % 4) + 4) % 4)]!;\n return (m + 0.5) * 16 + (threshold - DEFAULT_THRESHOLD);\n}\n","import { LUMA, WHITE } from './shared';\nimport type { Rgb, RgbaImage } from './types';\n\n/** Each pixel's gray, 0–255, after compositing it over `background`. */\nexport function toGray(image: RgbaImage, background: Rgb = WHITE): Float32Array {\n const n = image.width * image.height;\n const gray = new Float32Array(n);\n const d = image.data;\n for (let i = 0; i < n; i++) {\n const a = d[i * 4 + 3]! / 255;\n const r = d[i * 4]! * a + background[0] * (1 - a);\n const g = d[i * 4 + 1]! * a + background[1] * (1 - a);\n const b = d[i * 4 + 2]! * a + background[2] * (1 - a);\n gray[i] = LUMA[0] * r + LUMA[1] * g + LUMA[2] * b;\n }\n return gray;\n}\n","import { ATKINSON, FLOYD_STEINBERG, diffuse } from './diffuse';\nimport { toGray } from './gray';\nimport { DEFAULT_THRESHOLD, WHITE, bayerCutoff } from './shared';\nimport type { DitherOptions, RgbaImage } from './types';\n\n/** `image` quantized to ink: one byte per pixel, 1 = ink. */\nexport function ditherPixels(image: RgbaImage, options: DitherOptions = {}): Uint8Array {\n const { method = 'threshold', threshold = DEFAULT_THRESHOLD, background = WHITE, protect } = options;\n const { width, height } = image;\n if (image.data.length !== width * height * 4) {\n throw new RangeError(`ditherPixels: data holds ${image.data.length} bytes, a ${width}×${height} image needs ${width * height * 4}`);\n }\n if (protect && protect.length !== width * height) {\n throw new RangeError(`ditherPixels: protect holds ${protect.length} pixels, the image ${width * height}`);\n }\n const gray = toGray(image, background);\n switch (method) {\n case 'threshold': {\n const mask = new Uint8Array(width * height);\n for (let i = 0; i < mask.length; i++) mask[i] = gray[i]! < threshold ? 1 : 0;\n return mask;\n }\n case 'bayer': {\n const mask = new Uint8Array(width * height);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const i = y * width + x;\n const cutoff = protect?.[i] ? threshold : bayerCutoff(x, y, threshold);\n mask[i] = gray[i]! < cutoff ? 1 : 0;\n }\n }\n return mask;\n }\n case 'floyd-steinberg':\n return diffuse(gray, width, height, threshold, FLOYD_STEINBERG, 16, protect ?? null);\n case 'atkinson':\n return diffuse(gray, width, height, threshold, ATKINSON, 8, protect ?? null);\n }\n}\n","import type { Rgba } from './types';\n\nconst INK: Rgba = [0, 0, 0, 255];\nconst BLANK: Rgba = [0, 0, 0, 0];\n\n/** An ink mask as straight RGBA: `ink` where it is set, `blank` elsewhere. */\nexport function maskToRgba(\n mask: Uint8Array,\n width: number,\n height: number,\n colors: { ink?: Rgba; blank?: Rgba } = {},\n): Uint8ClampedArray<ArrayBuffer> {\n if (mask.length !== width * height) {\n throw new RangeError(`maskToRgba: mask holds ${mask.length} pixels, ${width}×${height} needs ${width * height}`);\n }\n const ink = colors.ink ?? INK;\n const blank = colors.blank ?? BLANK;\n const out = new Uint8ClampedArray(width * height * 4);\n for (let i = 0; i < width * height; i++) out.set(mask[i] ? ink : blank, i * 4);\n return out;\n}\n","import type { Rect } from '@weasel-js/geom';\n\n/** A per-pixel protect mask (1 = protected) from rects in pixel space. Rects\n * round outward to whole pixels and clip to the image. */\nexport function protectMask(rects: Iterable<Rect>, width: number, height: number): Uint8Array {\n const mask = new Uint8Array(width * height);\n for (const r of rects) {\n const x0 = Math.max(0, Math.floor(r.x));\n const y0 = Math.max(0, Math.floor(r.y));\n const x1 = Math.min(width, Math.ceil(r.x + r.width));\n const y1 = Math.min(height, Math.ceil(r.y + r.height));\n for (let y = y0; y < y1; y++) mask.fill(1, y * width + x0, y * width + Math.max(x0, x1));\n }\n return mask;\n}\n","import type { OutputGrid, Rect } from '@weasel-js/geom';\n\n/**\n * Rects to protect, in dot space (one unit per dot, from the grid's origin),\n * the pixel space an export rendered at the grid's resolution has.\n * `protectedRect` answers an item's rect in document units, which may be\n * larger than the item (a barcode's quiet zone), or false or null to leave\n * it to the dither.\n */\nexport function protectRects<T>(\n items: Iterable<T>,\n protectedRect: (item: T) => Rect | false | null,\n grid: OutputGrid,\n): Rect[] {\n const out: Rect[] = [];\n for (const item of items) {\n const r = protectedRect(item);\n if (!r) continue;\n out.push({\n x: (r.x - grid.origin.x) / grid.pitch,\n y: (r.y - grid.origin.y) / grid.pitch,\n width: r.width / grid.pitch,\n height: r.height / grid.pitch,\n });\n }\n return out;\n}\n","import { BAYER_4X4, LUMA } from '../shared';\n\nconst list = (xs: readonly number[]) => xs.map((x) => x.toFixed(4)).join(', ');\n\n/**\n * One fragment per device pixel. On an output grid, a dot wider than a pixel\n * takes the mean of its footprint on at most 4×4 taps and quantizes once, so\n * every pixel of the dot agrees; a dot narrower than a pixel cannot be shown,\n * and the pixel quantizes alone, on the Bayer cell of its own device pixel.\n */\nexport const DITHER_FRAG = /* glsl */ `#version 300 es\nprecision highp float;\nin vec2 v_uv;\nuniform sampler2D u_source;\nuniform vec2 u_resolution;\nuniform vec2 u_windowOrigin;\nuniform float u_method; // 0 threshold, 1 bayer\nuniform float u_threshold; // 0-255\nuniform vec3 u_background; // 0-1\nuniform vec4 u_ink; // premultiplied 0-1\nuniform vec4 u_blank; // premultiplied 0-1\nuniform vec2 u_pitch; // device px per dot\nuniform vec2 u_gridOrigin; // device px, top-down\nuniform sampler2D u_protect; // one texel per dot, red > 0.5 = protected\nuniform vec2 u_protectSize; // dots; 0 = none\nout vec4 outColor;\n\nconst float BAYER[16] = float[16](${list(BAYER_4X4)});\nconst vec3 LUMA = vec3(${list(LUMA)});\n\nfloat grayOf(vec4 p) {\n return dot(p.rgb + u_background * (1.0 - p.a), LUMA) * 255.0;\n}\n\nvec2 uvOf(vec2 devicePx) {\n vec2 local = (devicePx - u_windowOrigin) / u_resolution;\n return vec2(local.x, 1.0 - local.y);\n}\n\nvoid main() {\n vec2 px = u_windowOrigin + vec2(v_uv.x, 1.0 - v_uv.y) * u_resolution;\n vec2 dotIndex = floor((px - u_gridOrigin) / u_pitch);\n vec2 size = abs(u_pitch);\n bool fine = size.x < 1.0 || size.y < 1.0;\n\n float gray;\n if (fine) {\n gray = grayOf(texture(u_source, v_uv));\n } else {\n // A negative pitch (a flipped axis) puts start on the dot's far edge and\n // steps the taps back across it.\n vec2 start = u_gridOrigin + dotIndex * u_pitch;\n vec2 taps = clamp(ceil(size), vec2(1.0), vec2(4.0));\n vec4 sum = vec4(0.0);\n for (int j = 0; j < 4; j++) {\n if (float(j) >= taps.y) break;\n for (int i = 0; i < 4; i++) {\n if (float(i) >= taps.x) break;\n vec2 uv = uvOf(start + (vec2(float(i), float(j)) + 0.5) * u_pitch / taps);\n // Past the buffer nothing was drawn: the tap is transparent.\n if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) continue;\n sum += texture(u_source, uv);\n }\n }\n gray = grayOf(sum / (taps.x * taps.y));\n }\n\n bool protectedDot = u_protectSize.x > 0.0\n && all(greaterThanEqual(dotIndex, vec2(0.0))) && all(lessThan(dotIndex, u_protectSize))\n && texelFetch(u_protect, ivec2(dotIndex), 0).r > 0.5;\n\n float cutoff = u_threshold;\n if (u_method > 0.5 && !protectedDot) {\n vec2 cell = mod(fine ? floor(px) : dotIndex, 4.0);\n cutoff = (BAYER[int(cell.y) * 4 + int(cell.x)] + 0.5) * 16.0 + (u_threshold - 128.0);\n }\n outColor = gray < cutoff ? u_ink : u_blank;\n}\n`;\n","import { registerEffect, type Effect, type TextureHandle } from '@weasel-js/renderer';\nimport { DEFAULT_THRESHOLD, WHITE } from '../shared';\nimport type { Rgb, Rgba } from '../types';\nimport { DITHER_FRAG } from './frag';\nimport type { DeviceGrid } from './gridOnScreen';\n\nconst PROGRAM = registerEffect('weasel:dither', DITHER_FRAG);\n\nconst INK: Rgba = [0, 0, 0, 255];\nconst BLANK: Rgba = [0, 0, 0, 0];\nconst SCREEN: DeviceGrid = { pitch: [1, 1], origin: [0, 0] };\n\n/** How the `dither` effect quantizes what its group drew. */\nexport interface GpuDitherOptions {\n /** Default `'threshold'`. Error diffusion runs on the CPU only. */\n method?: 'threshold' | 'bayer';\n /** Gray (0–255) below it is ink. Default 128. */\n threshold?: number;\n /** What transparency composites over before gray is taken. Default white. */\n background?: Rgb;\n /** Default opaque black. */\n ink?: Rgba;\n /** Default transparent. */\n blank?: Rgba;\n /** `'screen'` (one dot per device pixel, the default), or an output grid\n * placed with `gridOnScreen`. */\n grid?: 'screen' | DeviceGrid;\n /** A protect mask in dot space, from `protectTexture`. Needs an output grid. */\n protect?: { texture: TextureHandle; width: number; height: number };\n}\n\nconst premultiplied = ([r, g, b, a]: Rgba): number[] => {\n const k = a / 255;\n return [(r / 255) * k, (g / 255) * k, (b / 255) * k, k];\n};\n\n/** Threshold or Bayer dithering as one effect pass. */\nexport function dither(options: GpuDitherOptions = {}): Effect[] {\n const method = options.method ?? 'threshold';\n if (method !== 'threshold' && method !== 'bayer') {\n throw new Error(`dither: '${String(method)}' is error diffusion, which runs on the CPU only (ditherPixels)`);\n }\n const grid = options.grid === undefined || options.grid === 'screen' ? SCREEN : options.grid;\n if (options.protect && grid === SCREEN) {\n throw new Error('dither: a protect mask is in dots, so it needs an output grid, not the screen');\n }\n const background = options.background ?? WHITE;\n const reach = grid === SCREEN ? 0 : Math.ceil(Math.max(Math.abs(grid.pitch[0]), Math.abs(grid.pitch[1])));\n return [{\n program: PROGRAM,\n reach,\n uniforms: {\n u_method: method === 'bayer' ? 1 : 0,\n u_threshold: options.threshold ?? DEFAULT_THRESHOLD,\n u_background: [background[0] / 255, background[1] / 255, background[2] / 255],\n u_ink: premultiplied(options.ink ?? INK),\n u_blank: premultiplied(options.blank ?? BLANK),\n u_pitch: [grid.pitch[0], grid.pitch[1]],\n u_gridOrigin: [grid.origin[0], grid.origin[1]],\n ...(options.protect\n ? { u_protect: options.protect.texture, u_protectSize: [options.protect.width, options.protect.height] }\n : { u_protectSize: [0, 0] }),\n },\n }];\n}\n","import type { OutputGrid, View } from '@weasel-js/geom';\n\n/** A dot grid in device pixels of the drawing buffer, from its top left. */\nexport interface DeviceGrid {\n pitch: readonly [number, number];\n origin: readonly [number, number];\n}\n\n/** `grid` where `view` at `dpr` puts it. In a layer group's effect thunk,\n * `dpr` is `dims.dpr`; in a node's draw, `ctx.pixelScale / view.scale.x`.\n * A flipped axis gives a negative pitch, which keeps dots numbered in\n * document order: dot 0 still sits at the grid's document origin. */\nexport function gridOnScreen(grid: OutputGrid, view: View, dpr: number): DeviceGrid {\n const sx = view.scale.x * dpr;\n const sy = view.scale.y * dpr;\n return {\n pitch: [grid.pitch * sx, grid.pitch * sy],\n origin: [(grid.origin.x - view.x) * sx, (grid.origin.y - view.y) * sy],\n };\n}\n","import { registerTexture, unregisterTexture, type TextureHandle } from '@weasel-js/renderer';\n\n/** A protect mask uploaded for `dither`, one texel per dot, with its size. */\nexport interface ProtectTexture {\n texture: TextureHandle;\n width: number;\n height: number;\n /** Unregister the texture and free its bitmap. */\n dispose(): void;\n}\n\n/** A protect mask (from `protectMask`, in dot space) as a texture `dither`\n * can read. The caller disposes it when the mask changes or the view goes. */\nexport async function protectTexture(mask: Uint8Array, width: number, height: number): Promise<ProtectTexture> {\n if (mask.length !== width * height) {\n throw new RangeError(`protectTexture: mask holds ${mask.length} dots, ${width}×${height} needs ${width * height}`);\n }\n const data = new Uint8ClampedArray(width * height * 4);\n for (let i = 0; i < width * height; i++) {\n const v = mask[i] ? 255 : 0;\n data[i * 4] = v;\n data[i * 4 + 1] = v;\n data[i * 4 + 2] = v;\n data[i * 4 + 3] = 255;\n }\n const bitmap = await createImageBitmap(new ImageData(data, width, height));\n const texture = registerTexture(bitmap);\n return {\n texture, width, height,\n dispose() {\n unregisterTexture(texture);\n bitmap.close();\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/dither",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.16.2",
|
|
4
|
+
"description": "Dithering to 1-bit ink: four methods on the CPU for exported pixels, and threshold and Bayer as a live renderer effect on the screen or an output target's dot grid, with regions kept at plain threshold.",
|
|
5
|
+
"weasel": {
|
|
6
|
+
"tier": "plugin",
|
|
7
|
+
"needs": ["webgl2"]
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@weasel-js/geom": "1.16.2",
|
|
24
|
+
"@weasel-js/renderer": "1.16.2"
|
|
25
|
+
},
|
|
26
|
+
"author": "orochi235",
|
|
27
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
31
|
+
"directory": "packages/dither"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
50
|
+
}
|
|
51
|
+
}
|