Published
Edited
Jun 26, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof stats = {
reset
const canvas = document.createElement("canvas")
canvas.width = WIDTH
canvas.height = HEIGHT

const offscreen = canvas.transferControlToOffscreen()

canvas.value = null

const dispose = mainEntry(offscreen, stats => {
canvas.value = stats
canvas.dispatchEvent(new Event("input", {bubbles: true}))
})

return canvas
}
Insert cell
Insert cell
import {aggregator} from "@iso2022jp/aggregator"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
sequence = times => f => Array.from({length: times}, (_, i) => f(i))
Insert cell
unsigned = value => value >>> 0
Insert cell
rotateRight = amount => string => {
const c = amount % string.length
return string.slice(-c) + string.slice(0, -c)
}
Insert cell
tokenize = pattern => text => Array.from(text.matchAll(pattern), m => m[0])
Insert cell
tokernizeNumbers = tokenize(/\d+/g)
Insert cell
// CNESWC => [CNESW, C]
decodeTransition = code => [code.slice(0, 5), +code.slice(-1)]
Insert cell
// [CNESW, C] => [[CNESW, C], [CWNES, C], [CSWNE, C], [CESWN, C]]
generateVonNeumannRotationalSymmetryTransitions = ([from, to]) => sequence(4)(i => [from[0] + rotateRight(i)(from.slice(1)), to])
Insert cell
// TODO: 8 to variable
// [CNESW(8), C] => [CNESW(10), C]
compileTransition = ([from, to]) => [parseInt(from, 8), to]
Insert cell
tokernizeColors = tokenize(/#[0-9a-f]+/ig)
Insert cell
// ABGR8888 (little endian)
decodeColor = color => {
const m = color.match(/^#(..)(..)(..)$/)
const alpha = 0xff
return unsigned(parseInt(m[1], 16) | parseInt(m[2], 16) << 8 | parseInt(m[3], 16) << 16 | alpha << 24)
}
Insert cell
Insert cell
transitions = new Map(tokernizeNumbers(TRANSITIONS).map(decodeTransition).flatMap(generateVonNeumannRotationalSymmetryTransitions).map(compileTransition))
Insert cell
colors = tokernizeColors(COLORS).map(decodeColor)
Insert cell
pattern = tokernizeNumbers(PATTERN).map(p => p.split('').map(Number))
Insert cell
mainEntry = (offscreen, callback) => {
const worker = createWorker(workerEntry, {
WIDTH,
HEIGHT,
sequence,
make2DArray,
aggregator,
drawCenterTo,
generate,
render,
})

const dispose = () => {
worker.terminate()
console.log(`${Date.now()}: worker terminated`)
}

console.log(`${Date.now()}: worker created`)

try {

// setup worker
const channnel = new MessageChannel()

const port = channnel.port1

// setup
worker.postMessage({
port: channnel.port2,
transitions,
colors,
pattern,
offscreen,
}, [channnel.port2, offscreen])

port.addEventListener('message', e => {
// got stats
callback(e.data)
})
port.addEventListener('messageerror', e => {
console.error(e)
})
port.start()

} catch (e) {
dispose()
throw e
}
return dispose
}
Insert cell
workerEntry = () => {

console.log(`${Date.now()}: worker started`)

// setup
globalThis.addEventListener('message', e => {

const {
port,
transitions,
colors,
pattern,
offscreen,
} = e.data

const g = offscreen.getContext("2d")

const aggregate = aggregator(3000) // Aggregation period for statistics in ms

// keep two states
let previous = make2DArray(WIDTH, HEIGHT)
let current = make2DArray(WIDTH, HEIGHT)

// renderer
const imageData = g.createImageData(WIDTH, HEIGHT)
const abgrView = new Uint32Array(imageData.data.buffer) // as ABGR8888 array

const renderState = render(abgrView)(colors)
const generateState = generate(transitions)

drawCenterTo(pattern, current)

const process = () => {

const stats = aggregate(() => {

// render
renderState(current)

// realize
g.putImageData(imageData, 0, 0)

// switch states
;[current, previous] = [previous, current]

// go
generateState(previous, current)

})

// refresh
delete stats.html // to clonable

port.postMessage(stats)
requestAnimationFrame(process)
}

port.addEventListener('message', e => {
// noop
})
port.addEventListener('messageerror', e => {
console.error(e)
})

port.start()

console.log(`${Date.now()}: worker started`)

requestAnimationFrame(process)

})
}
Insert cell
createWorker = (entry, imports) => {
const sources = [
...Object.entries(imports).map(([name, f]) => `const ${name} = ${f}\n`),
`(${entry})()\n`, // IIFE
]
const url = URL.createObjectURL(new Blob(sources, {type: 'text/javascript'})) // RFC 9239
return new Worker(url)
}
Insert cell
make2DArray = (width, height) => sequence(height)(_ => Array(width).fill(0))
Insert cell
drawCenterTo = (source, destination) => {

// TBD: jugged height, jugged width
const width = source[0].length
const height = source.length
const ox = Math.trunc((destination[0].length - width) / 2)
const oy = Math.trunc((destination.length - height) / 2)

for (let y = 0; y < height; ++y) {
const sourceLine = source[y]
const destinationLine = destination[oy + y]
for (let x = 0; x < width; ++x) {
destinationLine[ox + x] = sourceLine[x]
}

}

}
Insert cell
generate = transitions => (source, destination) => {

// TBD: jugged height, jugged width
const width = source[0].length
const height = source.length

for (let y = 1; y < height - 1; ++y) {

const sourceLine = source[y]
const destinationLine = destination[y]
for (let x = 1; x < width - 1; ++x) {

const c = sourceLine[x]
const n = source[y - 1][x]
const e = sourceLine[x + 1]
const s = source[y + 1][x]
const w = sourceLine[x - 1]

// CNESW
const code = c << 12 | n << 9 | e << 6 | s << 3 | w // TODO: 3,6,9,12 to variable
destinationLine[x] = transitions.get(code) ?? 0

}
}

}
Insert cell
render = abgrView => colors => state => {

// TBD: jugged height, jugged width
const width = state[0].length
const height = state.length

for (let y = 0; y < height; ++y) {

const sourceLine = state[y]
const destinationOffset = y * width
for (let x = 0; x < width; ++x) {
abgrView[destinationOffset + x] = colors[sourceLine[x]]
}

}

}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more