Files
yangxiangyuan 6dc79a2c12 feat: 新增多项功能并添加新工具
- 私人剪贴板新增限时直通URL生成功能
- doc_cloud_keeper新增标题复制按钮
- 新增贪蛇出动游戏工具
- 新增OSS中转直链工具
- 添加批量抽帧测试脚本
2026-08-11 10:22:31 +08:00

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;
})();