Skip to content

Commit

Permalink
designer refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
gtanczyk committed Oct 26, 2024
1 parent 580105e commit a80d1b9
Show file tree
Hide file tree
Showing 15 changed files with 1,256 additions and 598 deletions.
215 changes: 43 additions & 172 deletions games/masterplan/src/screens/designer/canvas-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React, { useRef, useEffect, useState, useCallback } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import styled from 'styled-components';
import { GRID_CENTER_X, GRID_CENTER_Y } from '../battle/consts';
import { UNIT_ASSET_PATHS } from '../battle/assets';
import { Unit } from './designer-types';
import { TerrainData } from '../battle/game/terrain/terrain-generator';
import { GridRenderer } from './components/canvas/grid-renderer';
import { UnitRenderer } from './components/canvas/unit-renderer';
import { TerrainRenderer } from './components/canvas/terrain-renderer';
import { AnimatedBorderRenderer } from './components/canvas/animated-border-renderer';
import { GridInteractionHandler } from './components/canvas/grid-interaction-handler';
import { UNIT_ASSET_PATHS } from '../battle/assets';
import { useRafLoop } from 'react-use';

interface CanvasGridProps {
width: number;
Expand Down Expand Up @@ -52,9 +57,7 @@ export const CanvasGrid: React.FC<CanvasGridProps> = React.memo(
terrainData,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationFrameRef = useRef<number>();
const [unitImages, setUnitImages] = useState<Record<string, HTMLImageElement>>({});
const [timestamp, setTimestamp] = useState<number>(0);

useEffect(() => {
const loadUnitImages = async () => {
Expand All @@ -72,129 +75,7 @@ export const CanvasGrid: React.FC<CanvasGridProps> = React.memo(
loadUnitImages();
}, []);

// Animation loop for the border animation
useEffect(() => {
const animate = (time: number) => {
setTimestamp(time);
animationFrameRef.current = requestAnimationFrame(animate);
};

animationFrameRef.current = requestAnimationFrame(animate);

return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, []);

const drawGrid = useCallback(
(ctx: CanvasRenderingContext2D) => {
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;

for (let x = 0; x <= width; x += cellWidth) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}

for (let y = 0; y <= height; y += cellHeight) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}

// Draw terrain height map
const terrainHeightMap = terrainData.heightMap;
const centerX = terrainHeightMap[0].length / 2;
const maxHeight = Math.max(...terrainHeightMap.flat());
for (let row = 0; row < terrainHeightMap.length / 2; row++) {
for (let col = -centerX; col < centerX; col++) {
const x = col * cellWidth;
const y = row * cellHeight;
const heightValue =
terrainHeightMap[row + (isPlayerArea ? (terrainHeightMap.length / 2) << 0 : 0)][centerX + col];
const color = `rgb(0, 0, 0, ${0.5 - heightValue / maxHeight / 2})`;
ctx.fillStyle = color;
ctx.fillRect(GRID_CENTER_X * cellWidth + x, y, cellWidth, cellHeight);
}
}
},
[width, height, cellWidth, cellHeight],
);

const drawAnimatedBorder = useCallback(
(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
timestamp: number,
selected: boolean,
) => {
ctx.save();

// Set up the animated dotted line pattern
const dashLength = 5;
const dashGap = 3;
const dashOffset = (timestamp / 50) % (dashLength + dashGap);

ctx.strokeStyle = selected ? '#FFFFFF' : '#FFD700'; // Yellowish color
ctx.lineWidth = 2;
ctx.setLineDash([dashLength, dashGap]);
ctx.lineDashOffset = -dashOffset;

// Draw the animated border
ctx.strokeRect(x, y, width, height);

ctx.restore();
},
[],
);

const drawUnits = useCallback(
(ctx: CanvasRenderingContext2D) => {
units.forEach((unit) => {
const x = (unit.col + GRID_CENTER_X) * cellWidth;
const y = (unit.row + GRID_CENTER_Y) * cellHeight;
const unitWidth = unit.sizeCol * cellWidth;
const unitHeight = unit.sizeRow * cellHeight;

if (unitImages[unit.type]) {
for (let xi = 0; xi < unit.sizeCol; xi++)
for (let yi = 0; yi < unit.sizeRow; yi++) {
ctx.drawImage(unitImages[unit.type], x + xi * cellWidth, y + yi * cellHeight, cellWidth, cellHeight);
// Draw small rectangle representing unit's life
ctx.fillStyle = isPlayerArea ? '#ff0000' : '#00ff00';
ctx.fillRect(x + xi * cellWidth, y + yi * cellHeight + 10, 10, 5);
}
} else {
// Fallback to colored rectangle if image is not loaded
ctx.fillStyle = unit.type === 'archer' ? 'green' : unit.type === 'tank' ? 'blue' : 'red';
ctx.fillRect(x, y, unitWidth, unitHeight);
}

// Draw animated border for player units
if (isPlayerArea) {
drawAnimatedBorder(ctx, x, y, unitWidth, unitHeight, timestamp, unit.id === selectedUnitId);
}

// Draw unit type text
ctx.fillStyle = 'white';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(unit.type, x + unitWidth / 2, y + unitHeight / 2);
});
},
[units, selectedUnitId, unitImages, cellWidth, cellHeight, isPlayerArea, timestamp, drawAnimatedBorder],
);

useEffect(() => {
useRafLoop(() => {
const canvas = canvasRef.current;
if (!canvas) return;

Expand All @@ -204,57 +85,47 @@ export const CanvasGrid: React.FC<CanvasGridProps> = React.memo(
// Clear the canvas
ctx.clearRect(0, 0, width, height);

// Draw the grid
drawGrid(ctx);

// Draw the units
drawUnits(ctx);
}, [
width,
height,
cellWidth,
cellHeight,
units,
selectedUnitId,
unitImages,
drawGrid,
drawUnits,
terrainData,
timestamp,
]);

const handleCanvasClick = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
if (!isPlayerArea) return; // Only allow interactions in the player area

const canvas = canvasRef.current;
if (!canvas) return;

const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * (canvas.width / rect.width);
const y = (event.clientY - rect.top) * (canvas.height / rect.height);

const col = Math.floor(x / cellWidth) - GRID_CENTER_X;
const row = Math.floor(y / cellHeight) - GRID_CENTER_Y;

onCellClick(col, row);
},
[cellWidth, cellHeight, onCellClick, isPlayerArea],
);
// Render layers
TerrainRenderer.render(ctx, { width, height, cellWidth, cellHeight, terrainData, isPlayerArea });
GridRenderer.render(ctx, { width, height, cellWidth, cellHeight });
UnitRenderer.render(ctx, {
units,
unitImages,
cellWidth,
cellHeight,
isPlayerArea,
});

if (isPlayerArea) {
AnimatedBorderRenderer.render(ctx, {
units,
selectedUnitId,
cellWidth,
cellHeight,
timestamp: Date.now(),
});
}
});

return (
<CanvasContainer
ref={canvasRef}
data-is-player-area={isPlayerArea}
width={width}
height={height}
onClick={handleCanvasClick}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
{...GridInteractionHandler.getEventHandlers({
canvasRef,
cellWidth,
cellHeight,
isPlayerArea,
onCellClick,
onMouseDown,
onMouseMove,
onMouseUp,
onTouchStart,
onTouchMove,
onTouchEnd,
})}
/>
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Unit } from '../../designer-types';
import { GridRenderer } from './grid-renderer';

interface AnimatedBorderRenderProps {
units: Unit[];
selectedUnitId: number | null;
cellWidth: number;
cellHeight: number;
timestamp: number;
}

export class AnimatedBorderRenderer {
private static readonly DASH_LENGTH = 5;
private static readonly DASH_GAP = 3;
private static readonly ANIMATION_SPEED = 50; // Lower = faster
private static readonly BORDER_WIDTH = 2;
private static readonly SELECTED_COLOR = '#FFFFFF';
private static readonly UNSELECTED_COLOR = '#FFD700';

static render(ctx: CanvasRenderingContext2D, props: AnimatedBorderRenderProps): void {
const { units, selectedUnitId, cellWidth, cellHeight, timestamp } = props;

units.forEach((unit) => {
this.renderBorder(ctx, {
unit,
isSelected: unit.id === selectedUnitId,
cellWidth,
cellHeight,
timestamp,
});
});
}

private static renderBorder(
ctx: CanvasRenderingContext2D,
props: {
unit: Unit;
isSelected: boolean;
cellWidth: number;
cellHeight: number;
timestamp: number;
},
): void {
const { unit, isSelected, cellWidth, cellHeight, timestamp } = props;
const { x, y } = GridRenderer.gridToCanvas(unit.col, unit.row, cellWidth, cellHeight);
const width = unit.sizeCol * cellWidth;
const height = unit.sizeRow * cellHeight;

ctx.save();

// Set up the animated dotted line pattern
const dashOffset = (timestamp / this.ANIMATION_SPEED) % (this.DASH_LENGTH + this.DASH_GAP);

ctx.strokeStyle = isSelected ? this.SELECTED_COLOR : this.UNSELECTED_COLOR;
ctx.lineWidth = this.BORDER_WIDTH;
ctx.setLineDash([this.DASH_LENGTH, this.DASH_GAP]);
ctx.lineDashOffset = -dashOffset;

// Draw the main border
ctx.strokeRect(x, y, width, height);

// Add glow effect for selected units
if (isSelected) {
this.renderGlowEffect(ctx, { x, y, width, height });
}

// Add corner indicators
this.renderCornerIndicators(ctx, { x, y, width, height, isSelected });

ctx.restore();
}

private static renderGlowEffect(
ctx: CanvasRenderingContext2D,
props: {
x: number;
y: number;
width: number;
height: number;
},
): void {
const { x, y, width, height } = props;

ctx.save();

// Create outer glow
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 4;
ctx.setLineDash([]); // Solid line for glow
ctx.strokeRect(x - 1, y - 1, width + 2, height + 2);

// Create inner glow
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
ctx.strokeRect(x - 0.5, y - 0.5, width + 1, height + 1);

ctx.restore();
}

private static renderCornerIndicators(
ctx: CanvasRenderingContext2D,
props: {
x: number;
y: number;
width: number;
height: number;
isSelected: boolean;
},
): void {
const { x, y, width, height, isSelected } = props;
const cornerSize = 4;

ctx.save();

// Reset dash pattern for solid corners
ctx.setLineDash([]);
ctx.lineWidth = isSelected ? 3 : 2;

// Top-left corner
this.drawCorner(ctx, x, y, cornerSize, 0, 0);
// Top-right corner
this.drawCorner(ctx, x + width, y, cornerSize, -1, 0);
// Bottom-left corner
this.drawCorner(ctx, x, y + height, cornerSize, 0, -1);
// Bottom-right corner
this.drawCorner(ctx, x + width, y + height, cornerSize, -1, -1);

ctx.restore();
}

private static drawCorner(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
xDir: number,
yDir: number,
): void {
ctx.beginPath();
// Horizontal line
ctx.moveTo(x, y);
ctx.lineTo(x + size * (xDir || 1), y);
// Vertical line
ctx.moveTo(x, y);
ctx.lineTo(x, y + size * (yDir || 1));
ctx.stroke();
}
}
Loading

0 comments on commit a80d1b9

Please sign in to comment.