Slide 68 de 74

Cenas: a câmera, a entrada e os insets

Animando, e reagindo a um 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,
			),
		],
	)
}

// O onFrame de uma scene reporta quantos milissegundos o último frame levou,
// então o giro que esta caixa faz é em segundos reais, não em frames — a
// mesma caixa gira na mesma velocidade a trinta quadros por segundo ou a sessenta.
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("Uma caixa girando", view, spin), 0.0)
}
Dirigindo com um 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,
			),
		],
	)
}

// Um analógico e um gatilho chegam pelo mesmo buraco e são distinguidos pelo
// nome, porque um botão é um comando que só é 0 ou 1. Nada é guardado aqui
// que o controle não tenha dito: onPad reporta o que *mudou*, então o modelo
// lembra a última posição de cada comando que lhe interessa.
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("Um controle", view, drive), Kart(0.0, 0.0, 0.0, 0.0))
}

Alguns atributos vão na cena e não numa forma:

  • eye(x, y, z) é onde a câmera está, aim(yaw, pitch) para onde ela olha, em radianos, e lens(Int) o quanto ela abrange, em graus. fog(Int) é o quão longe se enxerga, em metros, e background(Tone) o céu em que ela se dissolve, desenhado como uma faixa até o horizonte.
  • shadows(false) desliga as sombras do sol. Elas cobrem uns cinquenta metros em volta da câmera, um ground as recebe mas não projeta nenhuma, e ligá-las ou desligá-las reconstrói todo shader, então não é algo para mudar a cada quadro. A iluminação é fixa fora isso, escolhida para que uma cor saia daquela cor com um lado sombreado: o que deixa uma cena escura é a paleta.
  • grain(px) desenha o mundo nessa quantidade de pixels no lado curto e o amplia de volta sem filtro, e bits(n) permite a cada canal de cor essa quantidade de bits, com pontilhado em vez de arredondamento — então cinco é a cor de quinze bits dos portáteis antigos.
  • grab(Bool) pede à janela para prender o mouse, e crosshair(Bool) desenha uma mira.

Os eventos são da própria cena: onFrame os milissegundos que o último quadro levou — um quadro em andamento por vez, um intervalo informado como no máximo 100 — onRate a taxa de atualização da tela em hertz, entre 20 e 480 e mudando no máximo uma vez por segundo, onKeyDown e onKeyUp o nome de uma tecla, onLook o quanto o mouse andou, onGrab se a janela tem o ponteiro, onSize a largura e a altura da caixa quando ela começa e sempre que muda, e onPad um controle. Uma tecla é nomeada por si mesma quando é uma letra ou um dígito, e senão space, enter, escape, tab, shift, ctrl, up, down, left ou right.

Um navegador não tem evento para controle, então o runtime lê os controles uma vez por quadro e envia o que mudou: onPad informa qual controle, qual comando e onde ele está agora. Os analógicos, leftX leftY rightX rightY, vão de -1 a 1, e os gatilhos lt e rt de 0 a 1; a b x y lb rb up down left right back start guide lstick rstick são botões, 0 ou 1; qualquer outra coisa é informada por número. here é o próprio controle — 1 quando chega, 2 com um layout que o navegador não soube nomear, 0 quando sai, depois de soltar tudo o que segurava. Movimentos pequenos dentro de uma zona morta não são enviados. Um navegador não mostra controle nenhum até um ser tocado, e um instalado como Flatpak precisa de acesso ao dispositivo.

inset(attrs, views) mostra o mesmo mundo a partir do seu próprio eye, aim e lens — um retrovisor, uma janela de replay — disposto sobre ele como uma coluna, compartilhando céu, neblina e sombras, e desenhado só onde fica sobre a cena. Uma janela desenha uma cena e quantos insets quiser. Uma página servida mostra a caixa de uma cena vazia, então estes exemplos são só código.