import { CanvasSource, Container, defaultFilterVert, Filter, GlProgram, Sprite, Texture, UniformGroup } from 'pixi.js'; import { DREAM_DESAT, DREAM_GLOW, DREAM_LIFT, DREAM_RADIUS, DREAM_SOFT, DREAM_TINT, DUST_COUNT } from '../config'; // The dreamy look: a soft-focus glow over the whole world, a pastel grade, and sugar dust // floating in the air. Purely cosmetic. const fragment = /* glsl */ ` in vec2 vTextureCoord; out vec4 finalColor; uniform sampler2D uTexture; uniform highp vec4 uInputSize; uniform highp vec4 uInputClamp; uniform float uGlow; uniform float uSoft; uniform float uRadius; uniform float uDesat; uniform float uLift; uniform vec3 uTint; void main() { vec4 base = texture(uTexture, vTextureCoord); // Wide soft blur: 16 taps on a golden-angle spiral. vec4 sum = vec4(0.0); float wsum = 0.0; for (int i = 0; i < 16; i++) { float fi = float(i); float r = sqrt((fi + 0.5) / 16.0) * uRadius; float a = fi * 2.39996; vec2 uv = clamp(vTextureCoord + vec2(cos(a), sin(a)) * r * uInputSize.zw, uInputClamp.xy, uInputClamp.zw); float w = 1.0 - fi / 24.0; sum += texture(uTexture, uv) * w; wsum += w; } vec3 blur = (sum / wsum).rgb; vec3 c = base.rgb; // Bloom: bright areas bleed light around them (screen blend of the blur). float lum = dot(blur, vec3(0.299, 0.587, 0.114)); vec3 glow = 1.0 - (1.0 - c) * (1.0 - blur * smoothstep(0.45, 1.0, lum)); c = mix(c, glow, uGlow); // Soft focus: a veil of the blur everywhere, so every edge melts a little. c = mix(c, max(c, blur), uSoft); // Pastel grade: a little less saturated, shadows lifted toward a warm lilac. float l = dot(c, vec3(0.299, 0.587, 0.114)); c = mix(c, vec3(l), uDesat); c = c * (1.0 - uLift) + uTint * uLift * base.a; finalColor = vec4(c, base.a); } `; /** * The world is rendered into the filter at `resolution` (screen density × DREAM_SUPERSAMPLE) and * scaled back down: supersampling smooths every edge, vector shapes included. */ export function makeDreamFilter(resolution: number): Filter { const t = DREAM_TINT; return new Filter({ glProgram: GlProgram.from({ vertex: defaultFilterVert, fragment, name: 'dream-filter' }), resources: { dreamUniforms: new UniformGroup({ uGlow: { value: DREAM_GLOW, type: 'f32' }, uSoft: { value: DREAM_SOFT, type: 'f32' }, uRadius: { value: DREAM_RADIUS * resolution, type: 'f32' }, uDesat: { value: DREAM_DESAT, type: 'f32' }, uLift: { value: DREAM_LIFT, type: 'f32' }, uTint: { value: new Float32Array([(t >> 16) / 255, ((t >> 8) & 0xff) / 255, (t & 0xff) / 255]), type: 'vec3' }, }), }, resolution, antialias: 'on', }); } /** A soft round glow, white in the middle. */ function glowTexture(): Texture { const size = 64; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d')!; const grad = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); grad.addColorStop(0, 'rgba(255,255,255,1)'); grad.addColorStop(0.25, 'rgba(255,250,235,0.55)'); grad.addColorStop(1, 'rgba(255,240,220,0)'); ctx.fillStyle = grad; ctx.fillRect(0, 0, size, size); return new Texture({ source: new CanvasSource({ resource: canvas }) }); } interface Mote { sprite: Sprite; x: number; y: number; phase: number; speed: number; size: number; } const DUST_COLORS = [0xffffff, 0xfff1b8, 0xffd1e0, 0xd8ecff, 0xe8dcff]; /** * Sugar dust drifting up through the air, twinkling. Two layers: small motes behind the * terrain, bigger out-of-focus ones in front. Uses Math.random: purely visual. */ export class SugarDust { readonly back = new Container(); readonly front = new Container(); private readonly motes: Mote[] = []; private time = 0; private width = 2048; private height = 1024; constructor() { const texture = glowTexture(); for (let i = 0; i < DUST_COUNT; i++) { const inFront = i % 4 === 0; const sprite = new Sprite(texture); sprite.anchor.set(0.5); sprite.blendMode = 'add'; sprite.tint = DUST_COLORS[i % DUST_COLORS.length]; const size = inFront ? 18 + Math.random() * 26 : 5 + Math.random() * 9; sprite.width = size; sprite.height = size; (inFront ? this.front : this.back).addChild(sprite); this.motes.push({ sprite, x: Math.random(), y: Math.random(), phase: Math.random() * Math.PI * 2, speed: 6 + Math.random() * 14, size }); } } /** Spreads the dust over a map of this size. */ setArea(width: number, height: number): void { this.width = width; this.height = height; } update(dt: number): void { this.time += dt; for (const m of this.motes) { m.y -= (m.speed * dt) / this.height; if (m.y < -0.05) { m.y = 1.05; m.x = Math.random(); } const sway = Math.sin(this.time * 0.6 + m.phase) * 18; m.sprite.position.set(m.x * this.width + sway, m.y * this.height); const twinkle = 0.5 + 0.5 * Math.sin(this.time * 2.2 + m.phase * 3); m.sprite.alpha = (m.size > 15 ? 0.16 : 0.35) + twinkle * (m.size > 15 ? 0.12 : 0.45); } } }