Skip to content

Commit

Permalink
feat: Implement Wave Function Collapse page w/ backtracking in worker
Browse files Browse the repository at this point in the history
  • Loading branch information
Sheraff committed Oct 3, 2024
1 parent b8243af commit 9e218d2
Show file tree
Hide file tree
Showing 9 changed files with 684 additions and 4 deletions.
179 changes: 179 additions & 0 deletions src/pages/wave-function-collapse/carcassonne/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import image from './image.jpg'

export async function getImageBitmap(url: string) {
const data = await fetch(url)
const blob = await data.blob()
const bitmap = await createImageBitmap(blob)
return bitmap
}

export const tileSet = await getImageBitmap(image)

export const params = {
tile: {
width: 256, // px
height: 256, // px
},
grid: {
width: 16, // tiles
},
}

export const definition = [
['castle', 'castle', 'road', 'castle'],
['castle', 'castle', 'road', 'castle'],
['castle', 'castle', 'road', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'road', 'road', 'castle'],
['castle', 'road', 'road', 'castle'],
['castle', 'road', 'road', 'castle'],
['castle', 'road', 'road', 'castle'],
['castle', 'road', 'road', 'castle'],
['grass', 'castle', 'grass', 'castle'],
['grass', 'castle', 'grass', 'castle'],
['grass', 'castle', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'grass', 'castle', 'grass'],
['castle', 'grass', 'castle', 'grass'],
['castle', 'grass', 'castle', 'grass'],
['castle', 'grass', 'grass', 'grass'],
['castle', 'grass', 'grass', 'grass'],
['castle', 'grass', 'grass', 'grass'],
['castle', 'grass', 'grass', 'grass'],
['castle', 'grass', 'grass', 'grass'],
['castle', 'grass', 'road', 'road'],
['castle', 'grass', 'road', 'road'],
['castle', 'grass', 'road', 'road'],
['castle', 'road', 'road', 'grass'],
['castle', 'road', 'road', 'grass'],
['castle', 'road', 'road', 'grass'],
['castle', 'road', 'road', 'road'],
['castle', 'road', 'road', 'road'],
['castle', 'road', 'road', 'road'],
['castle', 'road', 'grass', 'road'],
['castle', 'road', 'grass', 'road'],
['castle', 'road', 'grass', 'road'],
['castle', 'road', 'grass', 'road'],
['road', 'grass', 'road', 'grass'],
['road', 'grass', 'road', 'grass'],
['road', 'grass', 'road', 'grass'],
['road', 'grass', 'road', 'grass'],
['road', 'grass', 'road', 'grass'],
['road', 'grass', 'road', 'grass'],
['road', 'grass', 'road', 'grass'],
['road', 'grass', 'road', 'grass'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'road', 'road', 'road'],
['grass', 'road', 'road', 'road'],
['grass', 'road', 'road', 'road'],
['grass', 'road', 'road', 'road'],
['road', 'road', 'road', 'road'],
['grass', 'grass', 'grass', 'grass'],
['grass', 'grass', 'grass', 'grass'],
['grass', 'grass', 'grass', 'grass'],
['grass', 'grass', 'grass', 'grass'],
['grass', 'grass', 'road', 'grass'],
['grass', 'grass', 'road', 'grass'],
['castle', 'castle', 'castle', 'castle'],
['castle', 'castle', 'grass', 'castle'],
['castle', 'castle', 'grass', 'castle'],
['castle', 'castle', 'grass', 'castle'],
['castle', 'castle', 'grass', 'castle'],
['grass', 'grass', 'road', 'road'],
['castle', 'grass', 'road', 'grass'],
['road', 'castle', 'road', 'castle'],
['grass', 'road', 'grass', 'road'],
['road', 'road', 'road', 'road'],
['castle', 'road', 'grass', 'castle'],
['grass', 'castle', 'grass', 'grass'],
['castle', 'castle', 'castle', 'castle'],
['castle', 'castle', 'castle', 'castle'],
['castle', 'castle', 'castle', 'castle'],
['grass', 'road', 'grass', 'road'],
['grass', 'road', 'road', 'road'],
['castle', 'road', 'road', 'castle'],
['castle', 'grass', 'road', 'road'],
['castle', 'grass', 'road', 'castle'],
['castle', 'grass', 'castle', 'castle'],
['castle', 'grass', 'castle', 'castle'],
['road', 'castle', 'road', 'castle'],
['castle', 'road', 'castle', 'grass'],
['road', 'road', 'castle', 'castle'],
['castle', 'castle', 'road', 'castle'],
['castle', 'castle', 'castle', 'castle'],
['road', 'grass', 'castle', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'castle', 'castle', 'castle'],
['castle', 'castle', 'road', 'road'],
['castle', 'road', 'castle', 'road'],
['castle', 'grass', 'castle', 'grass'],
['castle', 'castle', 'castle', 'grass'],
['castle', 'castle', 'grass', 'castle'],
['road', 'grass', 'road', 'road'],
['road', 'castle', 'castle', 'grass'],
['castle', 'castle', 'road', 'grass'],
['road', 'castle', 'castle', 'castle'],
['castle', 'grass', 'castle', 'castle'],
['grass', 'castle', 'castle', 'grass'],
['castle', 'road', 'grass', 'grass'],
['road', 'road', 'road', 'road'],
['road', 'castle', 'grass', 'road'],
['castle', 'road', 'castle', 'grass'],
['grass', 'road', 'castle', 'castle'],
['castle', 'castle', 'castle', 'road'],
['grass', 'grass', 'water', 'grass'],
['grass', 'water', 'water', 'grass'],
['grass', 'water', 'water', 'grass'],
['grass', 'water', 'road', 'water'],
['road', 'road', 'water', 'water'],
['water', 'grass', 'water', 'grass'],
['water', 'grass', 'water', 'grass'],
['water', 'grass', 'grass', 'grass'],
['water', 'castle', 'water', 'road'],
['castle', 'water', 'castle', 'water'],
['road', 'water', 'road', 'water'],
['water', 'castle', 'castle', 'water'],
['road', 'castle', 'road', 'castle'],
['grass', 'castle', 'grass', 'castle'],
['castle', 'castle', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'grass', 'road', 'road'],
['castle', 'road', 'road', 'grass'],
['castle', 'grass', 'grass', 'grass'],
['grass', 'road', 'road', 'road'],
['grass', 'road', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['grass', 'grass', 'road', 'road'],
['road', 'grass', 'road', 'grass'],
['grass', 'grass', 'road', 'road'],
['road', 'grass', 'road', 'grass'],
['grass', 'grass', 'road', 'grass'],
['grass', 'grass', 'grass', 'grass'],
['castle', 'grass', 'grass', 'grass'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'castle', 'grass', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['grass', 'castle', 'castle', 'castle'],
['castle', 'grass', 'grass', 'castle'],
['castle', 'road', 'road', 'castle'],
['castle', 'road', 'road', 'road'],
['castle', 'castle', 'grass', 'castle'],
['castle', 'road', 'road', 'castle'],
['castle', 'grass', 'road', 'road'],
['castle', 'road', 'road', 'grass'],
['road', 'road', 'road', 'road'],
['grass', 'road', 'road', 'road'],
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions src/pages/wave-function-collapse/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import styles from './styles.module.css'
import { Head } from "~/components/Head"
import type { RouteMeta } from "~/router"
import type { Incoming, Outgoing } from "./worker"
import Worker from "./worker?worker"
import { useEffect, useRef } from "react"
import * as utils from './utils'
import * as config from './carcassonne/definition'

export const meta: RouteMeta = {
title: 'Wave Function Collapse',
image: './screen.png'
}

const tiles = config.definition.map((row, y) => ({
name: y,
sides: row,
}))

const equivalents = tiles.map((tile) => tiles.filter((other) => other.sides.every((side, i) => side === tile.sides[i])))



function drawTile(ctx: CanvasRenderingContext2D, w: number, h: number, index: number, rotate: number, x: number, y: number) {
if (rotate === 1) x += 1
if (rotate === 2) y += 1, x += 1
if (rotate === 3) y += 1
ctx.save()
ctx.translate(x * w, y * h)
ctx.rotate(rotate * Math.PI / 2)
const setY = (index / config.params.grid.width) | 0
const setX = index % config.params.grid.width
ctx.drawImage(
config.tileSet,
setX * config.params.tile.width,
setY * config.params.tile.height,
config.params.tile.width,
config.params.tile.height,
0,
0,
w,
h
)
ctx.restore()
}

export default function () {
const ref = useRef<HTMLCanvasElement | null>(null)

// useEffect(() => {
// const ctx = ref.current?.getContext("2d")!
// if (!ctx) return

// drawTile(ctx, 100, 100, 3, 0, 2, 1)
// }, [])

useEffect(() => {
const canvas = ref.current
if (!canvas) return
canvas.width = canvas.clientWidth * window.devicePixelRatio
canvas.height = canvas.clientHeight * window.devicePixelRatio
const ctx = canvas.getContext("2d")!
if (!ctx) return

const worker = new Worker()
function post<I extends Incoming["type"]>(
type: I,
data: Extract<Incoming, { type: I }>["data"],
transfer?: Transferable[]
) {
worker.postMessage({ type, data }, { transfer })
}

const height = 30
const width = 30
const drawX = ctx.canvas.width / width
const drawY = ctx.canvas.height / height
let map: Extract<Outgoing, { type: "started" }>["data"]["map"]
let buffer: Extract<Outgoing, { type: "started" }>["data"]["buffer"]
let done = false
let get: (x: number, y: number, t: number) => 0 | 1



let i = 0
function loop() {
if (!done) rafId = requestAnimationFrame(loop)
// rafId = requestAnimationFrame(loop)
if (!map || !buffer || !get) return

i++
ctx.clearRect(0, 0, width * drawX, height * drawY)

// const grid = Array.from({ length: height }, () => Array(width).fill(0))

for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const indices = map.reduce((acc, _, t) => (get(x, y, t) && acc.push(t), acc), [] as number[])
if (indices.length === 0) continue
const index = indices[i % indices.length]
// const index = indices[0]
// grid[y][x] = indices.length
const tile = map[index]

// const options = equivalents[tile.name]
// const pick = options[Math.floor(Math.random() * options.length)]

drawTile(ctx, drawX, drawY, tile.name, tile.rotate, x, y)
}
}
for (const [x, y] of force) {
ctx.rect(x * drawX, y * drawY, drawX, drawY)
ctx.strokeStyle = "red"
ctx.lineWidth = 2
ctx.stroke()
}

// console.log("grid")
// console.log(grid.map(row => row.join(" ")).join("\n"))
}
let rafId = requestAnimationFrame(loop)

const onMessage = (e: MessageEvent<Outgoing>) => {
if (e.data.type === "started") {
map = e.data.data.map
buffer = e.data.data.buffer
get = utils.get.bind(null, width, height, map.length, new DataView(buffer))
} else if (e.data.type === "done") {
done = true
console.log("done", e.data.data.solved)
}
}

const seed = (count: number) => {
const forces: Array<[x: number, y: number, t: number]> = []
for (let i = 0; i < count; i++) {
forces.push([Math.floor(Math.random() * width), Math.floor(Math.random() * height), tiles[Math.floor(Math.random() * tiles.length)].name])
}
return forces
}

worker.addEventListener('message', onMessage)
const force = seed(4)
console.log(force)
post("start", { height, width, tiles, force })
return () => {
worker.terminate()
worker.removeEventListener('message', onMessage)
cancelAnimationFrame(rafId)
}
}, [])

return (
<div className={styles.main} >
<Head />
<canvas width="1000" height="1000" ref={ref}>
Your browser does not support the HTML5 canvas tag.
</canvas>
</div>
)
}
Binary file added src/pages/wave-function-collapse/screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions src/pages/wave-function-collapse/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.main {
margin: 0;
background: #051016;
color: white;
touch-action: none;
width: 100vw;
height: 100svh;
padding: 1em;

canvas {
border: 1px solid white;
aspect-ratio: 1;
width: 100%;
}
}
17 changes: 17 additions & 0 deletions src/pages/wave-function-collapse/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const get = (width: number, height: number, length: number, view: DataView, x: number, y: number, t: number) => {
const index = (y * width + x) * length + t
const byte = index >> 3 // index / 8
const offset = index & ~(byte << 3) // index % 8
return ((view.getUint8(byte) >> offset) & 1) as 0 | 1
}

export const set = (width: number, height: number, length: number, view: DataView, x: number, y: number, t: number, value: boolean) => {
const index = (y * width + x) * length + t
const byte = index >> 3 // index / 8
const offset = index & ~(byte << 3) // index % 8
if (value) {
view.setUint8(byte, view.getUint8(byte) | (1 << offset))
} else {
view.setUint8(byte, view.getUint8(byte) & ~(1 << offset))
}
}
Loading

0 comments on commit 9e218d2

Please sign in to comment.