// 程序化像素绘制(无外部图片资源)。对应 web 版 sprites.js。 // 全部使用 Flutter dart:ui 的 Canvas / Paint / Path,逐函数镜像。 import 'dart:ui' show Canvas, Paint, Path, Rect, Color, Offset, PaintingStyle, StrokeCap, TextAlign; import 'dart:math' show sin, cos, pi, max, min, sqrt, atan2; import 'package:flutter/widgets.dart'; import 'package:iwbtc/src/core/util.dart'; import 'package:iwbtc/src/game/views.dart'; Color rgba(int r, int g, int b, double a) => Color.fromARGB((a * 255).round().clamp(0, 255), r, g, b); class C { static const Color stone = Color(0xff6b6b7e); static const Color stoneDark = Color(0xff3a3a48); static const Color stoneLite = Color(0xff8f8fa6); static const Color grass = Color(0xff4f9e46); static const Color grassDark = Color(0xff2c5c2b); static const Color brick = Color(0xffa8543c); static const Color brickDark = Color(0xff632a1d); static const Color metal = Color(0xff5a6478); static const Color metalDark = Color(0xff333a4a); static const Color ice = Color(0xff8fd8f0); static const Color iceDark = Color(0xff4a90b8); static const Color water = Color(0xff2a6fd8); static const Color spike = Color(0xffcfdde9); static const Color spikeDark = Color(0xffa8bccf); static const Color kid = Color(0xffd94a55); static const Color kidDark = Color(0xff8e2530); static const Color skin = Color(0xfff5cfa8); static const Color hair = Color(0xff5a3a22); static const Color pant = Color(0xff2c3a7a); static const Color save = Color(0xffb06bff); static const Color goal = Color(0xffffc63a); static const Color gun = Color(0xffb8b8c8); static const Color apple = Color(0xffe03050); } void px(Canvas c, double x, double y, double w, double h, Color col) { final p = Paint()..color = col; c.drawRect(Rect.fromLTWH(x, y, w, h), p); } // ---------------- 地形 ---------------- void drawBlock(Canvas c, double x, double y, int variant, [bool lit = false]) { const s = TILE; Color base = C.stone, dark = C.stoneDark, lite = C.stoneLite; if (variant == 1) { base = C.grass; dark = C.grassDark; lite = const Color(0xff7fd06a); } else if (variant == 2) { base = C.brick; dark = C.brickDark; lite = const Color(0xffd1785c); } else if (variant == 3) { base = C.metal; dark = C.metalDark; lite = const Color(0xff8a94ad); } px(c, x, y, s, s, dark); px(c, x + 1, y + 1, s - 2, s - 2, base); if (variant == 2) { px(c, x + 1, y + 8, s - 2, 1, dark); px(c, x + 1, y + 17, s - 2, 1, dark); px(c, x + 10, y + 1, 1, 7, dark); px(c, x + 20, y + 9, 1, 8, dark); px(c, x + 6, y + 18, 1, 7, dark); } else if (variant == 1) { px(c, x + 1, y + 1, s - 2, 5, lite); for (int i = 0; i < 4; i++) px(c, x + 3 + i * 7, y + 2, 1, 4, dark); } else { final n = hashNoise(x.round(), y.round()); px(c, x + 2, y + 2, 6, 2, lite); if (n > 0.5) px(c, x + 18, y + 12, 8, 2, dark); if (n < 0.35) px(c, x + 6, y + 22, 7, 2, dark); } px(c, x, y, s, 1, lite); px(c, x, y, 1, s, lite); px(c, x, y + s - 1, s, 1, dark); px(c, x + s - 1, y, 1, s, dark); if (lit) px(c, x, y, s, s, rgba(255, 255, 255, 0.09)); } void drawIce(Canvas c, double x, double y) { const s = TILE; px(c, x, y, s, s, C.iceDark); px(c, x + 1, y + 1, s - 2, s - 2, C.ice); px(c, x + 2, y + 3, 10, 3, const Color(0xffe6fbff)); px(c, x + 18, y + 16, 8, 2, rgba(255, 255, 255, 0.7)); px(c, x, y, s, 1, const Color(0xffe6fbff)); px(c, x, y + s - 1, s, 1, C.iceDark); } void drawWater(Canvas c, double x, double y, int t) { const s = TILE; px(c, x, y, s, s, rgba(40, 110, 215, 0.55)); final off = sin(t / 260 + x * 0.09) * 3; px(c, x, y + 4 + off, s, 3, rgba(120, 200, 255, 0.35)); px(c, x, y + 16 - off, s, 2, rgba(120, 200, 255, 0.35)); } void drawOneWay(Canvas c, double x, double y) { const s = TILE; px(c, x, y, s, 6, C.metalDark); px(c, x, y, s, 3, const Color(0xff9aa6bd)); for (int i = 0; i < 8; i++) px(c, x + i * 4, y + 6, 2, 3, C.metalDark); } void drawSpike(Canvas c, double x, double y, int dir, int t) { const s = TILE; late double ax, ay, bx, by, cx2, cy2; if (dir == 0) { ax = x + s / 2; ay = y; bx = x; by = y + s; cx2 = x + s; cy2 = y + s; } else if (dir == 1) { ax = x + s / 2; ay = y + s; bx = x; by = y; cx2 = x + s; cy2 = y; } else if (dir == 2) { ax = x; ay = y + s / 2; bx = x + s; by = y; cx2 = x + s; cy2 = y + s; } else { ax = x + s; ay = y + s / 2; bx = x; by = y; cx2 = x; cy2 = y + s; } late Offset from, to; if (dir == 0) { from = Offset(x, y); to = Offset(x, y + s); } else if (dir == 1) { from = Offset(x, y + s); to = Offset(x, y); } else if (dir == 2) { from = Offset(x, y); to = Offset(x + s, y); } else { from = Offset(x + s, y); to = Offset(x, y); } final rect = Rect.fromLTWH(x, y, s, s); final g = LinearGradient( begin: _gradAlign(from, rect), end: _gradAlign(to, rect), colors: const [Color(0xfff4f9fd), Color(0xffcfdde9), Color(0xffa8bccf)], ); final p = Path(); p.moveTo(ax, ay); p.lineTo(bx, by); p.lineTo(cx2, cy2); p.close(); final paint = Paint()..shader = g.createShader(rect); c.drawPath(p, paint); final stroke = Paint() ..color = rgba(150, 175, 200, 0.9) ..style = PaintingStyle.stroke ..strokeWidth = 1; c.drawPath(p, stroke); } void drawKiller(Canvas c, double x, double y, int t) { const s = TILE; final bob = sin(t / 300 + x) * 1.5; c.save(); c.translate(x + s / 2, y + s / 2 + bob); final p = Paint()..color = C.apple; c.drawCircle(Offset(0, 2), 11, p); final p2 = Paint()..color = const Color(0xffff7a90); c.drawCircle(Offset(-4, -2), 4, p2); px(c, -1, -13, 2, 6, const Color(0xff6b4423)); final leaf = Paint()..color = const Color(0xff3ddc84); c.drawOval(Rect.fromLTWH(1, -13, 10, 5.2), leaf); px(c, -5, 0, 2, 2, const Color(0xff000000)); px(c, 3, 0, 2, 2, const Color(0xff000000)); c.restore(); } void drawVanish(Canvas c, double x, double y, int state, int t) { const s = TILE; final alpha = state == 0 ? 1.0 : state == 1 ? (sin(t / 60) > 0 ? 0.55 : 0.9) : 0.14; drawBlock(c, x, y, 3); if (alpha < 1) { // 模拟 globalAlpha:用半透明黑覆盖来压暗 px(c, x, y, s, s, Color.fromARGB(((1 - alpha) * 255).round().clamp(0, 255), 0, 0, 0)); } if (state != 2) { final sp = Paint() ..color = rgba(255, 200, 60, 0.55) ..style = PaintingStyle.stroke ..strokeWidth = 1; c.drawRect(Rect.fromLTWH(x + 2.5, y + 2.5, s - 5, s - 5), sp); } } void drawJumpRef(Canvas c, double x, double y, int t) { const s = TILE; final b = sin(t / 200 + x) * 2; final p = Path(); p.moveTo(x + s / 2, y + 8 + b); p.lineTo(x + s - 6, y + s - 10 + b); p.lineTo(x + 6, y + s - 10 + b); p.close(); c.drawPath(p, Paint()..color = const Color(0xff3ddc84)); final sp = Paint() ..color = const Color(0xff0d5c30) ..style = PaintingStyle.stroke ..strokeWidth = 1; c.drawPath(p, sp); } // ---------------- 玩家 ---------------- void drawKid(Canvas c, PlayerView p, int t) { final x = p.x.roundToDouble(), y = p.y.roundToDouble(); final w = p.w, h = p.h; final air = !p.grounded; c.save(); c.translate(x, y); // 身体(红衣) px(c, 2, 7, w - 4, 7, C.kid); px(c, 2, 7, w - 4, 1, shade(C.kid, 40)); if (air) { px(c, p.facing > 0 ? w - 3 : 0, 5, 3, 5, C.skin); } else { px(c, p.facing > 0 ? w - 3 : 0, 9, 3, 5, C.skin); } if (air) { px(c, 3, 14, 4, 4, C.pant); px(c, w - 7, 14, 4, 3, C.pant); } else { px(c, 2, 14, 4, 4, C.pant); px(c, w - 6, 14, 4, 4, C.pant); } px(c, 1, 0, w - 2, 8, C.skin); px(c, 1, 0, w - 2, 3, C.hair); px(c, p.facing > 0 ? w - 5 : 3, 1, 3, 2, C.hair); px(c, p.facing > 0 ? w - 6 : 3, 4, 2, 2, const Color(0xff101018)); px(c, p.facing > 0 ? w - 3 : 0, 4, 1, 2, const Color(0xff101018)); c.restore(); if (p.hasGun && !p.dead) { final gx = p.facing > 0 ? x + w - 1 : x - 7; final gy = y + 9; px(c, gx, gy, 8, 3, C.gun); px(c, p.facing > 0 ? gx + 6 : gx, gy + 1, 3, 2, const Color(0xff7a7a8c)); px(c, gx + 1, gy + 3, 3, 3, const Color(0xff6a6a7c)); } } // ---------------- 实体 ---------------- void drawSave(Canvas c, EntityView e, int t) { final cx = e.x + e.w / 2, cy = e.y + e.h / 2; final on = e.active; final pulse = 1 + sin(t / 180) * 0.09; if (on) { final p = Paint()..color = rgba(176, 107, 255, 0.28); c.drawCircle(Offset(cx, cy), 15 * pulse, p); } else { final r = 13 + sin(t / 26) * 2.2; final sp = Paint() ..color = rgba(138, 106, 184, 0.30 + sin(t / 26) * 0.12) ..style = PaintingStyle.stroke ..strokeWidth = 1.5; c.drawCircle(Offset(cx, cy), r, sp); } final col = on ? C.save : const Color(0xff8f74b5); final p = Paint()..color = col; c.drawCircle(Offset(cx - 3, cy - 1), 4.2, p); c.drawCircle(Offset(cx + 4, cy + 1), 4.2, p); c.drawCircle(Offset(cx, cy + 5), 4.2, p); c.drawCircle(Offset(cx + 1, cy - 5), 4.2, p); if (on) { c.drawCircle(Offset(cx - 1, cy - 2), 1.6, Paint()..color = const Color(0xffe5d0ff)); } px(c, cx + 1, cy - 9, 2, 4, const Color(0xff4a8a3a)); } void drawGoal(Canvas c, EntityView e, int t) { final x = e.x, y = e.y, w = e.w, h = e.h; px(c, x + 3, y + 2, w - 6, h - 2, const Color(0xff2a1e3a)); px(c, x + 5, y + 4, w - 10, h - 6, const Color(0xff140d20)); final gr = Rect.fromLTWH(x + 5, y + 4, w - 10, h - 6); final g = LinearGradient( begin: _gradAlign(Offset(x, y + h), gr), end: _gradAlign(Offset(x, y), gr), colors: [rgba(255, 200, 60, 0.55), rgba(255, 200, 60, 0)], ); c.drawRect(gr, Paint()..shader = g.createShader(gr)); for (int i = 0; i < 3; i++) { final pp = (((t / 700) + i / 3) % 1); px(c, x + 7 + ((i * 9) % (w - 12)), y + h - 4 - pp * (h - 8), 2, 2, C.goal); } px(c, x + 3, y + 2, w - 6, 2, C.goal); px(c, x + 3, y + 2, 2, h - 2, const Color(0xffc99a1e)); px(c, x + w - 5, y + 2, 2, h - 2, const Color(0xffc99a1e)); } void drawGun(Canvas c, EntityView e, int t) { final x = e.x, y = e.y + sin(t / 250) * 2; px(c, x + 2, y + 6, 20, 7, const Color(0xff4a4a5c)); px(c, x + 2, y + 6, 20, 2, const Color(0xff8a8aa0)); px(c, x + 18, y + 8, 9, 3, C.gun); px(c, x + 6, y + 13, 5, 8, const Color(0xff3a3a4c)); px(c, x + 8, y + 19, 3, 3, const Color(0xff2a2a3a)); px(c, x + 24, y + 7, 2, 5, const Color(0xffd0d0e0)); } void drawApple(Canvas c, EntityView e, int t) { final cx = e.x + e.w / 2, cy = e.y + e.h / 2 + sin(t / 320 + e.x) * 1.5; c.drawCircle(Offset(cx, cy), 8, Paint()..color = C.apple); c.drawCircle(Offset(cx - 3, cy - 3), 3, Paint()..color = const Color(0xffff8095)); px(c, cx - 1, cy - 11, 2, 4, const Color(0xff6b4423)); c.drawOval(Rect.fromLTWH(cx + 4 - 4, cy - 9 - 2, 8, 4), Paint()..color = const Color(0xff3ddc84)); } void drawWarp(Canvas c, EntityView e, int t) { final cx = e.x + e.w / 2, cy = e.y + e.h / 2; for (int i = 0; i < 3; i++) { final r = 14 - i * 4; final a = t / (300 + i * 120); final sp = Paint() ..color = Color.fromARGB(((0.85 - i * 0.2) * 255).round().clamp(0, 255), (170 - i * 30).clamp(0, 255), (90 + i * 40).clamp(0, 255), 255) ..style = PaintingStyle.stroke ..strokeWidth = 2; c.drawArc(Rect.fromLTWH(cx - r, cy - r, r * 2, r * 2), a, pi * 1.4, false, sp); } c.drawCircle(Offset(cx, cy), 5, Paint()..color = rgba(120, 60, 200, 0.35)); } void drawCamera(Canvas c, EntityView e, int t) { final x = e.x, y = e.y, w = e.w, h = e.h; _dashedRect(c, Rect.fromLTWH(x + 1, y + 1, w - 2, h - 2), rgba(80, 220, 255, 0.35 + sin(t / 300) * 0.15), 1, 4, 4); final ix = x + w / 2 - 9, iy = y + 6; px(c, ix, iy + 3, 18, 11, const Color(0xff1e1e2a)); px(c, ix + 2, iy + 5, 14, 7, const Color(0xff3a3a4c)); c.drawCircle(Offset(ix + 9, iy + 8.5), 3.5, Paint()..color = const Color(0xff7ae0ff)); px(c, ix + 12, iy, 5, 3, const Color(0xff1e1e2a)); px(c, ix + 13, iy - 1, 3, 2, const Color(0xffff4060)); } void drawSign(Canvas c, EntityView e, int t) { final x = e.x, y = e.y, w = e.w, h = e.h; px(c, x + w / 2 - 1, y + h - 8, 3, 8, const Color(0xff6b4423)); px(c, x, y + 2, w, h - 10, const Color(0xffc9a86b)); px(c, x + 2, y + 4, w - 4, h - 14, const Color(0xffe8d5a8)); px(c, x + 4, y + 8, w - 8, 2, const Color(0xff8a7550)); px(c, x + 4, y + 13, w - 12, 2, const Color(0xff8a7550)); } void drawTurret(Canvas c, EntityView e, int t) { final x = e.x, y = e.y; const s = TILE; px(c, x + 2, y + 6, s - 4, s - 6, const Color(0xff4a4a5e)); px(c, x + 4, y + 8, s - 8, s - 12, const Color(0xff6a6a80)); final dirx = e.dir == 'left' ? -1 : e.dir == 'right' ? 1 : 0; final diry = e.dir == 'up' ? -1 : e.dir == 'down' ? 1 : 0; final cx = x + s / 2, cy = y + s / 2; px(c, cx + dirx * 8 - 3, cy + diry * 8 - 3, 7, 7, const Color(0xff2a2a38)); final blink = (t ~/ 300) % 3 == 0; px(c, cx + dirx * 12 - 2, cy + diry * 12 - 2, 4, 4, blink ? const Color(0xffff4060) : const Color(0xff8a2030)); } void drawMovingPlatform(Canvas c, MoverView m) { final x = m.x, y = m.y, w = m.w, h = m.h; px(c, x, y, w, h, const Color(0xff3a3a4c)); px(c, x + 1, y + 1, w - 2, h - 2, const Color(0xff6a6a84)); px(c, x + 1, y + 1, w - 2, 2, const Color(0xff9a9ab4)); for (double i = 0; i < w; i += 8) px(c, x + i + 2, y + h - 4, 4, 2, const Color(0xff3a3a4c)); _dashedLine(c, Offset(m.ax + m.aw / 2, m.ay + m.ah / 2), Offset(m.bx + m.aw / 2, m.by + m.ah / 2), rgba(255, 255, 255, 0.10), 1, 3, 5); } void drawDropSpike(Canvas c, EntityView e, int t) { const s = TILE; if (!e.falling) { px(c, e.x + 4, e.y + 4, s - 8, s - 8, rgba(74, 74, 92, 0.85)); px(c, e.x + 6, e.y + 6, s - 12, s - 12, rgba(106, 106, 128, 0.85)); } else { drawSpike(c, e.x, e.y, 2, t); } } void drawBullet(Canvas c, BulletView b) { px(c, b.x - 1, b.y - 1, b.w + 2, b.h + 2, rgba(255, 240, 120, 0.35)); px(c, b.x, b.y, b.w, b.h, const Color(0xfffff6a0)); px(c, b.x, b.y + b.h - 2, b.w, 2, const Color(0xffffb040)); } void drawEBullet(Canvas c, BulletView b, int t) { final cx = b.x + b.w / 2, cy = b.y + b.h / 2; c.drawCircle(Offset(cx, cy), b.w / 2 + 1, Paint()..color = hex(b.col)); c.drawCircle(Offset(cx - 1.5, cy - 1.5), b.w / 4, Paint()..color = const Color(0xffffffff)); if (b.big) { final sp = Paint() ..color = rgba(255, 255, 255, 0.4) ..style = PaintingStyle.stroke; c.drawCircle(Offset(cx, cy), b.w / 2 + 4 + sin(t / 90) * 2, sp); } } // ---------------- 死亡特效 ---------------- void drawDeathRing(Canvas c, DeathFX fx) { final t = fx.t; if (t > 14) return; final r = 4 + t * 1.8; final a = max(0.0, 1 - t / 14); final sp = Paint() ..color = rgba(255, 143, 163, a * 0.85) ..style = PaintingStyle.stroke ..strokeWidth = 2; c.drawCircle(Offset(fx.x, fx.y), r, sp); c.drawCircle(Offset(fx.x, fx.y), r * 0.55, Paint()..color = rgba(255, 92, 122, a * 0.3)); } void drawDeathMark(Canvas c, DeathMark dm) { const s = 4.0; final sp = Paint() ..color = rgba(0, 0, 0, 0.55) ..style = PaintingStyle.stroke ..strokeWidth = 3 ..strokeCap = StrokeCap.round; _line(c, sp, dm.x - s, dm.y - s, dm.x + s, dm.y + s); _line(c, sp, dm.x + s, dm.y - s, dm.x - s, dm.y + s); final sp2 = Paint() ..color = const Color(0xffff2535) ..style = PaintingStyle.stroke ..strokeWidth = 2 ..strokeCap = StrokeCap.round; _line(c, sp2, dm.x - s, dm.y - s, dm.x + s, dm.y + s); _line(c, sp2, dm.x + s, dm.y - s, dm.x - s, dm.y + s); } // ---------------- 文本 ---------------- void drawText(Canvas c, String text, double x, double y, {Color color = const Color(0xffffffff), double size = 11, TextAlign align = TextAlign.center}) { final tp = TextPainter( text: TextSpan(text: text, style: TextStyle(color: color, fontSize: size, fontFamily: 'Courier')), textAlign: align, textDirection: TextDirection.ltr, ); tp.layout(); tp.paint(c, Offset(x - (align == TextAlign.center ? tp.width / 2 : 0), y)); } // ---------------- 虚线辅助 ---------------- void _dashedLine(Canvas c, Offset a, Offset b, Color col, double width, double dash, double gap) { final p = Paint()..color = col..strokeWidth = width; final dx = b.dx - a.dx, dy = b.dy - a.dy; final dist = sqrt(dx * dx + dy * dy); if (dist == 0) return; final ux = dx / dist, uy = dy / dist; double d = 0; while (d < dist) { final d2 = min(d + dash, dist); c.drawLine(Offset(a.dx + ux * d, a.dy + uy * d), Offset(a.dx + ux * d2, a.dy + uy * d2), p); d += dash + gap; } } void _dashedRect(Canvas c, Rect r, Color col, double width, double dash, double gap) { _dashedLine(c, r.topLeft, r.topRight, col, width, dash, gap); _dashedLine(c, r.topRight, r.bottomRight, col, width, dash, gap); _dashedLine(c, r.bottomRight, r.bottomLeft, col, width, dash, gap); _dashedLine(c, r.bottomLeft, r.topLeft, col, width, dash, gap); } void _line(Canvas c, Paint p, double x1, double y1, double x2, double y2) { c.drawLine(Offset(x1, y1), Offset(x2, y2), p); } // 把像素空间的绝对 Offset 换算成 Flutter LinearGradient 所需的 Alignment。 Alignment _gradAlign(Offset abs, Rect r) => Alignment(((abs.dx - r.left) - r.width / 2) / (r.width / 2), ((abs.dy - r.top) - r.height / 2) / (r.height / 2));