Conway's Game of Life rendered on a torus.
## 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"}) ```
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" })