website style and circuits art
parent
d1fa37524d
commit
a541740a29
@ -0,0 +1,316 @@
|
||||
const $canvas: HTMLCanvasElement = document.querySelector('#circuits-art')!
|
||||
|
||||
interface Grid<T> extends Iterable<[number, number, T]> {
|
||||
has(point: [number, number]): boolean
|
||||
get(point: [number, number]): T
|
||||
set(point: [number, number], value: T): void
|
||||
}
|
||||
|
||||
function createGrid<T>(): Grid<T> {
|
||||
const cells: Record<string, T> = {}
|
||||
|
||||
return {
|
||||
has([x, y]) {
|
||||
return cells[`${x},${y}`] !== undefined
|
||||
},
|
||||
get([x, y]) {
|
||||
return cells[`${x},${y}`]
|
||||
},
|
||||
set([x, y], value) {
|
||||
cells[`${x},${y}`] = value
|
||||
},
|
||||
*[Symbol.iterator]() {
|
||||
for (const [coord, value] of Object.entries(cells)) {
|
||||
const [x, y] = coord.split(',').map(s => parseInt(s))
|
||||
yield [x, y, value]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type WireCell = 'down' | 'down-left' | 'down-right' | 'dot'
|
||||
|
||||
type WireDirection = 'down' | 'down-left' | 'down-right'
|
||||
|
||||
type WireSteps = { position: Point2; direction: WireCell }[]
|
||||
|
||||
type Point2 = [number, number]
|
||||
|
||||
type State = {
|
||||
grid: Grid<WireCell>
|
||||
queuedWire: {
|
||||
index: number
|
||||
steps: WireSteps
|
||||
} | null
|
||||
badTries: 0
|
||||
}
|
||||
|
||||
type Renderer = {
|
||||
timer: number
|
||||
}
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
|
||||
const RENDERER_FPS = 30
|
||||
|
||||
function setup() {
|
||||
console.log('Setting up circuits art...')
|
||||
|
||||
$canvas.width = $canvas.clientWidth
|
||||
$canvas.height = $canvas.clientHeight
|
||||
|
||||
const g = $canvas.getContext('2d')!
|
||||
const state: State = {
|
||||
grid: createGrid(),
|
||||
queuedWire: null,
|
||||
badTries: 0,
|
||||
}
|
||||
|
||||
const startTime = new Date()
|
||||
|
||||
const handle = setInterval(() => {
|
||||
const time = new Date().getTime() - startTime.getTime()
|
||||
|
||||
update(state, g.canvas.width, g.canvas.height, time)
|
||||
render(g, state, time)
|
||||
}, 1000 / RENDERER_FPS)
|
||||
|
||||
renderer = { timer: handle }
|
||||
}
|
||||
|
||||
function update(state: State, width: number, height: number, time: number) {
|
||||
const w = (width / CELL_SIZE) | 0
|
||||
// const h = (height / CELL_SIZE) | 0
|
||||
|
||||
if (state.badTries > 1000) {
|
||||
console.log('finished')
|
||||
clearInterval(renderer!.timer)
|
||||
}
|
||||
|
||||
if (!state.queuedWire) {
|
||||
const sx = randomInt(0, w)
|
||||
if (!state.grid.has([sx, 0])) {
|
||||
const steps = generateWire(state.grid, [sx, 0])
|
||||
if (steps.length < 7) {
|
||||
state.badTries++
|
||||
return
|
||||
}
|
||||
|
||||
state.queuedWire = {
|
||||
index: 0,
|
||||
steps,
|
||||
}
|
||||
|
||||
state.grid.set(state.queuedWire.steps[0].position, 'dot')
|
||||
return
|
||||
}
|
||||
|
||||
state.badTries++
|
||||
} else {
|
||||
const wire = state.queuedWire
|
||||
|
||||
const step = wire.steps[wire.index]
|
||||
state.grid.set(step.position, step.direction)
|
||||
|
||||
if (wire.index + 1 < wire.steps.length) {
|
||||
state.grid.set(wire.steps[wire.index + 1].position, 'dot')
|
||||
}
|
||||
|
||||
wire.index++
|
||||
|
||||
if (wire.index >= wire.steps.length) {
|
||||
state.queuedWire = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CELL_SIZE = 27
|
||||
|
||||
const BACKGROUND_COLOR = '#ecffe3'
|
||||
const WIRE_COLOR = '#a6ce94'
|
||||
|
||||
const RENDER_CELL: Record<WireCell, (g: CanvasRenderingContext2D) => void> = {
|
||||
'down': g => {
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.moveTo(0, 0)
|
||||
g.lineTo(0, 1)
|
||||
g.stroke()
|
||||
},
|
||||
'down-left': g => {
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.moveTo(0, 0)
|
||||
g.lineTo(-1, 1)
|
||||
g.stroke()
|
||||
},
|
||||
'down-right': g => {
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.moveTo(0, 0)
|
||||
g.lineTo(+1, 1)
|
||||
g.stroke()
|
||||
},
|
||||
'dot': g => {
|
||||
g.fillStyle = BACKGROUND_COLOR
|
||||
g.beginPath()
|
||||
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||
g.fill()
|
||||
|
||||
g.strokeStyle = WIRE_COLOR
|
||||
g.beginPath()
|
||||
g.ellipse(0, 0, 0.15, 0.15, 0, 0, 2 * Math.PI)
|
||||
g.stroke()
|
||||
},
|
||||
}
|
||||
|
||||
function render(g: CanvasRenderingContext2D, state: State, time: number) {
|
||||
g.clearRect(0, 0, g.canvas.width, g.canvas.height)
|
||||
g.resetTransform()
|
||||
g.scale(CELL_SIZE, CELL_SIZE)
|
||||
g.lineWidth = 3 / CELL_SIZE
|
||||
|
||||
const w = (g.canvas.width / CELL_SIZE) | 0
|
||||
const h = (g.canvas.height / CELL_SIZE) | 0
|
||||
for (let y = 0; y <= h + 1; y++) {
|
||||
for (let x = 0; x <= w + 1; x++) {
|
||||
if (!state.grid.has([x, y])) continue
|
||||
|
||||
const cell = state.grid.get([x, y])
|
||||
|
||||
g.save()
|
||||
g.translate(x, y)
|
||||
|
||||
// g.fillStyle = '#f008'
|
||||
// g.beginPath()
|
||||
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||
// g.fill()
|
||||
|
||||
RENDER_CELL[cell](g)
|
||||
g.restore()
|
||||
}
|
||||
}
|
||||
|
||||
// if (state.queuedWire) {
|
||||
// for (const step of state.queuedWire.steps) {
|
||||
// const [x, y] = step.position
|
||||
|
||||
// g.fillStyle = '#00f8'
|
||||
// g.save()
|
||||
// g.translate(x, y)
|
||||
|
||||
// g.beginPath()
|
||||
// g.rect(-0.25, -0.25, 0.5, 0.5)
|
||||
// g.fill()
|
||||
// g.restore()
|
||||
// }
|
||||
// }
|
||||
|
||||
const [mx, my] = state.mouse
|
||||
g.save()
|
||||
g.fillStyle = '#0008'
|
||||
g.translate(Math.floor(mx / CELL_SIZE), Math.floor(my / CELL_SIZE))
|
||||
g.beginPath()
|
||||
g.rect(0, 0, 1, 1)
|
||||
g.fill()
|
||||
g.restore()
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (renderer) {
|
||||
clearInterval(renderer.timer)
|
||||
}
|
||||
setup()
|
||||
})
|
||||
|
||||
function randomInt(from: number, to: number) {
|
||||
return Math.floor(Math.random() * (to - from + 1))
|
||||
}
|
||||
|
||||
function randomChoice<T>(choices: T[]): T {
|
||||
return choices[randomInt(0, choices.length - 1)]
|
||||
}
|
||||
|
||||
function randomWeightedChoice<T>(choices: [T, number][]) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3 + 4 + 5 + 6
|
||||
randomWeightedChoice([
|
||||
['a', 3],
|
||||
['b', 4],
|
||||
['c', 5],
|
||||
['d', 6],
|
||||
])
|
||||
|
||||
const DIR_TO_VEC: Record<WireDirection, Point2> = {
|
||||
['down']: [0, 1],
|
||||
['down-left']: [-1, 1],
|
||||
['down-right']: [+1, 1],
|
||||
}
|
||||
|
||||
type ShortCircuitBoolean = boolean | (() => boolean)
|
||||
|
||||
const callOrBoolean = (v: ShortCircuitBoolean): boolean => (typeof v === 'boolean' ? v : v())
|
||||
const implies = (a: ShortCircuitBoolean, b: ShortCircuitBoolean) => !callOrBoolean(a) || callOrBoolean(b)
|
||||
|
||||
/**
|
||||
* Tells whether a given direction is not blocked by some other cell in a given grid and starting position
|
||||
*/
|
||||
const DIR_AVAILABLE_PREDICATE: Record<WireDirection, (pos: Point2, grid: Grid<WireCell>) => boolean> = {
|
||||
['down']: ([x, y], grid) =>
|
||||
!grid.has([x, y + 1]) &&
|
||||
implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) !== 'down-right') &&
|
||||
implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) !== 'down-left'),
|
||||
['down-left']: ([x, y], grid) => !grid.has([x - 1, y + 1]) && implies(grid.has([x - 1, y]), () => grid.get([x - 1, y]) === 'down-left'),
|
||||
['down-right']: ([x, y], grid) =>
|
||||
!grid.has([x + 1, y + 1]) && implies(grid.has([x + 1, y]), () => grid.get([x + 1, y]) === 'down-right'),
|
||||
}
|
||||
|
||||
function pruneDirections(grid: Grid<WireCell>, position: Point2, directions: WireDirection[]): WireDirection[] {
|
||||
return directions.filter(dir => DIR_AVAILABLE_PREDICATE[dir](position, grid))
|
||||
}
|
||||
|
||||
function generateWire(grid: Grid<WireCell>, startingPoint: Point2): { position: Point2; direction: WireCell }[] {
|
||||
const segmentLength = Math.floor(1 - Math.random() ** 2) * 10 + 30
|
||||
let currentPosition = startingPoint
|
||||
let currentDirection: WireDirection = randomChoice(['down', 'down', 'down', 'down-left', 'down-right'])
|
||||
|
||||
const steps: { position: Point2; direction: WireCell }[] = []
|
||||
|
||||
for (let i = 0; i < segmentLength; i++) {
|
||||
const availableDirections = pruneDirections(grid, currentPosition, ['down', 'down-left', 'down-right'])
|
||||
if (availableDirections.length === 0) {
|
||||
break
|
||||
} else {
|
||||
const dir =
|
||||
availableDirections.includes(currentDirection) && Math.random() < 0.25
|
||||
? currentDirection
|
||||
: randomChoice(availableDirections)
|
||||
|
||||
if ((currentDirection === 'down-left' && dir === 'down-right') || (currentDirection === 'down-right' && dir === 'down-left')) {
|
||||
break
|
||||
}
|
||||
|
||||
const [x, y] = currentPosition
|
||||
const [dx, dy] = DIR_TO_VEC[dir]
|
||||
|
||||
steps.push({
|
||||
position: [x, y],
|
||||
direction: dir,
|
||||
})
|
||||
|
||||
currentPosition = [x + dx, y + dy]
|
||||
currentDirection = dir
|
||||
}
|
||||
}
|
||||
|
||||
const last = steps.at(-1)
|
||||
if (last) {
|
||||
last.direction = 'dot'
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
// Enable top-level await, and other modern ESM features.
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
// Enable node-style module resolution, for things like npm package imports.
|
||||
"moduleResolution": "node",
|
||||
// Enable JSON imports.
|
||||
"resolveJsonModule": true,
|
||||
// Enable stricter transpilation for better output.
|
||||
"isolatedModules": true,
|
||||
// Astro directly run TypeScript code, no transpilation needed.
|
||||
"noEmit": true,
|
||||
"strictNullChecks": true,
|
||||
// Report an error when importing a file using a casing different from the casing on disk.
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
// Properly support importing CJS modules in ESM
|
||||
"esModuleInterop": true,
|
||||
// Skip typechecking libraries and .d.ts files
|
||||
"skipLibCheck": true,
|
||||
// Add alias for assets folder for easy reference to assets
|
||||
"baseUrl": ".",
|
||||
"paths": {},
|
||||
// TypeScript 5.0 changed how `isolatedModules` and `importsNotUsedAsValues` works, deprecating the later
|
||||
// Until the majority of users are on TypeScript 5.0, we'll have to supress those deprecation errors
|
||||
"ignoreDeprecations": "5.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue