@lacspace/image 1.0.0 → 1.1.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 CHANGED
@@ -58,6 +58,20 @@ console.log(formatBytes(r.size), "at q", r.quality, `${r.width}×${r.height}`);
58
58
 
59
59
  > It's a best-effort **ceiling** — the closest fit ≤ your budget within the quality/scale bounds, never padded up to it.
60
60
 
61
+ ## 🆕 Generators (v1.1.0)
62
+
63
+ Deterministic, no-AI building blocks — same input, same image:
64
+
65
+ ```ts
66
+ import { identicon, mesh, placeholder, encode } from "@lacspace/image";
67
+
68
+ const avatar = identicon("ada@lacspace.com", { size: 240 }); // symmetric avatar from any string
69
+ const bg = mesh(1200, 630, { seed: "brand" }); // smooth multi-point mesh gradient
70
+ const lqip = placeholder(600, 315, { seed: "hero-1" }); // tasteful seeded gradient placeholder
71
+
72
+ const png = await encode(avatar, { format: "png" });
73
+ ```
74
+
61
75
  ## The surface API
62
76
 
63
77
  `Surface` is a plain RGBA pixel buffer with a small, chainable drawing API. Everything is deterministic and identical across Node and the browser.
@@ -129,6 +143,10 @@ formatBytes(1536); // "1.5 KB"
129
143
  - `rasterizeSvg(svg, { width?, height?, background? })`
130
144
  - `Surface`, `gradient`, `radial`, `pattern`, `parseColor`, `parseSize`, `formatBytes`, `hasCanvas`, `canvasSupports`
131
145
 
146
+ ## Contributing 💙
147
+
148
+ New patterns, generators and encoders are welcome — see [CONTRIBUTING.md](https://github.com/lacspace/npm-packages/blob/main/CONTRIBUTING.md). Open an issue or PR; first-time contributors welcome.
149
+
132
150
  ## License
133
151
 
134
152
  [Lacspace Free Licence v1.0](https://github.com/lacspace/npm-packages/blob/main/LICENSE) — free to use, permissive, Lacspace-branded.
package/dist/index.cjs CHANGED
@@ -369,6 +369,119 @@ function pattern(width, height, base, kind, opts) {
369
369
  return new Surface(width, height).fill(base).pattern(kind, opts);
370
370
  }
371
371
 
372
+ // src/generators.ts
373
+ function hash32(str) {
374
+ let h = 2166136261 >>> 0;
375
+ for (let i = 0; i < str.length; i++) {
376
+ h ^= str.charCodeAt(i);
377
+ h = Math.imul(h, 16777619);
378
+ }
379
+ return h >>> 0;
380
+ }
381
+ function rng2(seed) {
382
+ let s = seed >>> 0 || 1;
383
+ return () => {
384
+ s |= 0;
385
+ s = s + 1831565813 | 0;
386
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
387
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
388
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
389
+ };
390
+ }
391
+ function hsl(h, s, l) {
392
+ h = (h % 360 + 360) % 360;
393
+ const c = (1 - Math.abs(2 * l - 1)) * s;
394
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
395
+ const m = l - c / 2;
396
+ let r = 0, g = 0, b = 0;
397
+ if (h < 60) [r, g, b] = [c, x, 0];
398
+ else if (h < 120) [r, g, b] = [x, c, 0];
399
+ else if (h < 180) [r, g, b] = [0, c, x];
400
+ else if (h < 240) [r, g, b] = [0, x, c];
401
+ else if (h < 300) [r, g, b] = [x, 0, c];
402
+ else [r, g, b] = [c, 0, x];
403
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), 255];
404
+ }
405
+ function identicon(seed, opts = {}) {
406
+ const size = opts.size ?? 240;
407
+ const grid = Math.max(3, (opts.grid ?? 5) | 1);
408
+ const h = hash32(seed);
409
+ const rand = rng2(h);
410
+ const fgRgba = hsl(h % 360, 0.62, 0.58);
411
+ const fg = opts.color ?? `rgb(${fgRgba[0]},${fgRgba[1]},${fgRgba[2]})`;
412
+ const s = new Surface(size, size).fill(opts.background ?? "#0f1117");
413
+ const pad = Math.round(size * (opts.padding ?? 0.12));
414
+ const cell = (size - pad * 2) / grid;
415
+ const half = Math.ceil(grid / 2);
416
+ for (let col = 0; col < half; col++) {
417
+ for (let row = 0; row < grid; row++) {
418
+ if (rand() > 0.5) {
419
+ const mirror = grid - 1 - col;
420
+ const y = pad + row * cell;
421
+ s.rect(pad + col * cell, y, Math.ceil(cell), Math.ceil(cell), fg);
422
+ if (mirror !== col) s.rect(pad + mirror * cell, y, Math.ceil(cell), Math.ceil(cell), fg);
423
+ }
424
+ }
425
+ }
426
+ return s;
427
+ }
428
+ var DEFAULT_MESH = ["#0BB9D9", "#3B82F6", "#7C3AED", "#EC4899", "#22D3EE"];
429
+ function mesh(width, height, opts = {}) {
430
+ const palette = (opts.colors ?? DEFAULT_MESH).map(parseColor);
431
+ const n = Math.max(2, opts.points ?? palette.length);
432
+ const seed = typeof opts.seed === "number" ? opts.seed : hash32(String(opts.seed ?? "mesh"));
433
+ const rand = rng2(seed);
434
+ const pts = Array.from({ length: n }, (_, i) => ({
435
+ x: rand() * width,
436
+ y: rand() * height,
437
+ c: palette[i % palette.length]
438
+ }));
439
+ const out = new Surface(width, height);
440
+ const d = out.data;
441
+ for (let y = 0; y < height; y++) {
442
+ for (let x = 0; x < width; x++) {
443
+ let wr = 0, wg = 0, wb = 0, wsum = 0;
444
+ for (const p of pts) {
445
+ const dx = x - p.x;
446
+ const dy = y - p.y;
447
+ const w = 1 / (dx * dx + dy * dy + 800);
448
+ wr += p.c[0] * w;
449
+ wg += p.c[1] * w;
450
+ wb += p.c[2] * w;
451
+ wsum += w;
452
+ }
453
+ const i = (y * width + x) * 4;
454
+ d[i] = wr / wsum;
455
+ d[i + 1] = wg / wsum;
456
+ d[i + 2] = wb / wsum;
457
+ d[i + 3] = 255;
458
+ }
459
+ }
460
+ return out;
461
+ }
462
+ function placeholder(width, height, opts = {}) {
463
+ const seed = typeof opts.seed === "number" ? opts.seed : hash32(String(opts.seed ?? `${width}x${height}`));
464
+ const rand = rng2(seed);
465
+ let from, to;
466
+ if (opts.colors) {
467
+ [from, to] = [parseColor(opts.colors[0]), parseColor(opts.colors[1])];
468
+ } else {
469
+ const base = Math.floor(rand() * 360);
470
+ from = hsl(base, 0.55, 0.55);
471
+ to = hsl(base + 40 + rand() * 60, 0.55, 0.45);
472
+ }
473
+ const angle = Math.floor(rand() * 180);
474
+ const s = new Surface(width, height).linearGradient({
475
+ angle,
476
+ stops: [
477
+ { offset: 0, color: `rgb(${from[0]},${from[1]},${from[2]})` },
478
+ { offset: 1, color: `rgb(${to[0]},${to[1]},${to[2]})` }
479
+ ]
480
+ });
481
+ if (opts.texture !== false) s.pattern("dots", { size: Math.max(20, Math.round(width / 24)), color: "rgba(255,255,255,0.08)" });
482
+ return s;
483
+ }
484
+
372
485
  // src/env.ts
373
486
  function hasCanvas() {
374
487
  if (typeof OffscreenCanvas !== "undefined") return true;
@@ -1589,9 +1702,12 @@ exports.fit = fit;
1589
1702
  exports.formatBytes = formatBytes;
1590
1703
  exports.gradient = gradient;
1591
1704
  exports.hasCanvas = hasCanvas;
1705
+ exports.identicon = identicon;
1706
+ exports.mesh = mesh;
1592
1707
  exports.parseColor = parseColor;
1593
1708
  exports.parseSize = parseSize;
1594
1709
  exports.pattern = pattern;
1710
+ exports.placeholder = placeholder;
1595
1711
  exports.radial = radial;
1596
1712
  exports.rasterizeSvg = rasterizeSvg;
1597
1713
  //# sourceMappingURL=index.cjs.map