gfx.canvas

index · gfx

Overview

A pixel canvas: {r,g,b,a} colours and table options over the C gfx.core surface. The surface holds the RGBA bytes and runs the pixel loops; the canvas checks and clips the inputs and calls down once per primitive.

Functions

NameSignature
canvascanvas(config) -> canvas, err

canvas(config) -> canvas, err

Create a new pixel canvas

config fielddefaultdescription
width800width in pixels, 32767 at most
height600height in pixels, 32767 at most
colors.bg{0,0,0,0}background, used by fill
colors.main{252,252,252,255}default colour for drawing

The new canvas is filled with colors.bg. Coordinates count from 1. A colour is {r, g, b, a}, 0–255; a left out is 255, in the config too: bg = {0, 0, 0} is opaque black, not the default's transparent. A colour argument left out takes colors.main. Every method clips to the canvas. line, circle and rect draw nothing for a NaN coordinate. A surface the C cannot allocate answers nil and its error.

Every drawing method returns the canvas, so calls chain. resize returns the canvas, or nil and the surface's error when the new size cannot be allocated; a size past the limits raises, as the raw surface does. sphere_tex returns a ticket under later. get, width, height, tostring, height_pyramid and height_max return what they read.

methoddescription
c:width(), c:height()size in pixels
c:fill(col)fill with col, default colors.bg
c:clear()reset to fully transparent
c:pixel(x, y, col)set one pixel
c:get(x, y)r, g, b, a, or nil off the canvas
c:line(x1, y1, x2, y2, col)Bresenham line; endpoints round to integers
c:circle(cx, cy, r, col)filled circle
c:rect(x1, y1, x2, y2, col, filled)filled rectangle, or a 1px outline
c:poly(pts, col)filled convex polygon, pts = {{x, y}, ...} or {{x=, y=}, ...}, 256 points at most; more raise
c:ellipse(cx, cy, rx, ry, col, steps)ellipse outline of steps segments, default 44
c:resize(w, h)resize, keep the top-left overlap
c:tostring()the raw RGBA bytes, w*h*4
c:blit(src, dx, dy, opts)composite a source, see below
c:quad_tex(src, pts, opts)textured convex quad, see below
c:sphere_tex(src, opts)ray-traced textured sphere, see below
c:sphere_land(ticket)composite a sphere submitted with later, see below
c:veil(inscatter, transmit, dx, dy, dw, dh)screen-space medium over a rect, see below
c:height_pyramid()build the height pyramid, see below
c:height_max(k, i, j)read the height pyramid, see below

A clipped line can differ by one pixel from the unclipped walk: the Bresenham error restarts at the clip point.

Sources

A source is a canvas, a sprite (any table with a surface and a source rect sx, sy, sw, sh), or a raw surface. A canvas or a raw surface supplies its whole area as the source rect.

blend is "alpha", "replace" or "add", and flip "h", "v" or "hv"; an unknown name raises. Left out, blend is "alpha" on blit and quad_tex, which composite a sprite over a scene, and "replace" on sphere_tex, which paints a body and bakes into a scratch. alpha, tint and shade are held to 0–255 on blit and quad_tex; sphere_tex draws nothing for an alpha outside it.

blit

c:blit(src, dx, dy, opts) draws src with its top-left at dx, dy.

opts fielddescription
sx, sy, sw, shsource sub-rect; default the source's own
flip"h", "v" or "hv"
tint{r, g, b[, a]} channel multiply, 255 leaves a channel alone
alphaglobal opacity, 0–255
blend"alpha" (default), "replace" or "add"
dw, dhdestination size; nearest-neighbour scaling
scalemultiplier on the source size; ignored when dw, dh are set
rotradians, clockwise on screen, about the scaled rect's centre

A canvas can blit onto itself. A plain copy is "replace" with no tint, scale or flip. It copies the canvas as it stood, however the rects overlap, so a scroll is one call. Any other blit onto itself reads what it has already written, top row first.

quad_tex

c:quad_tex(src, pts, opts) maps the source rect onto a convex quad. pts holds four points in ring order: the source's top-left, top-right, bottom-right and bottom-left corners. The map is the projective one the four corners pin. It is perspective-correct for a flat 3-D face. A quad with three collinear corners draws nothing. Sampling is nearest-neighbour.

opts fielddescription
sx, sy, sw, shsource sub-rect
shade{r, g, b} channel multiply, 255 = unchanged
alphaglobal opacity, 0–255
blend"alpha" (default), "replace" or "add"

sphere_tex

c:sphere_tex(src, opts) ray-traces every pixel in the sphere's screen box against a sphere in camera space. src is the base map: equirect, with rows linear in the pole component. Row j of an h-row map covers n.y = 1 - 2(j + 0.5)/h. The camera has x right, y up, z forward. The object frame has +y at the pole and +x at the zero meridian. Vectors are {x, y, z} or {x=, y=, z=}.

Camera and body:

  • cx, cy: the principal point, in screen pixels.
  • focal: the focal length, in pixels per unit tangent.
  • center: the sphere's centre in camera space.
  • radius: the sphere's radius, in the units of center.
  • basis: the camera-to-object rotation, a row-major 3×3 flat array. Default identity.
  • inside: true draws the inner face, for a camera inside the sphere. The texel and the lights read the outward normal; limb and rim are off. With the camera outside, the call draws nothing; the same holds for a camera inside without inside.

Lights:

  • lights: up to two {x, y, z, w}. Each is a unit direction from the surface to the light in camera space, and a weight. With no lights the texels draw at full brightness.
  • lights[i].occ: a sphere that shadows this light, {x, y, z, r, kp, ku, cmin}. That is its centre in camera space, its radius, the penumbra and umbra cone slopes, and the floor the cone covers to. The light's Lambert term is scaled by the visible part of its disc. A radius of 0 means no occluder.
  • lights[i].occ.ramp: {r, g, b}, each a row of 16 knots across the umbra, from the shadow's axis to the umbra's edge. It is the light the occluder's air puts into its shadow. Where the cone hides the light, the pixel gets floor + ramp * lambert * (1 - cover), per channel. A face turned from the hidden light keeps the floor.
  • lights[i].lift: {r, g, b}, what the light adds over the floor, per channel: floor + lift * lambert * cover. cover is what the cone passes. The weight does not scale the lift, so a light of weight 0 draws its lift alone.
  • lights[i].lift.fade: fades the lift by the other light. The lift is multiplied by 1 - fade * other, held at 0, where other is the other light's weight times its Lambert term past its cone. A fade equal to the dither's levels removes the lift where the other light reaches one step. Absent, the lift never fades.
  • fill: {x, y, z, r, g, b}, a third light added to the floor: a direction and a colour. The pixel gets floor + fill * lambert, per channel. It has no weight and no cone. It draws only when lights holds a light. fill.twilight sets its own terminator width; absent, the call's twilight applies.
  • floor_join: "larger" joins the ramp, the lifts and the fill by taking the largest, per channel. Absent, they add.

Ramp, lift and fill results are clamped to 0–1 and never dithered.

Shading:

  • twilight: widens the terminator; 0 is a hard cut.
  • dither: {levels=, floor=}. floor is the ambient light and is never stepped, so an unlit point reads it exactly. The shade over it is cut into levels steps by a 4×4 Bayer matrix anchored to the screen. levels = 0 shades smooth over the floor. Absent: smooth, no floor.
  • rim: {width=, gain=, tint={r, g, b}}, an additive rim at the limb.
  • limb: {a, b}, limb darkening a + b*mu.

Sampling and compositing:

  • filter: "nearest" (default) or "bilinear". Bilinear mixes colour and alpha of the 2×2 texels about the hit. Height stays nearest.
  • alpha: global opacity, 0–255; texel alpha multiplies it.
  • alpha_law: 256 integers, 0–255: the alpha drawn for each texel alpha byte, read before alpha applies.
  • blend: "replace" (default), "alpha" or "add".
  • sx, sy, sw, sh: source sub-rect.
  • gpu: false keeps this call on the CPU while a device is bound.
  • later: true submits the call and returns a ticket in place of the canvas; c:sphere_land(ticket) composites it. See below.

Where the call draws:

  • With a device bound through gfx.use_device, a call without height, whose sources are not the canvas itself, over a box of 16384 pixels or more, draws on the device, and the bytes are the CPU's. gpu = false keeps one call on the CPU, LILUSH_GFX_GPU=0 every call. gfx.device_stats says where the calls went.

Later:

  • later = true returns a ticket, an integer, and the device traces the sphere while the program paints on. c:sphere_land(ticket) waits the trace where it still runs and composites it then, so a frame submits its spheres at the top and lands each at its slot in the painter's order. At most four tickets are open at once; a fifth later call draws at once. A later call that draws at once for any reason, or draws nothing, still returns a ticket, already landed, so a program lands every ticket it took the same way. A land of a ticket already landed returns at once; a land of a value never issued raises, and so does a land of a ticket open on another canvas. A bad source, or a call with no center, returns the canvas, not a ticket, and a land of that raises.
  • A source written between a submit and its land holds the texels the trace read until the next upload, which waits the ticket first. A resize lands the open tickets before it copies; a canvas collected with a ticket open drops it. A device lost before a land leaves that ticket's box undrawn, and gfx.device_stats counts it under lost.

Windows:

  • windows: up to five finer maps, finest first, each a tangent patch {src=, c=, e1=, a=, b=, ox=, oy=, height=, max=}. c is a point of the sphere and e1 the along direction, both in the object frame. a and b are the half-extents: the tangent of the half-angle each side subtends at the centre. The far corner must lie within 45 degrees, a² + b² <= 1. The first window that holds a hit supplies its texel; the base map serves the rest. There is no feathering.
  • An entry is also read as the flat array {src, cx, cy, cz, e1x, e1y, e1z, a, b, height, max, ox, oy}, the form the C takes, which a writer that lays windows every frame fills without allocating.
  • ox, oy: whole numbers, default 0. They place the window's map on the patch's texel grid. The window covers columns ox .. ox + w - 1, and column I lives at canvas column I mod w; rows likewise. To scroll a map, move the origin and bake only the band that came into view. A window height map of another size must scale the origin to a whole texel, ox * hw / w.
  • gfx.patch_frame and gfx.patch_dir give a baker the frame and each texel's direction.

Height:

  • height: {src=, scale=, zero=, min=, max=, base=, steps=, normal=, march=} displaces the surface. src holds 16 bits a texel, red high, green low; h = zero + scale * h16 in the units of center, clamped into [min, max]. min defaults to zero, max to zero + 65535 * scale. base is the base map's own ceiling, max when absent. A window's height and max work the same over its patch. Its max defaults to the global max, or to its pyramid's top when it has one.
  • A ray marches the shell between radius + max and radius + min in steps samples (default 8). It hits where its radius falls under radius + h. Lights see the height's gradient normal, unless normal = "sphere".
  • An eye inside the shell starts its rays at itself. An eye at or under radius + min, or under its own ground, draws nothing.
  • Give src and every window's height a fresh height pyramid, and the ground is drawn one screen column at a time. The shading matches the ray march. march = "ray" marches one ray per pixel instead; steps is then the most cells a ray visits per level.

A value outside its range draws nothing:

  • more than two lights;
  • dither levels past 1024;
  • alpha past 255;
  • a radius or focal not above 0;
  • an unknown filter;
  • a light weight under 0, or a cone floor outside 0–1;
  • a twilight of −1 or less;
  • a lift fade under 0;
  • an alpha_law not 256 long;
  • a bad window.

veil

c:veil(inscatter, transmit, dx, dy, dw, dh) lays a medium over a rect, per channel: dst = dst * transmit + inscatter. Use it for haze, fog, or the sky seen from inside an atmosphere. One alpha cannot pass red and stop blue, so a blit cannot do this.

inscatter and transmit are two sources of one size, from 2×2 up to 256 on either axis; a grid outside that draws nothing. Both stretch over the rect with bilinear sampling and are read as RGB. The destination's alpha is left alone. A sprite's sub-rect is ignored: each source is read whole. dx, dy default to 1 and dw, dh to the canvas. The stretch spans the rect asked for, not the part left after clipping.

Height pyramid

A canvas of 16-bit height texels (red high, green low) can carry a maximum pyramid. Level k holds, per cell, the maximum of the 2×2 cells under it, up to a 1×1 top.

c:height_pyramid() builds it and returns levels, cells: the levels above the texels and the count of cells it built. A canvas with no pyramid builds it whole. A canvas written since its last build rebuilds only the cells over the written box, at every level. An untouched canvas builds nothing.

c:height_max() returns the level count. c:height_max(k, i, j) returns the maximum at level k, cell i, j, 1-indexed; level 0 is the texel itself. Both return nil off the pyramid.

Any write to the canvas leaves the pyramid stale. A stale pyramid reads as none: height_max returns nil, and sphere_tex marches that source by rays. A band across the map's seam spans the whole width and rebuilds the whole pyramid.