@forgeax/engine-image 0.1.20 → 0.1.21

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.mjs CHANGED
@@ -19,7 +19,8 @@ var IMAGE_ERROR_EXPECTED = {
19
19
  // .hint after switch (err.code) without parsing the message).
20
20
  "atlas-empty-input": "images.length >= 1",
21
21
  "atlas-size-exceeded": "image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint",
22
- "atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight"
22
+ "atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight",
23
+ "image-surface-invalid": "PixelSurface dimensions and authoring inputs satisfy the RGBA8 contract"
23
24
  };
24
25
  var ImageErrorImpl = class extends Error {
25
26
  code;
@@ -273,6 +274,314 @@ async function loadJpeg() {
273
274
  return mod.default ?? mod;
274
275
  }
275
276
 
277
+ // src/to-asset-pack.ts
278
+ function toAssetPack(decoded, meta) {
279
+ return {
280
+ schemaVersion: "1.0.0",
281
+ kind: "external-asset-package",
282
+ importer: "image",
283
+ source: "",
284
+ importSettings: {
285
+ colorSpace: meta.colorSpace,
286
+ mipmap: meta.mipmap,
287
+ addressMode: meta.addressMode,
288
+ filterMode: meta.filterMode,
289
+ ...meta.downscaleMaxDimension !== void 0 ? { downscaleMaxDimension: meta.downscaleMaxDimension } : {}
290
+ },
291
+ subAssets: [
292
+ {
293
+ guid: meta.guid,
294
+ sourceIndex: 0,
295
+ kind: "texture"
296
+ }
297
+ ]
298
+ };
299
+ }
300
+
301
+ // src/pixel-surface.ts
302
+ function invalid(operation, field, value, expected) {
303
+ return err(
304
+ imageError({
305
+ code: "image-surface-invalid",
306
+ operation,
307
+ field,
308
+ value,
309
+ expected
310
+ })
311
+ );
312
+ }
313
+ function finiteNumber(operation, field, value) {
314
+ if (!Number.isFinite(value)) {
315
+ return invalid(operation, field, String(value), "a finite number");
316
+ }
317
+ return ok(value);
318
+ }
319
+ function positiveDimension(operation, field, value) {
320
+ if (!Number.isInteger(value) || value <= 0) {
321
+ return invalid(operation, field, value, "a positive integer");
322
+ }
323
+ return ok(value);
324
+ }
325
+ function colorChannels(operation, color) {
326
+ const channels = Array.isArray(color) ? color : color !== null && typeof color === "object" ? [
327
+ color.r,
328
+ color.g,
329
+ color.b,
330
+ color.a
331
+ ] : void 0;
332
+ if (channels === void 0 || channels.length !== 4) {
333
+ return invalid(operation, "color", "malformed", "four finite RGBA8 channels");
334
+ }
335
+ const normalized = [0, 0, 0, 0];
336
+ for (let index = 0; index < channels.length; index += 1) {
337
+ const channel = channels[index];
338
+ if (channel === void 0 || !Number.isFinite(channel) || channel < 0 || channel > 255) {
339
+ return invalid(
340
+ operation,
341
+ `color[${index}]`,
342
+ channel ?? "missing",
343
+ "a finite number in [0, 255]"
344
+ );
345
+ }
346
+ normalized[index] = Math.round(channel);
347
+ }
348
+ return ok(normalized);
349
+ }
350
+ function rounded(operation, field, value) {
351
+ const result = finiteNumber(operation, field, value);
352
+ return result.ok ? ok(Math.round(result.value)) : result;
353
+ }
354
+ function rectangle(operation, x, y, width, height) {
355
+ const values = [x, y, width, height];
356
+ for (let index = 0; index < values.length; index += 1) {
357
+ const value = values[index];
358
+ if (value === void 0 || !Number.isFinite(value)) {
359
+ return invalid(
360
+ operation,
361
+ ["x", "y", "width", "height"][index] ?? "rectangle",
362
+ String(value),
363
+ "a finite number"
364
+ );
365
+ }
366
+ }
367
+ if (width <= 0 || height <= 0) {
368
+ return invalid(
369
+ operation,
370
+ width <= 0 ? "width" : "height",
371
+ width <= 0 ? width : height,
372
+ "a positive number"
373
+ );
374
+ }
375
+ return ok([Math.round(x), Math.round(y), Math.round(width), Math.round(height)]);
376
+ }
377
+ function writePixel(data, width, x, y, color) {
378
+ if (x < 0 || y < 0 || x >= width) return;
379
+ const offset = (y * width + x) * 4;
380
+ data[offset] = color[0] ?? 0;
381
+ data[offset + 1] = color[1] ?? 0;
382
+ data[offset + 2] = color[2] ?? 0;
383
+ data[offset + 3] = color[3] ?? 0;
384
+ }
385
+ function fillClippedRect(data, width, height, x, y, rectWidth, rectHeight, color) {
386
+ const left = Math.max(0, x);
387
+ const top = Math.max(0, y);
388
+ const right = Math.min(width, x + rectWidth);
389
+ const bottom = Math.min(height, y + rectHeight);
390
+ for (let row = top; row < bottom; row += 1) {
391
+ for (let column = left; column < right; column += 1) {
392
+ writePixel(data, width, column, row, color);
393
+ }
394
+ }
395
+ }
396
+ function nextRandom(state) {
397
+ let value = state >>> 0;
398
+ value ^= value << 13;
399
+ value ^= value >>> 17;
400
+ value ^= value << 5;
401
+ return value >>> 0;
402
+ }
403
+ function makeSurface(options) {
404
+ const width = positiveDimension("create", "width", options.width);
405
+ if (!width.ok) return width;
406
+ const height = positiveDimension("create", "height", options.height);
407
+ if (!height.ok) return height;
408
+ if (options.colorSpace !== void 0 && options.colorSpace !== "srgb" && options.colorSpace !== "linear") {
409
+ return invalid("create", "colorSpace", String(options.colorSpace), "'srgb' or 'linear'");
410
+ }
411
+ if (options.mipmap !== void 0 && typeof options.mipmap !== "boolean") {
412
+ return invalid("create", "mipmap", String(options.mipmap), "a boolean");
413
+ }
414
+ const data = new Uint8Array(width.value * height.value * 4);
415
+ const colorSpace = options.colorSpace ?? "srgb";
416
+ const mipmap = options.mipmap ?? false;
417
+ const surface = {
418
+ width: width.value,
419
+ height: height.value,
420
+ colorSpace,
421
+ mipmap,
422
+ data,
423
+ setPixel(x, y, color) {
424
+ const px = rounded("set-pixel", "x", x);
425
+ if (!px.ok) return px;
426
+ const py = rounded("set-pixel", "y", y);
427
+ if (!py.ok) return py;
428
+ const normalized = colorChannels("set-pixel", color);
429
+ if (!normalized.ok) return normalized;
430
+ writePixel(data, width.value, px.value, py.value, normalized.value);
431
+ return ok(void 0);
432
+ },
433
+ fillRect(x, y, rectWidth, rectHeight, color) {
434
+ const rect = rectangle("fill-rect", x, y, rectWidth, rectHeight);
435
+ if (!rect.ok) return rect;
436
+ const normalized = colorChannels("fill-rect", color);
437
+ if (!normalized.ok) return normalized;
438
+ fillClippedRect(
439
+ data,
440
+ width.value,
441
+ height.value,
442
+ rect.value[0] ?? 0,
443
+ rect.value[1] ?? 0,
444
+ rect.value[2] ?? 0,
445
+ rect.value[3] ?? 0,
446
+ normalized.value
447
+ );
448
+ return ok(void 0);
449
+ },
450
+ fillCircle(cx, cy, radius, color) {
451
+ const centerX = rounded("fill-circle", "cx", cx);
452
+ if (!centerX.ok) return centerX;
453
+ const centerY = rounded("fill-circle", "cy", cy);
454
+ if (!centerY.ok) return centerY;
455
+ const circleRadius = rounded("fill-circle", "radius", radius);
456
+ if (!circleRadius.ok) return circleRadius;
457
+ if (circleRadius.value <= 0) {
458
+ return invalid("fill-circle", "radius", circleRadius.value, "a positive number");
459
+ }
460
+ const normalized = colorChannels("fill-circle", color);
461
+ if (!normalized.ok) return normalized;
462
+ const radiusSquared = circleRadius.value * circleRadius.value;
463
+ const left = Math.max(0, centerX.value - circleRadius.value);
464
+ const right = Math.min(width.value - 1, centerX.value + circleRadius.value);
465
+ const top = Math.max(0, centerY.value - circleRadius.value);
466
+ const bottom = Math.min(height.value - 1, centerY.value + circleRadius.value);
467
+ for (let row = top; row <= bottom; row += 1) {
468
+ for (let column = left; column <= right; column += 1) {
469
+ const dx = column - centerX.value;
470
+ const dy = row - centerY.value;
471
+ if (dx * dx + dy * dy <= radiusSquared)
472
+ writePixel(data, width.value, column, row, normalized.value);
473
+ }
474
+ }
475
+ return ok(void 0);
476
+ },
477
+ blit(source, destinationX, destinationY, sourceRect) {
478
+ const destination = rounded("blit", "destinationX", destinationX);
479
+ if (!destination.ok) return destination;
480
+ const destinationYResult = rounded("blit", "destinationY", destinationY);
481
+ if (!destinationYResult.ok) return destinationYResult;
482
+ if (source === null || typeof source !== "object" || !Number.isInteger(source.width) || !Number.isInteger(source.height) || !(source.data instanceof Uint8Array) || source.data.length !== source.width * source.height * 4) {
483
+ return invalid("blit", "source", "malformed", "a valid PixelSurface");
484
+ }
485
+ const rect = rectangle(
486
+ "blit",
487
+ sourceRect?.x ?? 0,
488
+ sourceRect?.y ?? 0,
489
+ sourceRect?.width ?? source.width,
490
+ sourceRect?.height ?? source.height
491
+ );
492
+ if (!rect.ok) return rect;
493
+ const sourceX = rect.value[0] ?? 0;
494
+ const sourceY = rect.value[1] ?? 0;
495
+ const sourceWidth = rect.value[2] ?? 0;
496
+ const sourceHeight = rect.value[3] ?? 0;
497
+ const left = Math.max(0, sourceX);
498
+ const top = Math.max(0, sourceY);
499
+ const right = Math.min(source.width, sourceX + sourceWidth);
500
+ const bottom = Math.min(source.height, sourceY + sourceHeight);
501
+ if (right <= left || bottom <= top) return ok(void 0);
502
+ const snapshot = source.data.slice();
503
+ for (let row = top; row < bottom; row += 1) {
504
+ for (let column = left; column < right; column += 1) {
505
+ const targetX = destination.value + column - sourceX;
506
+ const targetY = destinationYResult.value + row - sourceY;
507
+ if (targetX < 0 || targetY < 0 || targetX >= width.value || targetY >= height.value)
508
+ continue;
509
+ const sourceOffset = (row * source.width + column) * 4;
510
+ writePixel(
511
+ data,
512
+ width.value,
513
+ targetX,
514
+ targetY,
515
+ snapshot.subarray(sourceOffset, sourceOffset + 4)
516
+ );
517
+ }
518
+ }
519
+ return ok(void 0);
520
+ },
521
+ fillNoise(seed, noiseOptions) {
522
+ if (!Number.isFinite(seed)) return invalid("noise", "seed", String(seed), "a finite number");
523
+ const min = noiseOptions?.min ?? 0;
524
+ const max = noiseOptions?.max ?? 255;
525
+ const alpha = noiseOptions?.alpha ?? 255;
526
+ for (const [field, value] of [
527
+ ["min", min],
528
+ ["max", max],
529
+ ["alpha", alpha]
530
+ ]) {
531
+ if (!Number.isFinite(value) || value < 0 || value > 255) {
532
+ return invalid("noise", field, value, "a finite number in [0, 255]");
533
+ }
534
+ }
535
+ if (min > max) return invalid("noise", "min", min, "a value no greater than max");
536
+ let state = Math.trunc(seed) >>> 0 || 1831565813;
537
+ const span = max - min;
538
+ for (let index = 0; index < data.length; index += 4) {
539
+ state = nextRandom(state);
540
+ const value = min + state / 4294967296 * span;
541
+ const channel = Math.round(value);
542
+ data[index] = channel;
543
+ data[index + 1] = channel;
544
+ data[index + 2] = channel;
545
+ data[index + 3] = Math.round(alpha);
546
+ }
547
+ return ok(void 0);
548
+ },
549
+ noise(seed, noiseOptions) {
550
+ return surface.fillNoise(seed, noiseOptions);
551
+ },
552
+ toDecodedImage() {
553
+ return {
554
+ bytes: data.slice(),
555
+ width: width.value,
556
+ height: height.value,
557
+ mime: "image/png",
558
+ colorSpace,
559
+ mipmap
560
+ };
561
+ },
562
+ toTextureAsset() {
563
+ return {
564
+ kind: "texture",
565
+ shape: {
566
+ viewDimension: "2d",
567
+ extent: { width: width.value, height: height.value }
568
+ },
569
+ format: colorSpace === "srgb" ? "rgba8unorm-srgb" : "rgba8unorm",
570
+ data: data.slice(),
571
+ colorSpace,
572
+ mips: mipmap ? { kind: "generate" } : { kind: "none" }
573
+ };
574
+ },
575
+ toAssetPack(meta) {
576
+ return toAssetPack(surface.toDecodedImage(), meta);
577
+ }
578
+ };
579
+ return ok(surface);
580
+ }
581
+ function createPixelSurface(options) {
582
+ return makeSurface(options);
583
+ }
584
+
276
585
  // src/source-key.ts
277
586
  function deriveImageSourceKey(role, _locator) {
278
587
  const normalizedRole = role.trim();
@@ -338,7 +647,7 @@ function reimportReuseMeta(decoded, existing) {
338
647
  }
339
648
  return out;
340
649
  }
341
- function invalid(guid, expected, reason) {
650
+ function invalid2(guid, expected, reason) {
342
651
  return err({
343
652
  code: "asset-package-invalid",
344
653
  expected,
@@ -377,7 +686,7 @@ async function readImageSurface(input, kind, expected) {
377
686
  const { envelope, artifacts } = input;
378
687
  const payload = envelope.payload;
379
688
  if (payload === null || typeof payload !== "object" || payload.kind !== kind) {
380
- return invalid(envelope.guid, expected, `${kind} owner validation failed`);
689
+ return invalid2(envelope.guid, expected, `${kind} owner validation failed`);
381
690
  }
382
691
  const body = envelope.artifacts.body ?? envelope.artifacts.atlas;
383
692
  let data = imageBytes(payload.data);
@@ -389,17 +698,17 @@ async function readImageSurface(input, kind, expected) {
389
698
  if (body.assetCodec?.name === "basis" && body.assetCodec.container !== void 0) {
390
699
  const candidate = payload;
391
700
  if (candidate.colorSpace !== "srgb" && candidate.colorSpace !== "linear") {
392
- return invalid(envelope.guid, expected, `${kind} color space is invalid`);
701
+ return invalid2(envelope.guid, expected, `${kind} color space is invalid`);
393
702
  }
394
703
  const target = compressedImageTarget(candidate.colorSpace);
395
704
  const transcoded = body.assetCodec.container === "ktx2" ? await parseKtx2(bytes).then(
396
705
  (parsed) => parsed.ok ? transcodeKtx2(parsed.value, target) : parsed
397
706
  ) : await transcodeBasis(bytes, target);
398
707
  if (!transcoded.ok) {
399
- return invalid(envelope.guid, expected, `codec:${transcoded.error.code}`);
708
+ return invalid2(envelope.guid, expected, `codec:${transcoded.error.code}`);
400
709
  }
401
710
  const mip = transcoded.value.mips[0];
402
- if (mip === void 0) return invalid(envelope.guid, expected, "codec:base-mip-missing");
711
+ if (mip === void 0) return invalid2(envelope.guid, expected, "codec:base-mip-missing");
403
712
  data = mip.data;
404
713
  return readDecodedSurface(
405
714
  envelope.guid,
@@ -422,7 +731,7 @@ async function readImageSurface(input, kind, expected) {
422
731
  }
423
732
  function readDecodedSurface(guid, expected, kind, candidate) {
424
733
  if (kind === "texture" ? !validTextureSurface(candidate) : !validEquirectSurface(candidate)) {
425
- return invalid(guid, expected, "image owner validation failed");
734
+ return invalid2(guid, expected, "image owner validation failed");
426
735
  }
427
736
  return ok(candidate);
428
737
  }
@@ -453,30 +762,6 @@ var equirectContribution = {
453
762
  }
454
763
  };
455
764
 
456
- // src/to-asset-pack.ts
457
- function toAssetPack(decoded, meta) {
458
- return {
459
- schemaVersion: "1.0.0",
460
- kind: "external-asset-package",
461
- importer: "image",
462
- source: "",
463
- importSettings: {
464
- colorSpace: meta.colorSpace,
465
- mipmap: meta.mipmap,
466
- addressMode: meta.addressMode,
467
- filterMode: meta.filterMode,
468
- ...meta.downscaleMaxDimension !== void 0 ? { downscaleMaxDimension: meta.downscaleMaxDimension } : {}
469
- },
470
- subAssets: [
471
- {
472
- guid: meta.guid,
473
- sourceIndex: 0,
474
- kind: "texture"
475
- }
476
- ]
477
- };
478
- }
479
-
480
- export { IMAGE_ERROR_EXPECTED, ImageErrorImpl, decodeHdr, decodeImageInBrowser, deriveImageSourceKey, equirectContribution, imageError, loadJpeg, loadUpng, reimportReuseMeta, subAssetKey, subAssetKeyEqual, textureContribution, toAssetPack };
765
+ export { IMAGE_ERROR_EXPECTED, ImageErrorImpl, createPixelSurface, decodeHdr, decodeImageInBrowser, deriveImageSourceKey, equirectContribution, imageError, loadJpeg, loadUpng, reimportReuseMeta, subAssetKey, subAssetKeyEqual, textureContribution, toAssetPack };
481
766
  //# sourceMappingURL=index.mjs.map
482
767
  //# sourceMappingURL=index.mjs.map