Skip to content

Commit

Permalink
Move visualizer renderer to separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
delasy committed Oct 19, 2024
1 parent 5d800a4 commit d18fcbb
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 175 deletions.
208 changes: 51 additions & 157 deletions components/Visualizer.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,12 @@
import React from "react";
import { Button } from "@/components/ui/button";
import {
type Entity,
EntityType,
Position,
ZombieSurvival,
} from "@/simulators/zombie-survival";
import {
type VisualizerContextImages,
useVisualizer,
} from "./VisualizerProvider";
import { Renderer } from "@/renderer";
import { ZombieSurvival } from "@/simulators/zombie-survival";
import { useVisualizer } from "@/components/VisualizerProvider";

const AUTO_REPLAY_SPEED = 1_500;
const REPLAY_SPEED = 600;

function getEntityImage(
entity: Entity,
images: VisualizerContextImages,
): HTMLImageElement {
switch (entity.getType()) {
case EntityType.Box: {
return images.box;
}
case EntityType.Player: {
return images.player;
}
case EntityType.Rock: {
return images.rock;
}
case EntityType.Zombie: {
return images.zombie;
}
}
}

function getImageOffset(entity: Entity): Position {
if (entity.getType() === EntityType.Zombie) {
return { x: 16, y: 0 };
}
return { x: 0, y: 0 };
}

function cloneMap(map: string[][]): string[][] {
return JSON.parse(JSON.stringify(map));
}

export function Visualizer({
autoReplay = false,
autoStart = false,
Expand All @@ -62,36 +24,27 @@ export function Visualizer({
}) {
const visualizer = useVisualizer();
const simulator = React.useRef<ZombieSurvival>(new ZombieSurvival(map));
const renderer = React.useRef<Renderer | null>(null);
const interval = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const timeout = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const canvas = React.useRef<HTMLCanvasElement | null>(null);
const running = React.useRef(false);
const cellSizeNum = Number.parseInt(cellSize, 10);
const h = ZombieSurvival.boardHeight(map) * cellSizeNum;
const w = ZombieSurvival.boardWidth(map) * cellSizeNum;
const [running, setRunning] = React.useState(false);

React.useEffect(() => {
if (canvas.current !== null && visualizer.ready) {
setupCanvas(canvas.current);
}
}, [visualizer.ready]);

function setupCanvas(canvas: HTMLCanvasElement) {
canvas.setAttribute("height", `${h * window.devicePixelRatio}`);
canvas.setAttribute("width", `${w * window.devicePixelRatio}`);
canvas.style.height = `${h}px`;
canvas.style.width = `${w}px`;

const ctx = canvas.getContext("2d");

if (ctx !== null) {
setupContext(ctx);
if (
canvas.current !== null &&
visualizer.ready &&
renderer.current === null
) {
renderer.current = new Renderer(
visualizer.getAssets(),
ZombieSurvival.boardHeight(map),
ZombieSurvival.boardWidth(map),
canvas.current,
Number.parseInt(cellSize, 10),
);
}
}

function setupContext(ctx: CanvasRenderingContext2D) {
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
}, [canvas, visualizer.ready]);

React.useEffect(() => {
if (autoStart) {
Expand All @@ -101,111 +54,52 @@ export function Visualizer({

function startSimulation() {
simulator.current = new ZombieSurvival(map);
running.current = true;
interval.current = setInterval(stepSimulation, REPLAY_SPEED);
}

function stepSimulation() {
if (simulator.current === null && running) {
return;
}

if (!simulator.current.finished()) {
simulator.current.step();
render();
return;
}
setRunning(true);

clearInterval(interval.current!);
interval.current = null;
interval.current = setInterval(() => {
// if (!running) {
// return;
// }

if (autoReplay) {
timeout.current = setTimeout(() => {
timeout.current = null;
startSimulation();
}, AUTO_REPLAY_SPEED);
if (!simulator.current.finished()) {
simulator.current.step();

return;
}
if (renderer.current !== null) {
renderer.current.render(simulator.current.getAllAliveEntities());
}

running.current = false;

if (onSimulationEnd) {
onSimulationEnd(!simulator.current.getPlayer().dead());
}
}

function render() {
if (canvas.current !== null) {
const ctx = canvas.current.getContext("2d");

if (ctx !== null) {
renderCtx(ctx);
return;
}
}
}

function renderCtx(ctx: CanvasRenderingContext2D) {
renderCtxBg(ctx);

const entities = simulator.current.getAllAliveEntities();
const images = visualizer.getImages();

for (const entity of entities) {
const entityImage = getEntityImage(entity, images);
const entityPosition = entity.getPosition();
clearInterval(interval.current!);
interval.current = null;

ctx.globalAlpha =
entity.getType() === EntityType.Zombie && entity.getHealth() === 1
? 0.5
: 1;
if (autoReplay) {
timeout.current = setTimeout(() => {
timeout.current = null;
startSimulation();
}, AUTO_REPLAY_SPEED);

const offset = getImageOffset(entity);

ctx.drawImage(
entityImage,
entityPosition.x * cellSizeNum + offset.x,
entityPosition.y * cellSizeNum + offset.y,
cellSizeNum,
cellSizeNum,
);
}

ctx.globalAlpha = 1.0;
}

function renderCtxBg(ctx: CanvasRenderingContext2D) {
ctx.clearRect(0, 0, w, h);

const canvasRatio = w / h;
const images = visualizer.getImages();
const bgRatio = images.bg.width / images.bg.height;

let drawWidth, drawHeight, offsetX, offsetY;
return;
}

if (bgRatio > canvasRatio) {
drawWidth = h * bgRatio;
drawHeight = h;
offsetX = (w - drawWidth) / 2;
offsetY = 0;
} else {
drawWidth = w;
drawHeight = w / bgRatio;
offsetX = 0;
offsetY = (h - drawHeight) / 2;
}
setRunning(false);

ctx.globalAlpha = 0.5;
ctx.drawImage(images.bg, offsetX, offsetY, drawWidth, drawHeight);
ctx.globalAlpha = 1.0;
if (onSimulationEnd) {
onSimulationEnd(!simulator.current.getPlayer().dead());
}
}, REPLAY_SPEED);
}

React.useEffect(() => {
if (canvas.current === null) {
return;
}

const observer = new IntersectionObserver(handleObserving);
const observer = new IntersectionObserver(handleObserving, {
threshold: 0,
});

observer.observe(canvas.current);

return () => {
Expand All @@ -216,7 +110,7 @@ export function Visualizer({
}, [canvas]);

function handleObserving([entry]: IntersectionObserverEntry[]) {
running.current = !entry.isIntersecting;
// running.current = !entry.isIntersecting;
}

React.useEffect(() => {
Expand All @@ -236,14 +130,14 @@ export function Visualizer({
<div>
{controls && (
<div className="flex gap-2 justify-center py-2">
<Button onClick={startSimulation} disabled={running.current}>
<Button onClick={startSimulation} disabled={running}>
Replay
</Button>
<Button
disabled={running.current}
disabled={running}
onClick={() => {
simulator.current = new ZombieSurvival(map);
running.current = false;
setRunning(false);
}}
>
Reset
Expand Down
33 changes: 15 additions & 18 deletions components/VisualizerProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
"use client";

import React from "react";
import { type RendererAssets } from "@/renderer";

export interface VisualizerContextImages {
bg: HTMLImageElement;
box: HTMLImageElement;
player: HTMLImageElement;
rock: HTMLImageElement;
zombie: HTMLImageElement;
}

export interface VisualizerContextValue {
getImages: () => VisualizerContextImages;
ready: boolean;
}
export type VisualizerContextValue =
| {
ready: false;
}
| {
getAssets: () => RendererAssets;
ready: true;
};

const VisualizerContext = React.createContext<VisualizerContextValue | null>(
null,
Expand All @@ -33,7 +30,7 @@ export default function VisualizerProvider({
children: React.ReactNode;
}): React.ReactNode {
const [ready, setReady] = React.useState(false);
const images = React.useRef<VisualizerContextImages | null>(null);
const assets = React.useRef<RendererAssets | null>(null);

React.useEffect(() => {
Promise.all([
Expand All @@ -45,7 +42,7 @@ export default function VisualizerProvider({
]).then((result) => {
setReady(true);

images.current = {
assets.current = {
bg: result[0],
box: result[1],
player: result[2],
Expand All @@ -55,18 +52,18 @@ export default function VisualizerProvider({
});
}, []);

function getImages(): VisualizerContextImages {
if (!ready) {
function getAssets(): RendererAssets {
if (!ready || assets.current === null) {
throw new Error(
"Tried accessing visualizer images before they are loaded",
);
}

return images.current!;
return assets.current;
}

return (
<VisualizerContext.Provider value={{ getImages, ready }}>
<VisualizerContext.Provider value={{ getAssets, ready }}>
{children}
</VisualizerContext.Provider>
);
Expand Down
Loading

0 comments on commit d18fcbb

Please sign in to comment.