Skip to content

Commit 8645567

Browse files
committed
NeonSnake: improve egg rendering performance
1 parent 6e51288 commit 8645567

4 files changed

Lines changed: 233 additions & 105 deletions

File tree

BlazorExperiments/Pages/NeonSnake.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
@inject IJSRuntime JS
77
<canvas id="obstacle-cache" style="display:none"></canvas>
8+
<canvas id="static-egg-cache" style="display:none"></canvas>
9+
<canvas id="running-egg-cache" style="display:none"></canvas>
810

911
<CanvasComponent Id="hexagon"
1012
InitializeAsync="InitializeAsync"

BlazorExperiments/Pages/NeonSnake.razor.cs

Lines changed: 97 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public partial class NeonSnake {
2828
double _obstacleSize = 0;
2929
const int ObstaclePadding = 14;
3030
double _cacheW = 0;
31+
double _eggEr, _eggEh, _eggCacheSize;
32+
const int EggCachePadding = 20;
3133
readonly List<Vector2> _bodyPoints = new(256);
3234
static readonly (double Ox, double Oy, double Rx, double Ry, double A)[] EggSpeckles =
3335
[(-0.35, -0.18, 0.18, 0.09, 0.4), (0.38, 0.2, 0.13, 0.07, -0.6), (-0.1, 0.35, 0.1, 0.06, 0.9)];
@@ -44,6 +46,10 @@ public async Task InitializeAsync() {
4446
_cacheW = _obstacleSize + ObstaclePadding * 2;
4547
var dpr = _canvas.WindowProperties.DevicePixelRatio;
4648
await JS.InvokeVoidAsync("neonSnakeCache.renderObstacle", _obstacleSize, ObstaclePadding, dpr);
49+
_eggEr = _cellSize * 0.22;
50+
_eggEh = _cellSize * 0.3;
51+
_eggCacheSize = await JS.InvokeAsync<double>("neonSnakeEggCache.renderStaticEgg", _eggEr, _eggEh, EggCachePadding, dpr);
52+
await JS.InvokeVoidAsync("neonSnakeEggCache.renderRunningEgg", _eggEr, _eggEh, EggCachePadding, dpr);
4753
var raw = await JS.InvokeAsync<string?>("localStorage.getItem", BestScoreKey);
4854
int savedBest = int.TryParse(raw, out var v) ? v : 0;
4955
_snake = new NeonSnakeGame.Snake(_cellSize) {
@@ -171,7 +177,14 @@ await ctx.FillStyleAsync(ScreenW * 0.5, ScreenH * 0.55, ScreenW * 0.2, ScreenW *
171177
await ctx.SaveAsync();
172178
await ctx.TranslateAsync(-_snake.CamX + shakeX, -_snake.CamY + NeonSnakeGame.Snake.HudH + shakeY);
173179

174-
await DrawGridAsync(ctx);
180+
const int borderMargin = 26; // lineWidth/2 + shadowBlur
181+
if (_snake.CamX < borderMargin || _snake.CamY < borderMargin ||
182+
_snake.CamX + ScreenW > _snake.WorldW - borderMargin ||
183+
_snake.CamY + ScreenH - NeonSnakeGame.Snake.HudH > _snake.WorldH - borderMargin)
184+
{
185+
await DrawGridAsync(ctx);
186+
}
187+
175188
await DrawObstaclesAsync(ctx);
176189
await DrawEggsAsync(ctx);
177190

@@ -223,133 +236,112 @@ async ValueTask DrawObstaclesAsync(Batch2D ctx) {
223236

224237
async ValueTask DrawEggsAsync(Batch2D ctx) {
225238
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
239+
var er = _eggEr;
240+
var eh = _eggEh;
241+
var halfCache = _eggCacheSize * 0.5;
242+
var camX = _snake.CamX;
243+
var camY = _snake.CamY;
244+
var viewRight = camX + ScreenW;
245+
var viewBottom = camY + ScreenH;
246+
var cullMargin = _cellSize * 1.5;
247+
var glowMargin = _cellSize * 0.5;
248+
249+
// Pass 1: non-running (static) eggs — use cached sprite
226250
foreach (var egg in _snake.Food) {
251+
if (egg.Running) continue;
227252
var cx = egg.ScreenX;
228253
var cy = egg.ScreenY;
229254

230-
if (cx + _cellSize * 1.5 < _snake.CamX || cx - _cellSize * 1.5 > _snake.CamX + ScreenW) continue;
231-
if (cy + _cellSize * 1.5 < _snake.CamY || cy - _cellSize * 1.5 > _snake.CamY + ScreenH) continue;
255+
if (cx + cullMargin < camX || cx - cullMargin > viewRight) continue;
256+
if (cy + cullMargin < camY || cy - cullMargin > viewBottom) continue;
232257

233-
var bob = egg.Running ? 0 : Math.Sin(now * 0.003 + egg.X * 2.1 + egg.Y * 3.7) * 2.5;
258+
var bob = Math.Sin(now * 0.003 + egg.X * 2.1 + egg.Y * 3.7) * 2.5;
234259
var dy = cy + bob;
235-
var er = _cellSize * 0.22;
236-
var eh = _cellSize * 0.3;
237-
var walkPhase = (now * 0.007) % (Math.PI * 2);
238-
239-
if (egg.Running) {
240-
for (var si = 0; si < 2; si++) {
241-
var side = si == 0 ? -1 : 1;
242-
var extend = 0.65 + 0.35 * Math.Sin(walkPhase + (si == 0 ? 0 : Math.PI));
243-
var lx = cx + side * er * 0.5;
244-
var ly = dy + eh * 0.7;
245-
var lw = er * 0.48;
246-
var lh = eh * 0.72 * extend;
247-
248-
await ctx.FillStyleAsync("#ffdc96");
249-
await ctx.FillRectAsync(lx - lw / 2, ly, lw, lh);
250-
251-
var footDirX = egg.RunDir.X != 0 ? egg.RunDir.X * er * 0.2 : 0;
252-
await ctx.FillStyleAsync("#ffb855");
253-
await ctx.BeginPathAsync();
254-
await ctx.EllipseAsync(lx + footDirX, ly + lh, lw * 0.95, lw * 0.52, 0, 0, Math.PI * 2);
255-
await ctx.FillAsync(FillRule.NonZero);
256-
}
257-
}
258260

259-
await ctx.FillStyleAsync(cx, dy, er * 0.1, cx, dy, er * 2.6,
260-
(0d, egg.Running ? "rgba(255,195,50,0.55)" : "rgba(90,255,215,0.45)"),
261-
(1d, "rgba(0,0,0,0)"));
262-
await ctx.BeginPathAsync();
263-
await ctx.EllipseAsync(cx, dy, er * 2.4, eh * 2.4, 0, 0, Math.PI * 2);
264-
await ctx.FillAsync(FillRule.NonZero);
265-
266-
await ctx.SaveAsync();
267-
await ctx.ShadowColorAsync(egg.Running ? "rgba(255,160,30,0.85)" : "rgba(60,220,185,0.8)");
268-
await ctx.ShadowBlurAsync(18);
269-
if (egg.Running) {
270-
await ctx.FillStyleAsync(cx - er * 0.3, dy - eh * 0.32, eh * 0.05, cx, dy, eh * 1.15,
271-
(0d, "#fff8e0"),
272-
(0.55, "#ffe070"),
273-
(1d, "#ff9020"));
274-
}
275-
else {
276-
await ctx.FillStyleAsync(cx - er * 0.3, dy - eh * 0.32, eh * 0.05, cx, dy, eh * 1.15,
277-
(0d, "#eefffa"),
278-
(0.55, "#88f0d8"),
279-
(1d, "#28b09a"));
280-
}
261+
// Offscreen-cached egg sprite (glow + body + outline + speckles + highlight)
262+
await ctx.DrawImageAsync("staticEggCache", cx - halfCache, dy - halfCache, _eggCacheSize, _eggCacheSize);
281263

264+
// Timer ring (dynamic per egg)
265+
var frac = Math.Max(0, egg.Timer / 5000);
266+
var ringR = eh * 1.48;
267+
await ctx.StrokeStyleAsync("rgba(60,200,175,0.2)");
268+
await ctx.LineWidthAsync(2.2);
282269
await ctx.BeginPathAsync();
283-
await ctx.EllipseAsync(cx, dy, er, eh, 0, 0, Math.PI * 2);
284-
await ctx.FillAsync(FillRule.NonZero);
285-
await ctx.RestoreAsync();
270+
await ctx.ArcAsync(cx, dy, ringR, 0, Math.PI * 2);
271+
await ctx.StrokeAsync();
286272

287-
await ctx.StrokeStyleAsync(egg.Running ? "rgba(255,200,80,0.9)" : "rgba(110,255,225,0.8)");
288-
await ctx.LineWidthAsync(1.5);
273+
var tColor = frac > 0.6 ? "#38ffda" : frac > 0.3 ? "#ffd050" : "#ff6830";
274+
await ctx.SaveAsync();
275+
await ctx.StrokeStyleAsync(tColor);
276+
await ctx.ShadowColorAsync(tColor);
277+
await ctx.ShadowBlurAsync(10);
278+
await ctx.LineWidthAsync(2.2);
279+
await ctx.LineCapAsync(LineCap.Round);
289280
await ctx.BeginPathAsync();
290-
await ctx.EllipseAsync(cx, dy, er, eh, 0, 0, Math.PI * 2);
281+
await ctx.ArcAsync(cx, dy, ringR, -Math.PI / 2, -Math.PI / 2 + frac * Math.PI * 2);
291282
await ctx.StrokeAsync();
283+
await ctx.RestoreAsync();
284+
}
292285

293-
await ctx.FillStyleAsync(egg.Running ? "rgba(175,85,0,0.28)" : "rgba(25,135,115,0.28)");
294-
foreach (var s in EggSpeckles) {
295-
await ctx.BeginPathAsync();
296-
await ctx.EllipseAsync(cx + s.Ox * er, dy + s.Oy * eh, s.Rx * er, s.Ry * eh, s.A, 0, Math.PI * 2);
297-
await ctx.FillAsync(FillRule.NonZero);
298-
}
286+
// Pass 2: running eggs — cached sprite + animated legs & eyes
287+
var walkPhase = (now * 0.007) % (Math.PI * 2);
288+
foreach (var egg in _snake.Food) {
289+
if (!egg.Running) continue;
290+
var cx = egg.ScreenX;
291+
var cy = egg.ScreenY;
299292

300-
await ctx.FillStyleAsync("rgba(255,255,255,0.58)");
301-
await ctx.BeginPathAsync();
302-
await ctx.EllipseAsync(cx - er * 0.28, dy - eh * 0.32, er * 0.38, eh * 0.2, -0.3, 0, Math.PI * 2);
303-
await ctx.FillAsync(FillRule.NonZero);
293+
if (cx + cullMargin < camX || cx - cullMargin > viewRight) continue;
294+
if (cy + cullMargin < camY || cy - cullMargin > viewBottom) continue;
304295

305-
if (egg.Running) {
306-
var rd = egg.RunDir;
307-
var eyeR = er * 0.165;
308-
var eyeCx = cx + rd.X * er * 0.38;
309-
var eyeCy = dy + rd.Y * eh * 0.38;
310-
var perpX = -rd.Y;
311-
var perpY = rd.X;
296+
var dy = cy; // no bob for running eggs
312297

313-
for (var side = -1; side <= 1; side += 2) {
314-
var ex = eyeCx + perpX * er * 0.28 * side;
315-
var ey = eyeCy + perpY * eh * 0.28 * side;
316-
await ctx.FillStyleAsync("rgba(248,252,255,0.95)");
317-
await ctx.BeginPathAsync();
318-
await ctx.ArcAsync(ex, ey, eyeR, 0, Math.PI * 2);
319-
await ctx.FillAsync(FillRule.NonZero);
298+
// Animated legs
299+
for (var si = 0; si < 2; si++) {
300+
var side = si == 0 ? -1 : 1;
301+
var extend = 0.65 + 0.35 * Math.Sin(walkPhase + (si == 0 ? 0 : Math.PI));
302+
var lx = cx + side * er * 0.5;
303+
var ly = dy + eh * 0.7;
304+
var lw = er * 0.48;
305+
var lh = eh * 0.72 * extend;
320306

321-
await ctx.FillStyleAsync("#18103a");
322-
await ctx.BeginPathAsync();
323-
await ctx.ArcAsync(ex + rd.X * eyeR * 0.22, ey + rd.Y * eyeR * 0.22, eyeR * 0.55, 0, Math.PI * 2);
324-
await ctx.FillAsync(FillRule.NonZero);
307+
await ctx.FillStyleAsync("#ffdc96");
308+
await ctx.FillRectAsync(lx - lw / 2, ly, lw, lh);
325309

326-
await ctx.FillStyleAsync("rgba(255,255,255,0.88)");
327-
await ctx.BeginPathAsync();
328-
await ctx.ArcAsync(ex - eyeR * 0.2, ey - eyeR * 0.2, eyeR * 0.24, 0, Math.PI * 2);
329-
await ctx.FillAsync(FillRule.NonZero);
330-
}
310+
var footDirX = egg.RunDir.X != 0 ? egg.RunDir.X * er * 0.2 : 0;
311+
await ctx.FillStyleAsync("#ffb855");
312+
await ctx.BeginPathAsync();
313+
await ctx.EllipseAsync(lx + footDirX, ly + lh, lw * 0.95, lw * 0.52, 0, 0, Math.PI * 2);
314+
await ctx.FillAsync(FillRule.NonZero);
331315
}
332316

333-
if (!egg.Running) {
334-
var frac = Math.Max(0, egg.Timer / 5000);
335-
var ringR = eh * 1.48;
336-
await ctx.StrokeStyleAsync("rgba(60,200,175,0.2)");
337-
await ctx.LineWidthAsync(2.2);
317+
// Offscreen-cached egg sprite (glow + body + outline + speckles + highlight)
318+
await ctx.DrawImageAsync("runningEggCache", cx - halfCache, dy - halfCache, _eggCacheSize, _eggCacheSize);
319+
320+
// Animated eyes
321+
var rd = egg.RunDir;
322+
var eyeR = er * 0.165;
323+
var eyeCx = cx + rd.X * er * 0.38;
324+
var eyeCy = dy + rd.Y * eh * 0.38;
325+
var perpX = -rd.Y;
326+
var perpY = rd.X;
327+
328+
for (var side = -1; side <= 1; side += 2) {
329+
var ex = eyeCx + perpX * er * 0.28 * side;
330+
var ey = eyeCy + perpY * eh * 0.28 * side;
331+
await ctx.FillStyleAsync("rgba(248,252,255,0.95)");
338332
await ctx.BeginPathAsync();
339-
await ctx.ArcAsync(cx, dy, ringR, 0, Math.PI * 2);
340-
await ctx.StrokeAsync();
333+
await ctx.ArcAsync(ex, ey, eyeR, 0, Math.PI * 2);
334+
await ctx.FillAsync(FillRule.NonZero);
341335

342-
var tColor = frac > 0.6 ? "#38ffda" : frac > 0.3 ? "#ffd050" : "#ff6830";
343-
await ctx.SaveAsync();
344-
await ctx.StrokeStyleAsync(tColor);
345-
await ctx.ShadowColorAsync(tColor);
346-
await ctx.ShadowBlurAsync(10);
347-
await ctx.LineWidthAsync(2.2);
348-
await ctx.LineCapAsync(LineCap.Round);
336+
await ctx.FillStyleAsync("#18103a");
349337
await ctx.BeginPathAsync();
350-
await ctx.ArcAsync(cx, dy, ringR, -Math.PI / 2, -Math.PI / 2 + frac * Math.PI * 2);
351-
await ctx.StrokeAsync();
352-
await ctx.RestoreAsync();
338+
await ctx.ArcAsync(ex + rd.X * eyeR * 0.22, ey + rd.Y * eyeR * 0.22, eyeR * 0.55, 0, Math.PI * 2);
339+
await ctx.FillAsync(FillRule.NonZero);
340+
341+
await ctx.FillStyleAsync("rgba(255,255,255,0.88)");
342+
await ctx.BeginPathAsync();
343+
await ctx.ArcAsync(ex - eyeR * 0.2, ey - eyeR * 0.2, eyeR * 0.24, 0, Math.PI * 2);
344+
await ctx.FillAsync(FillRule.NonZero);
353345
}
354346
}
355347
}

BlazorExperiments/wwwroot/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
</script>
7979
<script src="js/neon-snake-audio.js"></script>
8080
<script src="js/neon-snake-obstacle.js"></script>
81+
<script src="js/neon-snake-egg.js"></script>
8182
</body>
8283

8384
</html>

0 commit comments

Comments
 (0)