Skip to content

Commit 59d5e55

Browse files
committed
Extract renderer drawing primitives and combat effects
Split renderer-draw.ts (ship icons, thrust trails, interpolation, ordnance velocity) and renderer-effects.ts (combat beams, explosions, hex flashes) out of renderer.ts. Renderer methods now delegate to these pure functions, reducing renderer.ts from 1,771 to 1,522 lines.
1 parent 84b4e39 commit 59d5e55

File tree

3 files changed

+378
-267
lines changed

3 files changed

+378
-267
lines changed

src/client/renderer-draw.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Low-level Canvas drawing primitives for ships, ordnance, and movement interpolation.
3+
* Pure functions extracted from Renderer — no class state dependencies.
4+
*/
5+
6+
import {
7+
type HexCoord,
8+
type PixelCoord,
9+
hexToPixel,
10+
hexAdd,
11+
} from '../shared/hex';
12+
import { SHIP_STATS } from '../shared/constants';
13+
14+
/**
15+
* Draw a ship icon (arrow or octagon for orbital base) at the given position.
16+
*/
17+
export function drawShipIcon(
18+
ctx: CanvasRenderingContext2D,
19+
x: number,
20+
y: number,
21+
owner: number,
22+
alpha: number,
23+
heading: number,
24+
disabledTurns = 0,
25+
shipType = '',
26+
): void {
27+
const color = owner === 0 ? `rgba(79, 195, 247, ${alpha})` : `rgba(255, 152, 0, ${alpha})`;
28+
const stats = SHIP_STATS[shipType];
29+
const combat = stats?.combat ?? 2;
30+
const size = combat >= 15 ? 12 : combat >= 8 ? 10 : combat >= 4 ? 9 : 8;
31+
32+
ctx.save();
33+
ctx.translate(x, y);
34+
35+
// Damage glow for disabled ships (flickering red/orange)
36+
if (disabledTurns > 0) {
37+
const flickerPhase = performance.now() / 200 + x * 0.1;
38+
const intensity = 0.3 + 0.2 * Math.sin(flickerPhase) + 0.1 * Math.sin(flickerPhase * 2.7);
39+
const glowColor = disabledTurns >= 4 ? `rgba(255, 50, 50, ${intensity})` : `rgba(255, 150, 50, ${intensity})`;
40+
const glowRadius = 10 + disabledTurns;
41+
ctx.fillStyle = glowColor;
42+
ctx.beginPath();
43+
ctx.arc(0, 0, glowRadius, 0, Math.PI * 2);
44+
ctx.fill();
45+
}
46+
47+
ctx.rotate(heading);
48+
ctx.fillStyle = color;
49+
ctx.beginPath();
50+
if (shipType === 'orbitalBase') {
51+
const r = 12;
52+
for (let i = 0; i < 8; i++) {
53+
const angle = (Math.PI * 2 * i) / 8 - Math.PI / 8;
54+
const px = Math.cos(angle) * r;
55+
const py = Math.sin(angle) * r;
56+
if (i === 0) ctx.moveTo(px, py);
57+
else ctx.lineTo(px, py);
58+
}
59+
ctx.closePath();
60+
ctx.fill();
61+
ctx.strokeStyle = color;
62+
ctx.lineWidth = 1.5;
63+
ctx.beginPath();
64+
ctx.arc(0, 0, 6, 0, Math.PI * 2);
65+
ctx.stroke();
66+
} else {
67+
ctx.moveTo(size, 0);
68+
ctx.lineTo(-size * 0.6, -size * 0.5);
69+
ctx.lineTo(-size * 0.3, 0);
70+
ctx.lineTo(-size * 0.6, size * 0.5);
71+
ctx.closePath();
72+
ctx.fill();
73+
}
74+
ctx.restore();
75+
}
76+
77+
/**
78+
* Draw a thrust exhaust trail behind a moving ship.
79+
*/
80+
export function drawThrustTrail(
81+
ctx: CanvasRenderingContext2D,
82+
x: number,
83+
y: number,
84+
angle: number,
85+
progress: number,
86+
): void {
87+
const len = 12 + Math.sin(progress * 20) * 4;
88+
const spread = 0.3;
89+
const alpha = 0.6 * (1 - progress);
90+
91+
ctx.save();
92+
ctx.translate(x, y);
93+
ctx.rotate(angle);
94+
95+
const grad = ctx.createLinearGradient(0, 0, len, 0);
96+
grad.addColorStop(0, `rgba(255, 200, 50, ${alpha})`);
97+
grad.addColorStop(0.5, `rgba(255, 100, 20, ${alpha * 0.5})`);
98+
grad.addColorStop(1, 'transparent');
99+
100+
ctx.fillStyle = grad;
101+
ctx.beginPath();
102+
ctx.moveTo(0, -3);
103+
ctx.lineTo(len, -len * spread);
104+
ctx.lineTo(len, len * spread);
105+
ctx.lineTo(0, 3);
106+
ctx.closePath();
107+
ctx.fill();
108+
109+
ctx.restore();
110+
}
111+
112+
/**
113+
* Smoothly interpolate a position along a hex path with ease-in-out.
114+
*/
115+
export function interpolatePath(path: HexCoord[], progress: number, hexSize: number): PixelCoord {
116+
if (path.length <= 1) return hexToPixel(path[0], hexSize);
117+
118+
// Ease in-out
119+
const t = progress < 0.5
120+
? 2 * progress * progress
121+
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
122+
123+
const totalSegments = path.length - 1;
124+
const pathT = t * totalSegments;
125+
const segIndex = Math.min(Math.floor(pathT), totalSegments - 1);
126+
const segT = pathT - segIndex;
127+
128+
const from = hexToPixel(path[segIndex], hexSize);
129+
const to = hexToPixel(path[segIndex + 1], hexSize);
130+
131+
return {
132+
x: from.x + (to.x - from.x) * segT,
133+
y: from.y + (to.y - from.y) * segT,
134+
};
135+
}
136+
137+
/**
138+
* Draw an ordnance velocity vector (dashed line from current position to next).
139+
*/
140+
export function drawOrdnanceVelocity(
141+
ctx: CanvasRenderingContext2D,
142+
position: HexCoord,
143+
velocity: { dq: number; dr: number },
144+
px: PixelCoord,
145+
color: string,
146+
hexSize: number,
147+
): void {
148+
if (velocity.dq === 0 && velocity.dr === 0) return;
149+
const dest = hexToPixel(hexAdd(position, velocity), hexSize);
150+
ctx.strokeStyle = color;
151+
ctx.globalAlpha = 0.3;
152+
ctx.lineWidth = 0.5;
153+
ctx.setLineDash([2, 3]);
154+
ctx.beginPath();
155+
ctx.moveTo(px.x, px.y);
156+
ctx.lineTo(dest.x, dest.y);
157+
ctx.stroke();
158+
ctx.setLineDash([]);
159+
ctx.globalAlpha = 1;
160+
}

src/client/renderer-effects.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* Combat visual effects and hex flash rendering.
3+
* Pure Canvas drawing functions extracted from Renderer.
4+
*/
5+
6+
import type { PixelCoord } from '../shared/hex';
7+
8+
export interface CombatEffect {
9+
type: 'beam' | 'explosion' | 'gameOverExplosion';
10+
from: PixelCoord;
11+
to: PixelCoord;
12+
startTime: number;
13+
duration: number;
14+
color: string;
15+
}
16+
17+
export interface HexFlash {
18+
position: PixelCoord;
19+
startTime: number;
20+
duration: number;
21+
color: string;
22+
}
23+
24+
/**
25+
* Render and prune active combat visual effects (beams, explosions, game-over blasts).
26+
* Returns the filtered array with expired effects removed.
27+
*/
28+
export function drawCombatEffects(
29+
ctx: CanvasRenderingContext2D,
30+
effects: CombatEffect[],
31+
now: number,
32+
): CombatEffect[] {
33+
const live = effects.filter(e => now < e.startTime + e.duration);
34+
35+
for (const effect of live) {
36+
if (now < effect.startTime) continue;
37+
const progress = (now - effect.startTime) / effect.duration;
38+
39+
if (effect.type === 'beam') {
40+
drawBeamEffect(ctx, effect, progress);
41+
} else if (effect.type === 'explosion') {
42+
drawExplosionEffect(ctx, effect, progress);
43+
} else if (effect.type === 'gameOverExplosion') {
44+
drawGameOverExplosionEffect(ctx, effect, progress);
45+
}
46+
}
47+
48+
return live;
49+
}
50+
51+
function drawBeamEffect(ctx: CanvasRenderingContext2D, effect: CombatEffect, progress: number): void {
52+
const beamAlpha = 1 - progress;
53+
const beamProgress = Math.min(progress * 3, 1);
54+
55+
ctx.strokeStyle = effect.color;
56+
ctx.globalAlpha = beamAlpha * 0.8;
57+
ctx.lineWidth = 2 * (1 - progress);
58+
ctx.beginPath();
59+
ctx.moveTo(effect.from.x, effect.from.y);
60+
ctx.lineTo(
61+
effect.from.x + (effect.to.x - effect.from.x) * beamProgress,
62+
effect.from.y + (effect.to.y - effect.from.y) * beamProgress,
63+
);
64+
ctx.stroke();
65+
66+
// Glow line
67+
ctx.globalAlpha = beamAlpha * 0.3;
68+
ctx.lineWidth = 6 * (1 - progress);
69+
ctx.beginPath();
70+
ctx.moveTo(effect.from.x, effect.from.y);
71+
ctx.lineTo(
72+
effect.from.x + (effect.to.x - effect.from.x) * beamProgress,
73+
effect.from.y + (effect.to.y - effect.from.y) * beamProgress,
74+
);
75+
ctx.stroke();
76+
ctx.globalAlpha = 1;
77+
}
78+
79+
function drawExplosionEffect(ctx: CanvasRenderingContext2D, effect: CombatEffect, progress: number): void {
80+
const maxRadius = 20;
81+
const radius = maxRadius * progress;
82+
const alpha = 1 - progress;
83+
84+
ctx.strokeStyle = effect.color;
85+
ctx.lineWidth = 3 * (1 - progress);
86+
ctx.globalAlpha = alpha * 0.8;
87+
ctx.beginPath();
88+
ctx.arc(effect.from.x, effect.from.y, radius, 0, Math.PI * 2);
89+
ctx.stroke();
90+
91+
if (progress < 0.3) {
92+
ctx.fillStyle = effect.color;
93+
ctx.globalAlpha = (1 - progress / 0.3) * 0.6;
94+
ctx.beginPath();
95+
ctx.arc(effect.from.x, effect.from.y, radius * 0.5, 0, Math.PI * 2);
96+
ctx.fill();
97+
}
98+
ctx.globalAlpha = 1;
99+
}
100+
101+
function drawGameOverExplosionEffect(ctx: CanvasRenderingContext2D, effect: CombatEffect, progress: number): void {
102+
const maxRadius = 50;
103+
const alpha = 1 - progress;
104+
105+
// Outer expanding ring
106+
const outerRadius = maxRadius * progress;
107+
ctx.strokeStyle = effect.color;
108+
ctx.lineWidth = 4 * (1 - progress);
109+
ctx.globalAlpha = alpha * 0.7;
110+
ctx.beginPath();
111+
ctx.arc(effect.from.x, effect.from.y, outerRadius, 0, Math.PI * 2);
112+
ctx.stroke();
113+
114+
// Second ring (slightly behind)
115+
if (progress > 0.1) {
116+
const innerProgress = (progress - 0.1) / 0.9;
117+
const innerRadius = maxRadius * 0.7 * innerProgress;
118+
ctx.lineWidth = 3 * (1 - innerProgress);
119+
ctx.globalAlpha = (1 - innerProgress) * 0.5;
120+
ctx.beginPath();
121+
ctx.arc(effect.from.x, effect.from.y, innerRadius, 0, Math.PI * 2);
122+
ctx.stroke();
123+
}
124+
125+
// Bright core flash
126+
if (progress < 0.4) {
127+
const coreAlpha = (1 - progress / 0.4);
128+
const coreRadius = 15 * (1 - progress * 0.5);
129+
ctx.fillStyle = '#ffffff';
130+
ctx.globalAlpha = coreAlpha * 0.8;
131+
ctx.beginPath();
132+
ctx.arc(effect.from.x, effect.from.y, coreRadius, 0, Math.PI * 2);
133+
ctx.fill();
134+
135+
ctx.fillStyle = effect.color;
136+
ctx.globalAlpha = coreAlpha * 0.4;
137+
ctx.beginPath();
138+
ctx.arc(effect.from.x, effect.from.y, coreRadius * 2, 0, Math.PI * 2);
139+
ctx.fill();
140+
}
141+
142+
// Debris lines radiating outward
143+
if (progress > 0.05 && progress < 0.8) {
144+
const debrisAlpha = progress < 0.4 ? 1 : (0.8 - progress) / 0.4;
145+
ctx.strokeStyle = effect.color;
146+
ctx.globalAlpha = debrisAlpha * 0.6;
147+
ctx.lineWidth = 1.5;
148+
const seed = (effect.from.x * 7 + effect.from.y * 13) | 0;
149+
for (let d = 0; d < 8; d++) {
150+
const angle = (seed + d * 0.785) % (Math.PI * 2);
151+
const innerR = maxRadius * progress * 0.3;
152+
const outerR = maxRadius * progress * 0.7;
153+
ctx.beginPath();
154+
ctx.moveTo(
155+
effect.from.x + Math.cos(angle) * innerR,
156+
effect.from.y + Math.sin(angle) * innerR,
157+
);
158+
ctx.lineTo(
159+
effect.from.x + Math.cos(angle) * outerR,
160+
effect.from.y + Math.sin(angle) * outerR,
161+
);
162+
ctx.stroke();
163+
}
164+
}
165+
166+
ctx.globalAlpha = 1;
167+
}
168+
169+
/**
170+
* Render and prune hex flash highlights.
171+
* Returns the filtered array with expired flashes removed.
172+
*/
173+
export function drawHexFlashes(
174+
ctx: CanvasRenderingContext2D,
175+
flashes: HexFlash[],
176+
now: number,
177+
hexSize: number,
178+
): HexFlash[] {
179+
const live = flashes.filter(f => now < f.startTime + f.duration);
180+
181+
for (const flash of live) {
182+
if (now < flash.startTime) continue;
183+
const progress = (now - flash.startTime) / flash.duration;
184+
const alpha = (1 - progress) * 0.6;
185+
const radius = hexSize * (0.5 + progress * 0.5);
186+
187+
ctx.beginPath();
188+
ctx.arc(flash.position.x, flash.position.y, radius, 0, Math.PI * 2);
189+
ctx.fillStyle = flash.color;
190+
ctx.globalAlpha = alpha * 0.3;
191+
ctx.fill();
192+
ctx.strokeStyle = flash.color;
193+
ctx.lineWidth = 2 * (1 - progress);
194+
ctx.globalAlpha = alpha;
195+
ctx.stroke();
196+
ctx.globalAlpha = 1;
197+
}
198+
199+
return live;
200+
}

0 commit comments

Comments
 (0)