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.
| Name | Signature |
|---|---|
| canvas | canvas(config) -> canvas, err |
canvas(
config) ->canvas,err
Create a new pixel canvas
| config field | default | description |
|---|---|---|
width | 800 | width in pixels, 32767 at most |
height | 600 | height 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.
| method | description |
|---|---|
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.
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.
c:blit(src, dx, dy, opts) draws src with its top-left at dx, dy.
| opts field | description |
|---|---|
sx, sy, sw, sh | source sub-rect; default the source's own |
flip | "h", "v" or "hv" |
tint | {r, g, b[, a]} channel multiply, 255 leaves a channel alone |
alpha | global opacity, 0–255 |
blend | "alpha" (default), "replace" or "add" |
dw, dh | destination size; nearest-neighbour scaling |
scale | multiplier on the source size; ignored when dw, dh are set |
rot | radians, 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.
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 field | description |
|---|---|
sx, sy, sw, sh | source sub-rect |
shade | {r, g, b} channel multiply, 255 = unchanged |
alpha | global opacity, 0–255 |
blend | "alpha" (default), "replace" or "add" |
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:
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.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.{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.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".radius + min, or under its own ground, draws nothing.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:
levels past 1024;alpha past 255;radius or focal not above 0;filter;twilight of −1 or less;fade under 0;alpha_law not 256 long;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.
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.