Dream.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { CanvasSource, Container, defaultFilterVert, Filter, GlProgram, Sprite, Texture, UniformGroup } from 'pixi.js';
  2. import { DREAM_DESAT, DREAM_GLOW, DREAM_LIFT, DREAM_RADIUS, DREAM_SOFT, DREAM_TINT, DUST_COUNT } from '../config';
  3. // The dreamy look: a soft-focus glow over the whole world, a pastel grade, and sugar dust
  4. // floating in the air. Purely cosmetic.
  5. const fragment = /* glsl */ `
  6. in vec2 vTextureCoord;
  7. out vec4 finalColor;
  8. uniform sampler2D uTexture;
  9. uniform highp vec4 uInputSize;
  10. uniform highp vec4 uInputClamp;
  11. uniform float uGlow;
  12. uniform float uSoft;
  13. uniform float uRadius;
  14. uniform float uDesat;
  15. uniform float uLift;
  16. uniform vec3 uTint;
  17. void main() {
  18. vec4 base = texture(uTexture, vTextureCoord);
  19. // Wide soft blur: 16 taps on a golden-angle spiral.
  20. vec4 sum = vec4(0.0);
  21. float wsum = 0.0;
  22. for (int i = 0; i < 16; i++) {
  23. float fi = float(i);
  24. float r = sqrt((fi + 0.5) / 16.0) * uRadius;
  25. float a = fi * 2.39996;
  26. vec2 uv = clamp(vTextureCoord + vec2(cos(a), sin(a)) * r * uInputSize.zw, uInputClamp.xy, uInputClamp.zw);
  27. float w = 1.0 - fi / 24.0;
  28. sum += texture(uTexture, uv) * w;
  29. wsum += w;
  30. }
  31. vec3 blur = (sum / wsum).rgb;
  32. vec3 c = base.rgb;
  33. // Bloom: bright areas bleed light around them (screen blend of the blur).
  34. float lum = dot(blur, vec3(0.299, 0.587, 0.114));
  35. vec3 glow = 1.0 - (1.0 - c) * (1.0 - blur * smoothstep(0.45, 1.0, lum));
  36. c = mix(c, glow, uGlow);
  37. // Soft focus: a veil of the blur everywhere, so every edge melts a little.
  38. c = mix(c, max(c, blur), uSoft);
  39. // Pastel grade: a little less saturated, shadows lifted toward a warm lilac.
  40. float l = dot(c, vec3(0.299, 0.587, 0.114));
  41. c = mix(c, vec3(l), uDesat);
  42. c = c * (1.0 - uLift) + uTint * uLift * base.a;
  43. finalColor = vec4(c, base.a);
  44. }
  45. `;
  46. /**
  47. * The world is rendered into the filter at `resolution` (screen density × DREAM_SUPERSAMPLE) and
  48. * scaled back down: supersampling smooths every edge, vector shapes included.
  49. */
  50. export function makeDreamFilter(resolution: number): Filter {
  51. const t = DREAM_TINT;
  52. return new Filter({
  53. glProgram: GlProgram.from({ vertex: defaultFilterVert, fragment, name: 'dream-filter' }),
  54. resources: {
  55. dreamUniforms: new UniformGroup({
  56. uGlow: { value: DREAM_GLOW, type: 'f32' },
  57. uSoft: { value: DREAM_SOFT, type: 'f32' },
  58. uRadius: { value: DREAM_RADIUS * resolution, type: 'f32' },
  59. uDesat: { value: DREAM_DESAT, type: 'f32' },
  60. uLift: { value: DREAM_LIFT, type: 'f32' },
  61. uTint: { value: new Float32Array([(t >> 16) / 255, ((t >> 8) & 0xff) / 255, (t & 0xff) / 255]), type: 'vec3<f32>' },
  62. }),
  63. },
  64. resolution,
  65. antialias: 'on',
  66. });
  67. }
  68. /** A soft round glow, white in the middle. */
  69. function glowTexture(): Texture {
  70. const size = 64;
  71. const canvas = document.createElement('canvas');
  72. canvas.width = size;
  73. canvas.height = size;
  74. const ctx = canvas.getContext('2d')!;
  75. const grad = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
  76. grad.addColorStop(0, 'rgba(255,255,255,1)');
  77. grad.addColorStop(0.25, 'rgba(255,250,235,0.55)');
  78. grad.addColorStop(1, 'rgba(255,240,220,0)');
  79. ctx.fillStyle = grad;
  80. ctx.fillRect(0, 0, size, size);
  81. return new Texture({ source: new CanvasSource({ resource: canvas }) });
  82. }
  83. interface Mote {
  84. sprite: Sprite;
  85. x: number;
  86. y: number;
  87. phase: number;
  88. speed: number;
  89. size: number;
  90. }
  91. const DUST_COLORS = [0xffffff, 0xfff1b8, 0xffd1e0, 0xd8ecff, 0xe8dcff];
  92. /**
  93. * Sugar dust drifting up through the air, twinkling. Two layers: small motes behind the
  94. * terrain, bigger out-of-focus ones in front. Uses Math.random: purely visual.
  95. */
  96. export class SugarDust {
  97. readonly back = new Container();
  98. readonly front = new Container();
  99. private readonly motes: Mote[] = [];
  100. private time = 0;
  101. private width = 2048;
  102. private height = 1024;
  103. constructor() {
  104. const texture = glowTexture();
  105. for (let i = 0; i < DUST_COUNT; i++) {
  106. const inFront = i % 4 === 0;
  107. const sprite = new Sprite(texture);
  108. sprite.anchor.set(0.5);
  109. sprite.blendMode = 'add';
  110. sprite.tint = DUST_COLORS[i % DUST_COLORS.length];
  111. const size = inFront ? 18 + Math.random() * 26 : 5 + Math.random() * 9;
  112. sprite.width = size;
  113. sprite.height = size;
  114. (inFront ? this.front : this.back).addChild(sprite);
  115. this.motes.push({ sprite, x: Math.random(), y: Math.random(), phase: Math.random() * Math.PI * 2, speed: 6 + Math.random() * 14, size });
  116. }
  117. }
  118. /** Spreads the dust over a map of this size. */
  119. setArea(width: number, height: number): void {
  120. this.width = width;
  121. this.height = height;
  122. }
  123. update(dt: number): void {
  124. this.time += dt;
  125. for (const m of this.motes) {
  126. m.y -= (m.speed * dt) / this.height;
  127. if (m.y < -0.05) {
  128. m.y = 1.05;
  129. m.x = Math.random();
  130. }
  131. const sway = Math.sin(this.time * 0.6 + m.phase) * 18;
  132. m.sprite.position.set(m.x * this.width + sway, m.y * this.height);
  133. const twinkle = 0.5 + 0.5 * Math.sin(this.time * 2.2 + m.phase * 3);
  134. m.sprite.alpha = (m.size > 15 ? 0.16 : 0.35) + twinkle * (m.size > 15 ? 0.12 : 0.45);
  135. }
  136. }
  137. }