Slide 46 of 53
Three dimensions
A 3D scene, in a window
type Msg { Tick }
func view(model: Int): hive.ui.View {
shapes := [
hive.ui.ground([hive.ui.paint(hive.ui.Tone.HEX("#2d5c34"))], 20.0, 20.0),
hive.ui.box(
[
hive.ui.at(0.0, 0.5, -3.0),
hive.ui.paint(hive.ui.Tone.HEX("#b5651d")),
hive.ui.surface(hive.ui.Surface.Planks()),
],
1.0, 1.0, 1.0,
),
hive.ui.sphere([hive.ui.at(2.0, 1.0, -4.0), hive.ui.paint(hive.ui.Tone.HEX("#e0c341"))], 1.0),
// A cone takes both radii: nought at the top is the pointed one.
hive.ui.cone(
[
hive.ui.at(-2.0, 0.4, -3.0),
hive.ui.paint(hive.ui.Tone.HEX("#ff5a1f")),
hive.ui.surface(hive.ui.Surface.Gloss()),
],
0.0, 0.4, 0.8,
),
// And a colour with an alpha is drawn see-through.
hive.ui.box(
[hive.ui.at(0.0, 1.2, -1.5), hive.ui.paint(hive.ui.Tone.RGBA(120, 200, 255, 96))],
3.0, 2.0, 0.2,
),
]
return hive.ui.scene(
[hive.ui.grow(1), hive.ui.eye(0.0, 1.6, 4.0), hive.ui.aim(0.0, 0.0), hive.ui.lens(70), hive.ui.fog(60)],
shapes,
)
}
proc update(model: Int, msg: Msg, envelope: hive.syslink.Envelope): Int {
return model
}
proc main(): void {
hive.ui.window("A 3D scene", view, update, 0)
}
Animating it, and reacting to a frame
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: Float, msg: Msg, envelope: hive.syslink.Envelope): Float {
if msg is Msg.Tick(ms) {
return angle + hive.conv.itf(ms) * 0.002
}
return angle
}
proc main(): void {
hive.ui.window("A spinning box", view, spin, 0.0)
}
Driving it with a gamepad
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: Kart, msg: Msg, envelope: hive.syslink.Envelope): Kart {
mut Kart it = kart
if msg is Msg.Padded(pad, control, at) {
if control == "leftX" { it.steer = at }
if control == "rt" { it.throttle = at }
return it
}
if msg is Msg.Tick(ms) {
step := hive.conv.itf(ms) / 1000.0
it.x = it.x + it.steer * it.throttle * 16.0 * step
it.z = it.z - it.throttle * 16.0 * step
}
return it
}
proc main(): void {
hive.ui.window("A pad", view, drive, Kart(0.0, 0.0, 0.0, 0.0))
}
A scene is the one widget only a window can draw, and it is a module of its own — hive.ui.scene — because it is the one thing in the standard library that carries a download: a pinned three.js, fetched on the first build that needs it and cached ever after. A window that draws no scene links none of it. A scene is a value like every other view: a camera, a sky, and a vector of shapes, rebuilt by an ordinary func from the state as often as there are frames. Nothing here is a handle — no mesh to keep, no material to allocate, no canvas to hold — so a program answers what is there right now, and never what has changed.box, sphere, cylinder, cone, ground, label and line are the shapes, and each takes its dimensions as its payload rather than as attributes, because they are what the shape is: a sphere without a radius is not a sphere with a default one. cone takes both of its radii rather than only the base — a cone in the world is hardly ever the perfect one, a track marker is snub-nosed and a nose cone ends in a blade, and a shape that could only be the perfect one gets composed around rather than used. Nought at the top is the perfect one; a top equal to the bottom is a cylinder. ground already lies flat, so a floor costs no rotation and turn still means everywhere what it means anywhere; label always faces the viewer, because one you can read from one side only is not a label; and line takes no position at all, since a line is between two places and has no third to call its own.at, turn and paint place and colour a shape. surface says what it is made of, which is a different question from what colour it is, and the set reads from the ground up: what you drive on — Turf, Gravel, Tarmac, Kerb — then what things are built of — Concrete, Brick, Planks, Staves, Bark, Leaves, Crowd, Mesh — then what they are finished in: Speckle, Metal, Carbon, Rubber, Glass and Gloss. Each is drawn over whatever colour the shape already is — so one pattern serves a red kerb and a blue one, and Gloss adds no pattern at all. Where a shape says nothing its kind decides: a ground is Turf, a cylinder or a cone is Staves, a sphere is Speckle, and everything else is Planks. And a colour carrying an alpha is translucent — Tone.RGBA(r, g, b, a), or an eight-digit Tone.HEX — drawn see-through and writing no depth, which is how a ghost, a tinted screen or the plume behind something gets drawn.eye, aim, lens, fog, background, grab and crosshair go on the scene itself rather than on a shape: where the camera is and where it looks, how wide the lens is, how far you can see, the sky the fog fades into, and whether the window should hold the mouse. Lighting is deliberately not something a scene describes — the rig is fixed, chosen so that a solid painted a colour comes out that colour with a shaded side — so what makes a scene dark is its palette rather than a setting turned down. The events are a scene's own: onFrame reports how many milliseconds the last frame took, onKeyDown and onKeyUp a key's name, onLook how far the mouse moved, onGrab whether the window has the pointer, and onPad a gamepad.A gamepad is the one input a browser will not tell you about: there is no event for a stick moving, only a snapshot you have to ask for. So the runtime reads the pads once a frame and posts what changed — which makes a button on a pad the same kind of thing as a key going down, rather than a second way of hearing about input. onPad reports three things: which pad, which control on it, and where that control now is. One shape covers the whole device, because a button is a control that is only ever 0 or 1. leftX, leftY, rightX and rightY are the sticks; lt and rt the triggers; a, b, x, y, lb, rb, up, down, left, right, back, start, guide, lstick and rstick the buttons; and anything past those is reported by number. here is the pad itself: 1 when it arrives, 2 when it arrives with a layout the browser could not name, and 0 when it goes — and a pad being unplugged lets go of everything it was holding first, so a car whose controller ran flat mid-corner does not keep turning. A dead zone is applied before the event is sent, and nothing is sent until a control has actually moved, because a stick at rest is never quite at rest.A scene holds one more shape than the seven above, and that one is heard rather than drawn: it has the slide after this one to itself.A served page renders a scene's box empty — there is no socket for frames to travel down — so the examples below are shown as code only: open one in a window to actually see it.