Use objects for cell, add sand color variation, use imageData

main
Shad Amethyst 2 years ago
parent cb44905521
commit e4b86d18f8

@ -1,10 +1,11 @@
import { getContext2D, Touch } from '@shadryx/pptk'; import { getContext2D, Touch } from '@shadryx/pptk';
import { PixelPerfectCanvas, PixelPerfectTouch } from '@shadryx/pptk/solid'; import { PixelPerfectCanvas, PixelPerfectTouch } from '@shadryx/pptk/solid';
import { createEffect, createSignal, onCleanup } from 'solid-js'; 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 = { const PALETTE = {
red: [0xf9, 0x44, 0x35], red: [0xf4, 0x9d, 0x40],
brightRed: [0xff, 0xd0, 0xa0],
dark: 'rgba(0, 0, 0, 0.1)', dark: 'rgba(0, 0, 0, 0.1)',
} as const; } as const;
@ -15,12 +16,19 @@ export default function Simulation() {
const [state, setState] = createSignal(new State(width(), height())); const [state, setState] = createSignal(new State(width(), height()));
const [touchMap, setTouchMap] = createSignal(new Map<number, Touch>()); const [touchMap, setTouchMap] = createSignal(new Map<number, Touch>());
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; const radius = 2;
for (let dy = -radius; dy <= radius; dy++) { for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) { for (let dx = -radius; dx <= radius; dx++) {
if (dx ** 2 + dy ** 2 <= radius ** 2) { 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) { function draw(canvas: HTMLCanvasElement, state: State) {
const context = getContext2D(canvas); const context = canvas.getContext('2d')!;
const width = state.width; const width = state.width;
const height = state.height; const height = state.height;
@ -109,45 +117,101 @@ function draw(canvas: HTMLCanvasElement, state: State) {
const buffer = state.get(); 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 y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const cell = buffer.get(x, y); const cell = buffer.get(x, y);
if (cell === 0 || cell === -1) continue; if (!cell) continue;
if (cell === 1) { if (cell.type === CellType.SAND) {
const e = edge(buffer, x, y); const [dx, dy] = edge(buffer, x, y);
const lightness = 0.9 * Math.max(dx * 0.5 + dy * 0.5, 0);
const lightness = 0.75 + 0.25 * Math.max(e[0] * -0.5 + e[1] * -0.5, 0); const variation = 0.95 + 0.05 * (cell.variation ?? 1);
context.fillStyle = `rgb(${PALETTE.red.map(v => v * lightness).join(',')})`; 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)) {
set(x + 1, y, 0, 0, 0, 40);
if (buffer.get(x + 1, y) === 0) {
context.fillStyle = PALETTE.dark;
context.fillRect(x + 1, y, 1, 1);
} }
} }
} }
context.putImageData(imageData, 0, 0);
// const rawContext = canvas.getContext('2d'); // const rawContext = canvas.getContext('2d');
// const particles = state.get().buffer.reduce((acc, val) => acc + +!!val, 0); // const particles = state.get().buffer.reduce((acc, val) => acc + +!!val, 0);
// rawContext?.fillText(particles.toString(), 10, 10); // 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] { 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 dx = 0;
let dy = 0; let dy = 0;
if (!NEIGHBORS.includes(buffer.get(x - 1, y))) dx -= 1; for (const neighbor of NEIGHBORS) {
if (!NEIGHBORS.includes(buffer.get(x + 1, y))) dx += 1; if (isNeighbor(buffer.get(x + neighbor[0], y + neighbor[1]))) {
if (!NEIGHBORS.includes(buffer.get(x, y - 1))) dy -= 1; dx += neighbor[0];
if (!NEIGHBORS.includes(buffer.get(x, y + 1))) dy += 1; 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); const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) return [0, 0]; if (length === 0) return [0, 0];
return [dx / length, dy / length]; 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;
}

@ -1,28 +1,50 @@
import { update } from "./update.js"; 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 { export class StateBuffer {
constructor( constructor(
readonly buffer: number[], readonly buffer: Cell[],
readonly width: number, readonly width: number,
readonly height: 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) { if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
return -1; return WALL;
} }
return this.buffer[y * this.width + x]; 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; if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
this.buffer[y * this.width + x] = value; this.buffer[y * this.width + x] = value;
} }
fill(value: number) { fill(type: number) {
this.buffer.fill(value); if (type === 0) {
this.buffer.fill(undefined);
} else {
throw new Error('fill for non-air is not yet implemented');
}
} }
} }

@ -1,4 +1,4 @@
import { StateBuffer } from "./state.js"; import { Cell, CellType, StateBuffer } from "./state.js";
export function update( export function update(
@ -11,18 +11,19 @@ export function update(
target.fill(0); target.fill(0);
let swapped: [number, number][] = []; let swapped: [x: number, y: number][] = [];
// First pass: fall trivial cells // First pass: fall trivial cells
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const cell = source.get(x, y); const cell = source.get(x, y);
if (cell > 0) { if (cell?.type === CellType.SAND) {
if (source.get(x, y + 1) === 0) { if (isEmpty(source.get(x, y + 1))) {
target.set(x, y + 1, 2); cell.moved = true;
swapped.push([x, y]); swapped.push([x, y]);
target.set(x, y + 1, cell);
} else { } 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 // Second pass: try to move down cells using swapped
while (swapped.length > 0) { while (swapped.length > 0) {
const [x, y] = swapped.shift()!; const [x, y] = swapped.shift()!;
if (target.get(x, y - 1) === 1) { if (!hasMoved(target.get(x, y - 1)) && isSand(target.get(x, y - 1))) {
target.set(x, y, 2); swap(target, x, y, x, y - 1);
target.set(x, y - 1, 0);
swapped.push([x, y - 1]); swapped.push([x, y - 1]);
} }
} }
// Third pass: mark stuck cells // Third pass: mark stuck cells (NOTE: this is the only step that is direction-dependent)
const CANNOT_MOVE = [2, -1]; function cannotMove(cell: Cell): boolean {
if (!cell) return false;
return cell.moved;
}
for (let y = height; y >= 0; y--) { for (let y = height; y >= 0; y--) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const currentCell = target.get(x, y);
if (!currentCell) continue;
if ( if (
target.get(x, y) === 1 !currentCell.moved
&& CANNOT_MOVE.includes(target.get(x, y + 1)) && cannotMove(target.get(x, y + 1))
&& CANNOT_MOVE.includes(target.get(x + 1, y + 1)) && cannotMove(target.get(x + 1, y + 1))
&& CANNOT_MOVE.includes(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 // Fourth pass: try to move cells diagonally
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const currentCell = target.get(x, y);
if (!currentCell || currentCell.type !== CellType.SAND || hasMoved(currentCell)) {
continue;
}
if ( if (
target.get(x, y) === 1 && target.get(x, y + 1) === 2 hasMoved(target.get(x, y + 1)) || hasMoved(target.get(x, y + 2))
|| target.get(x, y) === 1 && target.get(x, y + 1) === 1 && target.get(x, y + 2) === 2
) { ) {
const left = target.get(x - 1, y + 1) === 0; const left = isEmpty(target.get(x - 1, y + 1));
const right = target.get(x + 1, y + 1) === 0; const right = isEmpty(target.get(x + 1, y + 1));
if (left && right && Math.random() < 0.5 || left && !right) { if (left && right && Math.random() < 0.5 || left && !right) {
target.set(x - 1, y + 1, 2); swap(target, x, y, x - 1, y + 1);
target.set(x, y, 0);
swapped.push([x, y]); swapped.push([x, y]);
} else if (right) { } else if (right) {
target.set(x + 1, y + 1, 2); swap(target, x, y, x + 1, y + 1);
target.set(x, y, 0);
swapped.push([x, y]); swapped.push([x, y]);
} }
} }
@ -78,17 +87,14 @@ export function update(
// Fifth pass: try to move remaining cells to free spots // Fifth pass: try to move remaining cells to free spots
while (swapped.length > 0) { while (swapped.length > 0) {
const [x, y] = swapped.shift()!; const [x, y] = swapped.shift()!;
if (target.get(x, y - 1) === 1) { if (isSand(target.get(x, y - 1))) {
target.set(x, y, 2); swap(target, x, y, x, y - 1);
target.set(x, y - 1, 0);
swapped.push([x, y - 1]); swapped.push([x, y - 1]);
} else if (target.get(x - 1, y - 1) === 1) { } else if (isSand(target.get(x - 1, y - 1))) {
target.set(x, y, 2); swap(target, x, y, x - 1, y - 1);
target.set(x - 1, y - 1, 0);
swapped.push([x - 1, y - 1]); swapped.push([x - 1, y - 1]);
} else if (target.get(x + 1, y - 1) === 1) { } else if (isSand(target.get(x + 1, y - 1))) {
target.set(x, y, 2); swap(target, x, y, x + 1, y - 1);
target.set(x + 1, y - 1, 0);
swapped.push([x + 1, y - 1]); swapped.push([x + 1, y - 1]);
} }
} }
@ -96,8 +102,9 @@ export function update(
// Sixth pass: convert 2 -> 1 // Sixth pass: convert 2 -> 1
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
if (target.get(x, y) === 2) { const cell = target.get(x, y);
target.set(x, y, 1); if (cell?.moved) {
cell.moved = false;
} }
} }
} }
@ -167,3 +174,31 @@ export function update(
// } // }
// } while (swapped.length > 0); // } 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));
}

Loading…
Cancel
Save