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 CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { Sharp } from 'sharp';
2
- import type { DitheringAlgorithm } from './types';
3
- export type { DitheringAlgorithm };
4
- interface SweetcornOptions {
5
- algorithm: DitheringAlgorithm;
6
- }
7
- export default function sweetcorn(image: Sharp, { algorithm }: SweetcornOptions): Promise<Sharp>;
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
- export default async function sweetcorn(image, { algorithm }) {
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
- // Astro supports outputting different formats, but dithered images like this respond quite
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
+ }
@@ -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.1",
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.com/delucis/sweetcorn",
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
  }