/* 贪蛇出动 - 渲染:Canvas 程序化绘制(背景网格/边界/金币/蛇/特效) */ (function () { 'use strict'; window.GS = window.GS || {}; const U = GS.U; class Render { constructor(canvas) { this.cv = canvas; this.ctx = canvas.getContext('2d'); this.w = 0; this.h = 0; this.dpr = 1; this.resize(); window.addEventListener('resize', () => this.resize()); } resize() { this.dpr = Math.min(window.devicePixelRatio || 1, 2); this.w = window.innerWidth; this.h = window.innerHeight; this.cv.width = Math.floor(this.w * this.dpr); this.cv.height = Math.floor(this.h * this.dpr); } draw(world, snakes, player, now) { const ctx = this.ctx; const cam = world.cam; ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); // 背景 ctx.fillStyle = '#222b57'; ctx.fillRect(0, 0, this.w, this.h); const sc = cam.scale; const ox = this.w / 2 - cam.x * sc; const oy = this.h / 2 - cam.y * sc; const toX = x => x * sc + ox; const toY = y => y * sc + oy; // 网格 ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; const gridStep = 100 * sc; const gx0 = ((ox % gridStep) + gridStep) % gridStep; const gy0 = ((oy % gridStep) + gridStep) % gridStep; ctx.beginPath(); for (let x = gx0; x < this.w; x += gridStep) { ctx.moveTo(x, 0); ctx.lineTo(x, this.h); } for (let y = gy0; y < this.h; y += gridStep) { ctx.moveTo(0, y); ctx.lineTo(this.w, y); } ctx.stroke(); // 边界(红粉色发光线)+ 界外压暗 const bx0 = toX(0), by0 = toY(0), bx1 = toX(world.size), by1 = toY(world.size); ctx.fillStyle = 'rgba(8,10,26,0.55)'; if (bx0 > 0) ctx.fillRect(0, 0, bx0, this.h); if (bx1 < this.w) ctx.fillRect(bx1, 0, this.w - bx1, this.h); if (by0 > 0) ctx.fillRect(0, 0, this.w, by0); if (by1 < this.h) ctx.fillRect(0, by1, this.w, this.h - by1); ctx.strokeStyle = '#ff4d6d'; ctx.lineWidth = 3; ctx.shadowColor = '#ff4d6d'; ctx.shadowBlur = 12; ctx.strokeRect(bx0, by0, bx1 - bx0, by1 - by0); ctx.shadowBlur = 0; // 金币(视口内) for (const c of world.coins) { const x = toX(c.x), y = toY(c.y); if (x < -40 || y < -40 || x > this.w + 40 || y > this.h + 40) continue; const pulse = 1 + Math.sin(now / 300 + c.phase) * 0.12; const r = c.r * sc * pulse; if (c.kind === 1) this._starCoin(ctx, x, y, r); else this._coin(ctx, x, y, r, c.kind === 2); } // 蛇:先画 AI 再画玩家 for (const s of snakes) if (!s.isPlayer && !s.dead) this._snake(ctx, s, toX, toY, sc, now); if (player && !player.dead) this._snake(ctx, player, toX, toY, sc, now); } _coin(ctx, x, y, r, big) { ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = big ? '#ffca28' : '#f5b921'; ctx.fill(); ctx.lineWidth = Math.max(1, r * 0.28); ctx.strokeStyle = big ? '#c98f00' : '#b8860b'; ctx.stroke(); ctx.beginPath(); ctx.arc(x - r * 0.25, y - r * 0.25, r * 0.4, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.fill(); } _starCoin(ctx, x, y, r) { ctx.save(); ctx.translate(x, y); ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.fillStyle = '#ffc233'; ctx.fill(); ctx.lineWidth = Math.max(1, r * 0.22); ctx.strokeStyle = '#c98f00'; ctx.stroke(); // 五角星 ctx.beginPath(); for (let i = 0; i < 10; i++) { const rr = i % 2 === 0 ? r * 0.62 : r * 0.28; const a = -Math.PI / 2 + i * Math.PI / 5; const px = Math.cos(a) * rr, py = Math.sin(a) * rr; if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.closePath(); ctx.fillStyle = '#fff3c4'; ctx.fill(); ctx.restore(); } _snake(ctx, s, toX, toY, sc, now) { const segs = s.segments(); const r = s.radius * sc; const wob = Math.sin(s.wobble) * 0.06; // 磁铁光环 if (now < s.magnetUntil) { const h = segs[0]; ctx.beginPath(); ctx.arc(toX(h.x), toY(h.y), 260 * sc, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(90,200,255,0.25)'; ctx.lineWidth = 2; ctx.stroke(); } // 护盾圈 if (s.shield || s.invincible(now)) { const h = segs[0]; const sr = r * 3.2; ctx.beginPath(); ctx.arc(toX(h.x), toY(h.y), sr, 0, Math.PI * 2); ctx.fillStyle = 'rgba(80,160,255,0.10)'; ctx.fill(); ctx.strokeStyle = 'rgba(120,200,255,0.8)'; ctx.lineWidth = 2.5; ctx.stroke(); } // 加速尾焰 if (s.boosting && segs.length > 2) { const t = segs[segs.length - 1]; ctx.beginPath(); ctx.arc(toX(t.x), toY(t.y), r * 0.8, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,220,120,0.35)'; ctx.fill(); } // 身体:从尾到头 for (let i = segs.length - 1; i >= 1; i--) { const g = segs[i]; const rr = r * (1 - (i / segs.length) * 0.25) * (1 + wob * Math.sin(i * 0.6)); ctx.beginPath(); ctx.arc(toX(g.x), toY(g.y), rr, 0, Math.PI * 2); ctx.fillStyle = (s.skin === 1 && i % 4 < 2) ? s.color.dark : s.color.body; ctx.fill(); } // 头 const h = segs[0]; const hx = toX(h.x), hy = toY(h.y); ctx.beginPath(); ctx.arc(hx, hy, r * 1.08, 0, Math.PI * 2); ctx.fillStyle = s.color.body; ctx.fill(); // 眼睛:朝向前方 const a = s.angle; const ex = Math.cos(a), ey = Math.sin(a); const px = -ey, py = ex; for (const side of [1, -1]) { const cx = hx + (ex * 0.45 + px * 0.5 * side) * r; const cy = hy + (ey * 0.45 + py * 0.5 * side) * r; ctx.beginPath(); ctx.arc(cx, cy, r * 0.34, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.beginPath(); ctx.arc(cx + ex * r * 0.12, cy + ey * r * 0.12, r * 0.15, 0, Math.PI * 2); ctx.fillStyle = '#111'; ctx.fill(); } // 名字 + 分数 ctx.font = `${Math.max(11, 13 * sc)}px "PingFang SC", sans-serif`; ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(255,255,255,0.92)'; ctx.fillText(String(Math.floor(s.score)), hx, hy - r * 2.4); ctx.fillStyle = s.isPlayer ? 'rgba(255,255,255,0.75)' : 'rgba(200,210,255,0.7)'; ctx.fillText(s.name, hx, hy - r * 2.4 + 14 * sc); } } GS.Render = Render; })();