6dc79a2c12
- 私人剪贴板新增限时直通URL生成功能 - doc_cloud_keeper新增标题复制按钮 - 新增贪蛇出动游戏工具 - 新增OSS中转直链工具 - 添加批量抽帧测试脚本
199 lines
6.5 KiB
JavaScript
199 lines
6.5 KiB
JavaScript
/* 贪蛇出动 - 渲染: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;
|
|
})();
|