@@ -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 }
0 commit comments