From b7fba447dc8a29c6941f041b782828f209746925 Mon Sep 17 00:00:00 2001 From: AnRil Date: Mon, 1 Jun 2026 01:23:40 +0700 Subject: [PATCH] Beautiful Canvas2D backgrounds (gradients, glows, bokeh) Replace the flat line-art backgrounds with 6 rich, full-screen artworks painted via the Canvas2D API (linear/radial gradients, screen/lighter blending, soft glows, bokeh, stars) and registered as Phaser canvas textures: - Nebula: colorful deep-space clouds (default) - Aurora: northern lights over a starfield - Sunset: synthwave sky with glowing sun + perspective grid - Ocean: sunlit underwater depths with caustics and bubbles - Dreamscape: soft pastel bokeh - Monad: signature purple cosmos - Backgrounds are now fixed full-screen images (no tilePositionY scroll, which would seam a gradient); motion comes from platforms + gwei particles - Settings background preview switched to a scaled image showing the whole art - Verified all 6 in-browser (menu, in-game, settings preview), no console errors --- src/config/backgrounds.js | 20 +-- src/scenes/BootScene.js | 281 ++++++++++++++++++++++---------------- src/scenes/GameScene.js | 5 +- src/scenes/MenuScene.js | 3 +- 4 files changed, 177 insertions(+), 132 deletions(-) diff --git a/src/config/backgrounds.js b/src/config/backgrounds.js index ec5e311..6e73803 100644 --- a/src/config/backgrounds.js +++ b/src/config/backgrounds.js @@ -1,18 +1,18 @@ /** - * Selectable backgrounds. Each `texture` is generated procedurally in - * BootScene.createBackgroundTextures and used as a tiling tileSprite. - * All patterns are designed to tile vertically so the parallax scroll is seamless. + * Selectable backgrounds. Each `texture` is a full-screen image painted with + * the Canvas2D API in BootScene.createBackgroundTextures (rich gradients, + * glows, bokeh, stars). They are drawn fixed (no tiling) so there are no seams. */ export const BACKGROUNDS = [ - { id: 'grid', name: 'Grid', texture: 'bg_grid' }, - { id: 'hex', name: 'Hex Nodes', texture: 'bg_hex' }, - { id: 'starfield', name: 'Starfield', texture: 'bg_starfield' }, - { id: 'synthwave', name: 'Synthwave', texture: 'bg_synthwave' }, - { id: 'circuit', name: 'Circuit', texture: 'bg_circuit' }, - { id: 'void', name: 'Void', texture: 'bg_void' }, + { id: 'nebula', name: 'Nebula', texture: 'bg_nebula' }, + { id: 'aurora', name: 'Aurora', texture: 'bg_aurora' }, + { id: 'sunset', name: 'Sunset', texture: 'bg_sunset' }, + { id: 'ocean', name: 'Ocean', texture: 'bg_ocean' }, + { id: 'dream', name: 'Dreamscape', texture: 'bg_dream' }, + { id: 'monad', name: 'Monad', texture: 'bg_monad' }, ]; -export const DEFAULT_BACKGROUND = 'grid'; +export const DEFAULT_BACKGROUND = 'nebula'; export function getBackground(id) { return BACKGROUNDS.find((b) => b.id === id) || BACKGROUNDS[0]; diff --git a/src/scenes/BootScene.js b/src/scenes/BootScene.js index 7deff47..8478678 100644 --- a/src/scenes/BootScene.js +++ b/src/scenes/BootScene.js @@ -1,4 +1,5 @@ import { Scene } from 'phaser'; +import { GAME_WIDTH, GAME_HEIGHT } from '../config/game.config.js'; export class BootScene extends Scene { constructor() { @@ -23,143 +24,185 @@ export class BootScene extends Scene { this.scene.start('MenuScene'); } - // Generates a texture via an off-screen graphics buffer. - _tex(key, w, h, draw) { - const g = this.make.graphics({ x: 0, y: 0, add: false }); - draw(g); - g.generateTexture(key, w, h); - g.destroy(); + // --- Canvas2D background painting (rich gradients, glows, bokeh) --- + + _canvasTex(key, draw) { + const w = GAME_WIDTH; + const h = GAME_HEIGHT; + if (this.textures.exists(key)) this.textures.remove(key); + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + draw(ctx, w, h); + this.textures.addCanvas(key, canvas); + } + + _vgrad(ctx, w, h, stops) { + const g = ctx.createLinearGradient(0, 0, 0, h); + stops.forEach(([o, c]) => g.addColorStop(o, c)); + ctx.fillStyle = g; + ctx.fillRect(0, 0, w, h); + } + + // Soft radial glow filling the canvas; color is an rgba string at full center. + _glow(ctx, x, y, r, color) { + const g = ctx.createRadialGradient(x, y, 0, x, y, r); + g.addColorStop(0, color); + g.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + } + + // Elongated, rotated glow used for aurora ribbons / light beams. + _ribbon(ctx, cx, cy, rx, ry, rot, color) { + ctx.save(); + ctx.translate(cx, cy); + ctx.rotate(rot); + ctx.scale(rx / ry, 1); + const g = ctx.createRadialGradient(0, 0, 0, 0, 0, ry); + g.addColorStop(0, color); + g.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = g; + ctx.beginPath(); + ctx.arc(0, 0, ry, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + + _stars(ctx, w, h, count, colors, maxR = 1.6, yMax = 1) { + for (let i = 0; i < count; i++) { + const x = Math.random() * w; + const y = Math.random() * h * yMax; + const r = Math.random() * maxR + 0.3; + ctx.globalAlpha = 0.3 + Math.random() * 0.7; + ctx.fillStyle = colors[(Math.random() * colors.length) | 0]; + ctx.beginPath(); + ctx.arc(x, y, r, 0, Math.PI * 2); + ctx.fill(); + } + ctx.globalAlpha = 1; } createBackgroundTextures() { - const S = 256; - - // 1) Grid — Monad blockchain grid (default look, larger so kept as bg_grid) - this._tex('bg_grid', S, S, (g) => { - g.fillStyle(0x0f001f, 1); - g.fillRect(0, 0, S, S); - g.lineStyle(1, 0x2e0059, 0.4); - for (let i = 0; i <= S; i += 32) { - g.moveTo(i, 0); g.lineTo(i, S); - g.moveTo(0, i); g.lineTo(S, i); - } - g.strokePath(); - // node dots at some intersections (deterministic -> tiles) - g.fillStyle(0x7c3aed, 0.35); - for (let x = 0; x <= S; x += 64) { - for (let y = 0; y <= S; y += 64) { - if (((x + y) / 64) % 2 === 0) g.fillCircle(x, y, 2); - } + // 1) Nebula — colorful deep-space clouds + this._canvasTex('bg_nebula', (ctx, w, h) => { + this._vgrad(ctx, w, h, [[0, '#05010f'], [1, '#0a0420']]); + ctx.globalCompositeOperation = 'screen'; + this._glow(ctx, w * 0.28, h * 0.28, w * 0.75, 'rgba(236,72,153,0.85)'); + this._glow(ctx, w * 0.80, h * 0.46, w * 0.70, 'rgba(34,211,238,0.6)'); + this._glow(ctx, w * 0.50, h * 0.72, w * 0.85, 'rgba(168,85,247,0.8)'); + this._glow(ctx, w * 0.15, h * 0.9, w * 0.5, 'rgba(245,158,11,0.35)'); + ctx.globalCompositeOperation = 'source-over'; + this._stars(ctx, w, h, 220, ['#ffffff', '#d8b4fe', '#a5f3fc', '#fbcfe8']); + // a few bright stars with halo + ctx.globalCompositeOperation = 'screen'; + for (let i = 0; i < 7; i++) { + const x = Math.random() * w; const y = Math.random() * h; + this._glow(ctx, x, y, 22, 'rgba(255,255,255,0.5)'); } + ctx.globalCompositeOperation = 'source-over'; }); - // 2) Hex Nodes — honeycomb - this._tex('bg_hex', S, S, (g) => { - g.fillStyle(0x0d0420, 1); - g.fillRect(0, 0, S, S); - const r = 22; - const w = Math.sqrt(3) * r; - const vSpace = 1.5 * r; - g.lineStyle(1.5, 0x6d28d9, 0.30); - for (let row = -1, ry = 0; ry < S + r; row++, ry = row * vSpace) { - const offset = (row % 2 === 0) ? 0 : w / 2; - for (let cx = -w; cx < S + w; cx += w) { - this._hex(g, cx + offset, ry, r); - } - } - // accent glow nodes - g.fillStyle(0xa855f7, 0.25); - for (let row = 0, ry = 0; ry < S; row += 2, ry = row * vSpace) { - for (let cx = 0; cx < S; cx += w * 2) g.fillCircle(cx, ry, 2.5); - } + // 2) Aurora — northern lights over a night sky + this._canvasTex('bg_aurora', (ctx, w, h) => { + this._vgrad(ctx, w, h, [[0, '#02030f'], [0.5, '#061a2e'], [1, '#01020a']]); + this._stars(ctx, w, h, 130, ['#ffffff', '#bfdbfe'], 1.4, 0.55); + ctx.globalCompositeOperation = 'lighter'; + this._ribbon(ctx, w * 0.45, h * 0.32, w * 0.9, h * 0.16, -0.35, 'rgba(34,245,170,0.45)'); + this._ribbon(ctx, w * 0.55, h * 0.45, w * 0.85, h * 0.14, 0.28, 'rgba(34,211,238,0.4)'); + this._ribbon(ctx, w * 0.5, h * 0.6, w * 1.0, h * 0.18, -0.18, 'rgba(168,85,247,0.38)'); + this._ribbon(ctx, w * 0.35, h * 0.52, w * 0.5, h * 0.1, 0.5, 'rgba(132,255,214,0.35)'); + ctx.globalCompositeOperation = 'source-over'; }); - // 3) Starfield — deep space - this._tex('bg_starfield', S, S, (g) => { - g.fillStyle(0x070016, 1); - g.fillRect(0, 0, S, S); - // faint distant dust - const dust = [0x1a0b3a, 0x12082b]; - for (let i = 0; i < 26; i++) { - g.fillStyle(Phaser.Utils.Array.GetRandom(dust), 0.5); - g.fillCircle(Phaser.Math.Between(0, S), Phaser.Math.Between(0, S), Phaser.Math.Between(20, 55)); + // 3) Sunset — synthwave sky with a glowing sun + horizon grid + this._canvasTex('bg_sunset', (ctx, w, h) => { + this._vgrad(ctx, w, h, [ + [0, '#241047'], [0.4, '#6d28d9'], [0.58, '#db2777'], + [0.74, '#fb6f4d'], [1, '#ffd27a'], + ]); + const sunY = h * 0.52; + ctx.globalCompositeOperation = 'lighter'; + this._glow(ctx, w * 0.5, sunY, w * 0.6, 'rgba(255,225,140,0.9)'); + this._glow(ctx, w * 0.5, sunY, w * 0.35, 'rgba(255,120,90,0.7)'); + ctx.globalCompositeOperation = 'source-over'; + // sun disc + const sun = ctx.createLinearGradient(0, sunY - 90, 0, sunY + 90); + sun.addColorStop(0, '#fff1a8'); + sun.addColorStop(1, '#ff5e8a'); + ctx.fillStyle = sun; + ctx.beginPath(); + ctx.arc(w * 0.5, sunY, 90, 0, Math.PI * 2); + ctx.fill(); + // horizon perspective grid + ctx.strokeStyle = 'rgba(255,45,149,0.55)'; + ctx.lineWidth = 1.5; + const horizon = h * 0.62; + for (let i = 1; i <= 9; i++) { + const y = horizon + Math.pow(i / 9, 2) * (h - horizon); + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } - // stars - const cols = [0xffffff, 0xd8b4fe, 0x93c5fd, 0xa855f7]; - for (let i = 0; i < 150; i++) { - g.fillStyle(Phaser.Utils.Array.GetRandom(cols), Phaser.Math.FloatBetween(0.35, 1)); - g.fillCircle(Phaser.Math.Between(0, S), Phaser.Math.Between(0, S), Phaser.Math.FloatBetween(0.6, 1.8)); + ctx.strokeStyle = 'rgba(0,229,255,0.4)'; + for (let i = -7; i <= 7; i++) { + ctx.beginPath(); + ctx.moveTo(w / 2 + i * 24, horizon); + ctx.lineTo(w / 2 + i * 120, h); + ctx.stroke(); } - // a few bright glows + this._stars(ctx, w, h, 50, ['#ffffff', '#ffe1c0'], 1.2, 0.4); + }); + + // 4) Ocean — sunlit depths + this._canvasTex('bg_ocean', (ctx, w, h) => { + this._vgrad(ctx, w, h, [ + [0, '#9fe9ff'], [0.22, '#34a7d8'], [0.5, '#0c5b8f'], + [0.78, '#06304f'], [1, '#021526'], + ]); + ctx.globalCompositeOperation = 'lighter'; + // caustic light beams from the surface for (let i = 0; i < 5; i++) { - const x = Phaser.Math.Between(0, S); const y = Phaser.Math.Between(0, S); - g.fillStyle(0xffffff, 0.18); g.fillCircle(x, y, 6); - g.fillStyle(0xffffff, 0.9); g.fillCircle(x, y, 1.6); + const x = w * (0.15 + i * 0.18); + this._ribbon(ctx, x, h * 0.18, w * 0.07, h * 0.45, 0.12 * (i % 2 ? 1 : -1), 'rgba(200,245,255,0.18)'); } + ctx.globalCompositeOperation = 'source-over'; + // bubbles + for (let i = 0; i < 40; i++) { + const x = Math.random() * w; const y = h * (0.35 + Math.random() * 0.65); + ctx.globalAlpha = 0.1 + Math.random() * 0.2; + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); ctx.arc(x, y, Math.random() * 3 + 1, 0, Math.PI * 2); ctx.fill(); + } + ctx.globalAlpha = 1; }); - // 4) Synthwave — neon horizontal grid - this._tex('bg_synthwave', S, S, (g) => { - g.fillStyle(0x0a0118, 1); - g.fillRect(0, 0, S, S); - // vertical faint lines - g.lineStyle(1, 0x3b0764, 0.5); - for (let x = 0; x <= S; x += 32) { g.moveTo(x, 0); g.lineTo(x, S); } - g.strokePath(); - // neon horizontal lines, alternating magenta/cyan - for (let y = 0; y <= S; y += 28) { - const cyan = (y / 28) % 2 === 0; - const col = cyan ? 0x00e5ff : 0xff2d95; - g.lineStyle(3, col, 0.10); g.beginPath(); g.moveTo(0, y); g.lineTo(S, y); g.strokePath(); - g.lineStyle(1, col, 0.55); g.beginPath(); g.moveTo(0, y); g.lineTo(S, y); g.strokePath(); + // 5) Dreamscape — soft pastel bokeh + this._canvasTex('bg_dream', (ctx, w, h) => { + this._vgrad(ctx, w, h, [ + [0, '#cdbdff'], [0.35, '#f2abfc'], [0.68, '#fbcfe8'], [1, '#ffd9b3'], + ]); + ctx.globalCompositeOperation = 'lighter'; + const orbs = ['rgba(255,255,255,0.35)', 'rgba(244,114,182,0.25)', 'rgba(167,139,250,0.25)', 'rgba(125,211,252,0.22)']; + for (let i = 0; i < 16; i++) { + const x = Math.random() * w; const y = Math.random() * h; + const r = 30 + Math.random() * 90; + this._glow(ctx, x, y, r, orbs[(Math.random() * orbs.length) | 0]); } + ctx.globalCompositeOperation = 'source-over'; + this._stars(ctx, w, h, 40, ['#ffffff']); }); - // 5) Circuit — PCB traces - this._tex('bg_circuit', S, S, (g) => { - g.fillStyle(0x04120e, 1); - g.fillRect(0, 0, S, S); - g.lineStyle(1, 0x0f766e, 0.45); - for (let i = 0; i <= S; i += 32) { g.moveTo(i, 0); g.lineTo(i, S); g.moveTo(0, i); g.lineTo(S, i); } - g.strokePath(); - // brighter trace accents (deterministic checker -> tiles) - g.lineStyle(2, 0x10b981, 0.5); - for (let x = 0; x < S; x += 32) { - for (let y = 0; y < S; y += 32) { - if (((x / 32) + (y / 32)) % 3 === 0) { g.beginPath(); g.moveTo(x, y); g.lineTo(x + 32, y); g.strokePath(); } - if (((x / 32) + (y / 32)) % 4 === 0) { g.beginPath(); g.moveTo(x, y); g.lineTo(x, y + 32); g.strokePath(); } - } - } - // solder nodes - g.fillStyle(0x34d399, 0.8); - for (let x = 0; x <= S; x += 32) { - for (let y = 0; y <= S; y += 32) { - if (((x + y) / 32) % 2 === 0) g.fillCircle(x, y, 2); - } - } + // 6) Monad — signature purple cosmos + this._canvasTex('bg_monad', (ctx, w, h) => { + this._vgrad(ctx, w, h, [[0, '#1a0533'], [0.5, '#2e0a52'], [1, '#0d0020']]); + ctx.globalCompositeOperation = 'screen'; + this._glow(ctx, w * 0.3, h * 0.24, w * 0.8, 'rgba(192,38,211,0.7)'); + this._glow(ctx, w * 0.78, h * 0.58, w * 0.75, 'rgba(124,58,237,0.8)'); + this._glow(ctx, w * 0.5, h * 0.92, w * 0.7, 'rgba(219,39,119,0.5)'); + this._glow(ctx, w * 0.5, h * 0.5, w * 0.55, 'rgba(168,85,247,0.25)'); + ctx.globalCompositeOperation = 'source-over'; + this._stars(ctx, w, h, 180, ['#ffffff', '#d8b4fe', '#f0abfc']); }); - - // 6) Void — minimal dark - this._tex('bg_void', S, S, (g) => { - g.fillStyle(0x060010, 1); - g.fillRect(0, 0, S, S); - g.fillStyle(0x1b1036, 0.6); - for (let x = 24; x < S; x += 48) { - for (let y = 24; y < S; y += 48) g.fillCircle(x, y, 1); - } - }); - } - - _hex(g, cx, cy, r) { - g.beginPath(); - for (let i = 0; i < 6; i++) { - const a = Math.PI / 180 * (60 * i - 90); - const px = cx + r * Math.cos(a); - const py = cy + r * Math.sin(a); - if (i === 0) g.moveTo(px, py); else g.lineTo(px, py); - } - g.closePath(); - g.strokePath(); } createParticleTextures() { diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index 90df5f9..c181291 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -122,6 +122,9 @@ export class GameScene extends Scene { this.player.update(this.cursors, this.wasd, this.touchLeft, this.touchRight, time, delta); + // Background is a fixed full-screen image (no tilePositionY scroll — the + // gradient would otherwise wrap with a visible seam). + // Cap downward speed so the player can never tunnel through a thin platform // in a single physics step (does not affect upward boost velocities). if (this.player.body.velocity.y > PHYSICS.maxFallSpeed) { @@ -134,8 +137,6 @@ export class GameScene extends Scene { this.cameras.main.scrollY = targetScrollY; } - this.bg.tilePositionY = Math.round(this.cameras.main.scrollY * 0.3); - this.minScrollY = Math.min(this.minScrollY, this.cameras.main.scrollY); const killLine = this.minScrollY + GAME_HEIGHT; diff --git a/src/scenes/MenuScene.js b/src/scenes/MenuScene.js index 265d050..143790e 100644 --- a/src/scenes/MenuScene.js +++ b/src/scenes/MenuScene.js @@ -295,7 +295,7 @@ export class MenuScene extends Scene { }).setOrigin(0.5)); let bgIndex = Math.max(0, BACKGROUNDS.findIndex((b) => b.id === currentBackground().id)); - const preview = this.add.tileSprite(cx, cy - 8, 280, 90, BACKGROUNDS[bgIndex].texture).setDepth(102); + const preview = this.add.image(cx, cy - 8, BACKGROUNDS[bgIndex].texture).setDisplaySize(280, 96).setDepth(102); m.elements.push(preview); const frame = this.add.rectangle(cx, cy - 8, 284, 94).setStrokeStyle(2, 0xa855f7); m.add(frame); @@ -309,6 +309,7 @@ export class MenuScene extends Scene { const bg = BACKGROUNDS[bgIndex]; setBackground(bg.id); preview.setTexture(bg.texture); + preview.setDisplaySize(280, 96); bgName.setText(bg.name.toUpperCase()); if (this.menuBg) this.menuBg.setTexture(bg.texture); sound.click();