From e4b86d18f8bcbbf94ccf76728b3a6007e34e436f Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Mon, 27 Feb 2023 22:42:26 +0100 Subject: [PATCH] :sparkles: Use objects for cell, add sand color variation, use imageData --- src/Simulation.tsx | 110 +++++++++++++++++++++++++++++++-------- src/simulation/state.ts | 34 +++++++++--- src/simulation/update.ts | 105 ++++++++++++++++++++++++------------- 3 files changed, 185 insertions(+), 64 deletions(-) diff --git a/src/Simulation.tsx b/src/Simulation.tsx index 6f7072b..4389e8c 100644 --- a/src/Simulation.tsx +++ b/src/Simulation.tsx @@ -1,10 +1,11 @@ import { getContext2D, Touch } from '@shadryx/pptk'; import { PixelPerfectCanvas, PixelPerfectTouch } from '@shadryx/pptk/solid'; import { createEffect, createSignal, onCleanup } from 'solid-js'; -import { State, StateBuffer } from './simulation/state.js'; +import { Cell, CellType, State, StateBuffer } from './simulation/state.js'; const PALETTE = { - red: [0xf9, 0x44, 0x35], + red: [0xf4, 0x9d, 0x40], + brightRed: [0xff, 0xd0, 0xa0], dark: 'rgba(0, 0, 0, 0.1)', } as const; @@ -15,12 +16,19 @@ export default function Simulation() { const [state, setState] = createSignal(new State(width(), height())); const [touchMap, setTouchMap] = createSignal(new Map()); - function circle(state: StateBuffer, x: number, y: number, value: number) { + function circle(state: StateBuffer, x: number, y: number, type: number) { + function createCell(): Cell { + return { + type, + moved: false, + variation: Math.random(), + }; + } const radius = 2; for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { if (dx ** 2 + dy ** 2 <= radius ** 2) { - state.set(x + dx, y + dy, value); + state.set(x + dx, y + dy, createCell()); } } } @@ -101,7 +109,7 @@ export default function Simulation() { } function draw(canvas: HTMLCanvasElement, state: State) { - const context = getContext2D(canvas); + const context = canvas.getContext('2d')!; const width = state.width; const height = state.height; @@ -109,45 +117,101 @@ function draw(canvas: HTMLCanvasElement, state: State) { const buffer = state.get(); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + + function set(x: number, y: number, r: number, g: number, b: number, a: number) { + const offset = (y * canvas.width + x) * 4; + imageData.data[offset] = r; + imageData.data[offset + 1] = g; + imageData.data[offset + 2] = b; + imageData.data[offset + 3] = a; + } + for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const cell = buffer.get(x, y); - if (cell === 0 || cell === -1) continue; - - if (cell === 1) { - const e = edge(buffer, x, y); - - const lightness = 0.75 + 0.25 * Math.max(e[0] * -0.5 + e[1] * -0.5, 0); - - context.fillStyle = `rgb(${PALETTE.red.map(v => v * lightness).join(',')})`; + if (!cell) continue; + + if (cell.type === CellType.SAND) { + const [dx, dy] = edge(buffer, x, y); + const lightness = 0.9 * Math.max(dx * 0.5 + dy * 0.5, 0); + const variation = 0.95 + 0.05 * (cell.variation ?? 1); + + set( + x, + y, + Math.round(lerp(PALETTE.red[0], PALETTE.brightRed[0], lightness) * variation), + Math.round(lerp(PALETTE.red[1], PALETTE.brightRed[1], lightness) * variation), + Math.round(lerp(PALETTE.red[2], PALETTE.brightRed[2], lightness) * variation), + 255 + ); } - context.fillRect(x, y, 1, 1); - - if (buffer.get(x + 1, y) === 0) { - context.fillStyle = PALETTE.dark; - context.fillRect(x + 1, y, 1, 1); + if (!buffer.get(x + 1, y)) { + set(x + 1, y, 0, 0, 0, 40); } } } + context.putImageData(imageData, 0, 0); + // const rawContext = canvas.getContext('2d'); // const particles = state.get().buffer.reduce((acc, val) => acc + +!!val, 0); // rawContext?.fillText(particles.toString(), 10, 10); } +const NEIGHBORS = [ + [-1, 0], + [1, 0], + [0, 1], + [0, -1], +] as const; + function edge(buffer: StateBuffer, x: number, y: number): [number, number] { - const NEIGHBORS = [buffer.get(x, y), -1]; + const currentCell = buffer.get(x, y); + function isNeighbor(cell: Cell): boolean { + return cell?.type === currentCell?.type || cell?.type === CellType.WALL; + } let dx = 0; let dy = 0; - if (!NEIGHBORS.includes(buffer.get(x - 1, y))) dx -= 1; - if (!NEIGHBORS.includes(buffer.get(x + 1, y))) dx += 1; - if (!NEIGHBORS.includes(buffer.get(x, y - 1))) dy -= 1; - if (!NEIGHBORS.includes(buffer.get(x, y + 1))) dy += 1; + for (const neighbor of NEIGHBORS) { + if (isNeighbor(buffer.get(x + neighbor[0], y + neighbor[1]))) { + dx += neighbor[0]; + dy += neighbor[1]; + } + } + // if (!isNeighbor(buffer.get(x - 1, y))) dx -= 1; + // if (!isNeighbor(buffer.get(x + 1, y))) dx += 1; + // if (!isNeighbor(buffer.get(x, y - 1))) dy -= 1; + // if (!isNeighbor(buffer.get(x, y + 1))) dy += 1; const length = Math.sqrt(dx * dx + dy * dy); if (length === 0) return [0, 0]; return [dx / length, dy / length]; } + +function edgeBlended(buffer: StateBuffer, x: number, y: number): [number, number] { + let [sx, sy] = edge(buffer, x, y); + let n = 1; + const currentCell = buffer.get(x, y); + + for (const neighbor of NEIGHBORS) { + const x2 = x + neighbor[0]; + const y2 = y + neighbor[1]; + + if (buffer.get(x2, y2)?.type === currentCell?.type) { + const [dx, dy] = edge(buffer, x2, y2); + sx += dx * 0.5; + sy += dy * 0.5; + n += 0.5; + } + } + + return [sx / n, sy / n]; +} + +function lerp(a: number, b: number, amount: number): number { + return a * (1 - amount) + b * amount; +} diff --git a/src/simulation/state.ts b/src/simulation/state.ts index d48e23a..7d251c3 100644 --- a/src/simulation/state.ts +++ b/src/simulation/state.ts @@ -1,28 +1,50 @@ import { update } from "./update.js"; +export const CellType = { + SAND: 1, + WALL: 2, +} + +export type CellType = (typeof CellType)[keyof typeof CellType]; + +export type Cell = { + type: CellType; + moved: boolean; + variation?: number; +} | undefined; + +const WALL: Cell = { + type: 2, + moved: true, +}; + export class StateBuffer { constructor( - readonly buffer: number[], + readonly buffer: Cell[], readonly width: number, readonly height: number ) {} - get(x: number, y: number): number { + get(x: number, y: number): Cell { if (x < 0 || y < 0 || x >= this.width || y >= this.height) { - return -1; + return WALL; } return this.buffer[y * this.width + x]; } - set(x: number, y: number, value: number) { + set(x: number, y: number, value: Cell) { if (x < 0 || y < 0 || x >= this.width || y >= this.height) return; this.buffer[y * this.width + x] = value; } - fill(value: number) { - this.buffer.fill(value); + fill(type: number) { + if (type === 0) { + this.buffer.fill(undefined); + } else { + throw new Error('fill for non-air is not yet implemented'); + } } } diff --git a/src/simulation/update.ts b/src/simulation/update.ts index 477501e..c5ec2e3 100644 --- a/src/simulation/update.ts +++ b/src/simulation/update.ts @@ -1,4 +1,4 @@ -import { StateBuffer } from "./state.js"; +import { Cell, CellType, StateBuffer } from "./state.js"; export function update( @@ -11,18 +11,19 @@ export function update( target.fill(0); - let swapped: [number, number][] = []; + let swapped: [x: number, y: number][] = []; // First pass: fall trivial cells for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const cell = source.get(x, y); - if (cell > 0) { - if (source.get(x, y + 1) === 0) { - target.set(x, y + 1, 2); + if (cell?.type === CellType.SAND) { + if (isEmpty(source.get(x, y + 1))) { + cell.moved = true; swapped.push([x, y]); + target.set(x, y + 1, cell); } else { - target.set(x, y, 1); + target.set(x, y, cell); } } } @@ -31,24 +32,30 @@ export function update( // Second pass: try to move down cells using swapped while (swapped.length > 0) { const [x, y] = swapped.shift()!; - if (target.get(x, y - 1) === 1) { - target.set(x, y, 2); - target.set(x, y - 1, 0); + if (!hasMoved(target.get(x, y - 1)) && isSand(target.get(x, y - 1))) { + swap(target, x, y, x, y - 1); swapped.push([x, y - 1]); } } - // Third pass: mark stuck cells - const CANNOT_MOVE = [2, -1]; + // Third pass: mark stuck cells (NOTE: this is the only step that is direction-dependent) + function cannotMove(cell: Cell): boolean { + if (!cell) return false; + return cell.moved; + } + for (let y = height; y >= 0; y--) { for (let x = 0; x < width; x++) { + const currentCell = target.get(x, y); + if (!currentCell) continue; + if ( - target.get(x, y) === 1 - && CANNOT_MOVE.includes(target.get(x, y + 1)) - && CANNOT_MOVE.includes(target.get(x + 1, y + 1)) - && CANNOT_MOVE.includes(target.get(x - 1, y + 1)) + !currentCell.moved + && cannotMove(target.get(x, y + 1)) + && cannotMove(target.get(x + 1, y + 1)) + && cannotMove(target.get(x - 1, y + 1)) ) { - target.set(x, y, 2); + currentCell.moved = true; } } } @@ -56,19 +63,21 @@ export function update( // Fourth pass: try to move cells diagonally for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { + const currentCell = target.get(x, y); + if (!currentCell || currentCell.type !== CellType.SAND || hasMoved(currentCell)) { + continue; + } + if ( - target.get(x, y) === 1 && target.get(x, y + 1) === 2 - || target.get(x, y) === 1 && target.get(x, y + 1) === 1 && target.get(x, y + 2) === 2 + hasMoved(target.get(x, y + 1)) || hasMoved(target.get(x, y + 2)) ) { - const left = target.get(x - 1, y + 1) === 0; - const right = target.get(x + 1, y + 1) === 0; + const left = isEmpty(target.get(x - 1, y + 1)); + const right = isEmpty(target.get(x + 1, y + 1)); if (left && right && Math.random() < 0.5 || left && !right) { - target.set(x - 1, y + 1, 2); - target.set(x, y, 0); + swap(target, x, y, x - 1, y + 1); swapped.push([x, y]); } else if (right) { - target.set(x + 1, y + 1, 2); - target.set(x, y, 0); + swap(target, x, y, x + 1, y + 1); swapped.push([x, y]); } } @@ -78,17 +87,14 @@ export function update( // Fifth pass: try to move remaining cells to free spots while (swapped.length > 0) { const [x, y] = swapped.shift()!; - if (target.get(x, y - 1) === 1) { - target.set(x, y, 2); - target.set(x, y - 1, 0); + if (isSand(target.get(x, y - 1))) { + swap(target, x, y, x, y - 1); swapped.push([x, y - 1]); - } else if (target.get(x - 1, y - 1) === 1) { - target.set(x, y, 2); - target.set(x - 1, y - 1, 0); + } else if (isSand(target.get(x - 1, y - 1))) { + swap(target, x, y, x - 1, y - 1); swapped.push([x - 1, y - 1]); - } else if (target.get(x + 1, y - 1) === 1) { - target.set(x, y, 2); - target.set(x + 1, y - 1, 0); + } else if (isSand(target.get(x + 1, y - 1))) { + swap(target, x, y, x + 1, y - 1); swapped.push([x + 1, y - 1]); } } @@ -96,8 +102,9 @@ export function update( // Sixth pass: convert 2 -> 1 for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - if (target.get(x, y) === 2) { - target.set(x, y, 1); + const cell = target.get(x, y); + if (cell?.moved) { + cell.moved = false; } } } @@ -167,3 +174,31 @@ export function update( // } // } while (swapped.length > 0); } + + + +function isSand(cell: Cell): boolean { + return cell?.type === 1; +} + +function hasMoved(cell: Cell): boolean { + return !!cell?.moved; +} + +function isEmpty(cell: Cell): boolean { + return cell === undefined; +} + +function swap(buffer: StateBuffer, x1: number, y1: number, x2: number, y2: number) { + function setMoved(cell: Cell): Cell { + if (!cell) return cell; + cell.moved = true; + return cell; + } + + const first = buffer.get(x1, y1); + const second = buffer.get(x2, y2); + + buffer.set(x2, y2, setMoved(first)); + buffer.set(x1, y1, setMoved(second)); +}