@dooph-software/fluid-gradients 1.0.0 → 3.0.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/README.md +26 -12
- package/dist/{chunk-KYCZHKFE.js → chunk-OGG3CQZ4.js} +32 -2
- package/dist/{chunk-KYCZHKFE.js.map → chunk-OGG3CQZ4.js.map} +1 -1
- package/dist/{chunk-CRRF2Q64.cjs → chunk-VF6TCELO.cjs} +32 -2
- package/dist/chunk-VF6TCELO.cjs.map +1 -0
- package/dist/cli.js +374 -40
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +280 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +273 -18
- package/dist/index.js.map +1 -1
- package/dist/palettes.cjs +2 -2
- package/dist/palettes.cjs.map +1 -1
- package/dist/palettes.d.cts +72 -0
- package/dist/palettes.d.ts +72 -0
- package/dist/palettes.js +1 -1
- package/package.json +3 -2
- package/skills/using-fluid-gradients/SKILL.md +3 -2
- package/dist/chunk-CRRF2Q64.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -11,25 +11,258 @@ import {
|
|
|
11
11
|
pickPalette,
|
|
12
12
|
validatePalette,
|
|
13
13
|
validatePalettes
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-OGG3CQZ4.js";
|
|
15
15
|
|
|
16
16
|
// src/render.ts
|
|
17
17
|
import { createCanvas, ImageData } from "@napi-rs/canvas";
|
|
18
18
|
import sharp from "sharp";
|
|
19
|
+
import { createNoise2D as createNoise2D2 } from "simplex-noise";
|
|
20
|
+
|
|
21
|
+
// src/sweep.ts
|
|
19
22
|
import { createNoise2D } from "simplex-noise";
|
|
23
|
+
var tuning = {
|
|
24
|
+
workHeight: 400,
|
|
25
|
+
velocityFrequency: 0.4,
|
|
26
|
+
/** Velocity grid cells per image height. */
|
|
27
|
+
velocityResolution: 80,
|
|
28
|
+
/** Travel at sweep 1, measured at drift speed. */
|
|
29
|
+
advectDistance: 1.6,
|
|
30
|
+
advectSteps: 32,
|
|
31
|
+
/** Drift speed relative to the current's RMS speed. */
|
|
32
|
+
drift: 1,
|
|
33
|
+
/** Eddy speed cap, relative to the drift. Two fields mixed at most √2 ×
|
|
34
|
+
* this, which must stay below 1 or eddies can close. */
|
|
35
|
+
eddyCap: 0.65,
|
|
36
|
+
/** Radians the current turns between its two fields over the trace. */
|
|
37
|
+
timeSpin: 1.2,
|
|
38
|
+
defocus: 8e-3,
|
|
39
|
+
smearLength: 0.5,
|
|
40
|
+
smearSteps: 40,
|
|
41
|
+
/** Fraction of the exposure at full weight before the window tapers. */
|
|
42
|
+
smearFlat: 0.6,
|
|
43
|
+
groundSoftness: 0.35,
|
|
44
|
+
groundFieldWeight: 1,
|
|
45
|
+
groundNoiseWeight: 0.6,
|
|
46
|
+
groundBlur: 0.02,
|
|
47
|
+
groundMaxCover: 0.9,
|
|
48
|
+
groundFrequency: 0.9
|
|
49
|
+
};
|
|
50
|
+
var smoothStep = (edge) => edge * edge * (3 - 2 * edge);
|
|
51
|
+
var clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
52
|
+
var fractal = (noise, x, y) => 0.5 * noise(x, y) + 0.25 * noise(x * 2, y * 2);
|
|
53
|
+
var Grid = class {
|
|
54
|
+
constructor(width, height) {
|
|
55
|
+
this.width = width;
|
|
56
|
+
this.height = height;
|
|
57
|
+
this.values = new Float32Array(width * height);
|
|
58
|
+
}
|
|
59
|
+
width;
|
|
60
|
+
height;
|
|
61
|
+
values;
|
|
62
|
+
sample(x, y) {
|
|
63
|
+
const gx = Math.max(0, Math.min(this.width - 1.0001, x));
|
|
64
|
+
const gy = Math.max(0, Math.min(this.height - 1.0001, y));
|
|
65
|
+
const x0 = Math.floor(gx);
|
|
66
|
+
const y0 = Math.floor(gy);
|
|
67
|
+
const fx = gx - x0;
|
|
68
|
+
const fy = gy - y0;
|
|
69
|
+
const v = this.values;
|
|
70
|
+
const i = y0 * this.width + x0;
|
|
71
|
+
const top = v[i] + (v[i + 1] - v[i]) * fx;
|
|
72
|
+
const bottom = v[i + this.width] + (v[i + this.width + 1] - v[i + this.width]) * fx;
|
|
73
|
+
return top + (bottom - top) * fy;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function applySweep(input) {
|
|
77
|
+
const { seed, width, height, pixels, rampPositionAt, rgbStops, sampleRamp, sweep, ground } = input;
|
|
78
|
+
const t = tuning;
|
|
79
|
+
const random = createSeededRandom(hashStringToInt(`${seed}::sweep`));
|
|
80
|
+
const currentNoiseA = createNoise2D(random);
|
|
81
|
+
const currentNoiseB = createNoise2D(random);
|
|
82
|
+
const coverageNoise = createNoise2D(random);
|
|
83
|
+
const coverageWarpNoise = createNoise2D(random);
|
|
84
|
+
const driftAngle = random() * Math.PI * 2;
|
|
85
|
+
const scale = Math.min(1, t.workHeight / height);
|
|
86
|
+
const workW = Math.max(2, Math.round(width * scale));
|
|
87
|
+
const workH = Math.max(2, Math.round(height * scale));
|
|
88
|
+
const aspect = width / height;
|
|
89
|
+
const driftX = Math.cos(driftAngle) * t.drift;
|
|
90
|
+
const driftY = Math.sin(driftAngle) * t.drift;
|
|
91
|
+
const margin = 2 * t.advectDistance * sweep + 0.1;
|
|
92
|
+
const cell = 1 / t.velocityResolution;
|
|
93
|
+
const velW = Math.ceil((aspect + 2 * margin) / cell) + 2;
|
|
94
|
+
const velH = Math.ceil((1 + 2 * margin) / cell) + 2;
|
|
95
|
+
const currents = [currentNoiseA, currentNoiseB].map((noise) => {
|
|
96
|
+
const vx = new Grid(velW, velH);
|
|
97
|
+
const vy = new Grid(velW, velH);
|
|
98
|
+
const f = t.velocityFrequency;
|
|
99
|
+
const e = 2e-3;
|
|
100
|
+
let total = 0;
|
|
101
|
+
for (let gy = 0; gy < velH; gy++) {
|
|
102
|
+
for (let gx = 0; gx < velW; gx++) {
|
|
103
|
+
const u = (gx * cell - margin) * f;
|
|
104
|
+
const v = (gy * cell - margin) * f;
|
|
105
|
+
const i = gy * velW + gx;
|
|
106
|
+
vx.values[i] = (fractal(noise, u, v + e) - fractal(noise, u, v - e)) / (2 * e);
|
|
107
|
+
vy.values[i] = -(fractal(noise, u + e, v) - fractal(noise, u - e, v)) / (2 * e);
|
|
108
|
+
total += vx.values[i] ** 2 + vy.values[i] ** 2;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const rms = Math.sqrt(total / vx.values.length) || 1;
|
|
112
|
+
const cap = t.eddyCap * t.drift;
|
|
113
|
+
for (let i = 0; i < vx.values.length; i++) {
|
|
114
|
+
const speed = Math.hypot(vx.values[i], vy.values[i]) / rms + 1e-9;
|
|
115
|
+
const limited = cap * Math.tanh(speed / cap) / speed / rms;
|
|
116
|
+
vx.values[i] *= limited;
|
|
117
|
+
vy.values[i] *= limited;
|
|
118
|
+
}
|
|
119
|
+
return { vx, vy };
|
|
120
|
+
});
|
|
121
|
+
const velocity = (u, v, phase, out) => {
|
|
122
|
+
const gx = (u + margin) / cell;
|
|
123
|
+
const gy = (v + margin) / cell;
|
|
124
|
+
const a = Math.cos(phase);
|
|
125
|
+
const b = Math.sin(phase);
|
|
126
|
+
out[0] = a * currents[0].vx.sample(gx, gy) + b * currents[1].vx.sample(gx, gy) + driftX;
|
|
127
|
+
out[1] = a * currents[0].vy.sample(gx, gy) + b * currents[1].vy.sample(gx, gy) + driftY;
|
|
128
|
+
};
|
|
129
|
+
const flowX = new Grid(workW, workH);
|
|
130
|
+
const flowY = new Grid(workW, workH);
|
|
131
|
+
const fieldSource = new Grid(workW, workH);
|
|
132
|
+
const sourceU = new Float32Array(workW * workH);
|
|
133
|
+
const sourceV = new Float32Array(workW * workH);
|
|
134
|
+
const steps = t.advectSteps;
|
|
135
|
+
const dt = sweep * t.advectDistance / t.drift / steps;
|
|
136
|
+
const k1 = [0, 0];
|
|
137
|
+
const k2 = [0, 0];
|
|
138
|
+
for (let y = 0; y < workH; y++) {
|
|
139
|
+
for (let x = 0; x < workW; x++) {
|
|
140
|
+
let u = x / workH;
|
|
141
|
+
let v = y / workH;
|
|
142
|
+
const i = y * workW + x;
|
|
143
|
+
velocity(u, v, 0, k1);
|
|
144
|
+
flowX.values[i] = k1[0];
|
|
145
|
+
flowY.values[i] = k1[1];
|
|
146
|
+
for (let s = 0; s < steps; s++) {
|
|
147
|
+
const phase = t.timeSpin * s / steps;
|
|
148
|
+
velocity(u, v, phase, k1);
|
|
149
|
+
velocity(u - k1[0] * dt / 2, v - k1[1] * dt / 2, phase, k2);
|
|
150
|
+
u -= k2[0] * dt;
|
|
151
|
+
v -= k2[1] * dt;
|
|
152
|
+
}
|
|
153
|
+
sourceU[i] = u;
|
|
154
|
+
sourceV[i] = v;
|
|
155
|
+
fieldSource.values[i] = rampPositionAt(u, v);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
boxBlur(fieldSource.values, workW, workH, Math.max(1, Math.round(t.defocus * sweep * workH)), 3);
|
|
159
|
+
const coverageSource = new Grid(workW, workH);
|
|
160
|
+
if (ground > 0) {
|
|
161
|
+
for (let y = 0; y < workH; y++) {
|
|
162
|
+
for (let x = 0; x < workW; x++) {
|
|
163
|
+
const i = y * workW + x;
|
|
164
|
+
const su = sourceU[i] * t.groundFrequency;
|
|
165
|
+
const sv = sourceV[i] * t.groundFrequency;
|
|
166
|
+
const warpX = fractal(coverageWarpNoise, su, sv);
|
|
167
|
+
const warpY = fractal(coverageWarpNoise, su + 5.2, sv + 1.3);
|
|
168
|
+
coverageSource.values[i] = t.groundFieldWeight * fieldSource.values[i] + t.groundNoiseWeight * fractal(coverageNoise, su + warpX, sv + warpY);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const sorted = Float32Array.from(coverageSource.values).sort();
|
|
172
|
+
const threshold = sorted[Math.min(sorted.length - 1, Math.floor(ground * t.groundMaxCover * sorted.length))];
|
|
173
|
+
for (let i = 0; i < coverageSource.values.length; i++) coverageSource.values[i] -= threshold;
|
|
174
|
+
boxBlur(coverageSource.values, workW, workH, Math.max(1, Math.round(t.groundBlur * workH)), 3);
|
|
175
|
+
}
|
|
176
|
+
const smearedField = new Grid(workW, workH);
|
|
177
|
+
const smearedCoverage = new Grid(workW, workH);
|
|
178
|
+
const smearSteps = t.smearSteps;
|
|
179
|
+
const step = sweep * t.smearLength * workH / 2 / Math.hypot(1, t.drift) / smearSteps;
|
|
180
|
+
const weights = new Float32Array(smearSteps + 1);
|
|
181
|
+
for (let s = 0; s <= smearSteps; s++) {
|
|
182
|
+
const along = s / (smearSteps + 1);
|
|
183
|
+
weights[s] = along <= t.smearFlat ? 1 : 0.5 + 0.5 * Math.cos(Math.PI * (along - t.smearFlat) / (1 - t.smearFlat));
|
|
184
|
+
}
|
|
185
|
+
for (let y = 0; y < workH; y++) {
|
|
186
|
+
for (let x = 0; x < workW; x++) {
|
|
187
|
+
let fieldTotal = weights[0] * fieldSource.sample(x, y);
|
|
188
|
+
let coverageTotal = ground > 0 ? weights[0] * coverageSource.sample(x, y) : 0;
|
|
189
|
+
let weightTotal = weights[0];
|
|
190
|
+
for (let direction = -1; direction <= 1; direction += 2) {
|
|
191
|
+
let px = x;
|
|
192
|
+
let py = y;
|
|
193
|
+
for (let s = 1; s <= smearSteps; s++) {
|
|
194
|
+
const mx = px + direction * flowX.sample(px, py) * step / 2;
|
|
195
|
+
const my = py + direction * flowY.sample(px, py) * step / 2;
|
|
196
|
+
px += direction * flowX.sample(mx, my) * step;
|
|
197
|
+
py += direction * flowY.sample(mx, my) * step;
|
|
198
|
+
if (px < 0 || py < 0 || px > workW - 1 || py > workH - 1) break;
|
|
199
|
+
const w = weights[s];
|
|
200
|
+
fieldTotal += w * fieldSource.sample(px, py);
|
|
201
|
+
if (ground > 0) coverageTotal += w * coverageSource.sample(px, py);
|
|
202
|
+
weightTotal += w;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
smearedField.values[y * workW + x] = fieldTotal / weightTotal;
|
|
206
|
+
smearedCoverage.values[y * workW + x] = coverageTotal / weightTotal;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const groundColor = rgbStops[0];
|
|
210
|
+
for (let py = 0; py < height; py++) {
|
|
211
|
+
for (let px = 0; px < width; px++) {
|
|
212
|
+
const sx = (px + 0.5) * scale - 0.5;
|
|
213
|
+
const sy = (py + 0.5) * scale - 0.5;
|
|
214
|
+
let [r, g, b] = sampleRamp(smearedField.sample(sx, sy));
|
|
215
|
+
if (ground > 0) {
|
|
216
|
+
const m = smoothStep(clamp01(smearedCoverage.sample(sx, sy) / t.groundSoftness + 0.5));
|
|
217
|
+
r = groundColor[0] + (r - groundColor[0]) * m;
|
|
218
|
+
g = groundColor[1] + (g - groundColor[1]) * m;
|
|
219
|
+
b = groundColor[2] + (b - groundColor[2]) * m;
|
|
220
|
+
}
|
|
221
|
+
const o = (py * width + px) * 4;
|
|
222
|
+
pixels[o] = r;
|
|
223
|
+
pixels[o + 1] = g;
|
|
224
|
+
pixels[o + 2] = b;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function boxBlur(values, width, height, radius, passes) {
|
|
229
|
+
const scratch = new Float32Array(values.length);
|
|
230
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
231
|
+
for (let y = 0; y < height; y++) {
|
|
232
|
+
for (let x = 0; x < width; x++) {
|
|
233
|
+
let total = 0;
|
|
234
|
+
for (let k = -radius; k <= radius; k++) total += values[y * width + Math.max(0, Math.min(width - 1, x + k))];
|
|
235
|
+
scratch[y * width + x] = total / (2 * radius + 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (let y = 0; y < height; y++) {
|
|
239
|
+
for (let x = 0; x < width; x++) {
|
|
240
|
+
let total = 0;
|
|
241
|
+
for (let k = -radius; k <= radius; k++) total += scratch[Math.max(0, Math.min(height - 1, y + k)) * width + x];
|
|
242
|
+
values[y * width + x] = total / (2 * radius + 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/render.ts
|
|
20
249
|
var lookDescriptions = {
|
|
21
250
|
baseNoiseFrequency: "Lower values give broader, softer color regions.",
|
|
22
251
|
domainWarpStrength: "How much the color field swirls.",
|
|
23
252
|
fractalOctaveCount: "More octaves add finer detail.",
|
|
24
253
|
colorTransitionContrast: "Sharpness of the edges between colors.",
|
|
25
|
-
grainOpacity: "Strength of the film-grain overlay."
|
|
254
|
+
grainOpacity: "Strength of the film-grain overlay.",
|
|
255
|
+
sweep: "Drifts the color into soft, curving wisps, like dye in water or folded silk, 0\u20131. 0 is the plain fluid.",
|
|
256
|
+
ground: "Roughly the share of the image, 0\u20131, left as a pale ground in the palette's lightest color."
|
|
26
257
|
};
|
|
27
258
|
var defaultLook = {
|
|
28
259
|
baseNoiseFrequency: 0.3,
|
|
29
260
|
domainWarpStrength: 1,
|
|
30
261
|
fractalOctaveCount: 2,
|
|
31
262
|
colorTransitionContrast: 1.75,
|
|
32
|
-
grainOpacity: 0.09
|
|
263
|
+
grainOpacity: 0.09,
|
|
264
|
+
sweep: 0,
|
|
265
|
+
ground: 0
|
|
33
266
|
};
|
|
34
267
|
var DEFAULT_WIDTH = 1200;
|
|
35
268
|
var DEFAULT_HEIGHT = 1500;
|
|
@@ -51,15 +284,20 @@ function sampleColorRamp(rgbStops, position) {
|
|
|
51
284
|
lowB + (highB - lowB) * blend
|
|
52
285
|
];
|
|
53
286
|
}
|
|
54
|
-
var
|
|
287
|
+
var smoothStep2 = (edge) => edge * edge * (3 - 2 * edge);
|
|
55
288
|
async function renderCanvas(options) {
|
|
56
289
|
const { seed, palette, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = options;
|
|
57
290
|
const look = { ...defaultLook, ...options.look };
|
|
291
|
+
for (const knob of ["sweep", "ground"]) {
|
|
292
|
+
if (!(look[knob] >= 0 && look[knob] <= 1)) {
|
|
293
|
+
throw new RangeError(`look.${knob} must be between 0 and 1. Got: ${look[knob]}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
58
296
|
const random = createSeededRandom(hashStringToInt(seed));
|
|
59
297
|
const rgbStops = palette.colors.map(hexToRgb);
|
|
60
|
-
const horizontalWarpNoise =
|
|
61
|
-
const verticalWarpNoise =
|
|
62
|
-
const colorFieldNoise =
|
|
298
|
+
const horizontalWarpNoise = createNoise2D2(random);
|
|
299
|
+
const verticalWarpNoise = createNoise2D2(random);
|
|
300
|
+
const colorFieldNoise = createNoise2D2(random);
|
|
63
301
|
const sampleFractalNoise = (noise, sampleX, sampleY) => {
|
|
64
302
|
let total = 0;
|
|
65
303
|
let amplitude = 0.5;
|
|
@@ -76,21 +314,24 @@ async function renderCanvas(options) {
|
|
|
76
314
|
const image = context.createImageData(width, height);
|
|
77
315
|
const pixels = image.data;
|
|
78
316
|
const aspectRatio = width / height;
|
|
317
|
+
const rampPositionAt = (sampleX, sampleY) => {
|
|
318
|
+
const horizontalWarpOffset = sampleFractalNoise(horizontalWarpNoise, sampleX, sampleY);
|
|
319
|
+
const verticalWarpOffset = sampleFractalNoise(verticalWarpNoise, sampleX + 5.2, sampleY + 1.3);
|
|
320
|
+
const rawFieldValue = sampleFractalNoise(
|
|
321
|
+
colorFieldNoise,
|
|
322
|
+
sampleX + look.domainWarpStrength * horizontalWarpOffset,
|
|
323
|
+
sampleY + look.domainWarpStrength * verticalWarpOffset
|
|
324
|
+
);
|
|
325
|
+
const normalizedValue = (rawFieldValue + 1) / 2;
|
|
326
|
+
return smoothStep2(
|
|
327
|
+
Math.max(0, Math.min(1, (normalizedValue - 0.5) * look.colorTransitionContrast + 0.5))
|
|
328
|
+
);
|
|
329
|
+
};
|
|
79
330
|
for (let pixelY = 0; pixelY < height; pixelY++) {
|
|
80
331
|
for (let pixelX = 0; pixelX < width; pixelX++) {
|
|
81
332
|
const sampleX = pixelX / width * look.baseNoiseFrequency * aspectRatio;
|
|
82
333
|
const sampleY = pixelY / height * look.baseNoiseFrequency;
|
|
83
|
-
const
|
|
84
|
-
const verticalWarpOffset = sampleFractalNoise(verticalWarpNoise, sampleX + 5.2, sampleY + 1.3);
|
|
85
|
-
const rawFieldValue = sampleFractalNoise(
|
|
86
|
-
colorFieldNoise,
|
|
87
|
-
sampleX + look.domainWarpStrength * horizontalWarpOffset,
|
|
88
|
-
sampleY + look.domainWarpStrength * verticalWarpOffset
|
|
89
|
-
);
|
|
90
|
-
const normalizedValue = (rawFieldValue + 1) / 2;
|
|
91
|
-
const contrastedValue = smoothStep(
|
|
92
|
-
Math.max(0, Math.min(1, (normalizedValue - 0.5) * look.colorTransitionContrast + 0.5))
|
|
93
|
-
);
|
|
334
|
+
const contrastedValue = rampPositionAt(sampleX, sampleY);
|
|
94
335
|
const [red, green, blue] = sampleColorRamp(rgbStops, contrastedValue);
|
|
95
336
|
const pixelIndex = (pixelY * width + pixelX) * 4;
|
|
96
337
|
pixels[pixelIndex] = red;
|
|
@@ -99,6 +340,20 @@ async function renderCanvas(options) {
|
|
|
99
340
|
pixels[pixelIndex + 3] = 255;
|
|
100
341
|
}
|
|
101
342
|
}
|
|
343
|
+
if (look.sweep > 0 || look.ground > 0) {
|
|
344
|
+
applySweep({
|
|
345
|
+
seed,
|
|
346
|
+
width,
|
|
347
|
+
height,
|
|
348
|
+
pixels,
|
|
349
|
+
// Height units: the fluid's x runs 0…aspect, y runs 0…1.
|
|
350
|
+
rampPositionAt: (u, v) => rampPositionAt(u * look.baseNoiseFrequency, v * look.baseNoiseFrequency),
|
|
351
|
+
rgbStops,
|
|
352
|
+
sampleRamp: (position) => sampleColorRamp(rgbStops, position),
|
|
353
|
+
sweep: look.sweep,
|
|
354
|
+
ground: look.ground
|
|
355
|
+
});
|
|
356
|
+
}
|
|
102
357
|
context.putImageData(image, 0, 0);
|
|
103
358
|
const grainPixels = new Uint8ClampedArray(width * height * 4);
|
|
104
359
|
for (let i = 0; i < width * height; i++) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/render.ts","../src/create.ts","../src/index.ts"],"sourcesContent":["/*\n * render — the noise pipeline that turns a seed and a palette into pixels.\n *\n * ## behavior\n * - `renderCanvas` seeds one stream from the seed, builds three simplex noise\n * fields from it, domain-warps the third by the first two, and maps the result\n * along the palette ramp; the film grain then draws from the same stream.\n * - `generateImage` wraps that and encodes: PNG straight off the canvas, WebP\n * through sharp at `DEFAULT_WEBP_QUALITY`.\n * - `lookDescriptions` is the one copy of the per-knob prose; `src/flags.ts`\n * builds CLI help from it and `scripts/sync-docs.mjs` builds the README table.\n *\n * ## constraints\n * - The order values are drawn from `random` is fixed: three `createNoise2D`\n * calls, then one grain value per pixel. Inserting a draw, removing one, or\n * reordering them shifts every pixel of every image — the grain loop is not\n * incidental noise that can be swapped for `Math.random`; it consumes the\n * tail of the same stream. `test/fluid-gradients.test.mjs` compares seed `aspect` /\n * `jewel-peacock` byte-for-byte against artwork already shipped in Aspect.\n * - `defaultLook` and the bare constants in the pixel loop — the `5.2` / `1.3`\n * warp offsets, `amplitude` starting at 0.5 and halving, `frequency`\n * doubling, the `smoothStep` around the contrast term — are the prototype's\n * values, not tuning suggestions. Any of them changes every image rendered\n * without an explicit `look`, which is almost all of them.\n * - Noise is sampled in normalized coordinates (`pixelX / width`), with the\n * aspect ratio applied to X only. This is what makes a 700×800 render the same\n * composition as the 1200×1500 default, which `scripts/generate-examples.mjs`\n * relies on to ship small previews of the real output. Sampling in absolute\n * pixels would make every size a different picture.\n * - `DEFAULT_WEBP_QUALITY` and the plain `sharp(...).webp({ quality })` call are\n * part of the shipped bytes. Adding encoder options (effort, lossless,\n * chroma subsampling) changes the fixture comparison even though nothing about\n * the rendered pixels moved.\n */\nimport { createCanvas, ImageData, type Canvas, type SKRSContext2D } from '@napi-rs/canvas';\nimport sharp from 'sharp';\nimport { createNoise2D } from 'simplex-noise';\nimport { paletteAccent, paletteComplement, type Palette, type PaletteName } from './palettes';\nimport { createSeededRandom, hashStringToInt } from './random';\n\n/**\n * Knobs that define the gradient's look. The defaults are the exact values from\n * the original prototype; changing them changes every image for a given seed.\n */\nexport type GradientLook = {\n /** Zoom of the noise field. Lower = broader, softer color regions. */\n baseNoiseFrequency: number;\n /** How far the warp noise displaces the color field. */\n domainWarpStrength: number;\n /** Fractal noise octaves. More = finer detail. */\n fractalOctaveCount: number;\n /** Sharpness of transitions between palette stops. */\n colorTransitionContrast: number;\n /** Opacity of the film-grain overlay, 0–1. */\n grainOpacity: number;\n};\n\n/** One-line description per knob. Shared by the CLI help and the generated README table. */\nexport const lookDescriptions: Readonly<Record<keyof GradientLook, string>> = {\n baseNoiseFrequency: 'Lower values give broader, softer color regions.',\n domainWarpStrength: 'How much the color field swirls.',\n fractalOctaveCount: 'More octaves add finer detail.',\n colorTransitionContrast: 'Sharpness of the edges between colors.',\n grainOpacity: 'Strength of the film-grain overlay.',\n};\n\nexport const defaultLook: Readonly<GradientLook> = {\n baseNoiseFrequency: 0.3,\n domainWarpStrength: 1.0,\n fractalOctaveCount: 2,\n colorTransitionContrast: 1.75,\n grainOpacity: 0.09,\n};\n\nexport type MeshGradientOptions<Name extends string = PaletteName> = {\n /** Any string. The same seed + palette + size + look always renders the same image. */\n seed: string;\n /** Palette object, palette name, or omitted to pick deterministically from the seed. */\n palette?: Palette | Name;\n /** Pixels. Defaults to 1200. */\n width?: number;\n /** Pixels. Defaults to 1500. */\n height?: number;\n look?: Partial<GradientLook>;\n};\n\nexport type MeshGradientFormatOptions = {\n /** Defaults to 'webp'. */\n format?: 'webp' | 'png';\n /** WebP quality, 1–100. Defaults to 82. */\n quality?: number;\n};\n\n/** Options after the palette has been resolved to an object. */\nexport type ResolvedMeshGradientOptions = Omit<MeshGradientOptions, 'palette'> & {\n palette: Palette;\n};\n\nexport type MeshGradientCanvas = {\n canvas: Canvas;\n palette: Palette;\n /** Mid-tone hex from the palette, handy as a UI accent next to the image. */\n accent: string;\n /** Opposite-hue color built to stand out on the image, for chips and geometry. */\n complement: string;\n};\n\nexport type MeshGradientImage = {\n buffer: Buffer;\n format: 'webp' | 'png';\n width: number;\n height: number;\n seed: string;\n palette: Palette;\n accent: string;\n complement: string;\n};\n\nexport const DEFAULT_WIDTH = 1200;\nexport const DEFAULT_HEIGHT = 1500;\nexport const DEFAULT_WEBP_QUALITY = 82;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const packed = parseInt(hex.slice(1), 16);\n return [(packed >> 16) & 255, (packed >> 8) & 255, packed & 255];\n}\n\nfunction sampleColorRamp(\n rgbStops: [number, number, number][],\n position: number,\n): [number, number, number] {\n const clamped = Math.max(0, Math.min(1, position));\n const scaled = clamped * (rgbStops.length - 1);\n const lowerIndex = Math.min(rgbStops.length - 2, Math.floor(scaled));\n const blend = scaled - lowerIndex;\n const [lowR, lowG, lowB] = rgbStops[lowerIndex];\n const [highR, highG, highB] = rgbStops[lowerIndex + 1];\n return [\n lowR + (highR - lowR) * blend,\n lowG + (highG - lowG) * blend,\n lowB + (highB - lowB) * blend,\n ];\n}\n\nconst smoothStep = (edge: number) => edge * edge * (3 - 2 * edge);\n\n/**\n * Render a domain-warped noise gradient onto a canvas. The pixel pipeline and\n * the order random values are drawn in are verbatim from the prototype — both\n * must stay fixed for seeds to keep producing the same image.\n */\nexport async function renderCanvas(\n options: ResolvedMeshGradientOptions,\n): Promise<MeshGradientCanvas> {\n const { seed, palette, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = options;\n const look = { ...defaultLook, ...options.look };\n\n const random = createSeededRandom(hashStringToInt(seed));\n const rgbStops = palette.colors.map(hexToRgb);\n\n const horizontalWarpNoise = createNoise2D(random);\n const verticalWarpNoise = createNoise2D(random);\n const colorFieldNoise = createNoise2D(random);\n\n const sampleFractalNoise = (\n noise: (x: number, y: number) => number,\n sampleX: number,\n sampleY: number,\n ) => {\n let total = 0;\n let amplitude = 0.5;\n let frequency = 1;\n for (let octave = 0; octave < look.fractalOctaveCount; octave++) {\n total += amplitude * noise(sampleX * frequency, sampleY * frequency);\n amplitude *= 0.5;\n frequency *= 2;\n }\n return total;\n };\n\n const canvas = createCanvas(width, height);\n const context = canvas.getContext('2d');\n const image = context.createImageData(width, height);\n const pixels = image.data;\n const aspectRatio = width / height;\n\n for (let pixelY = 0; pixelY < height; pixelY++) {\n for (let pixelX = 0; pixelX < width; pixelX++) {\n const sampleX = (pixelX / width) * look.baseNoiseFrequency * aspectRatio;\n const sampleY = (pixelY / height) * look.baseNoiseFrequency;\n\n const horizontalWarpOffset = sampleFractalNoise(horizontalWarpNoise, sampleX, sampleY);\n const verticalWarpOffset = sampleFractalNoise(verticalWarpNoise, sampleX + 5.2, sampleY + 1.3);\n\n const rawFieldValue = sampleFractalNoise(\n colorFieldNoise,\n sampleX + look.domainWarpStrength * horizontalWarpOffset,\n sampleY + look.domainWarpStrength * verticalWarpOffset,\n );\n const normalizedValue = (rawFieldValue + 1) / 2;\n\n const contrastedValue = smoothStep(\n Math.max(0, Math.min(1, (normalizedValue - 0.5) * look.colorTransitionContrast + 0.5)),\n );\n\n const [red, green, blue] = sampleColorRamp(rgbStops, contrastedValue);\n const pixelIndex = (pixelY * width + pixelX) * 4;\n pixels[pixelIndex] = red;\n pixels[pixelIndex + 1] = green;\n pixels[pixelIndex + 2] = blue;\n pixels[pixelIndex + 3] = 255;\n }\n }\n\n context.putImageData(image, 0, 0);\n\n // Film-grain overlay. Draws from the same random stream as the noise above.\n const grainPixels = new Uint8ClampedArray(width * height * 4);\n for (let i = 0; i < width * height; i++) {\n const brightness = Math.floor(random() * 256);\n grainPixels[i * 4] = brightness;\n grainPixels[i * 4 + 1] = brightness;\n grainPixels[i * 4 + 2] = brightness;\n grainPixels[i * 4 + 3] = 255;\n }\n const grainCanvas = createCanvas(width, height);\n grainCanvas.getContext('2d').putImageData(new ImageData(grainPixels, width, height), 0, 0);\n applyOverlay(context, grainCanvas, look.grainOpacity);\n\n return { canvas, palette, accent: paletteAccent(palette), complement: paletteComplement(palette) };\n}\n\nfunction applyOverlay(context: SKRSContext2D, overlay: Canvas, opacity: number) {\n context.save();\n context.globalAlpha = opacity;\n context.globalCompositeOperation = 'overlay';\n context.drawImage(overlay, 0, 0);\n context.restore();\n}\n\n/**\n * Render a gradient and encode it. WebP (the default) is encoded with sharp;\n * PNG comes straight from the canvas.\n */\nexport async function generateImage(\n options: ResolvedMeshGradientOptions & MeshGradientFormatOptions,\n): Promise<MeshGradientImage> {\n const { format = 'webp', quality = DEFAULT_WEBP_QUALITY } = options;\n const { canvas, palette, accent, complement } = await renderCanvas(options);\n const pngBuffer = canvas.encodeSync('png');\n\n let buffer: Buffer = pngBuffer;\n if (format === 'webp') {\n buffer = await sharp(pngBuffer).webp({ quality }).toBuffer();\n }\n\n return {\n buffer,\n format,\n width: canvas.width,\n height: canvas.height,\n seed: options.seed,\n palette,\n accent,\n complement,\n };\n}\n","import {\n findPalette,\n pickPalette,\n validatePalette,\n validatePalettes,\n type Palette,\n} from './palettes';\nimport {\n DEFAULT_HEIGHT,\n DEFAULT_WIDTH,\n generateImage,\n renderCanvas,\n type MeshGradientCanvas,\n type MeshGradientFormatOptions,\n type MeshGradientImage,\n type MeshGradientOptions,\n} from './render';\n\nexport type CreateMeshGradientsOptions<P extends readonly Palette[]> = {\n palettes: P;\n /** Default output width in pixels for this generator. Per-call `width` wins. Defaults to 1200. */\n width?: number;\n /** Default output height in pixels for this generator. Per-call `height` wins. Defaults to 1500. */\n height?: number;\n};\n\nexport type MeshGradients<Name extends string> = {\n palettes: readonly Palette[];\n /** Look up a palette in this set by name. Accepts any string and throws on a miss. */\n findPalette: (name: Name | (string & {})) => Palette;\n /** Deterministically pick a palette in this set from a seed. */\n pickPalette: (seed: string) => Palette;\n /** Render to an `@napi-rs/canvas` canvas, e.g. to draw on top before encoding. */\n render: (options: MeshGradientOptions<Name>) => Promise<MeshGradientCanvas>;\n /** Render and encode to WebP (default) or PNG. */\n generate: (\n options: MeshGradientOptions<Name> & MeshGradientFormatOptions,\n ) => Promise<MeshGradientImage>;\n};\n\n/**\n * Create a generator bound to a palette set and default output size. Palettes\n * and size are validated here, so a bad config fails when the module loads\n * instead of on first render.\n *\n * @example\n * export const gradients = createMeshGradients({\n * palettes: [...colorPalettes, { name: 'brand', colors: ['#f4f4f5', '#8fa579', '#1f2c44'] }],\n * width: 1920,\n * height: 1080,\n * });\n */\nexport function createMeshGradients<const P extends readonly Palette[]>(\n options: CreateMeshGradientsOptions<P>,\n): MeshGradients<P[number]['name']> {\n const { palettes, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = options;\n validatePalettes(palettes);\n validateDimension('width', width);\n validateDimension('height', height);\n\n const resolve = (seed: string, palette: Palette | string | undefined): Palette => {\n if (palette === undefined) return pickPalette(seed, palettes);\n if (typeof palette === 'string') return findPalette(palette, palettes);\n validatePalette(palette);\n return palette;\n };\n\n const resolveSize = (size: { width?: number; height?: number }) => {\n const resolved = { width: size.width ?? width, height: size.height ?? height };\n validateDimension('width', resolved.width);\n validateDimension('height', resolved.height);\n return resolved;\n };\n\n return {\n palettes,\n findPalette: (name) => findPalette(name, palettes),\n pickPalette: (seed) => pickPalette(seed, palettes),\n // async so an invalid palette rejects like every other render error.\n render: async (renderOptions) =>\n renderCanvas({\n ...renderOptions,\n ...resolveSize(renderOptions),\n palette: resolve(renderOptions.seed, renderOptions.palette),\n }),\n generate: async (generateOptions) =>\n generateImage({\n ...generateOptions,\n ...resolveSize(generateOptions),\n palette: resolve(generateOptions.seed, generateOptions.palette),\n }),\n };\n}\n\nfunction validateDimension(label: 'width' | 'height', value: number): void {\n if (!Number.isInteger(value) || value <= 0) {\n throw new Error(`${label} must be a positive integer. Got ${value}.`);\n }\n}\n","import { createMeshGradients } from './create';\nimport { colorPalettes } from './palettes';\n\nexport {\n createMeshGradients,\n type CreateMeshGradientsOptions,\n type MeshGradients,\n} from './create';\nexport {\n colorPalettes,\n complementColor,\n findPalette,\n MAX_PALETTE_COLORS,\n MIN_PALETTE_COLORS,\n paletteAccent,\n paletteComplement,\n pickPalette,\n validatePalette,\n validatePalettes,\n type HexColor,\n type Palette,\n type PaletteColors,\n type PaletteName,\n} from './palettes';\nexport {\n DEFAULT_HEIGHT,\n DEFAULT_WEBP_QUALITY,\n DEFAULT_WIDTH,\n defaultLook,\n lookDescriptions,\n type GradientLook,\n type MeshGradientCanvas,\n type MeshGradientFormatOptions,\n type MeshGradientImage,\n type MeshGradientOptions,\n} from './render';\nexport { createSeededRandom, hashStringToInt } from './random';\n\n/** Generator bound to the built-in palettes. */\nconst builtIn = createMeshGradients({ palettes: colorPalettes });\n\n/** Render and encode a gradient using the built-in palettes. */\nexport const generateMeshGradient = builtIn.generate;\n/** Render a gradient to a canvas using the built-in palettes. */\nexport const renderMeshGradient = builtIn.render;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/render.ts","../src/sweep.ts","../src/create.ts","../src/index.ts"],"sourcesContent":["/*\n * render — the noise pipeline that turns a seed and a palette into pixels.\n *\n * ## behavior\n * - `renderCanvas` seeds one stream from the seed, builds three simplex noise\n * fields from it, domain-warps the third by the first two, and maps the result\n * along the palette ramp; the film grain then draws from the same stream.\n * - `rampPositionAt` is the fluid itself: noise-space point → ramp position.\n * The pixel loop calls it once per pixel. `look.sweep` / `look.ground`\n * (0–1, default 0) also hand it to `applySweep` in `./sweep`, which samples it\n * at moved points (off the canvas too) and rewrites the pixels before grain.\n * At 0/0 that pass doesn't run.\n * - `generateImage` wraps that and encodes: PNG straight off the canvas, WebP\n * through sharp at `DEFAULT_WEBP_QUALITY`.\n * - `lookDescriptions` is the one copy of the per-knob prose; `src/flags.ts`\n * builds CLI help from it and `scripts/sync-docs.mjs` builds the README table.\n *\n * ## constraints\n * - The order values are drawn from `random` is fixed: three `createNoise2D`\n * calls, then one grain value per pixel. Inserting a draw, removing one, or\n * reordering them shifts every pixel of every image — the grain loop is not\n * incidental noise that can be swapped for `Math.random`; it consumes the\n * tail of the same stream. `test/fluid-gradients.test.mjs` compares seed `aspect` /\n * `jewel-peacock` byte-for-byte against artwork already shipped in Aspect.\n * - Sweep and ground must not disturb the default path. The sweep pass draws\n * from its own `<seed>::sweep` stream, never `random`, and grain still comes\n * after it from `random`. Keep `{ sweep: 0, ground: 0 }` identical to omitting them,\n * down to the bytes; the Aspect fixture test checks exactly that.\n * - `defaultLook` and the bare constants in `rampPositionAt` — the `5.2` / `1.3`\n * warp offsets, `amplitude` starting at 0.5 and halving, `frequency`\n * doubling, the `smoothStep` around the contrast term — are the prototype's\n * values, not tuning suggestions. Any of them changes every image rendered\n * without an explicit `look`, which is almost all of them.\n * - Noise is sampled in normalized coordinates (`pixelX / width`), with the\n * aspect ratio applied to X only. This is what makes a 700×800 render the same\n * composition as the 1200×1500 default, which `scripts/generate-examples.mjs`\n * relies on to ship small previews of the real output. Sampling in absolute\n * pixels would make every size a different picture.\n * - `DEFAULT_WEBP_QUALITY` and the plain `sharp(...).webp({ quality })` call are\n * part of the shipped bytes. Adding encoder options (effort, lossless,\n * chroma subsampling) changes the fixture comparison even though nothing about\n * the rendered pixels moved.\n */\nimport { createCanvas, ImageData, type Canvas, type SKRSContext2D } from '@napi-rs/canvas';\nimport sharp from 'sharp';\nimport { createNoise2D } from 'simplex-noise';\nimport { applySweep } from './sweep';\nimport { paletteAccent, paletteComplement, type Palette, type PaletteName } from './palettes';\nimport { createSeededRandom, hashStringToInt } from './random';\n\n/**\n * Knobs that define the gradient's look. The defaults are the exact values from\n * the original prototype; changing them changes every image for a given seed.\n */\nexport type GradientLook = {\n /** Zoom of the noise field. Lower = broader, softer color regions. */\n baseNoiseFrequency: number;\n /** How far the warp noise displaces the color field. */\n domainWarpStrength: number;\n /** Fractal noise octaves. More = finer detail. */\n fractalOctaveCount: number;\n /** Sharpness of transitions between palette stops. */\n colorTransitionContrast: number;\n /** Opacity of the film-grain overlay, 0–1. */\n grainOpacity: number;\n /** Carries color into soft, curving wisps, like dye drifting in water, 0–1. 0 is the plain fluid. */\n sweep: number;\n /** Share of the image opened to the palette's lightest stop as a pale ground, 0–1. */\n ground: number;\n};\n\n/** One-line description per knob. Shared by the CLI help and the generated README table. */\nexport const lookDescriptions: Readonly<Record<keyof GradientLook, string>> = {\n baseNoiseFrequency: 'Lower values give broader, softer color regions.',\n domainWarpStrength: 'How much the color field swirls.',\n fractalOctaveCount: 'More octaves add finer detail.',\n colorTransitionContrast: 'Sharpness of the edges between colors.',\n grainOpacity: 'Strength of the film-grain overlay.',\n sweep: 'Drifts the color into soft, curving wisps, like dye in water or folded silk, 0–1. 0 is the plain fluid.',\n ground: \"Roughly the share of the image, 0–1, left as a pale ground in the palette's lightest color.\",\n};\n\nexport const defaultLook: Readonly<GradientLook> = {\n baseNoiseFrequency: 0.3,\n domainWarpStrength: 1.0,\n fractalOctaveCount: 2,\n colorTransitionContrast: 1.75,\n grainOpacity: 0.09,\n sweep: 0,\n ground: 0,\n};\n\nexport type MeshGradientOptions<Name extends string = PaletteName> = {\n /** Any string. The same seed + palette + size + look always renders the same image. */\n seed: string;\n /** Palette object, palette name, or omitted to pick deterministically from the seed. */\n palette?: Palette | Name;\n /** Pixels. Defaults to 1200. */\n width?: number;\n /** Pixels. Defaults to 1500. */\n height?: number;\n look?: Partial<GradientLook>;\n};\n\nexport type MeshGradientFormatOptions = {\n /** Defaults to 'webp'. */\n format?: 'webp' | 'png';\n /** WebP quality, 1–100. Defaults to 82. */\n quality?: number;\n};\n\n/** Options after the palette has been resolved to an object. */\nexport type ResolvedMeshGradientOptions = Omit<MeshGradientOptions, 'palette'> & {\n palette: Palette;\n};\n\nexport type MeshGradientCanvas = {\n canvas: Canvas;\n palette: Palette;\n /** Mid-tone hex from the palette, handy as a UI accent next to the image. */\n accent: string;\n /** Opposite-hue color built to stand out on the image, for chips and geometry. */\n complement: string;\n};\n\nexport type MeshGradientImage = {\n buffer: Buffer;\n format: 'webp' | 'png';\n width: number;\n height: number;\n seed: string;\n palette: Palette;\n accent: string;\n complement: string;\n};\n\nexport const DEFAULT_WIDTH = 1200;\nexport const DEFAULT_HEIGHT = 1500;\nexport const DEFAULT_WEBP_QUALITY = 82;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const packed = parseInt(hex.slice(1), 16);\n return [(packed >> 16) & 255, (packed >> 8) & 255, packed & 255];\n}\n\nfunction sampleColorRamp(\n rgbStops: [number, number, number][],\n position: number,\n): [number, number, number] {\n const clamped = Math.max(0, Math.min(1, position));\n const scaled = clamped * (rgbStops.length - 1);\n const lowerIndex = Math.min(rgbStops.length - 2, Math.floor(scaled));\n const blend = scaled - lowerIndex;\n const [lowR, lowG, lowB] = rgbStops[lowerIndex];\n const [highR, highG, highB] = rgbStops[lowerIndex + 1];\n return [\n lowR + (highR - lowR) * blend,\n lowG + (highG - lowG) * blend,\n lowB + (highB - lowB) * blend,\n ];\n}\n\nconst smoothStep = (edge: number) => edge * edge * (3 - 2 * edge);\n\n/**\n * Render a domain-warped noise gradient onto a canvas. The pixel pipeline and\n * the order random values are drawn in are verbatim from the prototype — both\n * must stay fixed for seeds to keep producing the same image.\n */\nexport async function renderCanvas(\n options: ResolvedMeshGradientOptions,\n): Promise<MeshGradientCanvas> {\n const { seed, palette, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = options;\n const look = { ...defaultLook, ...options.look };\n for (const knob of ['sweep', 'ground'] as const) {\n if (!(look[knob] >= 0 && look[knob] <= 1)) {\n throw new RangeError(`look.${knob} must be between 0 and 1. Got: ${look[knob]}`);\n }\n }\n\n const random = createSeededRandom(hashStringToInt(seed));\n const rgbStops = palette.colors.map(hexToRgb);\n\n const horizontalWarpNoise = createNoise2D(random);\n const verticalWarpNoise = createNoise2D(random);\n const colorFieldNoise = createNoise2D(random);\n\n const sampleFractalNoise = (\n noise: (x: number, y: number) => number,\n sampleX: number,\n sampleY: number,\n ) => {\n let total = 0;\n let amplitude = 0.5;\n let frequency = 1;\n for (let octave = 0; octave < look.fractalOctaveCount; octave++) {\n total += amplitude * noise(sampleX * frequency, sampleY * frequency);\n amplitude *= 0.5;\n frequency *= 2;\n }\n return total;\n };\n\n const canvas = createCanvas(width, height);\n const context = canvas.getContext('2d');\n const image = context.createImageData(width, height);\n const pixels = image.data;\n const aspectRatio = width / height;\n\n /** Position along the palette ramp (0–1) at a point in noise space. */\n const rampPositionAt = (sampleX: number, sampleY: number) => {\n const horizontalWarpOffset = sampleFractalNoise(horizontalWarpNoise, sampleX, sampleY);\n const verticalWarpOffset = sampleFractalNoise(verticalWarpNoise, sampleX + 5.2, sampleY + 1.3);\n\n const rawFieldValue = sampleFractalNoise(\n colorFieldNoise,\n sampleX + look.domainWarpStrength * horizontalWarpOffset,\n sampleY + look.domainWarpStrength * verticalWarpOffset,\n );\n const normalizedValue = (rawFieldValue + 1) / 2;\n\n return smoothStep(\n Math.max(0, Math.min(1, (normalizedValue - 0.5) * look.colorTransitionContrast + 0.5)),\n );\n };\n\n for (let pixelY = 0; pixelY < height; pixelY++) {\n for (let pixelX = 0; pixelX < width; pixelX++) {\n const sampleX = (pixelX / width) * look.baseNoiseFrequency * aspectRatio;\n const sampleY = (pixelY / height) * look.baseNoiseFrequency;\n const contrastedValue = rampPositionAt(sampleX, sampleY);\n\n const [red, green, blue] = sampleColorRamp(rgbStops, contrastedValue);\n const pixelIndex = (pixelY * width + pixelX) * 4;\n pixels[pixelIndex] = red;\n pixels[pixelIndex + 1] = green;\n pixels[pixelIndex + 2] = blue;\n pixels[pixelIndex + 3] = 255;\n }\n }\n\n if (look.sweep > 0 || look.ground > 0) {\n applySweep({\n seed,\n width,\n height,\n pixels,\n // Height units: the fluid's x runs 0…aspect, y runs 0…1.\n rampPositionAt: (u, v) => rampPositionAt(u * look.baseNoiseFrequency, v * look.baseNoiseFrequency),\n rgbStops,\n sampleRamp: (position) => sampleColorRamp(rgbStops, position),\n sweep: look.sweep,\n ground: look.ground,\n });\n }\n\n context.putImageData(image, 0, 0);\n\n // Film-grain overlay. Draws from the same random stream as the noise above.\n const grainPixels = new Uint8ClampedArray(width * height * 4);\n for (let i = 0; i < width * height; i++) {\n const brightness = Math.floor(random() * 256);\n grainPixels[i * 4] = brightness;\n grainPixels[i * 4 + 1] = brightness;\n grainPixels[i * 4 + 2] = brightness;\n grainPixels[i * 4 + 3] = 255;\n }\n const grainCanvas = createCanvas(width, height);\n grainCanvas.getContext('2d').putImageData(new ImageData(grainPixels, width, height), 0, 0);\n applyOverlay(context, grainCanvas, look.grainOpacity);\n\n return { canvas, palette, accent: paletteAccent(palette), complement: paletteComplement(palette) };\n}\n\nfunction applyOverlay(context: SKRSContext2D, overlay: Canvas, opacity: number) {\n context.save();\n context.globalAlpha = opacity;\n context.globalCompositeOperation = 'overlay';\n context.drawImage(overlay, 0, 0);\n context.restore();\n}\n\n/**\n * Render a gradient and encode it. WebP (the default) is encoded with sharp;\n * PNG comes straight from the canvas.\n */\nexport async function generateImage(\n options: ResolvedMeshGradientOptions & MeshGradientFormatOptions,\n): Promise<MeshGradientImage> {\n const { format = 'webp', quality = DEFAULT_WEBP_QUALITY } = options;\n const { canvas, palette, accent, complement } = await renderCanvas(options);\n const pngBuffer = canvas.encodeSync('png');\n\n let buffer: Buffer = pngBuffer;\n if (format === 'webp') {\n buffer = await sharp(pngBuffer).webp({ quality }).toBuffer();\n }\n\n return {\n buffer,\n format,\n width: canvas.width,\n height: canvas.height,\n seed: options.seed,\n palette,\n accent,\n complement,\n };\n}\n","/*\n * sweep — the opt-in second pass behind `look.sweep` and `look.ground`.\n *\n * \"Sweep\" is dye drifting in slow water, photographed soft: the fluid's color\n * is carried through a large, smooth current, so fast parts of the current\n * pull it into thin curving wisps and slow parts leave soft folds, like silk\n * or a long-exposure flower. \"Ground\" is the pale base the color floats over,\n * in the palette's lightest stop, opened where the fluid is already lightest\n * so it reads as space for text. Neither draws objects, adds fiber texture,\n * or twirls.\n *\n * ## behavior\n * - `applySweep` rewrites the RGB of the fluid pass's pixels in place, before\n * grain. It samples the fluid through `rampPositionAt` (the same function\n * the fluid loop uses, evaluable off the canvas) and maps back through the\n * same ramp, so every color is a ramp color or a blend toward the ground.\n * - Current: the curl of a low-frequency noise (divergence-free, so dye never\n * piles up or thins out), soft-capped below a seeded drift, plus that drift.\n * It slowly turns between two such fields over the trace, which folds the\n * wisps.\n * - Advection: each point is traced backward through the current for\n * `sweep × 1.6` image heights of drift and takes the fluid's color\n * from where it lands. At sweep 0 nothing moves.\n * - Softening: a slight defocus blur, then a line integral convolution along\n * the present current: each point blurs along its own path for one exposure\n * (typical travel `sweep × 0.5` heights, flat-topped window), so wisps blur\n * along their length more than across it, and still water barely blurs.\n * - Ground: coverage is the advected ramp position plus a domain-warped noise\n * sampled at the same advected points; the lowest `ground × 0.9` share of\n * the image becomes the lightest stop through a soft edge, which the\n * softening then carries along.\n * - Everything runs on a working grid at most 400 px tall and is upsampled;\n * about 1 s at 1200×1500.\n *\n * ## constraints\n * - Never draw from the fluid pass's `random`. Everything here comes from its\n * own `<seed>::sweep` stream. One extra draw from the main stream shifts the\n * grain of every image, sweep or not, and breaks the byte-for-byte Aspect\n * fixture. `render.ts` calls in only when `sweep > 0 || ground > 0`.\n * - Keep the eddies slower than the drift (`eddyCap × √2 < 1`). Where an eddy\n * outruns the drift its streamlines close, and the dye winds into bullseye\n * rings — the twirl look that was rejected. A much higher\n * `velocityFrequency` or longer `advectDistance` turns it into marbling.\n * - Alternatives already tried and rejected, so don't drift back to them:\n * painting along curl-noise flow lines (reads as Van Gogh brushwork), fiber\n * or strand feathering (watercolor, fur), and stretching along one camera\n * motion (too linear). The references are smooth, soft photographs.\n * - The velocity grid's margin must cover the farthest a trace can travel.\n * Traces that leave it read a clamped current and draw a hard-edged curve.\n * - The exposure follows the raw velocity, not its direction. Normalizing it\n * flips the blur's direction across every stagnation point, which shows as\n * sharp X-shaped creases.\n * - The constants below are part of the output for any seed rendered with\n * sweep or ground, as `defaultLook` is for the fluid. Retune them only as a\n * deliberate visual change, with before/after renders.\n */\nimport { createNoise2D } from 'simplex-noise';\nimport { createSeededRandom, hashStringToInt } from './random';\n\ntype Rgb = [number, number, number];\n\nexport type SweepInput = {\n seed: string;\n width: number;\n height: number;\n /** RGBA from the fluid pass, rewritten in place. Alpha is left alone. */\n pixels: Uint8ClampedArray;\n /** The fluid's ramp position (0–1) at any point, in height units; defined off the canvas too. */\n rampPositionAt: (u: number, v: number) => number;\n rgbStops: Rgb[];\n sampleRamp: (position: number) => Rgb;\n sweep: number;\n ground: number;\n};\n\n/** Lengths are in image heights, frequencies in cycles per image height. */\nconst tuning = {\n workHeight: 400,\n velocityFrequency: 0.4,\n /** Velocity grid cells per image height. */\n velocityResolution: 80,\n /** Travel at sweep 1, measured at drift speed. */\n advectDistance: 1.6,\n advectSteps: 32,\n /** Drift speed relative to the current's RMS speed. */\n drift: 1.0,\n /** Eddy speed cap, relative to the drift. Two fields mixed at most √2 ×\n * this, which must stay below 1 or eddies can close. */\n eddyCap: 0.65,\n /** Radians the current turns between its two fields over the trace. */\n timeSpin: 1.2,\n defocus: 0.008,\n smearLength: 0.5,\n smearSteps: 40,\n /** Fraction of the exposure at full weight before the window tapers. */\n smearFlat: 0.6,\n groundSoftness: 0.35,\n groundFieldWeight: 1.0,\n groundNoiseWeight: 0.6,\n groundBlur: 0.02,\n groundMaxCover: 0.9,\n groundFrequency: 0.9,\n} as const;\n\nconst smoothStep = (edge: number) => edge * edge * (3 - 2 * edge);\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\n\ntype Noise = (x: number, y: number) => number;\nconst fractal = (noise: Noise, x: number, y: number) =>\n 0.5 * noise(x, y) + 0.25 * noise(x * 2, y * 2);\n\n/** A scalar grid sampled bilinearly in its own pixel coordinates. */\nclass Grid {\n readonly values: Float32Array;\n constructor(\n readonly width: number,\n readonly height: number,\n ) {\n this.values = new Float32Array(width * height);\n }\n sample(x: number, y: number) {\n const gx = Math.max(0, Math.min(this.width - 1.0001, x));\n const gy = Math.max(0, Math.min(this.height - 1.0001, y));\n const x0 = Math.floor(gx);\n const y0 = Math.floor(gy);\n const fx = gx - x0;\n const fy = gy - y0;\n const v = this.values;\n const i = y0 * this.width + x0;\n const top = v[i] + (v[i + 1] - v[i]) * fx;\n const bottom = v[i + this.width] + (v[i + this.width + 1] - v[i + this.width]) * fx;\n return top + (bottom - top) * fy;\n }\n}\n\nexport function applySweep(input: SweepInput) {\n const { seed, width, height, pixels, rampPositionAt, rgbStops, sampleRamp, sweep, ground } = input;\n const t = tuning;\n const random = createSeededRandom(hashStringToInt(`${seed}::sweep`));\n const currentNoiseA = createNoise2D(random);\n const currentNoiseB = createNoise2D(random);\n const coverageNoise = createNoise2D(random);\n const coverageWarpNoise = createNoise2D(random);\n const driftAngle = random() * Math.PI * 2;\n\n // Working grid, in its own pixels; height units are y / workH.\n const scale = Math.min(1, t.workHeight / height);\n const workW = Math.max(2, Math.round(width * scale));\n const workH = Math.max(2, Math.round(height * scale));\n const aspect = width / height;\n\n // ── Current: two curl-noise fields at unit RMS speed, plus the drift. ──\n const driftX = Math.cos(driftAngle) * t.drift;\n const driftY = Math.sin(driftAngle) * t.drift;\n // Speed never exceeds the drift plus √2 × the eddy cap (< 2× drift), and\n // `advectDistance` is measured at drift speed, so 2× covers any trace.\n const margin = 2 * t.advectDistance * sweep + 0.1;\n const cell = 1 / t.velocityResolution;\n const velW = Math.ceil((aspect + 2 * margin) / cell) + 2;\n const velH = Math.ceil((1 + 2 * margin) / cell) + 2;\n const currents = [currentNoiseA, currentNoiseB].map((noise) => {\n const vx = new Grid(velW, velH);\n const vy = new Grid(velW, velH);\n const f = t.velocityFrequency;\n const e = 0.002;\n let total = 0;\n for (let gy = 0; gy < velH; gy++) {\n for (let gx = 0; gx < velW; gx++) {\n const u = (gx * cell - margin) * f;\n const v = (gy * cell - margin) * f;\n const i = gy * velW + gx;\n vx.values[i] = (fractal(noise, u, v + e) - fractal(noise, u, v - e)) / (2 * e);\n vy.values[i] = -(fractal(noise, u + e, v) - fractal(noise, u - e, v)) / (2 * e);\n total += vx.values[i] ** 2 + vy.values[i] ** 2;\n }\n }\n // Unit RMS speed, then a soft cap below the drift: the current then always\n // makes headway along the drift, so no streamline closes into an eddy and\n // nothing stalls.\n const rms = Math.sqrt(total / vx.values.length) || 1;\n const cap = t.eddyCap * t.drift;\n for (let i = 0; i < vx.values.length; i++) {\n const speed = Math.hypot(vx.values[i], vy.values[i]) / rms + 1e-9;\n const limited = (cap * Math.tanh(speed / cap)) / speed / rms;\n vx.values[i] *= limited;\n vy.values[i] *= limited;\n }\n return { vx, vy };\n });\n const velocity = (u: number, v: number, phase: number, out: number[]) => {\n const gx = (u + margin) / cell;\n const gy = (v + margin) / cell;\n const a = Math.cos(phase);\n const b = Math.sin(phase);\n out[0] = a * currents[0].vx.sample(gx, gy) + b * currents[1].vx.sample(gx, gy) + driftX;\n out[1] = a * currents[0].vy.sample(gx, gy) + b * currents[1].vy.sample(gx, gy) + driftY;\n };\n\n // ── Advection: trace each point back to where its dye came from. ──\n const flowX = new Grid(workW, workH);\n const flowY = new Grid(workW, workH);\n const fieldSource = new Grid(workW, workH);\n const sourceU = new Float32Array(workW * workH);\n const sourceV = new Float32Array(workW * workH);\n const steps = t.advectSteps;\n const dt = (sweep * t.advectDistance) / t.drift / steps;\n const k1 = [0, 0];\n const k2 = [0, 0];\n for (let y = 0; y < workH; y++) {\n for (let x = 0; x < workW; x++) {\n let u = x / workH;\n let v = y / workH;\n const i = y * workW + x;\n velocity(u, v, 0, k1);\n flowX.values[i] = k1[0];\n flowY.values[i] = k1[1];\n for (let s = 0; s < steps; s++) {\n const phase = (t.timeSpin * s) / steps;\n velocity(u, v, phase, k1);\n velocity(u - (k1[0] * dt) / 2, v - (k1[1] * dt) / 2, phase, k2);\n u -= k2[0] * dt;\n v -= k2[1] * dt;\n }\n sourceU[i] = u;\n sourceV[i] = v;\n fieldSource.values[i] = rampPositionAt(u, v);\n }\n }\n boxBlur(fieldSource.values, workW, workH, Math.max(1, Math.round(t.defocus * sweep * workH)), 3);\n\n // ── Ground: opens first where the fluid is already near the light end. ──\n const coverageSource = new Grid(workW, workH);\n if (ground > 0) {\n for (let y = 0; y < workH; y++) {\n for (let x = 0; x < workW; x++) {\n const i = y * workW + x;\n const su = sourceU[i] * t.groundFrequency;\n const sv = sourceV[i] * t.groundFrequency;\n const warpX = fractal(coverageWarpNoise, su, sv);\n const warpY = fractal(coverageWarpNoise, su + 5.2, sv + 1.3);\n coverageSource.values[i] =\n t.groundFieldWeight * fieldSource.values[i] + t.groundNoiseWeight * fractal(coverageNoise, su + warpX, sv + warpY);\n }\n }\n const sorted = Float32Array.from(coverageSource.values).sort();\n const threshold = sorted[Math.min(sorted.length - 1, Math.floor(ground * t.groundMaxCover * sorted.length))];\n for (let i = 0; i < coverageSource.values.length; i++) coverageSource.values[i] -= threshold;\n boxBlur(coverageSource.values, workW, workH, Math.max(1, Math.round(t.groundBlur * workH)), 3);\n }\n\n // ── Exposure: average along the present current (line integral convolution). ──\n const smearedField = new Grid(workW, workH);\n const smearedCoverage = new Grid(workW, workH);\n // Blur is speed × exposure time, so it fades to nothing where the current\n // stalls; a fixed length there would flip direction between neighbors and\n // draw a hard crease. Exposure time is set so typical travel is the length.\n const smearSteps = t.smearSteps;\n const step = (sweep * t.smearLength * workH) / 2 / Math.hypot(1, t.drift) / smearSteps;\n const weights = new Float32Array(smearSteps + 1);\n for (let s = 0; s <= smearSteps; s++) {\n const along = s / (smearSteps + 1);\n weights[s] = along <= t.smearFlat ? 1 : 0.5 + 0.5 * Math.cos((Math.PI * (along - t.smearFlat)) / (1 - t.smearFlat));\n }\n for (let y = 0; y < workH; y++) {\n for (let x = 0; x < workW; x++) {\n let fieldTotal = weights[0] * fieldSource.sample(x, y);\n let coverageTotal = ground > 0 ? weights[0] * coverageSource.sample(x, y) : 0;\n let weightTotal = weights[0];\n for (let direction = -1; direction <= 1; direction += 2) {\n let px = x;\n let py = y;\n for (let s = 1; s <= smearSteps; s++) {\n const mx = px + (direction * flowX.sample(px, py) * step) / 2;\n const my = py + (direction * flowY.sample(px, py) * step) / 2;\n px += direction * flowX.sample(mx, my) * step;\n py += direction * flowY.sample(mx, my) * step;\n if (px < 0 || py < 0 || px > workW - 1 || py > workH - 1) break;\n const w = weights[s];\n fieldTotal += w * fieldSource.sample(px, py);\n if (ground > 0) coverageTotal += w * coverageSource.sample(px, py);\n weightTotal += w;\n }\n }\n smearedField.values[y * workW + x] = fieldTotal / weightTotal;\n smearedCoverage.values[y * workW + x] = coverageTotal / weightTotal;\n }\n }\n\n // ── Compose at full resolution. ──\n const groundColor = rgbStops[0];\n for (let py = 0; py < height; py++) {\n for (let px = 0; px < width; px++) {\n const sx = (px + 0.5) * scale - 0.5;\n const sy = (py + 0.5) * scale - 0.5;\n let [r, g, b] = sampleRamp(smearedField.sample(sx, sy));\n if (ground > 0) {\n const m = smoothStep(clamp01(smearedCoverage.sample(sx, sy) / t.groundSoftness + 0.5));\n r = groundColor[0] + (r - groundColor[0]) * m;\n g = groundColor[1] + (g - groundColor[1]) * m;\n b = groundColor[2] + (b - groundColor[2]) * m;\n }\n const o = (py * width + px) * 4;\n pixels[o] = r;\n pixels[o + 1] = g;\n pixels[o + 2] = b;\n }\n }\n}\n\nfunction boxBlur(values: Float32Array, width: number, height: number, radius: number, passes: number) {\n const scratch = new Float32Array(values.length);\n for (let pass = 0; pass < passes; pass++) {\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n let total = 0;\n for (let k = -radius; k <= radius; k++) total += values[y * width + Math.max(0, Math.min(width - 1, x + k))];\n scratch[y * width + x] = total / (2 * radius + 1);\n }\n }\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n let total = 0;\n for (let k = -radius; k <= radius; k++) total += scratch[Math.max(0, Math.min(height - 1, y + k)) * width + x];\n values[y * width + x] = total / (2 * radius + 1);\n }\n }\n }\n}\n","import {\n findPalette,\n pickPalette,\n validatePalette,\n validatePalettes,\n type Palette,\n} from './palettes';\nimport {\n DEFAULT_HEIGHT,\n DEFAULT_WIDTH,\n generateImage,\n renderCanvas,\n type MeshGradientCanvas,\n type MeshGradientFormatOptions,\n type MeshGradientImage,\n type MeshGradientOptions,\n} from './render';\n\nexport type CreateMeshGradientsOptions<P extends readonly Palette[]> = {\n palettes: P;\n /** Default output width in pixels for this generator. Per-call `width` wins. Defaults to 1200. */\n width?: number;\n /** Default output height in pixels for this generator. Per-call `height` wins. Defaults to 1500. */\n height?: number;\n};\n\nexport type MeshGradients<Name extends string> = {\n palettes: readonly Palette[];\n /** Look up a palette in this set by name. Accepts any string and throws on a miss. */\n findPalette: (name: Name | (string & {})) => Palette;\n /** Deterministically pick a palette in this set from a seed. */\n pickPalette: (seed: string) => Palette;\n /** Render to an `@napi-rs/canvas` canvas, e.g. to draw on top before encoding. */\n render: (options: MeshGradientOptions<Name>) => Promise<MeshGradientCanvas>;\n /** Render and encode to WebP (default) or PNG. */\n generate: (\n options: MeshGradientOptions<Name> & MeshGradientFormatOptions,\n ) => Promise<MeshGradientImage>;\n};\n\n/**\n * Create a generator bound to a palette set and default output size. Palettes\n * and size are validated here, so a bad config fails when the module loads\n * instead of on first render.\n *\n * @example\n * export const gradients = createMeshGradients({\n * palettes: [...colorPalettes, { name: 'brand', colors: ['#f4f4f5', '#8fa579', '#1f2c44'] }],\n * width: 1920,\n * height: 1080,\n * });\n */\nexport function createMeshGradients<const P extends readonly Palette[]>(\n options: CreateMeshGradientsOptions<P>,\n): MeshGradients<P[number]['name']> {\n const { palettes, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = options;\n validatePalettes(palettes);\n validateDimension('width', width);\n validateDimension('height', height);\n\n const resolve = (seed: string, palette: Palette | string | undefined): Palette => {\n if (palette === undefined) return pickPalette(seed, palettes);\n if (typeof palette === 'string') return findPalette(palette, palettes);\n validatePalette(palette);\n return palette;\n };\n\n const resolveSize = (size: { width?: number; height?: number }) => {\n const resolved = { width: size.width ?? width, height: size.height ?? height };\n validateDimension('width', resolved.width);\n validateDimension('height', resolved.height);\n return resolved;\n };\n\n return {\n palettes,\n findPalette: (name) => findPalette(name, palettes),\n pickPalette: (seed) => pickPalette(seed, palettes),\n // async so an invalid palette rejects like every other render error.\n render: async (renderOptions) =>\n renderCanvas({\n ...renderOptions,\n ...resolveSize(renderOptions),\n palette: resolve(renderOptions.seed, renderOptions.palette),\n }),\n generate: async (generateOptions) =>\n generateImage({\n ...generateOptions,\n ...resolveSize(generateOptions),\n palette: resolve(generateOptions.seed, generateOptions.palette),\n }),\n };\n}\n\nfunction validateDimension(label: 'width' | 'height', value: number): void {\n if (!Number.isInteger(value) || value <= 0) {\n throw new Error(`${label} must be a positive integer. Got ${value}.`);\n }\n}\n","import { createMeshGradients } from './create';\nimport { colorPalettes } from './palettes';\n\nexport {\n createMeshGradients,\n type CreateMeshGradientsOptions,\n type MeshGradients,\n} from './create';\nexport {\n colorPalettes,\n complementColor,\n findPalette,\n MAX_PALETTE_COLORS,\n MIN_PALETTE_COLORS,\n paletteAccent,\n paletteComplement,\n pickPalette,\n validatePalette,\n validatePalettes,\n type HexColor,\n type Palette,\n type PaletteColors,\n type PaletteName,\n} from './palettes';\nexport {\n DEFAULT_HEIGHT,\n DEFAULT_WEBP_QUALITY,\n DEFAULT_WIDTH,\n defaultLook,\n lookDescriptions,\n type GradientLook,\n type MeshGradientCanvas,\n type MeshGradientFormatOptions,\n type MeshGradientImage,\n type MeshGradientOptions,\n} from './render';\nexport { createSeededRandom, hashStringToInt } from './random';\n\n/** Generator bound to the built-in palettes. */\nconst builtIn = createMeshGradients({ palettes: colorPalettes });\n\n/** Render and encode a gradient using the built-in palettes. */\nexport const generateMeshGradient = builtIn.generate;\n/** Render a gradient to a canvas using the built-in palettes. */\nexport const renderMeshGradient = builtIn.render;\n"],"mappings":"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","names":["createNoise2D","smoothStep","createNoise2D"]}
|
package/dist/palettes.cjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
var
|
|
12
|
+
var _chunkVF6TCELOcjs = require('./chunk-VF6TCELO.cjs');
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
|
|
@@ -21,5 +21,5 @@ var _chunkCRRF2Q64cjs = require('./chunk-CRRF2Q64.cjs');
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
exports.MAX_PALETTE_COLORS =
|
|
24
|
+
exports.MAX_PALETTE_COLORS = _chunkVF6TCELOcjs.MAX_PALETTE_COLORS; exports.MIN_PALETTE_COLORS = _chunkVF6TCELOcjs.MIN_PALETTE_COLORS; exports.colorPalettes = _chunkVF6TCELOcjs.colorPalettes; exports.complementColor = _chunkVF6TCELOcjs.complementColor; exports.findPalette = _chunkVF6TCELOcjs.findPalette; exports.paletteAccent = _chunkVF6TCELOcjs.paletteAccent; exports.paletteComplement = _chunkVF6TCELOcjs.paletteComplement; exports.pickPalette = _chunkVF6TCELOcjs.pickPalette; exports.validatePalette = _chunkVF6TCELOcjs.validatePalette; exports.validatePalettes = _chunkVF6TCELOcjs.validatePalettes;
|
|
25
25
|
//# sourceMappingURL=palettes.cjs.map
|
package/dist/palettes.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/fluid-gradients/fluid-gradients/dist/palettes.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,2lBAAC","file":"/home/runner/work/fluid-gradients/fluid-gradients/dist/palettes.cjs"}
|
package/dist/palettes.d.cts
CHANGED
|
@@ -210,6 +210,60 @@ declare const colorPalettes: readonly [{
|
|
|
210
210
|
}, {
|
|
211
211
|
readonly name: "slate-harbor";
|
|
212
212
|
readonly colors: readonly ["#aeb7bd", "#6f7b84", "#3c454c", "#1a1f24"];
|
|
213
|
+
}, {
|
|
214
|
+
readonly name: "ember-mono";
|
|
215
|
+
readonly colors: readonly ["#ff9a6b", "#cf5604", "#632400", "#180501"];
|
|
216
|
+
}, {
|
|
217
|
+
readonly name: "tangerine-mono";
|
|
218
|
+
readonly colors: readonly ["#ffeee4", "#feb079", "#dc7100", "#824103"];
|
|
219
|
+
}, {
|
|
220
|
+
readonly name: "electric-blue-mono";
|
|
221
|
+
readonly colors: readonly ["#ebf2fe", "#a4c6fc", "#5d91e7", "#2a5397"];
|
|
222
|
+
}, {
|
|
223
|
+
readonly name: "magenta-mono";
|
|
224
|
+
readonly colors: readonly ["#feecf5", "#edaed1", "#cd6ca6", "#823564"];
|
|
225
|
+
}, {
|
|
226
|
+
readonly name: "emerald-mono";
|
|
227
|
+
readonly colors: readonly ["#e2f8ea", "#95d5af", "#2cab70", "#03663e"];
|
|
228
|
+
}, {
|
|
229
|
+
readonly name: "violet-mono";
|
|
230
|
+
readonly colors: readonly ["#f2f0fe", "#c8b7ff", "#9e71fd", "#5f38a7"];
|
|
231
|
+
}, {
|
|
232
|
+
readonly name: "crimson-mono";
|
|
233
|
+
readonly colors: readonly ["#ffedec", "#feaaa9", "#f44f5d", "#9e122b"];
|
|
234
|
+
}, {
|
|
235
|
+
readonly name: "aqua-mono";
|
|
236
|
+
readonly colors: readonly ["#dff7f8", "#8dd2d6", "#39a4a9", "#026266"];
|
|
237
|
+
}, {
|
|
238
|
+
readonly name: "burgundy-mono";
|
|
239
|
+
readonly colors: readonly ["#fbe0e4", "#dc9ea8", "#a7415a", "#560c24"];
|
|
240
|
+
}, {
|
|
241
|
+
readonly name: "indigo-mono";
|
|
242
|
+
readonly colors: readonly ["#eaeefe", "#a7b2f7", "#5a5ce2", "#261b81"];
|
|
243
|
+
}, {
|
|
244
|
+
readonly name: "lime-mono";
|
|
245
|
+
readonly colors: readonly ["#edfcd7", "#c2e87d", "#9fc93e", "#638119"];
|
|
246
|
+
}, {
|
|
247
|
+
readonly name: "terracotta-mono";
|
|
248
|
+
readonly colors: readonly ["#fdeae4", "#f6b19b", "#c5694c", "#73311b"];
|
|
249
|
+
}, {
|
|
250
|
+
readonly name: "ochre-mono";
|
|
251
|
+
readonly colors: readonly ["#f9edd5", "#dec082", "#a87f09", "#5b4404"];
|
|
252
|
+
}, {
|
|
253
|
+
readonly name: "olive-mono";
|
|
254
|
+
readonly colors: readonly ["#edf2d7", "#c2cb91", "#848e38", "#474d04"];
|
|
255
|
+
}, {
|
|
256
|
+
readonly name: "fern-mono";
|
|
257
|
+
readonly colors: readonly ["#e3f5df", "#a9d1a0", "#5f9752", "#2a5320"];
|
|
258
|
+
}, {
|
|
259
|
+
readonly name: "verdigris-mono";
|
|
260
|
+
readonly colors: readonly ["#d6f7f1", "#96d2c9", "#38988d", "#07534c"];
|
|
261
|
+
}, {
|
|
262
|
+
readonly name: "umber-mono";
|
|
263
|
+
readonly colors: readonly ["#feeada", "#e1bc9d", "#ab7a4f", "#623f1e"];
|
|
264
|
+
}, {
|
|
265
|
+
readonly name: "adobe-mono";
|
|
266
|
+
readonly colors: readonly ["#fee9e8", "#eeb3b0", "#bb6d6b", "#6c3434"];
|
|
213
267
|
}, {
|
|
214
268
|
readonly name: "ivory-ink";
|
|
215
269
|
readonly colors: readonly ["#f7f4ef", "#d8d2c8", "#8a8378", "#20201d"];
|
|
@@ -228,6 +282,24 @@ declare const colorPalettes: readonly [{
|
|
|
228
282
|
}, {
|
|
229
283
|
readonly name: "gunmetal-fog";
|
|
230
284
|
readonly colors: readonly ["#eceef1", "#c2c7cd", "#7b838d", "#343a42"];
|
|
285
|
+
}, {
|
|
286
|
+
readonly name: "silver-steel";
|
|
287
|
+
readonly colors: readonly ["#e2e5e8", "#c4c9cf", "#9aa2ac", "#646f7d"];
|
|
288
|
+
}, {
|
|
289
|
+
readonly name: "petal-bloom";
|
|
290
|
+
readonly colors: readonly ["#e4eeff", "#bcd2ff", "#dfa6e9", "#fa787b", "#fba96a"];
|
|
291
|
+
}, {
|
|
292
|
+
readonly name: "tulip-blur";
|
|
293
|
+
readonly colors: readonly ["#e6f1f6", "#a8d4e8", "#f1c4d0", "#ee9a6e", "#f4b81c"];
|
|
294
|
+
}, {
|
|
295
|
+
readonly name: "silk-rose";
|
|
296
|
+
readonly colors: readonly ["#fcedf4", "#f2cfdd", "#f0c1b8", "#d68ba5", "#b07a9f"];
|
|
297
|
+
}, {
|
|
298
|
+
readonly name: "sultans-eagle";
|
|
299
|
+
readonly colors: readonly ["#f2da38", "#e3c42a", "#c9a21d", "#a1353a", "#6a1c26"];
|
|
300
|
+
}, {
|
|
301
|
+
readonly name: "zhuge-nu";
|
|
302
|
+
readonly colors: readonly ["#f0f3ee", "#dfc652", "#4fa79b", "#1f8283", "#0f5252"];
|
|
231
303
|
}];
|
|
232
304
|
/** Union of every built-in palette name, e.g. `"mint-sea"`. */
|
|
233
305
|
type PaletteName = (typeof colorPalettes)[number]['name'];
|