| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- 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<f32>' },
- }),
- },
- 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);
- }
- }
- }
|