---
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"})
```
````