website style and circuits art

next-astro
parent d1fa37524d
commit a541740a29

@ -6,6 +6,7 @@ import PageLayout from '../layouts/PageLayout.astro'
<section class="principal">
<div class="circuit-layer">
<canvas id="circuits-art"></canvas>
<script src="../scripts/circuits-art.ts"></script>
</div>
<div class="logo">
<img src="/images/logo-circuit-board.svg" alt="phc logo" />
@ -113,7 +114,7 @@ import PageLayout from '../layouts/PageLayout.astro'
<svg width="100%" height="2rem" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet">
<defs>
<pattern id="zig-zag-2" x="0" y="0" width="2" height="1" patternUnits="userSpaceOnUse">
<path fill="#ccf5ce" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
<path fill="#f5f2cc" d="M 0,1 L 1,0 L 2,1 L 0,1"></path>
</pattern>
</defs>
<rect fill="url(#zig-zag-2)" x="0" y="0" width="1000" height="1"></rect>

@ -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
}

@ -199,7 +199,9 @@ footer {
padding: 6rem 0;
background: #f0f0f0;
background: #ecffe3;
// circuit color
// background: #a6ce94;
flex-wrap: wrap;
@ -212,6 +214,13 @@ footer {
top: 0;
width: 100%;
height: 100%;
padding-top: 6rem;
canvas {
width: 100%;
height: 100%;
}
}
.logo {
@ -228,7 +237,7 @@ footer {
}
.whats-phc {
background: #e0e0e0;
background: #e4c5ff;
border: 4px solid #222;
border-radius: 8px;
@ -358,7 +367,7 @@ footer {
}
section.projects {
background: #ccf5ce;
background: #f5f2cc;
padding-bottom: 6rem;
@ -373,7 +382,7 @@ footer {
.project {
// background: #fcddff;
background: #f5cecc;
background: #ffa89c;
color: #000e;
border: 3px solid #222;

@ -16,5 +16,8 @@
"@fontsource/source-sans-pro": "^4.5.11",
"astro": "^2.3.1",
"sass": "^1.62.1"
},
"devDependencies": {
"typescript": "^5.0.4"
}
}

@ -6,6 +6,7 @@ specifiers:
'@fontsource/source-sans-pro': ^4.5.11
astro: ^2.3.1
sass: ^1.62.1
typescript: ^5.0.4
dependencies:
'@fontsource/open-sans': 4.5.14
@ -14,6 +15,9 @@ dependencies:
astro: 2.3.1_sass@1.62.1
sass: 1.62.1
devDependencies:
typescript: 5.0.4
packages:
/@ampproject/remapping/2.2.1:
@ -2737,7 +2741,6 @@ packages:
resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==}
engines: {node: '>=12.20'}
hasBin: true
dev: false
/undici/5.20.0:
resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}

@ -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…
Cancel
Save