Koya/Image
General image operations (load, gradient generation, resize, blur) that produce textures under /ram.
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 #
/rom/... and uploads to VRAM as /ram/<id>.textureSemanticmay also be provided astextureLoad.- 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>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(windowId, { src: string, dst?: string, radius: number, passes?: number, mipmaps?: boolean }): Promise<string>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>/ram/... and returns the resulting key.stopsmust contain at least two entries. Each stop acceptsat(or aliasoffset) in [0..1] andcolor/colourRGBA.linear.from/linear.toare normalized UV coordinates; defaults to{x:0,y:0}->{x:1,y:1}.radial.centerandradial.radiusare normalized; optionalfocalandinnerRadiussupport richer radial ramps.spreadcontrols sampling outside the [0..1] range:pad(default),repeat, orreflect/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>/ram/... image, removing its CPU data and retiring its GPU texture after in-flight frames complete.destroy(windowId, key): Promise<boolean>/rom/... or file-backed texture without modifying the source asset.unload(windowId, path): Promise<boolean>/ram/... image and returns encoded bytes.qualityis 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>/ram/... image pixels.phashis DCT-based and usually better for appearance regression checks.dhashis gradient-based and lightweight.
perceptualHash(windowId, { src: string, algorithm?: "phash"|"dhash" }): Promise<string>/ram/....- dst: destination key. Defaults to
/ram/{windowId}:{elementId}. - includeTree: when
true, renders the element and its children; whenfalse(default) renders only the element's own renderable. - size: logical size
{ x, y }. When omitted, uses the element'sbounds.size. - scaling: when
true(default), multiplies logical size by the window'sbufferScaleso pixels match on-screen sampling. Whenfalse, 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 soresize/blur/cropcan 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/dstaccept/ram/...keys. If you pass a/rom/...path toload, it will upload as/ram/<id>.loaddefaults to colour SRGB premultiplied uploads. UsetextureSemantic: 'data-linear-no-premul'for linear data textures.gradientwrites 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' });