sweetcorn 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +9 -6
- package/dist/index.js +15 -54
- package/dist/processors.d.ts +10 -0
- package/dist/processors.js +44 -0
- package/dist/threshold-maps.json +100 -0
- package/dist/types.d.ts +37 -1
- package/package.json +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Sharp } from 'sharp';
|
|
2
|
-
import type {
|
|
3
|
-
export type { DitheringAlgorithm };
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import type { SweetcornOptions } from './types.js';
|
|
3
|
+
export type { DitheringAlgorithm } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create a dithered image — turn smooth pixels into crunchy kernels! 🌽
|
|
6
|
+
* @param image A Sharp image instance, e.g. created using `sharp('my-file.png')`.
|
|
7
|
+
* @param options Options configuring Sweetcorn.
|
|
8
|
+
* @returns A new Sharp image instance.
|
|
9
|
+
*/
|
|
10
|
+
export default function sweetcorn(image: Sharp, options: SweetcornOptions): Promise<Sharp>;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { applyDiffusionKernel, applyThresholdMap } from './processors.js';
|
|
2
|
+
import diffusionKernels from './diffusion-kernels.js';
|
|
1
3
|
import thresholdMaps from './threshold-maps.json' with { type: 'json' };
|
|
2
|
-
import diffusionKernels from './diffusion-kernels';
|
|
3
4
|
let sharp;
|
|
4
5
|
async function loadSharp() {
|
|
5
6
|
let sharpImport;
|
|
@@ -13,7 +14,14 @@ async function loadSharp() {
|
|
|
13
14
|
sharpImport.cache(false);
|
|
14
15
|
return sharpImport;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Create a dithered image — turn smooth pixels into crunchy kernels! 🌽
|
|
19
|
+
* @param image A Sharp image instance, e.g. created using `sharp('my-file.png')`.
|
|
20
|
+
* @param options Options configuring Sweetcorn.
|
|
21
|
+
* @returns A new Sharp image instance.
|
|
22
|
+
*/
|
|
23
|
+
export default async function sweetcorn(image, options) {
|
|
24
|
+
const { algorithm } = options;
|
|
17
25
|
if (!sharp)
|
|
18
26
|
sharp = await loadSharp();
|
|
19
27
|
// Convert image to greyscale before dithering.
|
|
@@ -21,13 +29,13 @@ export default async function sweetcorn(image, { algorithm }) {
|
|
|
21
29
|
image.gamma(2.2, 1).greyscale();
|
|
22
30
|
// Get raw pixel data for this image.
|
|
23
31
|
const rawPixels = await image.raw().toBuffer({ resolveWithObject: true });
|
|
24
|
-
const thresholdMap = thresholdMaps[algorithm];
|
|
25
|
-
const diffusionKernel = diffusionKernels[algorithm];
|
|
32
|
+
const thresholdMap = options.thresholdMap || thresholdMaps[algorithm];
|
|
33
|
+
const diffusionKernel = options.diffusionKernel || diffusionKernels[algorithm];
|
|
26
34
|
if (thresholdMap) {
|
|
27
|
-
applyThresholdMap(rawPixels, thresholdMap);
|
|
35
|
+
applyThresholdMap(rawPixels.data, rawPixels.info.width, thresholdMap);
|
|
28
36
|
}
|
|
29
37
|
else if (diffusionKernel) {
|
|
30
|
-
applyDiffusionKernel(rawPixels, diffusionKernel);
|
|
38
|
+
applyDiffusionKernel(rawPixels.data, rawPixels.info.width, rawPixels.info.height, diffusionKernel);
|
|
31
39
|
}
|
|
32
40
|
else if (algorithm === 'white-noise') {
|
|
33
41
|
// White noise dithering (pretty rough and ugly)
|
|
@@ -43,54 +51,7 @@ export default async function sweetcorn(image, { algorithm }) {
|
|
|
43
51
|
rawPixels.data[index] = pixelValue < 128 ? 0 : 255;
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
|
-
//
|
|
47
|
-
// predictably to different compression methods. PNG and lossless WebP outperform lossy
|
|
48
|
-
// formats for this type of image, with lossless WebP producing slightly smaller images, so we
|
|
49
|
-
// use that here.
|
|
54
|
+
// Convert raw pixel data back into a Sharp image.
|
|
50
55
|
const outputImage = sharp(rawPixels.data, { raw: rawPixels.info });
|
|
51
56
|
return outputImage;
|
|
52
57
|
}
|
|
53
|
-
function applyDiffusionKernel(rawPixels, kernel) {
|
|
54
|
-
const kernelWidth = kernel[0].length;
|
|
55
|
-
const kernelHeight = kernel.length;
|
|
56
|
-
const kernelRadius = Math.floor((kernelWidth - 1) / 2);
|
|
57
|
-
for (let index = 0; index < rawPixels.data.length; index++) {
|
|
58
|
-
const original = rawPixels.data[index];
|
|
59
|
-
const quantized = original < 128 ? 0 : 255;
|
|
60
|
-
rawPixels.data[index] = quantized;
|
|
61
|
-
const error = original - quantized;
|
|
62
|
-
const [x, y] = [index % rawPixels.info.width, Math.floor(index / rawPixels.info.width)];
|
|
63
|
-
const width = rawPixels.info.width;
|
|
64
|
-
const height = rawPixels.info.height;
|
|
65
|
-
for (let diffX = 0; diffX < kernelWidth; diffX++) {
|
|
66
|
-
for (let diffY = 0; diffY < kernelHeight; diffY++) {
|
|
67
|
-
const diffusionWeight = kernel[diffY][diffX];
|
|
68
|
-
if (diffusionWeight === 0)
|
|
69
|
-
continue;
|
|
70
|
-
const offsetX = diffX - kernelRadius;
|
|
71
|
-
const offsetY = diffY;
|
|
72
|
-
const neighborX = x + offsetX;
|
|
73
|
-
const neighborY = y + offsetY;
|
|
74
|
-
// Ensure we don't go out of bounds
|
|
75
|
-
if (neighborX >= 0 && neighborY >= 0 && neighborX < width && neighborY < height) {
|
|
76
|
-
const neighborIndex = neighborY * width + neighborX;
|
|
77
|
-
rawPixels.data[neighborIndex] = clamp(rawPixels.data[neighborIndex] + error * diffusionWeight);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function clamp(value, min = 0, max = 255) {
|
|
84
|
-
return Math.min(Math.max(value, min), max);
|
|
85
|
-
}
|
|
86
|
-
/** Applies a threshold map to the raw pixel data for ordered dithering. */
|
|
87
|
-
function applyThresholdMap(rawPixels, thresholdMap) {
|
|
88
|
-
const mapWidth = thresholdMap[0].length;
|
|
89
|
-
const mapHeight = thresholdMap.length;
|
|
90
|
-
for (let index = 0; index < rawPixels.data.length; index++) {
|
|
91
|
-
const pixelValue = rawPixels.data[index];
|
|
92
|
-
const [x, y] = [index % rawPixels.info.width, Math.floor(index / rawPixels.info.width)];
|
|
93
|
-
const threshold = thresholdMap[y % mapHeight][x % mapWidth];
|
|
94
|
-
rawPixels.data[index] = pixelValue < threshold ? 0 : 255;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Any array-like structure indexed by numbers and with a `length`, e.g. an `Array` or `Buffer`. */
|
|
2
|
+
interface ArrayOrBuffer<T> {
|
|
3
|
+
length: number;
|
|
4
|
+
[n: number]: T;
|
|
5
|
+
}
|
|
6
|
+
/** Applies a threshold map to raw pixel data for ordered dithering. */
|
|
7
|
+
export declare function applyThresholdMap(pixels: ArrayOrBuffer<number>, width: number, map: number[][]): void;
|
|
8
|
+
/** Applies an error diffusion kernel to raw pixel data. */
|
|
9
|
+
export declare function applyDiffusionKernel(pixels: ArrayOrBuffer<number>, width: number, height: number, kernel: number[][]): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Applies a threshold map to raw pixel data for ordered dithering. */
|
|
2
|
+
export function applyThresholdMap(pixels, width, map) {
|
|
3
|
+
const mapWidth = map[0].length;
|
|
4
|
+
const mapHeight = map.length;
|
|
5
|
+
for (let index = 0; index < pixels.length; index++) {
|
|
6
|
+
const pixelValue = pixels[index];
|
|
7
|
+
const [x, y] = [index % width, Math.floor(index / width)];
|
|
8
|
+
const threshold = map[y % mapHeight][x % mapWidth];
|
|
9
|
+
pixels[index] = pixelValue < threshold ? 0 : 255;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Applies an error diffusion kernel to raw pixel data. */
|
|
13
|
+
export function applyDiffusionKernel(pixels, width, height, kernel) {
|
|
14
|
+
const kernelWidth = kernel[0].length;
|
|
15
|
+
const kernelHeight = kernel.length;
|
|
16
|
+
const kernelRadius = Math.floor((kernelWidth - 1) / 2);
|
|
17
|
+
for (let index = 0; index < pixels.length; index++) {
|
|
18
|
+
const original = pixels[index];
|
|
19
|
+
const quantized = original < 128 ? 0 : 255;
|
|
20
|
+
pixels[index] = quantized;
|
|
21
|
+
const error = original - quantized;
|
|
22
|
+
// (x, y) co-ordinates of the current pixel in the image.
|
|
23
|
+
const [x, y] = [index % width, Math.floor(index / width)];
|
|
24
|
+
// Distribute the error to neighbouring pixels based on the kernel.
|
|
25
|
+
for (let diffX = 0; diffX < kernelWidth; diffX++) {
|
|
26
|
+
for (let diffY = 0; diffY < kernelHeight; diffY++) {
|
|
27
|
+
const diffusionWeight = kernel[diffY][diffX];
|
|
28
|
+
if (diffusionWeight === 0)
|
|
29
|
+
continue;
|
|
30
|
+
const neighbourX = x + diffX - kernelRadius;
|
|
31
|
+
const neighbourY = y + diffY;
|
|
32
|
+
// Ensure we don't go out of bounds
|
|
33
|
+
if (neighbourX >= 0 && neighbourY >= 0 && neighbourX < width && neighbourY < height) {
|
|
34
|
+
const neighbourIndex = neighbourY * width + neighbourX;
|
|
35
|
+
pixels[neighbourIndex] = eightBitClamp(pixels[neighbourIndex] + error * diffusionWeight);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Clamps a number between `0` and `255`. */
|
|
42
|
+
function eightBitClamp(value) {
|
|
43
|
+
return Math.min(Math.max(value, 0), 255);
|
|
44
|
+
}
|
package/dist/threshold-maps.json
CHANGED
|
@@ -37,6 +37,106 @@
|
|
|
37
37
|
[63, 191, 31, 159, 55, 183, 23, 151, 61, 189, 29, 157, 53, 181, 21, 149],
|
|
38
38
|
[255, 127, 223, 95, 247, 119, 215, 87, 253, 125, 221, 93, 245, 117, 213, 85]
|
|
39
39
|
],
|
|
40
|
+
"dot-4": [
|
|
41
|
+
[192, 80, 96, 208],
|
|
42
|
+
[64, 0, 16, 112],
|
|
43
|
+
[176, 48, 32, 128],
|
|
44
|
+
[240, 160, 144, 224]
|
|
45
|
+
],
|
|
46
|
+
"dot-6": [
|
|
47
|
+
[238, 203, 119, 147, 210, 245],
|
|
48
|
+
[196, 98, 63, 112, 140, 217],
|
|
49
|
+
[91, 56, 28, 35, 105, 133],
|
|
50
|
+
[84, 21, 0, 7, 70, 126],
|
|
51
|
+
[189, 49, 14, 42, 161, 168],
|
|
52
|
+
[231, 182, 77, 154, 175, 224]
|
|
53
|
+
],
|
|
54
|
+
"dot-diagonal-6": [
|
|
55
|
+
[112, 84, 98, 126, 154, 140],
|
|
56
|
+
[70, 0, 14, 168, 238, 224],
|
|
57
|
+
[56, 42, 28, 182, 196, 210],
|
|
58
|
+
[126, 154, 140, 112, 84, 112],
|
|
59
|
+
[168, 238, 224, 70, 0, 14],
|
|
60
|
+
[182, 196, 210, 56, 42, 28]
|
|
61
|
+
],
|
|
62
|
+
"dot-8": [
|
|
63
|
+
[12, 36, 68, 108, 100, 60, 28, 4],
|
|
64
|
+
[44, 116, 152, 184, 176, 144, 92, 20],
|
|
65
|
+
[76, 160, 208, 232, 224, 200, 136, 52],
|
|
66
|
+
[124, 192, 240, 252, 248, 216, 168, 84],
|
|
67
|
+
[120, 188, 236, 252, 244, 212, 164, 80],
|
|
68
|
+
[72, 156, 204, 228, 220, 196, 132, 48],
|
|
69
|
+
[40, 112, 148, 180, 172, 140, 88, 16],
|
|
70
|
+
[8, 32, 64, 104, 96, 56, 24, 0]
|
|
71
|
+
],
|
|
72
|
+
"dot-diagonal-8": [
|
|
73
|
+
[96, 40, 48, 104, 140, 188, 196, 148],
|
|
74
|
+
[32, 0, 8, 56, 180, 236, 244, 204],
|
|
75
|
+
[88, 24, 16, 64, 172, 228, 252, 212],
|
|
76
|
+
[120, 80, 72, 112, 132, 164, 220, 156],
|
|
77
|
+
[136, 184, 192, 144, 100, 44, 52, 108],
|
|
78
|
+
[176, 232, 240, 200, 36, 4, 12, 60],
|
|
79
|
+
[168, 224, 248, 208, 92, 28, 20, 68],
|
|
80
|
+
[128, 160, 216, 152, 124, 84, 76, 116]
|
|
81
|
+
],
|
|
82
|
+
"dot-diagonal-10": [
|
|
83
|
+
[46, 79, 138, 198, 242, 245, 206, 145, 85, 49],
|
|
84
|
+
[69, 99, 160, 209, 202, 200, 210, 167, 104, 70],
|
|
85
|
+
[138, 161, 195, 171, 136, 134, 165, 196, 165, 139],
|
|
86
|
+
[198, 209, 171, 109, 74, 71, 104, 165, 209, 200],
|
|
87
|
+
[243, 202, 136, 73, 5, 2, 66, 130, 197, 241],
|
|
88
|
+
[245, 201, 135, 70, 2, 0, 65, 128, 194, 242],
|
|
89
|
+
[204, 211, 167, 104, 65, 66, 99, 160, 208, 206],
|
|
90
|
+
[144, 166, 196, 166, 130, 128, 160, 195, 171, 145],
|
|
91
|
+
[75, 104, 165, 209, 196, 193, 208, 171, 109, 76],
|
|
92
|
+
[50, 81, 139, 200, 241, 243, 206, 145, 87, 53]
|
|
93
|
+
],
|
|
94
|
+
"dot-diagonal-16": [
|
|
95
|
+
[126, 116, 100, 80, 82, 102, 118, 120, 128, 138, 154, 174, 172, 152, 136, 134],
|
|
96
|
+
[114, 66, 54, 36, 38, 56, 68, 104, 140, 188, 200, 218, 216, 198, 186, 150],
|
|
97
|
+
[98, 52, 26, 22, 24, 30, 58, 88, 156, 202, 228, 232, 230, 224, 196, 166],
|
|
98
|
+
[78, 34, 8, 6, 4, 18, 40, 84, 174, 220, 246, 248, 250, 236, 214, 170],
|
|
99
|
+
[76, 32, 10, 0, 2, 20, 42, 86, 178, 222, 244, 254, 252, 234, 212, 168],
|
|
100
|
+
[96, 50, 16, 12, 14, 28, 60, 90, 158, 204, 238, 242, 240, 226, 194, 164],
|
|
101
|
+
[112, 64, 48, 46, 44, 62, 70, 106, 142, 190, 206, 208, 210, 192, 184, 148],
|
|
102
|
+
[124, 110, 94, 74, 72, 92, 108, 122, 130, 144, 160, 180, 182, 162, 146, 132],
|
|
103
|
+
[128, 138, 154, 174, 172, 152, 136, 134, 126, 116, 100, 80, 82, 102, 118, 120],
|
|
104
|
+
[140, 188, 200, 218, 216, 198, 186, 150, 114, 66, 54, 36, 38, 56, 68, 104],
|
|
105
|
+
[156, 202, 228, 232, 230, 224, 196, 166, 98, 52, 26, 22, 24, 30, 58, 88],
|
|
106
|
+
[174, 220, 246, 248, 250, 236, 214, 170, 78, 34, 8, 6, 4, 18, 40, 84],
|
|
107
|
+
[178, 222, 244, 254, 252, 234, 212, 168, 76, 32, 10, 0, 2, 20, 42, 86],
|
|
108
|
+
[158, 204, 238, 242, 240, 226, 194, 164, 96, 50, 16, 12, 14, 28, 60, 90],
|
|
109
|
+
[142, 190, 206, 208, 210, 192, 184, 148, 112, 64, 48, 46, 44, 62, 70, 106],
|
|
110
|
+
[130, 144, 160, 180, 182, 162, 146, 132, 124, 110, 94, 74, 72, 92, 108, 122]
|
|
111
|
+
],
|
|
112
|
+
"dot-horizontal-6": [
|
|
113
|
+
[245, 231, 217, 210, 224, 238],
|
|
114
|
+
[161, 147, 133, 126, 140, 154],
|
|
115
|
+
[77, 63, 49, 42, 56, 70],
|
|
116
|
+
[35, 21, 7, 0, 14, 28],
|
|
117
|
+
[119, 105, 91, 84, 98, 112],
|
|
118
|
+
[203, 189, 175, 168, 182, 196]
|
|
119
|
+
],
|
|
120
|
+
"dot-vertical-6x6": [
|
|
121
|
+
[245, 161, 77, 35, 119, 203],
|
|
122
|
+
[231, 147, 63, 21, 105, 189],
|
|
123
|
+
[217, 133, 49, 7, 91, 175],
|
|
124
|
+
[210, 126, 42, 0, 84, 168],
|
|
125
|
+
[224, 140, 56, 14, 98, 182],
|
|
126
|
+
[238, 154, 70, 28, 112, 196]
|
|
127
|
+
],
|
|
128
|
+
"dot-vertical-5x3": [
|
|
129
|
+
[153, 51, 0, 102, 204],
|
|
130
|
+
[170, 68, 17, 119, 221],
|
|
131
|
+
[187, 85, 34, 136, 238]
|
|
132
|
+
],
|
|
133
|
+
"dot-horizontal-3x5": [
|
|
134
|
+
[153, 170, 187],
|
|
135
|
+
[51, 68, 85],
|
|
136
|
+
[0, 17, 34],
|
|
137
|
+
[102, 119, 136],
|
|
138
|
+
[204, 221, 238]
|
|
139
|
+
],
|
|
40
140
|
"blue-noise": [
|
|
41
141
|
[
|
|
42
142
|
134, 22, 92, 59, 163, 190, 12, 155, 181, 119, 233, 47, 175, 12, 113, 141, 47, 183, 111, 196,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
-
import type diffusionKernels from './diffusion-kernels';
|
|
1
|
+
import type diffusionKernels from './diffusion-kernels.js';
|
|
2
2
|
import type thresholdMaps from './threshold-maps.json';
|
|
3
3
|
export type DitheringAlgorithm = keyof typeof thresholdMaps | keyof typeof diffusionKernels | 'white-noise' | 'threshold';
|
|
4
|
+
export interface SweetcornOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The name of one of Sweetcorn’s built-in dithering algorithms to use.
|
|
7
|
+
*
|
|
8
|
+
* @see https://delucis.github.io/sweetcorn/algorithms/
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* algorithm: 'floyd-steinberg'
|
|
12
|
+
*/
|
|
13
|
+
algorithm?: DitheringAlgorithm | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* A custom threshold map to use for ordered dithering.
|
|
16
|
+
*
|
|
17
|
+
* @see https://delucis.github.io/sweetcorn/guides/byo-algorithm/#defining-a-custom-threshold-map-for-sweetcorn
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* thresholdMap: [
|
|
21
|
+
* [0, 0.5],
|
|
22
|
+
* [0.75, 0.25],
|
|
23
|
+
* ]
|
|
24
|
+
*/
|
|
25
|
+
thresholdMap?: number[][] | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* A custom diffusion kernel to use for error diffusion dithering.
|
|
28
|
+
*
|
|
29
|
+
* @see https://delucis.github.io/sweetcorn/guides/byo-algorithm/#defining-a-custom-diffusion-kernel-for-sweetcorn
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* diffusionKernel: [
|
|
33
|
+
* [0, 0, 0.2],
|
|
34
|
+
* [0.1, 0.4, 0.1],
|
|
35
|
+
* [0, 0.2, 0],
|
|
36
|
+
* ]
|
|
37
|
+
*/
|
|
38
|
+
diffusionKernel?: number[][] | undefined;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweetcorn",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "JavaScript image dithering tools for Sharp",
|
|
6
6
|
"keywords": [
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
-
"homepage": "https://github.
|
|
32
|
+
"homepage": "https://delucis.github.io/sweetcorn/",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
35
|
"url": "https://github.com/delucis/sweetcorn.git",
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"bugs": "https://github.com/delucis/sweetcorn/issues",
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "tsc"
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"pretest": "pnpm build",
|
|
42
|
+
"test": "node --test"
|
|
41
43
|
}
|
|
42
44
|
}
|