Setting the file. One moment. Error Diffusion · Media Use · heygen-com/hyperframes · Skills Docs⋯
scripts/11 files
Next
Script Index Gen
scripts/lib/error-diffusion.mjs
JavaScript·230 lines·6 KB
,
1
],
8 ],
9 divisor: 16,
10 },
11 atkinson: {
12 kernel: [
13 [1, 0, 1],
14 [2, 0, 1],
15 [-1, 1, 1],
16 [0, 1, 1],
17 [1, 1, 1],
18 [0, 2, 1],
19 ],
20 divisor: 8,
21 },
22 "jarvis-judice-ninke": {
23 kernel: [
24 [1, 0, 7],
25 [2, 0, 5],
26 [-2, 1, 3],
27 [-1, 1, 5],
28 [0, 1, 7],
29 [1, 1, 5],
30 [2, 1, 3],
31 [-2, 2, 1],
32 [-1, 2, 3],
33 [0, 2, 5],
34 [1, 2, 3],
35 [2, 2, 1],
36 ],
37 divisor: 48,
38 },
39 stucki: {
40 kernel: [
41 [1, 0, 8],
42 [2, 0, 4],
43 [-2, 1, 2],
44 [-1, 1, 4],
45 [0, 1, 8],
46 [1, 1, 4],
47 [2, 1, 2],
48 [-2, 2, 1],
49 [-1, 2, 2],
50 [0, 2, 4],
51 [1, 2, 2],
52 [2, 2, 1],
53 ],
54 divisor: 42,
55 },
56 burkes: {
57 kernel: [
58 [1, 0, 8],
59 [2, 0, 4],
60 [-2, 1, 2],
61 [-1, 1, 4],
62 [0, 1, 8],
63 [1, 1, 4],
64 [2, 1, 2],
65 ],
66 divisor: 32,
67 },
68 sierra: {
69 kernel: [
70 [1, 0, 5],
71 [2, 0, 3],
72 [-2, 1, 2],
73 [-1, 1, 4],
74 [0, 1, 5],
75 [1, 1, 4],
76 [2, 1, 2],
77 [-1, 2, 2],
78 [0, 2, 3],
79 [1, 2, 2],
80 ],
81 divisor: 32,
82 },
83 "sierra-lite": {
84 kernel: [
85 [1, 0, 2],
86 [-1, 1, 1],
87 [0, 1, 1],
88 ],
89 divisor: 4,
90 },
91 "two-row-sierra": {
92 kernel: [
93 [1, 0, 4],
94 [2, 0, 3],
95 [-2, 1, 1],
96 [-1, 1, 2],
97 [0, 1, 3],
98 [1, 1, 2],
99 [2, 1, 1],
100 ],
101 divisor: 16,
102 },
103};
104
105const DEFAULTS = {
106 algorithm: "floyd-steinberg",
107 brightness: 1,
108 contrast: 1.2,
109 detail: 1,
110 palette: ["#000000", "#ffffff"],
111 pointSize: 3,
112};
113
114export function errorDiffusionBufferLength(width, height, pointSize) {
115 return Math.ceil(width / pointSize) * Math.ceil(height / pointSize) * 3;
116}
117
118export function applyErrorDiffusionRgba(data, width, height, options = {}, errorBuffer) {
119 if (!Number.isInteger(width) || width < 1 || !Number.isInteger(height) || height < 1) {
120 throw new Error("width and height must be positive integers");
121 }
122 if (!data || data.length !== width * height * 4) {
123 throw new Error(`RGBA data must contain ${width * height * 4} bytes`);
124 }
125
126 const algorithm = options.algorithm ?? DEFAULTS.algorithm;
127 const diffusion = ERROR_DIFFUSION_ALGORITHMS[algorithm];
128 if (!diffusion) throw new Error(`unknown error-diffusion algorithm: ${algorithm}`);
129
130 const pointSize = integerInRange(options.pointSize ?? DEFAULTS.pointSize, 1, 20, "pointSize");
131 const brightness = numberInRange(options.brightness ?? DEFAULTS.brightness, 0.5, 2, "brightness");
132 const contrast = numberInRange(options.contrast ?? DEFAULTS.contrast, 0.5, 2, "contrast");
133 const detail = numberInRange(options.detail ?? DEFAULTS.detail, 0.1, 1, "detail");
134 const palette = parsePalette(options.palette ?? DEFAULTS.palette);
135 const blockColumns = Math.ceil(width / pointSize);
136 const blockRows = Math.ceil(height / pointSize);
137 const errorLength = errorDiffusionBufferLength(width, height, pointSize);
138 const errors = errorBuffer ?? new Float32Array(errorLength);
139 if (!(errors instanceof Float32Array) || errors.length !== errorLength) {
140 throw new Error(`errorBuffer must be a Float32Array of length ${errorLength}`);
141 }
142 errors.fill(0);
143
144 const centerOffset = Math.floor(pointSize / 2);
145 for (let blockRow = 0; blockRow < blockRows; blockRow++) {
146 const blockY = blockRow * pointSize;
147 for (let blockColumn = 0; blockColumn < blockColumns; blockColumn++) {
148 const blockX = blockColumn * pointSize;
149 const centerX = Math.min(blockX + centerOffset, width - 1);
150 const centerY = Math.min(blockY + centerOffset, height - 1);
151 const rgbaIndex = (centerY * width + centerX) * 4;
152 const errorIndex = (blockRow * blockColumns + blockColumn) * 3;
153 const red = correctedChannel(data[rgbaIndex], errors[errorIndex], brightness, contrast);
154 const green = correctedChannel(
155 data[rgbaIndex + 1],
156 errors[errorIndex + 1],
157 brightness,
158 contrast,
159 );
160 const blue = correctedChannel(
161 data[rgbaIndex + 2],
162 errors[errorIndex + 2],
163 brightness,
164 contrast,
165 );
166 const luminance = 0.299 * red + 0.587 * green + 0.114 * blue;
167 const output = palette[Math.min(palette.length - 1, Math.floor(luminance * palette.length))];
168
169 for (let y = blockY; y < Math.min(blockY + pointSize, height); y++) {
170 for (let x = blockX; x < Math.min(blockX + pointSize, width); x++) {
171 const outputIndex = (y * width + x) * 4;
172 data[outputIndex] = Math.round(output[0] * 255);
173 data[outputIndex + 1] = Math.round(output[1] * 255);
174 data[outputIndex + 2] = Math.round(output[2] * 255);
175 }
176 }
177
178 for (const [dx, dy, weight] of diffusion.kernel) {
179 const targetColumn = blockColumn + dx;
180 const targetRow = blockRow + dy;
181 if (
182 targetColumn < 0 ||
183 targetColumn >= blockColumns ||
184 targetRow < 0 ||
185 targetRow >= blockRows
186 ) {
187 continue;
188 }
189 const target = (targetRow * blockColumns + targetColumn) * 3;
190 const scale = (weight / diffusion.divisor) * detail;
191 errors[target] += (red - output[0]) * scale;
192 errors[target + 1] += (green - output[1]) * scale;
193 errors[target + 2] += (blue - output[2]) * scale;
194 }
195 }
196 }
197 return data;
198}
199
200function correctedChannel(byte, error, brightness, contrast) {
201 return Math.min(1, Math.max(0, ((byte / 255 - 0.5) * contrast + 0.5) * brightness + error));
202}
203
204function parsePalette(colors) {
205 if (!Array.isArray(colors) || colors.length < 2 || colors.length > 6) {
206 throw new Error("palette must contain 2 to 6 colors");
207 }
208 return colors.map((color) => {
209 const match = /^#([0-9a-f]{6})$/i.exec(color);
210 if (!match) throw new Error(`palette color must use #rrggbb: ${color}`);
211 const value = Number.parseInt(match[1], 16);
212 return [(value >> 16) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255];
213 });
214}
215
216function numberInRange(value, min, max, name) {
217 const number = Number(value);
218 if (!Number.isFinite(number) || number < min || number > max) {
219 throw new Error(`${name} must be between ${min} and ${max}`);
220 }
221 return number;
222}
223
224function integerInRange(value, min, max, name) {
225 const number = Number(value);
226 if (!Number.isInteger(number) || number < min || number > max) {
227 throw new Error(`${name} must be an integer between ${min} and ${max}`);
228 }
229 return number;
230}