Skip to content

Commit

Permalink
ants poc
Browse files Browse the repository at this point in the history
  • Loading branch information
Sheraff committed Oct 4, 2024
1 parent 8ed643e commit d7ba502
Show file tree
Hide file tree
Showing 5 changed files with 476 additions and 1 deletion.
137 changes: 137 additions & 0 deletions src/pages/ants/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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, useState } from "react"

export const meta: RouteMeta = {
title: 'Ants',
}

export default function () {
const [available] = useState(window.crossOriginIsolated)
const ref = useRef<HTMLCanvasElement | null>(null)

useEffect(() => {
const canvas = ref.current
if (!canvas) return
const side = Math.min(canvas.clientWidth, canvas.clientHeight)
canvas.width = side * window.devicePixelRatio
canvas.height = side * 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 width = ctx.canvas.width
const height = ctx.canvas.height
let data: Uint8Array
let done = false

const channels = 4
const image = ctx.createImageData(width, height, { colorSpace: 'srgb' })
const imageData = new Uint8Array(image.data.buffer)
const colors = {
ant: [0xaa, 0xaa, 0xaa, 0xff],
antAndFood: [0xdd, 0xff, 0xdd, 0xff],
food: [0, 0xff, 0, 0xff],
pheromoneIn: [0, 0x80, 0, 0x80],
pheromoneOut: [0x80, 0, 0, 0x80],
pheromoneBoth: [0x80, 0x80, 0, 0x80],
anthill: [0xff, 0, 0, 0xff],
obstacle: [0, 0, 0xff, 0xff],
void: [0, 0, 0, 0xff],
}

let i = 0
function loop() {
if (!done) rafId = requestAnimationFrame(loop)
if (!data) return

i++

for (let i = 0; i < data.length; i++) {
const point = data[i]
const isAnt
= point & 0b00000001
const isFood
= point & 0b00000010
const isPheromoneIn
= point & 0b00000100
const isPheromoneOut
= point & 0b00001000
const isAnthill
= point & 0b00010000
const isObstacle
= point & 0b00100000

const index = i * channels

if (isAnt && isFood) {
imageData.set(colors.antAndFood, index)
} else if (isAnt) {
imageData.set(colors.ant, index)
} else if (isFood) {
imageData.set(colors.food, index)
} else if (isPheromoneIn && isPheromoneOut) {
imageData.set(colors.pheromoneBoth, index)
} else if (isPheromoneIn) {
imageData.set(colors.pheromoneIn, index)
} else if (isPheromoneOut) {
imageData.set(colors.pheromoneOut, index)
} else if (isAnthill) {
imageData.set(colors.anthill, index)
} else if (isObstacle) {
imageData.set(colors.obstacle, index)
} else {
imageData.set(colors.void, index)
}
}

ctx.putImageData(image, 0, 0)
}
let rafId = requestAnimationFrame(loop)

const onMessage = (e: MessageEvent<Outgoing>) => {
if (e.data.type === "started") {
data = new Uint8Array(e.data.data.buffer)
} else if (e.data.type === "done") {
done = true
console.log("done")
}
}

worker.addEventListener('message', onMessage)
post("start", {
width,
height,
count: 10000,
vision: 20,
})
return () => {
worker.terminate()
worker.removeEventListener('message', onMessage)
cancelAnimationFrame(rafId)
}
}, [])

return (
<div className={styles.main}>
<div className={styles.head}>
<Head />
</div>
{available && <canvas width="1000" height="1000" ref={ref}>
Your browser does not support the HTML5 canvas tag.
</canvas>}
{!available && <p>SharedArrayBuffer is not available, the service worker must have not auto-installed, try reloading the page.</p>}
</div>
)
}
110 changes: 110 additions & 0 deletions src/pages/ants/median-angle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
*
* Implementation of median angle function
* A More Efficient Way Of Obtaining A Unique Median Estimate For Circular Data
* 2003 / B. Sango Otieno & Christine M. Anderson-Cook
* from the annexes in https://digitalcommons.wayne.edu/cgi/viewcontent.cgi?referer=&httpsredir=1&article=1738&context=jmasm
*
*/

/**
* Median Estimate For Circular Data
* @param array - array of radians (angles or other circular data modulo 2𝜋)
*/
export default function circularMedian(array: number[]) {
const sx = array.sort()
const difsin: number[] = []
const numties: number[] = []

// Checks if sample size is odd or even
const posmed = array.length % 2 === 0
? checkeven(array)
: checkodd(array)

for (let i = 0; i < posmed.length; i++) {
let positive = 0
let negative = 0
let ties = 0
const ref = posmed[i]
for (let j = 0; j < sx.length; j++) {
const value = sx[j] - ref
const sin = Math.sin(value)
if (sin > 0) positive++
else if (sin < 0) negative++
else ties++
}
difsin[i] = positive - negative
numties[i] = ties
}

// Checks for ties
const cm = posmed.filter((x, i) => difsin[i] === 0 || Math.abs(difsin[i]) > numties[i])
return cm.length
? averageAngle(cm)
: Infinity
}

function averageAngle(array: number[]) {
const y = array.reduce((sum, current) => sum + Math.sin(current))
const x = array.reduce((sum, current) => sum + Math.cos(current))
return x === 0 && y === 0
? Infinity
: Math.atan2(y, x)
// If both x and y are zero, then no circular mean exists, so assign it a large number
}

function checkeven(array: number[]) {
const check = []
// Computes possible medians
const posmed = posmedf(array)
const max = array.length / 2
for (let i = 0; i < posmed.length; i++) {
// Takes posmed[i] as the center, i.e. draws diameter at posmed[i] and counts observations on either side of the diameter
const center = posmed[i]
let positive = 0
for (let j = 0; j < array.length; j++) {
const value = array[j] - center
const cos = Math.cos(value)
if (cos > 0) positive++
}
check[i] = positive < max
? Infinity
: posmed[i]
}

return check.filter(x => x !== Infinity)
}

function checkodd(array: number[]) {
const check = []
// Each observation is a possible median
const posmed = array
const max = (array.length - 1) / 2
for (let i = 0; i < posmed.length; i++) {
// Takes posmed[i] as the center, i.e. draws diameter at posmed[i] and counts observations on either side of the diameter
const center = posmed[i]
let positive = 0
for (let j = 0; j < array.length; j++) {
const value = array[j] - center
const cos = Math.cos(value)
if (cos > 0) positive++
}
check[i] = positive > max
? Infinity
: posmed[i]
}

return check.filter(x => x !== Infinity)
}

function posmedf(array: number[]) {
const sx2 = [...array]
sx2.push(sx2.shift()!)
// Determines closest neighbors of a fixed observation
const posmed = []
for (let i = 0; i < array.length; i++) {
posmed[i] = averageAngle([array[i], sx2[i]])
}
// Computes circular mean of two adjacent observations
return posmed.filter(x => x !== Infinity)
}
24 changes: 24 additions & 0 deletions src/pages/ants/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.main {
margin: 0;
background: #051016;
color: white;
touch-action: none;
width: 100vw;
height: 100svh;
padding: 1em;

canvas {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 1px solid white;
aspect-ratio: 1;
width: 100lvmin;
}
}

.head {
position: relative;
z-index: 1;
}
Loading

0 comments on commit d7ba502

Please sign in to comment.