Conway's Game of Life rendered on a torus.
---
format: typebulb/v1
name: Donut Life
---
**code.tsx**
```tsx
import { App, Component, div } from "domeleon";
import { Scene, PerspectiveCamera, WebGLRenderer, Group, PlaneGeometry, MeshBasicMaterial, Mesh, DoubleSide } from "three";
function getViewportFitScale (camera: PerspectiveCamera, objectSize: number, fillRatio = 0.9) {
const fov = (camera.fov * Math.PI) / 180;
const visibleHeight = 2 * Math.tan(fov / 2) * camera.position.z;
const visibleWidth = visibleHeight * camera.aspect;
return (Math.min(visibleWidth, visibleHeight) * fillRatio) / objectSize;
}
class DonutLife extends Component {
private readonly majorDiv = 36;
private readonly minorDiv = 18;
private readonly R = 0.15; // major radius
private readonly r = 0.08; // minor radius
private scene = new Scene();
private camera = new PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 1000);
private renderer!: WebGLRenderer;
private globe = new Group();
private cells: number[][] = []
private meshes: Mesh[] = []
view() {
return div({ class: "globe-container", onMounted: el => this.setup(el as HTMLElement) })
}
private setup(container: HTMLElement) {
const canvas = document.createElement("canvas")
container.appendChild(canvas);
this.camera.position.z = 3
this.renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
this.renderer.setSize(innerWidth, innerHeight, false)
this.scene.add(this.globe)
const geom = new PlaneGeometry(0.02, 0.02)
for (let i = 0; i < this.minorDiv; i++) {
this.cells[i] = []
for (let j = 0; j < this.majorDiv; j++) {
this.cells[i][j] = Math.random() > 0.7 ? 1 : 0
const mesh = this.createCell(i, j, geom)
this.meshes.push(mesh)
this.globe.add(mesh)
}
}
this.onResize()
addEventListener("resize", this.onResize)
this.animate()
setInterval(() => this.step(), 150)
}
private createCell(i: number, j: number, geom: PlaneGeometry) {
const minorAngle = (i / this.minorDiv) * Math.PI * 2
const majorAngle = (j / this.majorDiv) * Math.PI * 2
const [cosMin, sinMin, cosMaj, sinMaj] = [Math.cos(minorAngle), Math.sin(minorAngle), Math.cos(majorAngle), Math.sin(majorAngle)]
const x = (this.R + this.r * cosMin) * cosMaj
const y = this.r * sinMin
const z = (this.R + this.r * cosMin) * sinMaj
const cell = new Mesh(geom, new MeshBasicMaterial({ color: 0x003366, side: DoubleSide }))
cell.position.set(x, y, z)
cell.lookAt(x + cosMin * cosMaj, y + sinMin, z + cosMin * sinMaj)
return cell
}
private onResize = () => {
this.camera.aspect = innerWidth / innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(innerWidth, innerHeight, false)
const scale = getViewportFitScale(this.camera, 2 * (this.R + this.r))
this.globe.scale.setScalar(scale)
}
private step() {
const next: number[][] = []
for (let i = 0; i < this.minorDiv; i++) {
next[i] = []
for (let j = 0; j < this.majorDiv; j++) {
let alive = 0
for (let di = -1; di <= 1; di++) {
for (let dj = -1; dj <= 1; dj++) {
if (di === 0 && dj === 0) continue
alive += this.cells[(i + di + this.minorDiv) % this.minorDiv][(j + dj + this.majorDiv) % this.majorDiv]
}
}
const current = this.cells[i][j]
next[i][j] = (current && (alive === 2 || alive === 3)) || (!current && alive === 3) ? 1 : 0
}
}
for (let k = 0; k < this.meshes.length; k++) {
const state = next[Math.floor(k / this.majorDiv)][k % this.majorDiv]
const mesh = this.meshes[k];
(mesh.material as MeshBasicMaterial).color.setHex(state ? 0xffa500 : 0x003366)
mesh.scale.setScalar(state ? 1 : 0.3)
}
this.cells = next
}
private animate = () => {
requestAnimationFrame(this.animate)
this.globe.rotation.y += 0.005;
this.globe.rotation.x += 0.002;
this.renderer.render(this.scene, this.camera)
}
}
new App({ root: new DonutLife(), id: "app" })
```
**styles.css**
```css
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
.globe-container, canvas {
width: 100vw;
height: 100vh;
display: block;
}
```
**index.html**
```html
<div id="app"></div>
```
**config.json**
```json
{
"dependencies": {
"domeleon": "^0.5.2",
"three": "^0.183.2"
},
"description": "Conway's Game of Life rendered on a torus."
}
```
**notes.md**
````md
## Using Domeleon
Domeleon is a compact abstraction over any virtual DOM, including Preact (the default), React, and Vue. A Domeleon Component is a class representing application state rather than a wrapper around a DOM element. Components may define a pure view function of their state, and compose into hierarchies that can be updated, serialized, validated, or routed.
### Example
**HTML**
```html
<div id="app"></div>
```
**Code**
```ts
import { App, Component, div, button, formField, inputRange, canvas } from "domeleon"
import { inputNumber } from "domeleon/maskito"
class Counter extends Component {
count: number
role: string
#canvasEl: HTMLCanvasElement
#canvasCtx: CanvasRenderingContext2D
constructor(initial: number, role: string) {
super()
this.count = initial
this.role = role
}
view() {
return div({ class: "counter" },
formField ({ // databound input
target: this,
inputFn: inputRange, // inputSelect, inputText, inputTextArea, inputCheckbox etc.
prop: () => this.count,
label: this.role,
fieldAttrs: { class: "field-class"},
labelAttrs: { class: "label-class"},
inputProps: { attrs: { min: 100, max: 250, step: 1 } }
}),
button({ onClick: () => this.inc(+1) }, "+"), // always use closure to bind to preserve "this"
button({ onClick: () => this.inc(-1) }, "-"),
this.count,
canvas({
width: 400,
height: 20,
onMounted: el => {
this.#canvasEl = el as HTMLCanvasElement
this.#canvasCtx = this.#canvasEl.getContext("2d")
return () => { // dispose
this.#canvasEl = undefined
this.#canvasCtx = undefined
}
}
})
)
}
inc(x: number) {
this.count+=x
this.update() // triggers render on app
}
// `onRendered` called after VDOM has created/updated DOM elements
// useful for operations that require raw DOM access, such as canvas operations
override onRendered () {
this.drawOnCanvas()
}
drawOnCanvas() {
const canvas = this.#canvasEl
const cc = this.#canvasCtx
cc.clearRect(0, 0, canvas.width, canvas.height)
const barWidth = (this.count / 250) * canvas.width
cc.fillStyle = "#4a90e2"
cc.fillRect(0, 0, barWidth, canvas.height)
}
}
class Game extends Component {
wins = new Counter(10, "wins")
losses = new Counter(5, "losses")
view() {
return div(
this.wins.view(),
this.losses.view()
)
}
}
new App({ root: new Game(), id: "app"})
```
````