type Msg {
Tick { ms: Int }
}
func view(angle: Float): hive.ui.View {
return hive.ui.scene(
[
hive.ui.grow(1),
hive.ui.eye(0.0, 1.5, 5.0),
hive.ui.aim(0.0, 0.0),
hive.ui.onFrame(Msg.Tick(_)),
],
[
hive.ui.ground([hive.ui.paint(hive.ui.Tone.Muted())], 10.0, 10.0),
hive.ui.box(
[hive.ui.at(0.0, 1.0, 0.0), hive.ui.turn(0.0, angle, 0.0), hive.ui.paint(hive.ui.Tone.Good())],
1.0, 1.0, 1.0,
),
],
)
}
// A scene's onFrame reports how many milliseconds the last frame took, so
// the turn this box makes is in real seconds rather than in frames — the
// same box spins at the same speed at thirty frames a second or sixty.
proc spin(angle: mut Float, msg: Msg): Msg {
if msg is Msg.Tick(ms) {
angle += hive.conv.itf(ms) * 0.002
}
return msg
}
proc main(): void {
spawn(hive.ui.window("A spinning box", view, spin), 0.0)
}type Msg {
Tick { ms: Int }
Padded { pad: Int, control: Str, at: Float }
}
type Kart {
x: Float
z: Float
steer: Float
throttle: Float
}
func view(kart: Kart): hive.ui.View {
return hive.ui.scene(
[
hive.ui.grow(1),
hive.ui.eye(kart.x, 4.0, kart.z + 9.0),
hive.ui.aim(0.0, -0.25),
hive.ui.lens(70),
hive.ui.onFrame(Msg.Tick(_)),
hive.ui.onPad(Msg.Padded(_, _, _)),
],
[
hive.ui.ground(
[hive.ui.paint(hive.ui.Tone.HEX("#61686f")), hive.ui.surface(hive.ui.Surface.Tarmac())],
120.0, 120.0,
),
hive.ui.box(
[
hive.ui.at(kart.x, 0.4, kart.z),
hive.ui.paint(hive.ui.Tone.HEX("#00d2be")),
hive.ui.surface(hive.ui.Surface.Gloss()),
],
1.6, 0.6, 3.4,
),
],
)
}
// A stick and a trigger arrive through the same hole and are told apart by
// name, because a button is a control that is only ever 0 or 1. Nothing is
// held on to here that the pad did not say: onPad reports what *changed*,
// so the model remembers the last position of each control it cares about.
proc drive(kart: mut Kart, msg: Msg): Msg {
if msg is Msg.Padded(pad, control, at) {
if control == "leftX" { kart.steer = at }
if control == "rt" { kart.throttle = at }
} else if msg is Msg.Tick(ms) {
step := hive.conv.itf(ms) / 1000.0
kart.x = kart.x + kart.steer * kart.throttle * 16.0 * step
kart.z = kart.z - kart.throttle * 16.0 * step
}
return msg
}
proc main(): void {
spawn(hive.ui.window("A pad", view, drive), Kart(0.0, 0.0, 0.0, 0.0))
}Some attributes go on the scene rather than on a shape:
eye(x, y, z) is where the camera is, aim(yaw, pitch) where it looks, in radians, and lens(Int) how wide it sees, in degrees. fog(Int) is how far you can see, in metres, and background(Tone) the sky it fades into, drawn as a band down to the horizon.shadows(false) turns the sun's shadows off. They cover about fifty metres round the camera, a ground takes them but casts none, and switching them rebuilds every shader, so it is not something to change every frame. The lighting is fixed otherwise, chosen so a colour comes out that colour with a shaded side: what makes a scene dark is its palette.grain(px) draws the world into that many pixels on its short edge and blows it back up unfiltered, and bits(n) allows each colour channel that many bits, dithered rather than rounded — so five is the fifteen-bit colour of old handhelds.grab(Bool) asks the window to hold the mouse, and crosshair(Bool) draws one.The events are a scene's own: onFrame the milliseconds the last frame took — one frame in flight at a time, a gap reported as at most 100 — onRate the screen's refresh in hertz, between 20 and 480 and changing at most once a second, onKeyDown and onKeyUp a key's name, onLook how far the mouse moved, onGrab whether the window has the pointer, onSize the box's width and height when it starts and whenever it changes, and onPad a gamepad. A key is named by itself when it is a letter or a digit, and otherwise space, enter, escape, tab, shift, ctrl, up, down, left or right.
A browser has no event for a gamepad, so the runtime reads pads once a frame and posts what changed: onPad reports which pad, which control and where it now is. Sticks, leftX leftY rightX rightY, run from -1 to 1, and the triggers lt and rt from 0 to 1; a b x y lb rb up down left right back start guide lstick rstick are buttons, 0 or 1; anything else is reported by number. here is the pad itself — 1 when it arrives, 2 with a layout the browser could not name, 0 when it goes, after letting go of everything it held. Small movements inside a dead zone are not sent. A browser shows no pad until one is touched, and one installed as a Flatpak needs access to the device.
inset(attrs, views) shows the same world from its own eye, aim and lens — a mirror, a replay box — laid out over it like a column, sharing its sky, fog and shadows, and drawn only where it lies over the scene. One window draws one scene and any number of insets. A served page shows a scene's box empty, so these examples are code only.