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 { 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<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;
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;
}

@ -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');
}
}
}

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

Loading…
Cancel
Save