Add game of life post

post/wasm-gol-2
Ashelyn Dawn 1 year ago
parent c42e58a54d
commit fb972bc222

@ -1,8 +1,11 @@
'use client' 'use client'
import EventEmitter from 'events'
import { useState, useEffect, MouseEvent } from 'react' import { useState, useEffect, MouseEvent } from 'react'
type AppearanceMode = undefined | 'light' | 'dark' type AppearanceMode = undefined | 'light' | 'dark'
export const themeSignal = new EventEmitter()
export default function Appearance() { export default function Appearance() {
const [appearance, setAppearance] = useState<AppearanceMode>(undefined) const [appearance, setAppearance] = useState<AppearanceMode>(undefined)
@ -11,6 +14,8 @@ export default function Appearance() {
document.body.parentElement?.classList.remove('light') document.body.parentElement?.classList.remove('light')
document.body.parentElement?.classList.remove('dark') document.body.parentElement?.classList.remove('dark')
document.body.parentElement?.classList.add(appearance) document.body.parentElement?.classList.add(appearance)
themeSignal.emit('change')
} }
}, [appearance]) }, [appearance])

@ -1,10 +1,12 @@
'use client' 'use client'
import { useEffect, useRef } from "react" import { useEffect, useRef } from "react"
import { themeSignal } from "../Appearance"
interface Module { interface Module {
setup?: (wabt?: any) => Promise<undefined> setup?: (wabt?: any) => Promise<undefined>
cleanup?: () => Promise<undefined> cleanup?: () => Promise<undefined>
onThemeChange?: () => void
} }
export default function PageScript({ script, wasm }: { script?: string, wasm?: Uint8Array }) { export default function PageScript({ script, wasm }: { script?: string, wasm?: Uint8Array }) {
@ -35,5 +37,18 @@ export default function PageScript({ script, wasm }: { script?: string, wasm?: U
} }
}, []) }, [])
useEffect(() => {
function onThemeChange() {
const mod = moduleRef.current
if (mod?.onThemeChange && typeof mod?.onThemeChange === 'function') {
mod.onThemeChange()
}
}
themeSignal.addListener('change', onThemeChange)
return () => { themeSignal.removeListener('change', onThemeChange) }
}, [])
return null return null
} }

@ -34,7 +34,23 @@ export default async function Post({ params: { slug } }: { params: { slug: strin
<p className={styles.subtitle}>{post.subtitle}</p> <p className={styles.subtitle}>{post.subtitle}</p>
<Markdown>{post.body}</Markdown> <Markdown>{post.body}</Markdown>
{post.script && <PageScript script={post.script} wasm={post.wasm} />}
{post.script && <PageScript script={post.script.path} wasm={post.wasm?.contents} />}
{(post.script || post.wasm) && (
<>
<hr className={styles.postFooter} />
<h2 id="resources">Post Resources:</h2>
<ul>
{post.script && (
<li><a target="_blank" href={post.script.path}>{post.script.name}</a></li>
)}
{post.wasm && (
<li><a target="_blank" href={post.wasm.path}>{post.wasm.name}</a></li>
)}
</ul>
</>
)}
</article> </article>
</> </>
); );
@ -60,8 +76,6 @@ Code.theme = {
} }
function CodeBlock({ children }: { children: ReactElement }) { function CodeBlock({ children }: { children: ReactElement }) {
console.log(children)
// extract props normally passed to pre element // extract props normally passed to pre element
const { className: langKey, children: sourceText, ...rest } = children?.props ?? {} const { className: langKey, children: sourceText, ...rest } = children?.props ?? {}
const language = langKey?.replace(/^lang-/i, '') const language = langKey?.replace(/^lang-/i, '')

@ -0,0 +1,459 @@
---
title: Pure Wasm Game of Life
subtitle: Because I'm truly a pedant at heart
script: wasm-life-1/controller.js
wasm: wasm-life-1/game.wat
unlisted: true
---
Lately in doing research on WebAssembly I've been lookig around for examples
of things implemented in it, and I've come across several blog posts based on
the fantastic *Rust And WebAssembly* tutorial for
[Conway's Game of Life](https://rustwasm.github.io/docs/book/game-of-life/introduction.html).
And I mean no shade towards the folks who wrote those, but I feel it's mildly
disingenuous to say that those are "Conway's Game of Life in WebAssembly" when
the actual game code was written entirely in Rust. WebAssembly is in that case
really no more than a compilation target, not actually the language used.
So of course I knew what I had to do . . .
Welcome to Conway's Game of Life, *actually implemented* in WebAssembly:
<canvas
id="game"
width="800"
height="600"
data-pixelsize="3"
style="width:100%; aspect-ratio: 4/3; image-rendering: pixelated;"
/>
<p style="display: flex; align-items: flex-start; margin-top: 4px; height: 64px">
<span style="flex: 1" id="frameTimes"/>
<button id="reset">Reset</button>
</p>
What you're looking at here is Conway's Game of Life where ***all of the game code***
is written in pure, raw, not-a-compiler-in-sight WebAssembly. Its performance is
pretty comparable to the Rust versions, and I'm glad to say the code for it isn't
even that much of a mess!
Let's dive into how it works together, shall we?
## Overview
As with most things in wasm we need to decide ahead of time what we are going
to implement directly in it, and what things are going to stay in the Javascript
part. I went with implementing the core game logic in wasm, leaving the initialization
and display code in JS.
I chose the first because I didn't want to have to deal with getting a float
value back from `Math.random()` in wasm, and the second because DOM manipulation,
canvas, and WebGL are all a bit of a pain to do manually from WebAssembly.
For convenience in this implementation I am using a byte per cell. Packing a bit
per cell into less memory space would be great, but I'm trying to keep it
*relatively* simple at first. I'm planning to revisit that in a later post though.
## Code samples
For now, let's look at a few selections from the code (links to full source will be
at the [bottom of the page](#resources)).
### Globals and Initialization
```wasm
(module
(memory (export "shared_memory") 1)
(global $boardWidth (mut i32) (i32.const 0))
(global $boardHeight (mut i32) (i32.const 0))
(global $boardBufferLength (mut i32) (i32.const 0))
(global $buffer0ptr (mut i32) (i32.const -1))
(global $buffer1ptr (mut i32) (i32.const -1))
(global $currentBuffer (mut i32) (i32.const -1))
```
I start the wasm module out with 1 page of memory (64 KiB), and define global
variables for the board dimensions, the length of each board buffer (in bytes),
the locations of each buffer, and which one is currently selected.
You can see that each of these gets initialized in the next function:
```wasm
(func (export "initializeBoard") (param $width i32) (param $height i32)
;; Store width and height for later
(global.set $boardWidth (local.get $width))
(global.set $boardHeight (local.get $height))
;; Compute total cells per board
local.get $width
local.get $height
i32.mul
global.set $boardBufferLength
;; Request enough memory for both boards
global.get $boardBufferLength
i32.const 2
i32.mul
call $growMemoryForBoards
;; Set pointer locations for our two boards
(global.set $buffer0ptr (i32.const 0))
(global.set $buffer1ptr (global.get $boardBufferLength))
;; Set current board
(global.set $currentBuffer (i32.const 0))
)
```
In the case that `$growMemoryForBoards` fails it will crash the WebAssembly
module, but considering I don't have a backup plan for how to make do with
less memory, that's acceptable to me.
### Manipulating the board
Next let's check out some of the basic board manipulation functions that our
Javascript code calls during initialization and display:
```wasm
(func $getValueAtPosition (export "getValueAtPosition") (param $row i32) (param $column i32) (result i32)
(local $position i32)
local.get $row
local.get $column
call $getIndexForPosition
local.tee $position
i32.const 0
i32.lt_s
if
i32.const 0
return
end
local.get $position
call $getBoardPtr
i32.add
i32.load8_u
)
(func $setValueAtPosition (export "setValueAtPosition") (param $row i32) (param $column i32) (param $value i32)
(local $position i32)
local.get $row
local.get $column
call $getIndexForPosition
local.tee $position
i32.const 0
i32.lt_s
if
return
end
local.get $position
call $getBoardPtr
i32.add
local.get $value
i32.store8
)
```
As you can see both rely on another function called `$getIndexForPosition`, check
its return value to make sure it didn't give -1, and then add that position to
the current board pointer. Not too bad so far!
That helper function `$getIndexForPosition` is also relatively simple:
```wasm
(func $getIndexForPosition (param $row i32) (param $column i32) (result i32)
local.get $row
i32.const 0
global.get $boardHeight
call $positionInRange
local.get $column
i32.const 0
global.get $boardWidth
call $positionInRange
i32.and
i32.eqz
if
i32.const -1
return
end
global.get $boardWidth
local.get $row
i32.mul
local.get $column
i32.add
)
```
It again does some basic bounds checking, then some math with the board with,
row and column. Generally this all matches so far to how you might implement
this in any other language.
### Updating the board
Okay so this is where stuff starts to get a bit messy. WebAssembly *ostensibly*
has loops, but they're really more just a conditional jump. So the main function
for updating the board (which has to iterate through every position) gets to
be a bit verbose:
```wasm
(func $tick (export "tick")
(local $row i32)
(local $column i32)
(local $value i32)
i32.const 0
local.set $row
loop $rows
;; start at the beginning of a row
i32.const 0
local.set $column
;; for every column in the row
loop $columns
;; compute new value
local.get $row
local.get $column
call $getNewValueAtPosition
local.set $value
;; place in next board
call $swapBoards
local.get $row
local.get $column
local.get $value
call $setValueAtPosition
call $swapBoards
;; increment column
local.get $column
i32.const 1
i32.add
local.tee $column
;; loop back if less than width
global.get $boardWidth
i32.lt_s
br_if $columns
end
;;increment row
local.get $row
i32.const 1
i32.add
local.tee $row
;; loop back if less than heeight
global.get $boardHeight
i32.lt_s
br_if $rows
end
;; swap to the new board
call $swapBoards
)
```
The `$swapBoards` function here is not that important to look at, it just
changes the current board flag so that `$getBoardPtr` returns the correct one.
I am kind of annoyed that I have to swap the board back and forth all the time,
but we'll see if that becomes an issue later.
But what's this `$getNewValueAtPosition` function? Let's have a look at that!
. . . prepare yourself, this one's a doozy.
```wasm
(func $getNewValueAtPosition (param $row i32) (param $column i32) (result i32)
(local $count i32)
local.get $row
i32.const 1
i32.sub
local.get $column
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
call $getValueAtPosition
local.get $row
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
local.get $row
i32.const 1
i32.sub
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
i32.const 1
i32.sub
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
i32.add
i32.add
i32.add
i32.add
i32.add
i32.add
i32.add
;; Exactly 3 neighbors
local.tee $count
i32.const 3
i32.eq
if
;; becomes or stays alive
i32.const 1
return
end
;; If currently dead
local.get $row
local.get $column
call $getValueAtPosition
i32.eqz
if
;; Stay dead
i32.const 0
return
end
;; 2 neighbors
local.get $count
i32.const 2
i32.eq
if
i32.const 1
return
end
i32.const 0
return
)
```
This is (effectively) an unrolled loop. I could make this code shorter
but un-unrolling my loop, but I couldn't find a way to do that which didn't
immediately result in more instructions being run overall, so *for the moment*
I'm leaving it like this.
But once you know what each chunk is doing, yeah it's pretty simple! Each of the
`$getValueAtPosition` calls adds either a 0 or a 1 to the stack, and then we add
all of those up, store it in a variable, and check it against our various possible
outcomes.
Not that bad really, it's just rather verbose.
### And the glue
Lastly let's look at some of the JS that ties this together. I'm not going to
look that closely at the bit that loads the WebAssembly and initializes the
module - I assume most ~~(sane)~~ folks are using a bindings generator or bundler
or something else that does that for them. But let's look at the board initialization
and drawing code.
Starting with the board initialization, you can see it's rather short:
```js
function initialize() {
const { gameExports, width, height } = gameState
gameExports.initializeBoard(width, height)
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const filled = Math.random() > .5;
gameExports.setValueAtPosition(row, column, filled ? 1 : 0)
}
}
}
```
Oh the pleasures of a high-level language - the conciseness is just lovely isn't it?
And hopefully you can see why I didn't want to import `Math.random()` into my
WebAssembly - then I'd have to deal with floats, and more iteration, and it'd just
not be fun.
Okay now for the drawing code:
```js
function drawBoard() {
const { gameExports, width, height, pixelSize, ctx, canvas } = gameState
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = 'currentColor'
ctx.beginPath()
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const alive = gameExports.getValueAtPosition(row, column)
if (!alive) continue
const x = column * pixelSize
const y = row * pixelSize
ctx.moveTo(x, y)
ctx.lineTo(x + pixelSize, y)
ctx.lineTo(x + pixelSize, y + pixelSize)
ctx.lineTo(x, y + pixelSize)
ctx.lineTo(x, y)
}
}
ctx.fill()
}
```
This is a pretty simple use of the `<canvas>` element, I think maybe in the
future if I want to optimize this I'd probably look into only updating the
changed cells or something like that, so that it doesn't need to redraw
the *entire* board each time. But on anything up to about a 400x300 grid
this was staying at roughly 5ms per frame on my machine, which should be
suitable for keeping about 60 frames per second - particularly if I can
optimize the board update function a bit as well.

@ -0,0 +1,142 @@
let gameState = {
running: false,
pixelSize: 0,
lastReported: null,
frameTimes: [],
frames: 0,
width: 0,
height: 0,
canvas: null,
ctx: null,
gameExports: null
}
const initialMessage = "Click the board above to start simulation"
export async function setup(wasmModule) {
gameState.gameExports = wasmModule.exports
const canvas = gameState.canvas = document.querySelector('#game')
gameState.ctx = gameState.canvas.getContext("2d")
const pixelSize = gameState.pixelSize = parseInt(canvas.getAttribute('data-pixelsize') || '2')
gameState.width = Math.floor(parseInt(canvas.width) / pixelSize)
gameState.height = Math.floor(parseInt(canvas.height) / pixelSize)
initialize()
drawBoard()
const frameTimesElem = document.getElementById('frameTimes')
const resetButton = document.getElementById('reset')
frameTimesElem.innerText = initialMessage
gameState.canvas.addEventListener('click', () => {
if (!gameState.running) {
if (frameTimesElem.innerText === initialMessage)
frameTimesElem.innerText = 'Starting...'
gameState.running = true
frameLoop()
} else {
gameState.running = false;
gameState.frameTimes = []
gameState.lastReported = null
}
})
resetButton.addEventListener('click', () => {
gameState.running = false
gameState.frameTimes = []
gameState.lastReported = null
frameTimesElem.innerText = initialMessage
initialize()
drawBoard()
})
}
export async function onThemeChange() {
drawBoard()
}
export async function cleanup() {
gameState.running = false
}
function initialize() {
const { gameExports, width, height } = gameState
gameExports.initializeBoard(width, height)
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const filled = Math.random() > .5;
gameExports.setValueAtPosition(row, column, filled ? 1 : 0)
}
}
}
function frameLoop() {
const { gameExports, running, frameTimes } = gameState
if (!gameState.lastReported)
gameState.lastReported = performance.now()
if (!running) return
const beforeTick = performance.now()
gameExports.tick()
const afterTick = performance.now()
drawBoard()
const afterDraw = performance.now()
// Push raw frame time
frameTimes.push(afterDraw - beforeTick)
if (frameTimes.length >= 10) {
const averageFrame = (frameTimes.reduce((a, b) => a + b) / frameTimes.length).toFixed(2);
const lastReported = gameState.lastReported
const current = gameState.lastReported = performance.now()
const tenFrameTime = (current - lastReported) / 1000
const fps = Math.floor(10 / tenFrameTime).toFixed(0)
gameState.frameTimes = []
const pagesUsed = gameExports.getPagesUsed()
document.getElementById('frameTimes').innerHTML =
`Frames per second: ${fps}<br/>
Milliseconds per frame: ${averageFrame}<br/>
Memory allocated: ${pagesUsed} pages (${pagesUsed * 64} KiB)`
}
console.log(`tick took ${afterTick - beforeTick}ms`)
console.log(`draw took ${afterDraw - afterTick}ms`)
requestAnimationFrame(frameLoop)
}
function drawBoard() {
const { gameExports, width, height, pixelSize, ctx, canvas } = gameState
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = 'currentColor'
ctx.beginPath()
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const alive = gameExports.getValueAtPosition(row, column)
if (!alive) continue
const x = column * pixelSize
const y = row * pixelSize
ctx.moveTo(x, y)
ctx.lineTo(x + pixelSize, y)
ctx.lineTo(x + pixelSize, y + pixelSize)
ctx.lineTo(x, y + pixelSize)
ctx.lineTo(x, y)
}
}
ctx.fill()
}

@ -0,0 +1,337 @@
(module
(memory (export "shared_memory") 1)
(global $boardWidth (mut i32) (i32.const 0))
(global $boardHeight (mut i32) (i32.const 0))
(global $boardBufferLength (mut i32) (i32.const 0))
(global $buffer0ptr (mut i32) (i32.const -1))
(global $buffer1ptr (mut i32) (i32.const -1))
(global $currentBuffer (mut i32) (i32.const -1))
(func (export "initializeBoard") (param $width i32) (param $height i32)
;; Store width and height for later
(global.set $boardWidth (local.get $width))
(global.set $boardHeight (local.get $height))
;; Compute total cells per board
local.get $width
local.get $height
i32.mul
global.set $boardBufferLength
;; Request enough memory for both boards
global.get $boardBufferLength
i32.const 2
i32.mul
call $growMemoryForBoards
;; Set pointer locations for our two boards
(global.set $buffer0ptr (i32.const 0))
(global.set $buffer1ptr (global.get $boardBufferLength))
;; Set current board
(global.set $currentBuffer (i32.const 0))
)
(func (export "getPagesUsed") (result i32)
memory.size
)
(func $growMemoryForBoards (param $totalBytes i32)
(local $targetPages i32)
;; figure out target page size
local.get $totalBytes
i32.const 1
i32.sub
i32.const 65536 ;; size of a page
i32.div_u
i32.const 1
i32.add
;; get difference
memory.size
i32.sub
;; grow
memory.grow
drop ;; ignore result, we're gonna crash anyways
;; perhaps we should have a way to report errors back to JS next time
)
(func $getBoardPtr (result i32)
global.get $currentBuffer
i32.eqz
if (result i32)
global.get $buffer0ptr
else
global.get $buffer1ptr
end
)
(func $swapBoards
global.get $currentBuffer
i32.eqz
if (result i32)
i32.const 1
else
i32.const 0
end
global.set $currentBuffer
)
(func $positionInRange (param $position i32) (param $min i32) (param $max i32) (result i32)
local.get $position
local.get $min
i32.lt_s
if
i32.const 0
return
end
local.get $position
local.get $max
i32.ge_s
if
i32.const 0
return
end
i32.const 1
return
)
(func $getIndexForPosition (param $row i32) (param $column i32) (result i32)
local.get $row
i32.const 0
global.get $boardHeight
call $positionInRange
local.get $column
i32.const 0
global.get $boardWidth
call $positionInRange
i32.and
i32.eqz
if
i32.const -1
return
end
global.get $boardWidth
local.get $row
i32.mul
local.get $column
i32.add
)
(func $getValueAtPosition (export "getValueAtPosition") (param $row i32) (param $column i32) (result i32)
(local $position i32)
local.get $row
local.get $column
call $getIndexForPosition
local.tee $position
i32.const 0
i32.lt_s
if
i32.const 0
return
end
local.get $position
call $getBoardPtr
i32.add
i32.load8_u
)
(func $setValueAtPosition (export "setValueAtPosition") (param $row i32) (param $column i32) (param $value i32)
(local $position i32)
local.get $row
local.get $column
call $getIndexForPosition
local.tee $position
i32.const 0
i32.lt_s
if
return
end
local.get $position
call $getBoardPtr
i32.add
local.get $value
i32.store8
)
(func $getNewValueAtPosition (param $row i32) (param $column i32) (result i32)
(local $count i32)
local.get $row
i32.const 1
i32.sub
local.get $column
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
call $getValueAtPosition
local.get $row
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
local.get $row
i32.const 1
i32.sub
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
i32.const 1
i32.sub
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
i32.add
i32.add
i32.add
i32.add
i32.add
i32.add
i32.add
;; Exactly 3 neighbors
local.tee $count
i32.const 3
i32.eq
if
;; becomes or stays alive
i32.const 1
return
end
;; If currently dead
local.get $row
local.get $column
call $getValueAtPosition
i32.eqz
if
;; Stay dead
i32.const 0
return
end
;; 2 neighbors
local.get $count
i32.const 2
i32.eq
if
i32.const 1
return
end
i32.const 0
return
)
(func $tick (export "tick")
(local $row i32)
(local $column i32)
(local $value i32)
i32.const 0
local.set $row
loop $rows
;; start at the beginning of a row
i32.const 0
local.set $column
;; for every column in the row
loop $columns
;; compute new value
local.get $row
local.get $column
call $getNewValueAtPosition
local.set $value
;; place in next board
call $swapBoards
local.get $row
local.get $column
local.get $value
call $setValueAtPosition
call $swapBoards
;; increment column
local.get $column
i32.const 1
i32.add
local.tee $column
;; loop back if less than width
global.get $boardWidth
i32.lt_s
br_if $columns
end
;;increment row
local.get $row
i32.const 1
i32.add
local.tee $row
;; loop back if less than heeight
global.get $boardHeight
i32.lt_s
br_if $rows
end
;; swap to the new board
call $swapBoards
)
)

@ -1,23 +1,30 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200&display=swap');
:root, :root.dark { :root, :root.dark {
--background:#2F2536; --backgroundDark:#2F2536;
--foreground:#E1B8FF; --foregroundDark:#E1B8FF;
--illustrationDark: white;
--backgroundLight: #f5eaff; --backgroundLight: #f5eaff;
--foregroundLight: #49332d; --foregroundLight: #49332d;
--illustrationLight: #8c002e;
--padding: 16px; --padding: 16px;
--background: var(--backgroundDark);
--foreground: var(--foregroundDark);
--illustration: var(--illustrationDark);
} }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
--background: var(--backgroundLight); --background: var(--backgroundLight);
--foreground: var(--foregroundLight); --foreground: var(--foregroundLight);
--illustration: var(--illustrationLight);
} }
} }
:root.light { :root.light {
--background: var(--backgroundLight); --background: var(--backgroundLight);
--foreground: var(--foregroundLight); --foreground: var(--foregroundLight);
--illustration: var(--illustrationLight);
} }
@ -71,3 +78,39 @@ body {
min-width: 300px; min-width: 300px;
margin: 0 auto; margin: 0 auto;
} }
#container main canvas {
color: var(--illustration);
}
#container main p > code {
padding: 2px 4px;
position: relative;
font-style: italic;
}
#container main p > code::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: currentColor;
border-radius: 8px;
opacity: .15;
pointer-events: none;
}
#container main p > code::after {
content: ' ';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: solid 1px currentColor;
opacity: .8;
border-radius: 8px;
pointer-events: none;
}

@ -21,3 +21,13 @@
.post p a { .post p a {
color: inherit; color: inherit;
} }
hr.postFooter {
margin-top: calc(3 * var(--padding));
color: inherit;
opacity: .4;
}
.postFooter ~ ul li a {
color: inherit;
}

@ -15,8 +15,17 @@ export interface PostMeta {
export interface Post extends Omit<PostMeta, "filePath"> { export interface Post extends Omit<PostMeta, "filePath"> {
body: string, body: string,
script?: string, script?: Script,
wasm?: Uint8Array, wasm?: Wasm
}
export interface Script {
name: string,
path: string,
}
export interface Wasm extends Script {
contents: Uint8Array
} }
type Attributes = { [key: string]: any } type Attributes = { [key: string]: any }
@ -75,6 +84,7 @@ export async function loadSinglePage(slug: string): Promise<Post | null> {
const { attributes, body } = frontmatter(fileContents) const { attributes, body } = frontmatter(fileContents)
const data: Attributes = attributes as Attributes const data: Attributes = attributes as Attributes
let script: Script | undefined = undefined;
if (data.script) { if (data.script) {
const scriptPath = path.join(process.cwd(), 'public/scripts', data.script) const scriptPath = path.join(process.cwd(), 'public/scripts', data.script)
try { try {
@ -82,10 +92,14 @@ export async function loadSinglePage(slug: string): Promise<Post | null> {
} catch { } catch {
throw new Error(`Could not find script ${scriptPath} referenced in post ${slug}`) throw new Error(`Could not find script ${scriptPath} referenced in post ${slug}`)
} finally { } finally {
data.script = `/scripts/${data.script}` script = {
name: data.script.replace(/^.*\//, ''),
path: `/scripts/${data.script}`
}
} }
} }
let wasm: Wasm | undefined = undefined;
if (data.wasm) { if (data.wasm) {
const wabt = await wabtModule() const wabt = await wabtModule()
const wasmPath = path.join(process.cwd(), 'public/scripts', data.wasm) const wasmPath = path.join(process.cwd(), 'public/scripts', data.wasm)
@ -95,15 +109,19 @@ export async function loadSinglePage(slug: string): Promise<Post | null> {
const wasmIntermed = wabt.parseWat(fileName, wasmText) const wasmIntermed = wabt.parseWat(fileName, wasmText)
const { buffer: wasmBinary } = wasmIntermed.toBinary({ write_debug_names: true }) const { buffer: wasmBinary } = wasmIntermed.toBinary({ write_debug_names: true })
data.wasm = wasmBinary wasm = {
name: fileName,
path: `/scripts/${data.wasm}`,
contents: wasmBinary
}
} }
const { filePath, ...otherMeta } = postMeta const { filePath, ...otherMeta } = postMeta
return { return {
...otherMeta, ...otherMeta,
script: data.script, script,
wasm: data.wasm, wasm,
body body
} }
} }

Loading…
Cancel
Save