Compare commits
3 Commits
5b3afc2287
...
217d587863
| Author | SHA1 | Date |
|---|---|---|
|
|
217d587863 | |
|
|
efba557e96 | |
|
|
63760d4cb8 |
|
|
@ -164,6 +164,7 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => {
|
||||||
'-Wpedantic',
|
'-Wpedantic',
|
||||||
'-Werror',
|
'-Werror',
|
||||||
'-Wshadow',
|
'-Wshadow',
|
||||||
|
'-Wconversion',
|
||||||
...features,
|
...features,
|
||||||
'-I', include,
|
'-I', include,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,6 @@ build/assets/include
|
||||||
-Wall
|
-Wall
|
||||||
-Wextra
|
-Wextra
|
||||||
-Wpedantic
|
-Wpedantic
|
||||||
|
-Wshadow
|
||||||
|
-Wconversion
|
||||||
-Werror
|
-Werror
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
export interface EventWithPoint {
|
||||||
|
readonly clientX: number;
|
||||||
|
readonly clientY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRealPoint = (canvas: HTMLCanvasElement, e: EventWithPoint): DOMPoint => {
|
||||||
|
const matrix = new DOMMatrix();
|
||||||
|
const scale = Math.min(canvas.clientWidth / canvas.width, canvas.clientHeight / canvas.height);
|
||||||
|
|
||||||
|
const realWidth = canvas.width * scale;
|
||||||
|
const realHeight = canvas.height * scale;
|
||||||
|
|
||||||
|
const offsetLeft = (canvas.clientWidth - realWidth) / 2;
|
||||||
|
const offsetTop = (canvas.clientHeight - realHeight) / 2;
|
||||||
|
|
||||||
|
matrix.translateSelf(offsetLeft, offsetTop);
|
||||||
|
matrix.scaleSelf(scale);
|
||||||
|
matrix.invertSelf();
|
||||||
|
|
||||||
|
const point = new DOMPoint(e.clientX, e.clientY);
|
||||||
|
|
||||||
|
return point.matrixTransform(matrix);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
export default abstract class Entity {
|
||||||
|
protected hovered = false;
|
||||||
|
|
||||||
|
constructor(public position: [number, number], public size: [number, number]) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(ctx: CanvasRenderingContext2D) {
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.translate(...this.position);
|
||||||
|
ctx.scale(...this.size);
|
||||||
|
|
||||||
|
this.draw(ctx);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPointInBounds(x: number, y: number) {
|
||||||
|
return (
|
||||||
|
this.left <= x && x <= this.right &&
|
||||||
|
this.top <= y && y <= this.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get centerX() { return this.position[0] + this.size[0] / 2; }
|
||||||
|
get centerY() { return this.position[1] + this.size[0] / 2; }
|
||||||
|
|
||||||
|
get left() { return this.position[0]; }
|
||||||
|
get top() { return this.position[1]; }
|
||||||
|
|
||||||
|
get right() { return this.position[0] + this.size[0]; }
|
||||||
|
get bottom() { return this.position[1] + this.size[1]; }
|
||||||
|
|
||||||
|
get width() { return this.size[0]; }
|
||||||
|
get height() { return this.size[1]; }
|
||||||
|
|
||||||
|
public handleClick(x: number, y: number) {
|
||||||
|
if (this.isPointInBounds(x, y)) {
|
||||||
|
this.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleMouseMove(x: number, y: number) {
|
||||||
|
this.hovered = this.isPointInBounds(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract draw(ctx: CanvasRenderingContext2D): void;
|
||||||
|
protected onClick() {}
|
||||||
|
public abstract update(dt: number): void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { createCanvas } from "@common/display/canvas";
|
||||||
|
import Spinner from "./spinner";
|
||||||
|
import type Entity from "./entity";
|
||||||
|
import { getRealPoint } from "@common/dom";
|
||||||
|
import { nextFrame, range } from "@common/utils";
|
||||||
|
import Tile from "./tile";
|
||||||
|
|
||||||
|
const MAP_SIZE = 12;
|
||||||
|
const MAP_PIXEL_SIZE = 1000;
|
||||||
|
const TILE_SIZE = MAP_PIXEL_SIZE / (MAP_SIZE + 4);
|
||||||
|
const SPINNER_SIZE = 200;
|
||||||
|
const canvas = createCanvas(MAP_PIXEL_SIZE + SPINNER_SIZE, MAP_PIXEL_SIZE);
|
||||||
|
const spinner = new Spinner([MAP_PIXEL_SIZE, 0], [SPINNER_SIZE, SPINNER_SIZE]);
|
||||||
|
const map = createMap();
|
||||||
|
|
||||||
|
const entities: Entity[] = [
|
||||||
|
spinner,
|
||||||
|
...map.flat(),
|
||||||
|
];
|
||||||
|
|
||||||
|
async function update(dt: number) {
|
||||||
|
entities.forEach(entity => entity.update(dt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render(ctx: CanvasRenderingContext2D) {
|
||||||
|
ctx.fillStyle = 'green';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
entities.forEach(entity => entity.render(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onClick(e: MouseEvent) {
|
||||||
|
const point = getRealPoint(canvas, e);
|
||||||
|
|
||||||
|
entities.forEach(entity => entity.handleClick(point.x, point.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMouseMove(e: MouseEvent) {
|
||||||
|
const point = getRealPoint(canvas, e);
|
||||||
|
|
||||||
|
entities.forEach(entity => entity.handleMouseMove(point.x, point.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMap() {
|
||||||
|
const map = range(MAP_SIZE)
|
||||||
|
.map(x =>
|
||||||
|
range(MAP_SIZE)
|
||||||
|
.map(y =>
|
||||||
|
new Tile([x + 2, y + 2], TILE_SIZE)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const startTile = new Tile([1, (MAP_SIZE / 2)], TILE_SIZE * 2);
|
||||||
|
|
||||||
|
delete map[0][MAP_SIZE - 2];
|
||||||
|
delete map[1][MAP_SIZE - 2];
|
||||||
|
delete map[1][MAP_SIZE - 1];
|
||||||
|
|
||||||
|
map[0][MAP_SIZE - 1] = startTile;
|
||||||
|
|
||||||
|
// TODO walls
|
||||||
|
|
||||||
|
for (let x = 0; x < map.length; x++) {
|
||||||
|
const column = map[x];
|
||||||
|
if (!column) continue;
|
||||||
|
for (let y = 0; y < column.length; y++) {
|
||||||
|
const tile = column[y];
|
||||||
|
if (!tile) continue;
|
||||||
|
tile.connections = [
|
||||||
|
map[x - 1]?.[y],
|
||||||
|
map[x + 1]?.[y],
|
||||||
|
map[x]?.[y - 1],
|
||||||
|
map[x]?.[y + 1],
|
||||||
|
].filter(t => t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTile.connections = [
|
||||||
|
map[0][MAP_SIZE - 3],
|
||||||
|
map[1][MAP_SIZE - 3],
|
||||||
|
map[2][MAP_SIZE - 2],
|
||||||
|
map[2][MAP_SIZE - 1],
|
||||||
|
].filter(t => t);
|
||||||
|
|
||||||
|
startTile.connections.forEach(t => t.connections.push(startTile));
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function main() {
|
||||||
|
canvas.style.imageRendering = 'auto';
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
spinner.addListener(console.log);
|
||||||
|
canvas.addEventListener('click', onClick);
|
||||||
|
canvas.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
let prevFrame = performance.now();
|
||||||
|
while (true) {
|
||||||
|
const now = await nextFrame();
|
||||||
|
const dt = (now - prevFrame) / 1000;
|
||||||
|
prevFrame = now;
|
||||||
|
|
||||||
|
update(dt);
|
||||||
|
render(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import Entity from "./entity";
|
||||||
|
|
||||||
|
export type SpinnerListener = (angle: number) => void;
|
||||||
|
|
||||||
|
export default class Spinner extends Entity {
|
||||||
|
private readonly probabilities = [0.3, 0.3, 0.2, 0.2];
|
||||||
|
private readonly colors = ['yellow', 'green', 'blue', 'red'];
|
||||||
|
private readonly startAngle = -Math.PI / 2 - this.probabilities[0] * 2 * Math.PI;
|
||||||
|
|
||||||
|
private angle = this.startAngle;
|
||||||
|
private speed = 0;
|
||||||
|
private friction = 0.3;
|
||||||
|
private fired = true;
|
||||||
|
private listeners = new Set<SpinnerListener>();
|
||||||
|
|
||||||
|
protected override draw(ctx: CanvasRenderingContext2D) {
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0.5, 0.5, 0.5, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
|
||||||
|
ctx.lineCap = 'butt';
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
ctx.font = '0.1px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.lineWidth = 0.2;
|
||||||
|
|
||||||
|
let angle = this.startAngle;
|
||||||
|
for (let i = 0; i < this.probabilities.length; i++) {
|
||||||
|
ctx.strokeStyle = this.colors[i];
|
||||||
|
ctx.beginPath();
|
||||||
|
const nextAngle = angle + this.probabilities[i] * 2 * Math.PI;
|
||||||
|
ctx.arc(0.5, 0.5, 0.4, angle, nextAngle);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const center = (angle + nextAngle) / 2;
|
||||||
|
ctx.fillText(`${i + 1}`, 0.5 + Math.cos(center) * 0.4, 0.5 + Math.sin(center) * 0.4);
|
||||||
|
|
||||||
|
angle = nextAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = 'black';
|
||||||
|
ctx.lineWidth = 0.01;
|
||||||
|
|
||||||
|
angle = this.startAngle;
|
||||||
|
for (let i = 0; i < this.probabilities.length; i++) {
|
||||||
|
const nextAngle = angle + this.probabilities[i] * 2 * Math.PI;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0.5, 0.5);
|
||||||
|
ctx.lineTo(0.5 + Math.cos(angle) * 0.49, 0.5 + Math.sin(angle) * 0.49);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
angle = nextAngle;
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0.5, 0.5, 0.3, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.lineWidth = 0.03;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0.5, 0.5, 0.49, 0, Math.PI * 2);
|
||||||
|
ctx.moveTo(0.5, 0.5);
|
||||||
|
ctx.lineTo(0.5 + Math.cos(this.angle) * 0.4, 0.5 + Math.sin(this.angle) * 0.4);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override update(dt: number) {
|
||||||
|
if (this.fired) return;
|
||||||
|
if (this.speed < 0.1) {
|
||||||
|
this.fire();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.angle += this.speed * dt;
|
||||||
|
this.speed *= 1.0 - this.friction * dt;
|
||||||
|
this.friction += 0.7 * dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override onClick() {
|
||||||
|
if (!this.fired) return;
|
||||||
|
|
||||||
|
this.fired = false;
|
||||||
|
this.speed = 25 + Math.random() * 25;
|
||||||
|
this.friction = 0.3 + Math.random() * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(listener: SpinnerListener) {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(listener: SpinnerListener) {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fire() {
|
||||||
|
if (this.fired) return;
|
||||||
|
this.fired = true;
|
||||||
|
|
||||||
|
let checkAngle = this.angle % (Math.PI * 2);
|
||||||
|
|
||||||
|
for (let a = 0; a < 2; a++) {
|
||||||
|
let angle = (this.startAngle + Math.PI * 2) % (Math.PI * 2);
|
||||||
|
for (let i = 0; i < this.probabilities.length; i++) {
|
||||||
|
const nextAngle = angle + this.probabilities[i] * 2 * Math.PI;
|
||||||
|
|
||||||
|
if (angle <= checkAngle && checkAngle <= nextAngle) {
|
||||||
|
this.listeners.forEach(l => l(i + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
angle = nextAngle;
|
||||||
|
}
|
||||||
|
checkAngle += Math.PI * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import Entity from "./entity";
|
||||||
|
|
||||||
|
export default class Tile extends Entity {
|
||||||
|
public connections: Tile[] = [];
|
||||||
|
|
||||||
|
constructor(position: [number, number], size: number, public image: HTMLImageElement | null = null) {
|
||||||
|
super([position[0] * size, position[1] * size], [size, size]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected draw(ctx: CanvasRenderingContext2D) {
|
||||||
|
if (this.image) {
|
||||||
|
ctx.drawImage(this.image, 0, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hovered) {
|
||||||
|
ctx.fillStyle = `rgba(255, 255, 255, 0.1)`;
|
||||||
|
ctx.fillRect(0, 0, 1, 1);
|
||||||
|
|
||||||
|
ctx.lineWidth = 1 / this.width;
|
||||||
|
ctx.strokeStyle = 'red';
|
||||||
|
|
||||||
|
for (const tile of this.connections) {
|
||||||
|
const center = [
|
||||||
|
0.5 + (tile.centerX - this.centerX) / this.width,
|
||||||
|
0.5 + (tile.centerY - this.centerY) / this.height,
|
||||||
|
];
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0.5, 0.5);
|
||||||
|
ctx.lineTo(center[0], center[1]);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.lineWidth = 1 / this.width;
|
||||||
|
ctx.strokeStyle = `rgba(0, 0, 0, 0.5)`;
|
||||||
|
ctx.strokeRect(0, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(dt: number) {
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue