v0.7.1 (build 925)

Koya/Image provides simple, composable image operations. It works with any texture key under /ram/... and can also load assets from /rom/....

Operations update or create textures in the renderer's texture pool; reusing a key overwrites safely.

gradient generates multi-stop linear/radial textures on CPU and uploads them through the same fast texture path as other image operations.

Import #

import * as Image from 'Koya/Image';

Functions #

load
Decodes an asset at /rom/... and uploads to VRAM as /ram/<id>.
  • textureSemantic may also be provided as textureLoad.
  • Default is 'color-srgb-premul', which uploads SRGB premultiplied colour textures.
  • Use 'data-linear-no-premul' for normal maps, height maps, and other linear data textures.
load(windowId, { path: string, id: string, mipmaps?: boolean, textureSemantic?: TextureSemantic }): Promise<string>
resize
Resizes src and writes to dst (or overwrites src if dst omitted). Returns the resulting key.
resize(windowId, { src: string, dst?: string, width: number, height: number, mipmaps?: boolean }): Promise<string>
blur
Applies a separable box blur with configurable radius and passes.
blur(windowId, { src: string, dst?: string, radius: number, passes?: number, mipmaps?: boolean }): Promise<string>
crop
Crops a rectangle from src starting at (x, y) with the given width and height. The region is clamped to the source bounds.
crop(windowId, { src: string, dst?: string, x: number, y: number, width: number, height: number, mipmaps?: boolean }): Promise<string>
gradient
Creates a procedural gradient texture under /ram/... and returns the resulting key.
  • stops must contain at least two entries. Each stop accepts at (or alias offset) in [0..1] and color/colour RGBA.
  • linear.from/linear.to are normalized UV coordinates; defaults to {x:0,y:0} -> {x:1,y:1}.
  • radial.center and radial.radius are normalized; optional focal and innerRadius support richer radial ramps.
  • spread controls sampling outside the [0..1] range: pad (default), repeat, or reflect/mirror.
gradient(windowId, { type: "linear"|"radial", dst?: string, width?: number, height?: number, size?: { x: number, y: number }, stops: Array<{ at?: number, offset?: number, color?: [number,number,number,number], colour?: [number,number,number,number] }>, linear?: { from?: { x: number, y: number }, to?: { x: number, y: number } }, radial?: { center?: { x: number, y: number }, radius?: number, focal?: { x: number, y: number }, innerRadius?: number }, spread?: "pad"|"repeat"|"reflect"|"mirror", mipmaps?: boolean }): Promise<string>
destroy
Destroys a writable /ram/... image, removing its CPU data and retiring its GPU texture after in-flight frames complete.
destroy(windowId, key): Promise<boolean>
unload
Evicts a renderer-cached /rom/... or file-backed texture without modifying the source asset.
unload(windowId, path): Promise<boolean>
encode
Encodes a cached /ram/... image and returns encoded bytes.
  • quality is used for JPG/JPEG (1–100).
  • Supported formats currently map to stb writers.
  • No file I/O is performed; callers can upload/store bytes however they prefer.
encode(windowId, { src: string, format?: "png"|"jpg"|"jpeg"|"bmp"|"tga", quality?: number }): Promise<Uint8Array>
perceptualHash
Computes a psychovisual hash from cached /ram/... image pixels.
  • phash is DCT-based and usually better for appearance regression checks.
  • dhash is gradient-based and lightweight.
perceptualHash(windowId, { src: string, algorithm?: "phash"|"dhash" }): Promise<string>
render
Renders a single UI element (and optionally its subtree) into a VRAM texture under /ram/....
  • dst: destination key. Defaults to /ram/{windowId}:{elementId}.
  • includeTree: when true, renders the element and its children; when false (default) renders only the element's own renderable.
  • size: logical size { x, y }. When omitted, uses the element's bounds.size.
  • scaling: when true (default), multiplies logical size by the window's bufferScale so pixels match on-screen sampling. When false, uses 1:1 logical mapping.
  • mipmaps: enable mipmap generation for the render target (defaults to true).
  • cacheCPU: when true (default), reads back pixels to populate the CPU image cache so resize/blur/crop can operate on the result.
render(windowId, elementId, { dst?: string, includeTree?: boolean, size?: { x: number, y: number }, scaling?: boolean, mipmaps?: boolean, cacheCPU?: boolean }): Promise<string>

Texture lifetime and descriptor limit #

Each window currently has a hard capacity of 512 texture descriptor sets. The renderer's default texture and any other engine-managed textures use entries from the same pool, so applications cannot assume that all 512 entries are available. Keep the resident working set comfortably below this ceiling.

  • Call destroy(windowId, key) for writable /ram/... images. It removes the CPU-side image data and retires the GPU texture.
  • Call unload(windowId, path) for renderer-cached /rom/... or file-backed textures. It releases the cached GPU resource without changing the source asset.
  • Detach the texture from live UI elements first. Reclamation is timeline-deferred, so a retired descriptor remains allocated until every already-submitted frame that references it has completed. Loading replacements immediately can therefore cause a short-lived overlap in descriptor use.

Notes #

  • src/dst accept /ram/... keys. If you pass a /rom/... path to load, it will upload as /ram/<id>.
  • load defaults to colour SRGB premultiplied uploads. Use textureSemantic: 'data-linear-no-premul' for linear data textures.
  • gradient writes premultiplied-alpha RGBA pixels so results blend consistently with built-in renderer blending.
  • Current implementation runs on CPU for portability; a GPU path will replace it.

Example #

Walk through the full image pipeline: generate a procedural gradient, load an asset from /rom, chain resize/blur/crop transforms, assign the result as a UI texture, render a UI element back to a texture, and finally encode and hash the output. Each operation returns a /ram/... key that can be piped into the next step.

import * as Compositor from 'Koya/Compositor';
import * as UI from 'Helix/UserInterface';
import * as Image from 'Koya/Image';

const w = await Compositor.createWindow({ role: 'overlay', anchor: 'top-left', size: { x: 400, y: 300 } });
const el = await UI.createElement(w, {
  renderable: { type: 'box', aabb: { min: { x: 0, y: 0 }, max: { x: 400, y: 300 } }, position: { x: 0, y: 0 } }
});
await UI.attachRoot(w, el);

// Procedural gradient texture (multi-stop linear)
const grad = await Image.gradient(w, {
  dst: '/ram/bg-gradient',
  type: 'linear',
  size: { x: 512, y: 512 },
  stops: [
    { at: 0.0, color: [0.08, 0.10, 0.16, 1.0] },
    { at: 0.45, color: [0.20, 0.18, 0.45, 1.0] },
    { at: 1.0, color: [0.78, 0.42, 0.96, 1.0] }
  ],
  linear: { from: { x: 0, y: 0 }, to: { x: 1, y: 1 } },
  spread: 'pad'
});
await UI.setTexture(w, el, grad);

const key = await Image.load(w, { path: '/rom/arch.png', id: 'logo' });
const thumb = await Image.resize(w, { src: key, dst: '/ram/logo-thumb', width: 200, height: 150 });
const blurred = await Image.blur(w, { src: thumb, radius: 12, passes: 2 });
const cropped = await Image.crop(w, { src: blurred, dst: '/ram/logo-cropped', x: 20, y: 20, width: 100, height: 80 });
await UI.setTexture(w, el, cropped);

// Render an element to a texture
const win = await Compositor.createWindow({ role: 'overlay', anchor: 'top-left', size: { x: 800, y: 600 } });
const root = await UI.createElement(win, { layout: { type: 'row' } });
await UI.attachRoot(win, root);
const child = await UI.createElement(win, {
  renderable: { type: 'box', aabb: { min: { x: 0, y: 0 }, max: { x: 128, y: 64 } }, colour: '#fff' }
});
await UI.attach(win, root, child);

const texKey = await Image.render(win, child, {});
await UI.setTexture(win, child, texKey);

await Image.render(win, child, { dst: '/ram/rentext', scaling: false, includeTree: true });

// Encode and hash
const pngBytes = await Image.encode(w, { src: '/ram/logo-cropped', format: 'png' });
const jpgBytes = await Image.encode(w, { src: '/ram/logo-cropped', format: 'jpg', quality: 92 });
const phash = await Image.perceptualHash(w, { src: '/ram/logo-cropped', algorithm: 'phash' });
const dhash = await Image.perceptualHash(w, { src: '/ram/logo-cropped', algorithm: 'dhash' });