@wolsok/thanos 18.3.2 → 18.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +23 -0
  2. package/esm2022/lib/canvas-particle-renderer.js +31 -24
  3. package/esm2022/lib/canvas-particle-renderer.js.map +1 -1
  4. package/esm2022/lib/capture-scale.js +9 -6
  5. package/esm2022/lib/capture-scale.js.map +1 -1
  6. package/esm2022/lib/choose-particle-renderer.js +4 -3
  7. package/esm2022/lib/choose-particle-renderer.js.map +1 -1
  8. package/esm2022/lib/create-ws-thanos-options.js +2 -0
  9. package/esm2022/lib/create-ws-thanos-options.js.map +1 -1
  10. package/esm2022/lib/flow-field.js +58 -0
  11. package/esm2022/lib/flow-field.js.map +1 -0
  12. package/esm2022/lib/frame-rate.js +13 -0
  13. package/esm2022/lib/frame-rate.js.map +1 -0
  14. package/esm2022/lib/particle-renderer.js.map +1 -1
  15. package/esm2022/lib/particles.js +20 -10
  16. package/esm2022/lib/particles.js.map +1 -1
  17. package/esm2022/lib/simplex-noise.js +8 -4
  18. package/esm2022/lib/simplex-noise.js.map +1 -1
  19. package/esm2022/lib/vaporizing-front.js +32 -9
  20. package/esm2022/lib/vaporizing-front.js.map +1 -1
  21. package/esm2022/lib/webgl/webgl-particle-renderer.js +67 -9
  22. package/esm2022/lib/webgl/webgl-particle-renderer.js.map +1 -1
  23. package/esm2022/lib/webgl/webgl-shaders.js +50 -16
  24. package/esm2022/lib/webgl/webgl-shaders.js.map +1 -1
  25. package/esm2022/lib/wind.js +55 -0
  26. package/esm2022/lib/wind.js.map +1 -0
  27. package/esm2022/lib/ws-thanos.options.js.map +1 -1
  28. package/esm2022/lib/ws-thanos.service.js +47 -26
  29. package/esm2022/lib/ws-thanos.service.js.map +1 -1
  30. package/lib/canvas-particle-renderer.d.ts +9 -7
  31. package/lib/capture-scale.d.ts +6 -5
  32. package/lib/choose-particle-renderer.d.ts +3 -2
  33. package/lib/flow-field.d.ts +24 -0
  34. package/lib/frame-rate.d.ts +10 -0
  35. package/lib/particle-renderer.d.ts +6 -0
  36. package/lib/particles.d.ts +14 -6
  37. package/lib/vaporizing-front.d.ts +26 -4
  38. package/lib/webgl/webgl-particle-renderer.d.ts +3 -2
  39. package/lib/webgl/webgl-shaders.d.ts +7 -0
  40. package/lib/wind.d.ts +40 -0
  41. package/lib/ws-thanos.options.d.ts +6 -0
  42. package/lib/ws-thanos.service.d.ts +2 -1
  43. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"webgl-shaders.js","sourceRoot":"","sources":["../../../../../../../libs/public/ws-thanos/src/lib/webgl/webgl-shaders.ts"],"names":[],"mappings":"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","sourcesContent":["import { MIN_PARTICLE_ALPHA } from '../particles';\nimport { FLOW_FIELD, FRONT_FRAY } from '../vaporizing-front';\nimport { WsThanosCrumble } from '../ws-thanos.options';\nimport { SIMPLEX_NOISE_3D } from './simplex-noise.glsl';\n\n/** attribute locations shared by all programs */\nexport const AttributeLocation = {\n POSITION: 0,\n VELOCITY: 1,\n ACCELERATION: 2,\n ALPHA: 3,\n COLOR: 4,\n RELEASED_AT: 5,\n CRACK: 6,\n CELL_RANDOM: 7,\n} as const;\n\n/** crumble mode as passed to the shaders */\nexport const CRUMBLE_MODE: Record<WsThanosCrumble, number> = {\n dust: 0,\n cracks: 1,\n shards: 2,\n chunks: 3,\n};\n\nexport const UPDATE_FEEDBACK_VARYINGS = [\n 'vPosition',\n 'vVelocity',\n 'vAcceleration',\n 'vAlpha',\n 'vReleasedAt',\n];\n\nexport const CRACK_FEEDBACK_VARYINGS = ['vCrack', 'vCellRandom'];\n\n/** floats per particle written by the crack program */\nexport const CRACK_LENGTH = 5;\n\nconst L = AttributeLocation;\n\n/** GLSL float literal */\nconst f = (value: number): string =>\n Number.isInteger(value) ? value.toFixed(1) : String(value);\n\nconst IS_VISIBLE_INSIDE = /* glsl */ `\nbool isVisibleInside(vec2 position, float alpha, vec2 size) {\n return position.x <= size.x && position.x >= 0.0 &&\n position.y <= size.y && position.y >= 0.0 &&\n alpha >= ${f(MIN_PARTICLE_ALPHA)};\n}\n`;\n\nconst HASH = /* glsl */ `\nuint hash(uint x) {\n x ^= x >> 16;\n x *= 0x7feb352du;\n x ^= x >> 15;\n x *= 0x846ca68bu;\n x ^= x >> 16;\n return x;\n}\n\nfloat toUnit(uint x) {\n return float(x) / 4294967295.0;\n}\n`;\n\n/**\n * Shards shift and tilt around their center while uShardGrow rises.\n * aCrack: shard center (xy), distance to the next coarse (z) and fine (w) crack, in device pixels.\n */\nconst SHARD_OFFSET = /* glsl */ `\nvec2 shardOffset(vec2 position, vec4 crack, float cellRandom) {\n if (uShardGrow == 0.0) {\n return vec2(0.0);\n }\n float angle = (cellRandom - 0.5) * 0.14 * uShardGrow;\n vec2 relative = position - crack.xy;\n vec2 rotated = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * relative;\n float direction = fract(cellRandom * 7.31) * 6.2831853;\n vec2 drift = vec2(cos(direction), sin(direction) * 0.6 - 0.4) * 5.0 * uPixelScale * uShardGrow;\n return rotated - relative + drift;\n}\n`;\n\n/** runs once: which shard each particle belongs to and how far it is from the next crack */\nexport const CRACK_VERTEX_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = ${L.POSITION}) in vec2 aPosition;\n\nuniform float uCellSize;\nuniform float uSeed;\n\nout vec4 vCrack;\nout float vCellRandom;\n\n${HASH}\n\nvec2 cellRandom(vec2 cell, uint saltedSeed) {\n uvec2 c = uvec2(ivec2(cell) + 32768);\n uint h = hash(c.x ^ hash(c.y ^ saltedSeed));\n return vec2(toUnit(h), toUnit(hash(h)));\n}\n\n/** voronoi cell of p: center (xy), distance to the cell border (z), random (w), in cell units */\nvec4 voronoi(vec2 p, uint salt) {\n uint saltedSeed = hash(floatBitsToUint(uSeed) ^ salt);\n vec2 n = floor(p);\n vec2 f = fract(p);\n vec2 nearestCell = vec2(0.0);\n vec2 nearest = vec2(0.0);\n float nearestDistance = 8.0;\n for (int j = -1; j <= 1; j++) {\n for (int i = -1; i <= 1; i++) {\n vec2 g = vec2(float(i), float(j));\n vec2 r = g + cellRandom(n + g, saltedSeed) - f;\n float d = dot(r, r);\n if (d < nearestDistance) {\n nearestDistance = d;\n nearest = r;\n nearestCell = g;\n }\n }\n }\n // exact distance to the border between the nearest and its neighbour cells\n float border = 8.0;\n for (int j = -2; j <= 2; j++) {\n for (int i = -2; i <= 2; i++) {\n vec2 g = nearestCell + vec2(float(i), float(j));\n vec2 r = g + cellRandom(n + g, saltedSeed) - f;\n vec2 between = r - nearest;\n if (dot(between, between) > 0.00001) {\n border = min(border, dot(0.5 * (nearest + r), normalize(between)));\n }\n }\n }\n uint randomSeed = hash(floatBitsToUint(uSeed) ^ (salt + 7u));\n return vec4(p + nearest, border, cellRandom(n + nearestCell, randomSeed).x);\n}\n\nvoid main() {\n vec4 coarse = voronoi(aPosition / uCellSize, 1u);\n float fineSize = uCellSize * 0.42;\n vec4 fine = voronoi(aPosition / fineSize, 2u);\n vCrack = vec4(coarse.xy * uCellSize, coarse.z * uCellSize, fine.z * fineSize);\n vCellRandom = coarse.w;\n}\n`;\n\n/** moves the particles, the GPU port of updateParticlesOnCpu plus the crumble modes */\nexport const UPDATE_VERTEX_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = ${L.POSITION}) in vec2 aPosition;\nlayout(location = ${L.VELOCITY}) in vec2 aVelocity;\nlayout(location = ${L.ACCELERATION}) in vec2 aAcceleration;\nlayout(location = ${L.ALPHA}) in float aAlpha;\nlayout(location = ${L.RELEASED_AT}) in float aReleasedAt;\nlayout(location = ${L.CRACK}) in vec4 aCrack;\nlayout(location = ${L.CELL_RANDOM}) in float aCellRandom;\n\nuniform vec2 uSize;\nuniform float uDeltaTSec;\nuniform float uAnimationT;\nuniform float uParticleAcceleration;\nuniform float uSeed;\nuniform int uCrumble;\nuniform float uPixelScale;\n/** see vaporizingFront() */\nuniform float uFrontTime;\nuniform float uFrontRadiusPow;\nuniform float uFade;\n/** see crumbleShape(), in device pixels */\nuniform float uCoarseCrack;\nuniform float uFineCrack;\nuniform float uShardGrow;\n\nout vec2 vPosition;\nout vec2 vVelocity;\nout vec2 vAcceleration;\nout float vAlpha;\n/** 0 while attached, < 0 while a chunk falls, > 0 once it is dust */\nout float vReleasedAt;\n\nconst int CHUNKS = ${CRUMBLE_MODE.chunks};\n/** part of the animation a chunk flies before it crumbles */\nconst float CHUNK_LIFE = 0.12;\n/** weak, the effect canvas ends at the bottom of the element, so chunks jump up and arc */\nconst float CHUNK_GRAVITY = 4.0;\n\n${IS_VISIBLE_INSIDE}\n${SIMPLEX_NOISE_3D}\n${HASH}\n${SHARD_OFFSET}\n\n/** simplex noise mapped to [0, 1] like SimplexNoise({ min: 0 }) */\nfloat noise01(vec3 position) {\n return snoise(position * ${f(FLOW_FIELD.frequency)}) * 0.5 + 0.5;\n}\n\n/** random number in [0, 1] per particle, frame and salt */\nfloat random(uint salt) {\n uint frame = floatBitsToUint(uAnimationT) ^ floatBitsToUint(uSeed);\n return toUnit(hash(uint(gl_VertexID) ^ hash(frame ^ salt)));\n}\n\nvec2 dustAcceleration() {\n return vec2(random(1u), -random(2u));\n}\n\n/** has the vaporizing front passed this position, see isBehindFront() */\nbool isBehindFront(vec2 position) {\n float pXLength = position.x + mod(position.x, uDeltaTSec) * 0.5;\n float pYLength = uSize.y - position.y;\n pXLength += sin((pXLength / ${f(FRONT_FRAY.xWave)} + ${f(FRONT_FRAY.xPhase)}) * uFrontTime\n + uSeed * ${f(FRONT_FRAY.xSeed)}) * ${f(FRONT_FRAY.xAmplitude)};\n pYLength += cos((pYLength / ${f(FRONT_FRAY.yWave)} + ${f(FRONT_FRAY.yPhase)}) * uFrontTime\n + uSeed * ${f(FRONT_FRAY.ySeed)}) * ${f(FRONT_FRAY.yAmplitude)};\n return pXLength * pXLength + pYLength * pYLength > uFrontRadiusPow;\n}\n\nvoid main() {\n vPosition = aPosition;\n vVelocity = aVelocity;\n vAcceleration = aAcceleration;\n vAlpha = aAlpha;\n vReleasedAt = aReleasedAt;\n\n // only update particles that are inside view and visible\n if (!isVisibleInside(aPosition, aAlpha, uSize)) {\n return;\n }\n\n float releaseT = max(uAnimationT, 0.0001);\n\n if (aReleasedAt == 0.0) {\n bool inCrack = aCrack.z < uCoarseCrack || aCrack.w < uFineCrack;\n // chunks break off as a whole, so the shard center decides\n vec2 frontPosition = uCrumble == CHUNKS ? aCrack.xy : aPosition;\n\n if (inCrack || isBehindFront(frontPosition)) {\n vPosition += shardOffset(aPosition, aCrack, aCellRandom);\n vReleasedAt = releaseT;\n if (inCrack) {\n // the crack crumbles to dust that trickles out gently\n vAcceleration = dustAcceleration() * 0.3;\n } else if (uCrumble == CHUNKS) {\n // the whole chunk shares velocity and gravity, so it flies in one piece\n float side = fract(aCellRandom * 13.7) - 0.5;\n vVelocity = vec2(side * 90.0, -60.0 - aCellRandom * 70.0) * uPixelScale;\n vAcceleration = vec2(0.0, CHUNK_GRAVITY);\n vReleasedAt = -releaseT;\n } else {\n vAcceleration = dustAcceleration();\n }\n }\n } else if (aReleasedAt < 0.0) {\n // a flying chunk crumbles to dust after a while\n if (uAnimationT + aReleasedAt > CHUNK_LIFE + aCellRandom * 0.08) {\n vAcceleration = dustAcceleration();\n vReleasedAt = releaseT;\n }\n } else {\n // flow along the noise velocity field\n vAcceleration += vec2(\n noise01(vec3(aPosition.x, aPosition.y, uSeed + ${f(FLOW_FIELD.xSeedOffset)})),\n -noise01(vec3(aPosition.x, uSeed / ${f(FLOW_FIELD.ySeedDivisor)}, aPosition.y))\n );\n }\n\n vVelocity += vAcceleration * uParticleAcceleration * uDeltaTSec;\n vPosition += vVelocity * uDeltaTSec;\n vAlpha *= uFade;\n}\n`;\n\n/** transform feedback only, nothing is rasterized */\nexport const FEEDBACK_FRAGMENT_SHADER = /* glsl */ `#version 300 es\nprecision mediump float;\nout vec4 outColor;\nvoid main() {\n outColor = vec4(0.0);\n}\n`;\n\n/** draws every particle as one device pixel */\nexport const DRAW_VERTEX_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = ${L.POSITION}) in vec2 aPosition;\nlayout(location = ${L.ALPHA}) in float aAlpha;\nlayout(location = ${L.COLOR}) in vec4 aColor;\nlayout(location = ${L.RELEASED_AT}) in float aReleasedAt;\nlayout(location = ${L.CRACK}) in vec4 aCrack;\nlayout(location = ${L.CELL_RANDOM}) in float aCellRandom;\n\nuniform vec2 uSize;\nuniform float uPixelScale;\nuniform float uShardGrow;\n\nout vec4 vColor;\n\n${IS_VISIBLE_INSIDE}\n${SHARD_OFFSET}\n\nvoid main() {\n vec2 position = aPosition;\n if (aReleasedAt == 0.0) {\n // attached shards shift and tilt before they break off\n position += shardOffset(aPosition, aCrack, aCellRandom);\n }\n if (!isVisibleInside(position, aAlpha, uSize)) {\n // move outside of clip space\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0);\n gl_PointSize = 1.0;\n vColor = vec4(0.0);\n return;\n }\n vec2 clip = (floor(position) + 0.5) / uSize * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n gl_PointSize = 1.0;\n float alpha = aAlpha / 255.0;\n // premultiplied alpha\n vColor = vec4(aColor.rgb * alpha, alpha);\n}\n`;\n\nexport const DRAW_FRAGMENT_SHADER = /* glsl */ `#version 300 es\nprecision mediump float;\nin vec4 vColor;\nout vec4 outColor;\nvoid main() {\n outColor = vColor;\n}\n`;\n"]}
1
+ {"version":3,"file":"webgl-shaders.js","sourceRoot":"","sources":["../../../../../../../libs/public/ws-thanos/src/lib/webgl/webgl-shaders.ts"],"names":[],"mappings":"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","sourcesContent":["import { FRAME_RATE } from '../frame-rate';\nimport { MIN_PARTICLE_ALPHA } from '../particles';\nimport { FLOW_FIELD, FRONT_FRAY } from '../vaporizing-front';\nimport { WsThanosCrumble } from '../ws-thanos.options';\nimport { SIMPLEX_NOISE_3D } from './simplex-noise.glsl';\n\n/** attribute locations shared by all programs */\nexport const AttributeLocation = {\n POSITION: 0,\n VELOCITY: 1,\n ACCELERATION: 2,\n ALPHA: 3,\n COLOR: 4,\n RELEASED_AT: 5,\n CRACK: 6,\n CELL_RANDOM: 7,\n} as const;\n\n/** crumble mode as passed to the shaders */\nexport const CRUMBLE_MODE: Record<WsThanosCrumble, number> = {\n dust: 0,\n cracks: 1,\n shards: 2,\n chunks: 3,\n};\n\nexport const UPDATE_FEEDBACK_VARYINGS = [\n 'vPosition',\n 'vVelocity',\n 'vAcceleration',\n 'vAlpha',\n 'vReleasedAt',\n];\n\nexport const CRACK_FEEDBACK_VARYINGS = ['vCrack', 'vCellRandom'];\n\n/** floats per particle written by the crack program */\nexport const CRACK_LENGTH = 5;\n\nconst L = AttributeLocation;\n\n/** GLSL float literal */\nconst f = (value: number): string =>\n Number.isInteger(value) ? value.toFixed(1) : String(value);\n\nconst IS_VISIBLE_INSIDE = /* glsl */ `\nbool isVisibleInside(vec2 position, float alpha, vec2 size) {\n return position.x <= size.x && position.x >= 0.0 &&\n position.y <= size.y && position.y >= 0.0 &&\n alpha >= ${f(MIN_PARTICLE_ALPHA)};\n}\n`;\n\nconst HASH = /* glsl */ `\nuint hash(uint x) {\n x ^= x >> 16;\n x *= 0x7feb352du;\n x ^= x >> 15;\n x *= 0x846ca68bu;\n x ^= x >> 16;\n return x;\n}\n\nfloat toUnit(uint x) {\n return float(x) / 4294967295.0;\n}\n`;\n\n/**\n * Shards shift and tilt around their center while uShardGrow rises.\n * aCrack: shard center (xy), distance to the next coarse (z) and fine (w) crack, in device pixels.\n */\nconst SHARD_OFFSET = /* glsl */ `\nvec2 shardOffset(vec2 position, vec4 crack, float cellRandom) {\n if (uShardGrow == 0.0) {\n return vec2(0.0);\n }\n float angle = (cellRandom - 0.5) * 0.14 * uShardGrow;\n vec2 relative = position - crack.xy;\n vec2 rotated = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * relative;\n float direction = fract(cellRandom * 7.31) * 6.2831853;\n vec2 drift = vec2(cos(direction), sin(direction) * 0.6 - 0.4) * 5.0 * uPixelScale * uShardGrow;\n return rotated - relative + drift;\n}\n`;\n\n/** runs once: which shard each particle belongs to and how far it is from the next crack */\nexport const CRACK_VERTEX_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = ${L.POSITION}) in vec2 aPosition;\n\nuniform float uCellSize;\nuniform float uSeed;\n\nout vec4 vCrack;\nout float vCellRandom;\n\n${HASH}\n\nvec2 cellRandom(vec2 cell, uint saltedSeed) {\n uvec2 c = uvec2(ivec2(cell) + 32768);\n uint h = hash(c.x ^ hash(c.y ^ saltedSeed));\n return vec2(toUnit(h), toUnit(hash(h)));\n}\n\n/** voronoi cell of p: center (xy), distance to the cell border (z), random (w), in cell units */\nvec4 voronoi(vec2 p, uint salt) {\n uint saltedSeed = hash(floatBitsToUint(uSeed) ^ salt);\n vec2 n = floor(p);\n vec2 f = fract(p);\n vec2 nearestCell = vec2(0.0);\n vec2 nearest = vec2(0.0);\n float nearestDistance = 8.0;\n for (int j = -1; j <= 1; j++) {\n for (int i = -1; i <= 1; i++) {\n vec2 g = vec2(float(i), float(j));\n vec2 r = g + cellRandom(n + g, saltedSeed) - f;\n float d = dot(r, r);\n if (d < nearestDistance) {\n nearestDistance = d;\n nearest = r;\n nearestCell = g;\n }\n }\n }\n // exact distance to the border between the nearest and its neighbour cells\n float border = 8.0;\n for (int j = -2; j <= 2; j++) {\n for (int i = -2; i <= 2; i++) {\n vec2 g = nearestCell + vec2(float(i), float(j));\n vec2 r = g + cellRandom(n + g, saltedSeed) - f;\n vec2 between = r - nearest;\n if (dot(between, between) > 0.00001) {\n border = min(border, dot(0.5 * (nearest + r), normalize(between)));\n }\n }\n }\n uint randomSeed = hash(floatBitsToUint(uSeed) ^ (salt + 7u));\n return vec4(p + nearest, border, cellRandom(n + nearestCell, randomSeed).x);\n}\n\nvoid main() {\n vec4 coarse = voronoi(aPosition / uCellSize, 1u);\n float fineSize = uCellSize * 0.42;\n vec4 fine = voronoi(aPosition / fineSize, 2u);\n vCrack = vec4(coarse.xy * uCellSize, coarse.z * uCellSize, fine.z * fineSize);\n vCellRandom = coarse.w;\n}\n`;\n\n/** moves the particles, the GPU port of updateParticlesOnCpu plus the crumble modes */\nexport const UPDATE_VERTEX_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = ${L.POSITION}) in vec2 aPosition;\nlayout(location = ${L.VELOCITY}) in vec2 aVelocity;\nlayout(location = ${L.ACCELERATION}) in vec2 aAcceleration;\nlayout(location = ${L.ALPHA}) in float aAlpha;\nlayout(location = ${L.RELEASED_AT}) in float aReleasedAt;\nlayout(location = ${L.CRACK}) in vec4 aCrack;\nlayout(location = ${L.CELL_RANDOM}) in float aCellRandom;\n\nuniform vec2 uSize;\nuniform float uDeltaTSec;\nuniform float uAnimationT;\nuniform float uParticleAcceleration;\nuniform float uSeed;\nuniform int uCrumble;\nuniform float uPixelScale;\n/** see vaporizingFront() */\nuniform float uFrontTime;\nuniform float uFrontRadiusPow;\nuniform float uFade;\n/** see crumbleShape(), in device pixels */\nuniform float uCoarseCrack;\nuniform float uFineCrack;\nuniform float uShardGrow;\n/** where the dust blows to, rotates the forces written for the default wind */\nuniform mat2 uWind;\nuniform vec2 uFrontOrigin;\n/** see FlowField, sampled once by FLOW_FIELD_FRAGMENT_SHADER over the effect canvas */\nuniform highp sampler2D uFlowField;\n\nout vec2 vPosition;\nout vec2 vVelocity;\nout vec2 vAcceleration;\nout float vAlpha;\n/** 0 while attached, < 0 while a chunk falls, > 0 once it is dust */\nout float vReleasedAt;\n\nconst int CHUNKS = ${CRUMBLE_MODE.chunks};\n/** part of the animation a chunk flies before it crumbles */\nconst float CHUNK_LIFE = 0.12;\n/** weak and always down, chunks jump with the wind and arc */\nconst float CHUNK_GRAVITY = 4.0;\n\n${IS_VISIBLE_INSIDE}\n${HASH}\n${SHARD_OFFSET}\n\n/** random number in [0, 1] per particle, frame and salt */\nfloat random(uint salt) {\n uint frame = floatBitsToUint(uAnimationT) ^ floatBitsToUint(uSeed);\n return toUnit(hash(uint(gl_VertexID) ^ hash(frame ^ salt)));\n}\n\nvec2 dustAcceleration() {\n return uWind * vec2(random(1u), -random(2u));\n}\n\n/** has the vaporizing front passed this position, see isBehindFront() */\nbool isBehindFront(vec2 position) {\n float pXLength = position.x - uFrontOrigin.x + mod(position.x, uDeltaTSec) * 0.5;\n float pYLength = uFrontOrigin.y - position.y;\n pXLength += sin((pXLength / ${f(FRONT_FRAY.xWave)} + ${f(FRONT_FRAY.xPhase)}) * uFrontTime\n + uSeed * ${f(FRONT_FRAY.xSeed)}) * ${f(FRONT_FRAY.xAmplitude)};\n pYLength += cos((pYLength / ${f(FRONT_FRAY.yWave)} + ${f(FRONT_FRAY.yPhase)}) * uFrontTime\n + uSeed * ${f(FRONT_FRAY.ySeed)}) * ${f(FRONT_FRAY.yAmplitude)};\n return pXLength * pXLength + pYLength * pYLength > uFrontRadiusPow;\n}\n\nvoid main() {\n vPosition = aPosition;\n vVelocity = aVelocity;\n vAcceleration = aAcceleration;\n vAlpha = aAlpha;\n vReleasedAt = aReleasedAt;\n\n // only update particles that are inside view and visible\n if (!isVisibleInside(aPosition, aAlpha, uSize)) {\n return;\n }\n\n float releaseT = max(uAnimationT, 0.0001);\n\n if (aReleasedAt == 0.0) {\n bool inCrack = aCrack.z < uCoarseCrack || aCrack.w < uFineCrack;\n // chunks break off as a whole, so the shard center decides\n vec2 frontPosition = uCrumble == CHUNKS ? aCrack.xy : aPosition;\n\n if (inCrack || isBehindFront(frontPosition)) {\n vPosition += shardOffset(aPosition, aCrack, aCellRandom);\n vReleasedAt = releaseT;\n if (inCrack) {\n // the crack crumbles to dust that trickles out gently\n vAcceleration = dustAcceleration() * 0.3;\n } else if (uCrumble == CHUNKS) {\n // the whole chunk shares velocity and gravity, so it flies in one piece\n float side = fract(aCellRandom * 13.7) - 0.5;\n vVelocity = uWind * vec2(side * 90.0, -60.0 - aCellRandom * 70.0) * uPixelScale;\n vAcceleration = vec2(0.0, CHUNK_GRAVITY);\n vReleasedAt = -releaseT;\n } else {\n vAcceleration = dustAcceleration();\n }\n }\n } else if (aReleasedAt < 0.0) {\n // a flying chunk crumbles to dust after a while\n if (uAnimationT + aReleasedAt > CHUNK_LIFE + aCellRandom * 0.08) {\n vAcceleration = dustAcceleration();\n vReleasedAt = releaseT;\n }\n } else {\n // flow along the noise velocity field, as much per second at any frame rate\n vec2 flow = textureLod(uFlowField, aPosition / uSize, 0.0).rg;\n vAcceleration += uWind * vec2(flow.x, -flow.y) * (uDeltaTSec * ${f(FRAME_RATE)});\n }\n\n vVelocity += vAcceleration * uParticleAcceleration * uDeltaTSec;\n vPosition += vVelocity * uDeltaTSec;\n vAlpha *= uFade;\n}\n`;\n\n/** one triangle that covers the whole viewport */\nexport const FULL_VIEWPORT_VERTEX_SHADER = /* glsl */ `#version 300 es\nvoid main() {\n vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);\n}\n`;\n\n/**\n * Runs once: the noise field released particles flow along, a texel covers uCell device pixels.\n * Red and green are the x and y flow, simplex noise mapped to [0, 1], the same noise as FlowField on the CPU.\n */\nexport const FLOW_FIELD_FRAGMENT_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\n\nuniform float uSeed;\nuniform vec2 uCell;\n\nout vec4 outColor;\n\n${SIMPLEX_NOISE_3D}\n\nfloat noise01(vec3 position) {\n return snoise(position * ${f(FLOW_FIELD.frequency)}) * 0.5 + 0.5;\n}\n\nvoid main() {\n // the texel centers, where linear filtering returns exactly this value\n vec2 position = gl_FragCoord.xy * uCell;\n outColor = vec4(\n noise01(vec3(position, uSeed + ${f(FLOW_FIELD.xSeedOffset)})),\n noise01(vec3(position.x, uSeed / ${f(FLOW_FIELD.ySeedDivisor)}, position.y)),\n 0.0,\n 1.0\n );\n}\n`;\n\n/** transform feedback only, nothing is rasterized */\nexport const FEEDBACK_FRAGMENT_SHADER = /* glsl */ `#version 300 es\nprecision mediump float;\nout vec4 outColor;\nvoid main() {\n outColor = vec4(0.0);\n}\n`;\n\n/** draws every particle as one device pixel */\nexport const DRAW_VERTEX_SHADER = /* glsl */ `#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = ${L.POSITION}) in vec2 aPosition;\nlayout(location = ${L.ALPHA}) in float aAlpha;\nlayout(location = ${L.COLOR}) in vec4 aColor;\nlayout(location = ${L.RELEASED_AT}) in float aReleasedAt;\nlayout(location = ${L.CRACK}) in vec4 aCrack;\nlayout(location = ${L.CELL_RANDOM}) in float aCellRandom;\n\nuniform vec2 uSize;\nuniform float uPixelScale;\nuniform float uShardGrow;\n\nout vec4 vColor;\n\n${IS_VISIBLE_INSIDE}\n${SHARD_OFFSET}\n\nvoid main() {\n vec2 position = aPosition;\n if (aReleasedAt == 0.0) {\n // attached shards shift and tilt before they break off\n position += shardOffset(aPosition, aCrack, aCellRandom);\n }\n if (!isVisibleInside(position, aAlpha, uSize)) {\n // move outside of clip space\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0);\n gl_PointSize = 1.0;\n vColor = vec4(0.0);\n return;\n }\n vec2 clip = (floor(position) + 0.5) / uSize * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n gl_PointSize = 1.0;\n float alpha = aAlpha / 255.0;\n // premultiplied alpha\n vColor = vec4(aColor.rgb * alpha, alpha);\n}\n`;\n\nexport const DRAW_FRAGMENT_SHADER = /* glsl */ `#version 300 es\nprecision mediump float;\nin vec4 vColor;\nout vec4 outColor;\nvoid main() {\n outColor = vColor;\n}\n`;\n"]}
@@ -0,0 +1,55 @@
1
+ /** the dust blows to the top right unless told otherwise */
2
+ export const DEFAULT_WIND_DIRECTION = 45;
3
+ /** how far the default wind carries the dust: one width to the right, four heights up */
4
+ const ROOM_X = 1;
5
+ const ROOM_Y = 4;
6
+ /** released dust flies within this many degrees of the wind direction */
7
+ const SPREAD = 45;
8
+ const toRadians = (degrees) => (degrees * Math.PI) / 180;
9
+ /** a direction that is no number blows the default way */
10
+ const validDegrees = (degrees) => Number.isFinite(degrees) ? degrees : DEFAULT_WIND_DIRECTION;
11
+ /**
12
+ * @param degrees direction the dust blows to, clockwise from up like a css gradient angle:
13
+ * 0 up, 90 right, 180 down, 270 left
14
+ */
15
+ export function windOf(degrees = DEFAULT_WIND_DIRECTION) {
16
+ const valid = validDegrees(degrees);
17
+ const direction = toRadians(valid);
18
+ const rotation = toRadians(valid - DEFAULT_WIND_DIRECTION);
19
+ return {
20
+ x: Math.sin(direction),
21
+ y: -Math.cos(direction),
22
+ cos: Math.cos(rotation),
23
+ sin: Math.sin(rotation),
24
+ };
25
+ }
26
+ /**
27
+ * The effect canvas reaches past the element where the dust can fly:
28
+ * for the default wind one width to the right and four heights up, like before there was a wind.
29
+ */
30
+ export function effectMargins(degrees = DEFAULT_WIND_DIRECTION) {
31
+ const from = validDegrees(degrees) - SPREAD;
32
+ // the dust flies into every direction between from and from + 2 * SPREAD
33
+ const reach = (side) => {
34
+ const toSide = (((side - from) % 360) + 360) % 360;
35
+ if (toSide <= 2 * SPREAD) {
36
+ return 1;
37
+ }
38
+ const nearest = Math.max(Math.cos(toRadians(side - from)), Math.cos(toRadians(side - from - 2 * SPREAD)));
39
+ // round away float noise, cos(90°) is not exactly 0
40
+ return Math.max(0, Math.round(nearest * 1e6) / 1e6);
41
+ };
42
+ return {
43
+ left: ROOM_X * reach(270),
44
+ right: ROOM_X * reach(90),
45
+ top: ROOM_Y * reach(0),
46
+ bottom: ROOM_Y * reach(180),
47
+ };
48
+ }
49
+ export function effectScale(margins) {
50
+ return {
51
+ width: 1 + margins.left + margins.right,
52
+ height: 1 + margins.top + margins.bottom,
53
+ };
54
+ }
55
+ //# sourceMappingURL=wind.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wind.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/wind.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AA0BzC,yFAAyF;AACzF,MAAM,MAAM,GAAG,CAAC,CAAC;AACjB,MAAM,MAAM,GAAG,CAAC,CAAC;AACjB,yEAAyE;AACzE,MAAM,MAAM,GAAG,EAAE,CAAC;AAElB,MAAM,SAAS,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;AAEzE,0DAA0D;AAC1D,MAAM,YAAY,GAAG,CAAC,OAAe,EAAU,EAAE,CAC/C,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;AAE9D;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,OAAO,GAAG,sBAAsB;IACrD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,GAAG,sBAAsB,CAAC,CAAC;IAC3D,OAAO;QACL,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;QACtB,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;QACvB,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;QACvB,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;KACxB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAO,GAAG,sBAAsB;IAC5D,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;IAC5C,yEAAyE;IACzE,MAAM,KAAK,GAAG,CAAC,IAAY,EAAU,EAAE;QACrC,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QACnD,IAAI,MAAM,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC;YACzB,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CACtB,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,EAChC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAC9C,CAAC;QACF,oDAAoD;QACpD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;IACtD,CAAC,CAAC;IACF,OAAO;QACL,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC;QACzB,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC;QACzB,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC;QACtB,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC;KAC5B,CAAC;AACJ,CAAC;AAQD,MAAM,UAAU,WAAW,CAAC,OAAsB;IAChD,OAAO;QACL,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,KAAK;QACvC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM;KACzC,CAAC;AACJ,CAAC","sourcesContent":["/** the dust blows to the top right unless told otherwise */\nexport const DEFAULT_WIND_DIRECTION = 45;\n\n/**\n * Where the dust blows to. The simulation is written for the default wind,\n * every force it applies is rotated by the difference to this one.\n */\nexport interface Wind {\n /** unit vector of the direction in screen coordinates, y points down */\n x: number;\n y: number;\n /** rotation from the default wind to this one */\n cos: number;\n sin: number;\n}\n\n/**\n * room around the element for the dust to fly in, in element widths (left, right)\n * and element heights (top, bottom)\n */\nexport interface EffectMargins {\n left: number;\n right: number;\n top: number;\n bottom: number;\n}\n\n/** how far the default wind carries the dust: one width to the right, four heights up */\nconst ROOM_X = 1;\nconst ROOM_Y = 4;\n/** released dust flies within this many degrees of the wind direction */\nconst SPREAD = 45;\n\nconst toRadians = (degrees: number): number => (degrees * Math.PI) / 180;\n\n/** a direction that is no number blows the default way */\nconst validDegrees = (degrees: number): number =>\n Number.isFinite(degrees) ? degrees : DEFAULT_WIND_DIRECTION;\n\n/**\n * @param degrees direction the dust blows to, clockwise from up like a css gradient angle:\n * 0 up, 90 right, 180 down, 270 left\n */\nexport function windOf(degrees = DEFAULT_WIND_DIRECTION): Wind {\n const valid = validDegrees(degrees);\n const direction = toRadians(valid);\n const rotation = toRadians(valid - DEFAULT_WIND_DIRECTION);\n return {\n x: Math.sin(direction),\n y: -Math.cos(direction),\n cos: Math.cos(rotation),\n sin: Math.sin(rotation),\n };\n}\n\n/**\n * The effect canvas reaches past the element where the dust can fly:\n * for the default wind one width to the right and four heights up, like before there was a wind.\n */\nexport function effectMargins(degrees = DEFAULT_WIND_DIRECTION): EffectMargins {\n const from = validDegrees(degrees) - SPREAD;\n // the dust flies into every direction between from and from + 2 * SPREAD\n const reach = (side: number): number => {\n const toSide = (((side - from) % 360) + 360) % 360;\n if (toSide <= 2 * SPREAD) {\n return 1;\n }\n const nearest = Math.max(\n Math.cos(toRadians(side - from)),\n Math.cos(toRadians(side - from - 2 * SPREAD))\n );\n // round away float noise, cos(90°) is not exactly 0\n return Math.max(0, Math.round(nearest * 1e6) / 1e6);\n };\n return {\n left: ROOM_X * reach(270),\n right: ROOM_X * reach(90),\n top: ROOM_Y * reach(0),\n bottom: ROOM_Y * reach(180),\n };\n}\n\n/** size of the effect canvas in element sizes */\nexport interface EffectScale {\n width: number;\n height: number;\n}\n\nexport function effectScale(margins: EffectMargins): EffectScale {\n return {\n width: 1 + margins.left + margins.right,\n height: 1 + margins.top + margins.bottom,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ws-thanos.options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,QAAQ;CACA,CAAC","sourcesContent":["/**\n * How the element breaks apart (needs WebGL2, the canvas fallback always uses 'dust'):\n * - dust: particles blow away along the vaporizing front\n * - cracks: cracks spread through the element before it turns to dust\n * - shards: cracks split the element into shards that shift and tilt before they turn to dust\n * - chunks: whole chunks break off, jump away and then crumble to dust\n */\nexport const WS_THANOS_CRUMBLES = [\n 'dust',\n 'cracks',\n 'shards',\n 'chunks',\n] as const;\n\n/** one of {@link WS_THANOS_CRUMBLES} */\nexport type WsThanosCrumble = (typeof WS_THANOS_CRUMBLES)[number];\n\nexport interface WsThanosOptions {\n animationLength: number;\n maxParticleCount: number;\n /**\n * the most pixels the element is captured with, every one a particle:\n * a big element is captured at a lower pixel ratio, below css pixels if needed.\n * Unlike maxParticleCount, which leaves out random pixels, the particles get coarser.\n * No limit when unset.\n */\n maxCapturePixels?: number;\n particleAcceleration: number;\n /** play a windy, sandy sound while vaporizing */\n sound: boolean;\n /** volume of the snap sound between 0 and 1 */\n soundVolume: number;\n /** how the element breaks apart */\n crumble: WsThanosCrumble;\n /** warm up the effect while the browser is idle, so the first snap does not stutter */\n preload: boolean;\n}\n"]}
1
+ {"version":3,"file":"ws-thanos.options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,QAAQ;CACA,CAAC","sourcesContent":["/**\n * How the element breaks apart (needs WebGL2, the canvas fallback always uses 'dust'):\n * - dust: particles blow away along the vaporizing front\n * - cracks: cracks spread through the element before it turns to dust\n * - shards: cracks split the element into shards that shift and tilt before they turn to dust\n * - chunks: whole chunks break off, jump away and then crumble to dust\n */\nexport const WS_THANOS_CRUMBLES = [\n 'dust',\n 'cracks',\n 'shards',\n 'chunks',\n] as const;\n\n/** one of {@link WS_THANOS_CRUMBLES} */\nexport type WsThanosCrumble = (typeof WS_THANOS_CRUMBLES)[number];\n\nexport interface WsThanosOptions {\n animationLength: number;\n maxParticleCount: number;\n /**\n * the most pixels the element is captured with, every one a particle:\n * a big element is captured at a lower pixel ratio, below css pixels if needed.\n * Unlike maxParticleCount, which leaves out random pixels, the particles get coarser.\n * No limit when unset.\n */\n maxCapturePixels?: number;\n particleAcceleration: number;\n /** play a windy, sandy sound while vaporizing */\n sound: boolean;\n /** volume of the snap sound between 0 and 1 */\n soundVolume: number;\n /** how the element breaks apart */\n crumble: WsThanosCrumble;\n /**\n * the direction the dust blows to in degrees, clockwise from up like a css gradient angle:\n * 0 up, 90 right, 180 down, 270 left. The vaporizing front sweeps against it.\n * 45, to the top right, when unset.\n */\n windDirection?: number;\n /** warm up the effect while the browser is idle, so the first snap does not stutter */\n preload: boolean;\n}\n"]}
@@ -1,15 +1,21 @@
1
1
  import { DOCUMENT, Injectable, inject } from '@angular/core';
2
2
  import { default as html2canvas } from 'html2canvas';
3
- import { animationFrameScheduler, defer, finalize, from, interval, map, switchMap, takeWhile, tap, timeInterval, } from 'rxjs';
4
- import { EFFECT_HEIGHT_SCALE, EFFECT_WIDTH_SCALE } from './capture-scale';
3
+ import { animationFrames, defer, finalize, from, map, switchMap, takeWhile, tap, } from 'rxjs';
5
4
  import { fitImagesForCapture } from './fit-images';
6
5
  import { chooseParticleRenderer, GpuContextBudget, } from './choose-particle-renderer';
7
6
  import { createParticles } from './particles';
8
7
  import { SnapSoundService } from './snap-sound.service';
8
+ import { frontGeometry } from './vaporizing-front';
9
+ import { effectMargins, effectScale, windOf, } from './wind';
9
10
  import { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';
10
11
  import * as i0 from "@angular/core";
11
12
  /** a short cross fade hides small differences between capture and element */
12
13
  const FULL_COVERAGE_FADE_OUT_MS = 200;
14
+ /**
15
+ * The longest step the simulation takes: after a stalled frame or a hidden tab
16
+ * the particles carry on instead of jumping away.
17
+ */
18
+ const MAX_FRAME_SEC = 0.1;
13
19
  /** resolves once the browser has nothing else to do */
14
20
  function idle(window) {
15
21
  return new Promise((resolve) => typeof window.requestIdleCallback === 'function'
@@ -59,7 +65,7 @@ export class WsThanosService {
59
65
  'background:linear-gradient(#fff,#888)',
60
66
  ].join(';');
61
67
  body.appendChild(elem);
62
- const { maxParticleCount, maxCapturePixels, particleAcceleration } = this.thanosOptions;
68
+ const { maxParticleCount, maxCapturePixels, particleAcceleration, windDirection, } = this.thanosOptions;
63
69
  let rendererChoice;
64
70
  let renderer;
65
71
  try {
@@ -72,6 +78,8 @@ export class WsThanosService {
72
78
  ({ renderer } = this.createRenderer(rendererChoice, capture, {
73
79
  particleAcceleration,
74
80
  crumble: 'shards',
81
+ wind: windOf(windDirection),
82
+ margins: effectMargins(windDirection),
75
83
  }));
76
84
  renderer.render({
77
85
  deltaTSec: 0,
@@ -87,14 +95,16 @@ export class WsThanosService {
87
95
  }
88
96
  }
89
97
  vaporizeIntern(elem, options) {
90
- const { animationLength, maxParticleCount, maxCapturePixels, particleAcceleration, crumble, } = options;
98
+ const { animationLength, maxParticleCount, maxCapturePixels, particleAcceleration, crumble, windDirection, } = options;
91
99
  elem.style.opacity = elem.style.opacity || '1';
92
100
  const rendererChoice = this.chooseRenderer(maxParticleCount, maxCapturePixels);
93
101
  let effect;
94
- const capture = this.capture(elem, (width, height) => rendererChoice.captureScale(width, height));
102
+ const wind = windOf(windDirection);
103
+ const margins = effectMargins(windDirection);
104
+ const capture = this.capture(elem, (width, height) => rendererChoice.captureScale(width, height, effectScale(margins)));
95
105
  return from(capture).pipe(map((capture) => {
96
- const { renderer, particles } = this.createRenderer(rendererChoice, capture, { particleAcceleration, crumble });
97
- this.placeEffectCanvas(elem, renderer.canvas, capture.width, capture.height);
106
+ const { renderer, particles } = this.createRenderer(rendererChoice, capture, { particleAcceleration, crumble, wind, margins });
107
+ this.placeEffectCanvas(elem, renderer.canvas, capture.width, capture.height, margins);
98
108
  renderer.canvas.dataset['wsThanosRenderer'] = renderer.kind;
99
109
  renderer.canvas.dataset['wsThanosCrumble'] = renderer.crumble;
100
110
  // when every pixel became a particle, the particles replace the element right away
@@ -112,13 +122,18 @@ export class WsThanosService {
112
122
  };
113
123
  return effect;
114
124
  }), switchMap(({ renderer, sound }) => {
115
- let time = 0;
116
- return interval(1000 / 60, animationFrameScheduler).pipe(timeInterval(), tap((deltaT) => (time += deltaT.interval)), map((deltaT) => ({
117
- deltaTSec: deltaT.interval / 1000,
118
- animationT: time / animationLength,
119
- maxWidth: renderer.canvas.width,
120
- maxHeight: renderer.canvas.height,
121
- })), tap((animationState) => renderer.render(animationState)), takeWhile((animationState) => animationState.animationT <= 1),
125
+ let previous = 0;
126
+ // one step per frame the screen shows, at whatever rate it refreshes
127
+ return animationFrames().pipe(map(({ elapsed }) => {
128
+ const deltaTSec = Math.min((elapsed - previous) / 1000, MAX_FRAME_SEC);
129
+ previous = elapsed;
130
+ return {
131
+ deltaTSec,
132
+ animationT: elapsed / animationLength,
133
+ maxWidth: renderer.canvas.width,
134
+ maxHeight: renderer.canvas.height,
135
+ };
136
+ }), tap((animationState) => renderer.render(animationState)), takeWhile((animationState) => animationState.animationT <= 1),
122
137
  // a completed effect lets the sound blow away, a cancelled one stops it
123
138
  tap({ unsubscribe: () => sound?.stop(), error: () => sound?.stop() }));
124
139
  }), finalize(() => {
@@ -162,29 +177,35 @@ export class WsThanosService {
162
177
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
163
178
  return { imageData, width, height, scale };
164
179
  }
165
- createRenderer(rendererChoice, { imageData, scale }, { particleAcceleration, crumble, }) {
166
- const effectWidth = imageData.width * EFFECT_WIDTH_SCALE;
167
- const effectHeight = imageData.height * EFFECT_HEIGHT_SCALE;
168
- const particles = createParticles(imageData, rendererChoice.maxParticleCount, effectHeight);
180
+ /** the effect canvas reaches past the element where the wind blows the dust */
181
+ createRenderer(rendererChoice, { imageData, scale }, { particleAcceleration, crumble, wind, margins, }) {
182
+ const { width, height } = imageData;
183
+ const offsetX = Math.round(width * margins.left);
184
+ const offsetY = Math.round(height * margins.top);
185
+ const bottom = Math.round(height * margins.bottom);
186
+ const particles = createParticles(imageData, rendererChoice.maxParticleCount, { x: offsetX, y: offsetY });
169
187
  const renderer = rendererChoice.create(particles, {
170
- width: effectWidth,
171
- height: effectHeight,
188
+ width: offsetX + width + Math.round(width * margins.right),
189
+ height: offsetY + height + bottom,
172
190
  // particles move in device pixels, keep their speed in css pixels
173
191
  particleAcceleration: particleAcceleration * scale,
174
192
  pixelScale: scale,
175
193
  crumble,
176
194
  seed: Math.random() * 1000,
195
+ wind,
196
+ front: frontGeometry(particles.bounds, wind, { top: offsetY, bottom }),
177
197
  });
178
198
  return { renderer, particles };
179
199
  }
180
- /** the element sits at the bottom left of the larger effect canvas */
181
- placeEffectCanvas(elem, canvas, width, height) {
200
+ /** the effect canvas covers the element and the margins around it */
201
+ placeEffectCanvas(elem, canvas, width, height, margins) {
182
202
  const { left, top } = this.offsetInParent(elem);
203
+ const scale = effectScale(margins);
183
204
  canvas.style.position = 'absolute';
184
- canvas.style.left = `${left}px`;
185
- canvas.style.top = `${top - height * (EFFECT_HEIGHT_SCALE - 1)}px`;
186
- canvas.style.width = `${width * EFFECT_WIDTH_SCALE}px`;
187
- canvas.style.height = `${height * EFFECT_HEIGHT_SCALE}px`;
205
+ canvas.style.left = `${left - width * margins.left}px`;
206
+ canvas.style.top = `${top - height * margins.top}px`;
207
+ canvas.style.width = `${width * scale.width}px`;
208
+ canvas.style.height = `${height * scale.height}px`;
188
209
  canvas.style.zIndex = '2000';
189
210
  canvas.style.pointerEvents = 'none';
190
211
  elem.insertAdjacentElement('beforebegin', canvas);
@@ -1 +1 @@
1
- {"version":3,"file":"ws-thanos.service.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { DOCUMENT, Injectable, inject } from '@angular/core';\n\nimport { default as html2canvas } from 'html2canvas';\nimport {\n animationFrameScheduler,\n defer,\n finalize,\n from,\n interval,\n map,\n Observable,\n switchMap,\n takeWhile,\n tap,\n timeInterval,\n} from 'rxjs';\nimport { AnimationState } from './animation.state';\nimport { EFFECT_HEIGHT_SCALE, EFFECT_WIDTH_SCALE } from './capture-scale';\nimport { fitImagesForCapture } from './fit-images';\nimport {\n chooseParticleRenderer,\n GpuContextBudget,\n ParticleRendererChoice,\n} from './choose-particle-renderer';\nimport { ParticleRenderer } from './particle-renderer';\nimport { createParticles, Particles } from './particles';\nimport { SnapSound, SnapSoundService } from './snap-sound.service';\nimport { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';\nimport type { WsThanosOptions } from './ws-thanos.options';\n\n/** a short cross fade hides small differences between capture and element */\nconst FULL_COVERAGE_FADE_OUT_MS = 200;\n\ninterface RunningEffect {\n renderer: ParticleRenderer;\n sound?: SnapSound;\n}\n\n/** the captured element, its css size and the device pixels per css pixel */\ninterface Capture {\n imageData: ImageData;\n width: number;\n height: number;\n scale: number;\n}\n\n/** resolves once the browser has nothing else to do */\nfunction idle(window: Window): Promise<void> {\n return new Promise((resolve) =>\n typeof window.requestIdleCallback === 'function'\n ? window.requestIdleCallback(() => resolve())\n : window.setTimeout(resolve, 200)\n );\n}\n\n@Injectable({ providedIn: 'root' })\nexport class WsThanosService {\n private readonly thanosOptions: WsThanosOptions = inject(\n WS_THANOS_OPTIONS_TOKEN\n );\n private readonly document = inject(DOCUMENT);\n private readonly snapSound = inject(SnapSoundService);\n /** shared by all effects, browsers only allow a few WebGL contexts */\n private readonly gpuBudget = new GpuContextBudget();\n private preloaded?: Promise<void>;\n\n /**\n * start the vaporize-effect.\n * @param options override the provided options for this element\n */\n public vaporize(\n elem: HTMLElement,\n options?: Partial<WsThanosOptions>\n ): Observable<AnimationState> {\n // nothing is prepared before someone subscribes\n return defer(() =>\n this.vaporizeIntern(elem, { ...this.thanosOptions, ...options })\n );\n }\n\n /**\n * Snap a tiny invisible element once, so the first real snap starts without a stutter:\n * html2canvas, reading back its canvas and compiling the WebGL shaders are slow the first time.\n * Runs once while the browser is idle, the directive starts it unless `preload` is off.\n */\n public preload(): Promise<void> {\n this.preloaded ??= this.preloadIntern().catch((error) =>\n console.debug('ws-thanos: preload failed', error)\n );\n return this.preloaded;\n }\n\n private async preloadIntern(): Promise<void> {\n const { body, defaultView: window } = this.document;\n if (window == null || body == null) {\n return;\n }\n await idle(window);\n\n const elem = this.document.createElement('div');\n elem.textContent = 'ws';\n elem.setAttribute('aria-hidden', 'true');\n // draws what cards usually draw: text, a gradient, rounded borders and a shadow.\n // html2canvas ignores clip-path, so it captures the element while the page shows nothing\n elem.style.cssText = [\n 'position:fixed;left:0;top:0;width:32px;height:32px',\n 'clip-path:inset(50%);pointer-events:none',\n 'border:2px solid #888;border-radius:8px;box-shadow:0 2px 4px #0004',\n 'background:linear-gradient(#fff,#888)',\n ].join(';');\n body.appendChild(elem);\n\n const { maxParticleCount, maxCapturePixels, particleAcceleration } =\n this.thanosOptions;\n let rendererChoice: ParticleRendererChoice | undefined;\n let renderer: ParticleRenderer | undefined;\n try {\n // the scale does not matter for a warm up\n const capture = await this.capture(elem, () => 1);\n await idle(window);\n // take a WebGL context only now, not while waiting for idle time\n rendererChoice = this.chooseRenderer(maxParticleCount, maxCapturePixels);\n // dust skips the crack shader, the other variants compile every shader\n ({ renderer } = this.createRenderer(rendererChoice, capture, {\n particleAcceleration,\n crumble: 'shards',\n }));\n renderer.render({\n deltaTSec: 0,\n animationT: 0,\n maxWidth: renderer.canvas.width,\n maxHeight: renderer.canvas.height,\n });\n } finally {\n renderer?.dispose();\n elem.remove();\n rendererChoice?.release();\n }\n }\n\n private vaporizeIntern(\n elem: HTMLElement,\n options: WsThanosOptions\n ): Observable<AnimationState> {\n const {\n animationLength,\n maxParticleCount,\n maxCapturePixels,\n particleAcceleration,\n crumble,\n } = options;\n\n elem.style.opacity = elem.style.opacity || '1';\n\n const rendererChoice = this.chooseRenderer(\n maxParticleCount,\n maxCapturePixels\n );\n let effect: RunningEffect | undefined;\n\n const capture = this.capture(elem, (width, height) =>\n rendererChoice.captureScale(width, height)\n );\n return from(capture).pipe(\n map((capture): RunningEffect => {\n const { renderer, particles } = this.createRenderer(\n rendererChoice,\n capture,\n { particleAcceleration, crumble }\n );\n\n this.placeEffectCanvas(\n elem,\n renderer.canvas,\n capture.width,\n capture.height\n );\n renderer.canvas.dataset['wsThanosRenderer'] = renderer.kind;\n renderer.canvas.dataset['wsThanosCrumble'] = renderer.crumble;\n // when every pixel became a particle, the particles replace the element right away\n const fadeOutMs = particles.sampled\n ? Math.floor(animationLength * 0.8)\n : FULL_COVERAGE_FADE_OUT_MS;\n elem.style.transition = `opacity ${fadeOutMs}ms ease-out`;\n elem.style.opacity = '0';\n\n const { sound, soundVolume } = options;\n effect = {\n renderer,\n sound: sound\n ? this.snapSound.play(\n animationLength,\n soundVolume,\n renderer.crumble\n )\n : undefined,\n };\n return effect;\n }),\n switchMap(({ renderer, sound }) => {\n let time = 0;\n return interval(1000 / 60, animationFrameScheduler).pipe(\n timeInterval(),\n tap((deltaT) => (time += deltaT.interval)),\n map((deltaT): AnimationState => ({\n deltaTSec: deltaT.interval / 1000,\n animationT: time / animationLength,\n maxWidth: renderer.canvas.width,\n maxHeight: renderer.canvas.height,\n })),\n tap((animationState) => renderer.render(animationState)),\n takeWhile((animationState) => animationState.animationT <= 1),\n // a completed effect lets the sound blow away, a cancelled one stops it\n tap({ unsubscribe: () => sound?.stop(), error: () => sound?.stop() })\n );\n }),\n finalize(() => {\n effect?.renderer.dispose();\n rendererChoice.release();\n })\n );\n }\n\n private chooseRenderer(\n maxParticleCount: number,\n maxCapturePixels: number | undefined\n ): ParticleRendererChoice {\n return chooseParticleRenderer({\n maxParticleCount,\n maxCapturePixels,\n devicePixelRatio: this.document.defaultView?.devicePixelRatio ?? 1,\n createCanvas: () => this.document.createElement('canvas'),\n gpuBudget: this.gpuBudget,\n });\n }\n\n /** draw the element into an image at the scale the renderer needs */\n private async capture(\n elem: HTMLElement,\n captureScale: ParticleRendererChoice['captureScale']\n ): Promise<Capture> {\n const window = this.document.defaultView;\n const { width, height } = elem.getBoundingClientRect();\n const scale = captureScale(width, height);\n // a canvas that is read back is drawn on the CPU: a GPU canvas would compile\n // the shaders for everything html2canvas draws on the first read back\n const canvas = this.document.createElement('canvas');\n canvas.width = Math.floor(Math.ceil(width) * scale);\n canvas.height = Math.floor(Math.ceil(height) * scale);\n const context = canvas.getContext('2d', { willReadFrequently: true });\n if (context == null) {\n throw new Error('Could not get a 2d context for the capture');\n }\n await html2canvas(elem, {\n canvas,\n backgroundColor: null,\n scale,\n allowTaint: true,\n windowHeight: window?.innerHeight,\n windowWidth: window?.innerWidth,\n scrollY: -(window?.scrollY ?? 0),\n onclone: (_document, clonedElem) =>\n fitImagesForCapture(clonedElem, scale),\n });\n const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n return { imageData, width, height, scale };\n }\n\n private createRenderer(\n rendererChoice: ParticleRendererChoice,\n { imageData, scale }: Capture,\n {\n particleAcceleration,\n crumble,\n }: Pick<WsThanosOptions, 'particleAcceleration' | 'crumble'>\n ): { renderer: ParticleRenderer; particles: Particles } {\n const effectWidth = imageData.width * EFFECT_WIDTH_SCALE;\n const effectHeight = imageData.height * EFFECT_HEIGHT_SCALE;\n const particles = createParticles(\n imageData,\n rendererChoice.maxParticleCount,\n effectHeight\n );\n const renderer = rendererChoice.create(particles, {\n width: effectWidth,\n height: effectHeight,\n // particles move in device pixels, keep their speed in css pixels\n particleAcceleration: particleAcceleration * scale,\n pixelScale: scale,\n crumble,\n seed: Math.random() * 1000,\n });\n return { renderer, particles };\n }\n\n /** the element sits at the bottom left of the larger effect canvas */\n private placeEffectCanvas(\n elem: HTMLElement,\n canvas: HTMLCanvasElement,\n width: number,\n height: number\n ): void {\n const { left, top } = this.offsetInParent(elem);\n canvas.style.position = 'absolute';\n canvas.style.left = `${left}px`;\n canvas.style.top = `${top - height * (EFFECT_HEIGHT_SCALE - 1)}px`;\n canvas.style.width = `${width * EFFECT_WIDTH_SCALE}px`;\n canvas.style.height = `${height * EFFECT_HEIGHT_SCALE}px`;\n canvas.style.zIndex = '2000';\n canvas.style.pointerEvents = 'none';\n elem.insertAdjacentElement('beforebegin', canvas);\n }\n\n /** position of the element inside its parent, where absolute children are placed */\n private offsetInParent(elem: HTMLElement): { left: number; top: number } {\n const parent = elem.parentElement;\n if (parent == null) {\n return { left: 0, top: 0 };\n }\n parent.style.position = parent.style.position || 'relative';\n // absolute positions are relative to the parent's padding box\n const elemRect = elem.getBoundingClientRect();\n const parentRect = parent.getBoundingClientRect();\n return {\n left:\n elemRect.left - parentRect.left - parent.clientLeft + parent.scrollLeft,\n top: elemRect.top - parentRect.top - parent.clientTop + parent.scrollTop,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"ws-thanos.service.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { DOCUMENT, Injectable, inject } from '@angular/core';\n\nimport { default as html2canvas } from 'html2canvas';\nimport {\n animationFrames,\n defer,\n finalize,\n from,\n map,\n Observable,\n switchMap,\n takeWhile,\n tap,\n} from 'rxjs';\nimport { AnimationState } from './animation.state';\nimport { fitImagesForCapture } from './fit-images';\nimport {\n chooseParticleRenderer,\n GpuContextBudget,\n ParticleRendererChoice,\n} from './choose-particle-renderer';\nimport { ParticleRenderer } from './particle-renderer';\nimport { createParticles, Particles } from './particles';\nimport { SnapSound, SnapSoundService } from './snap-sound.service';\nimport { frontGeometry } from './vaporizing-front';\nimport {\n EffectMargins,\n effectMargins,\n effectScale,\n Wind,\n windOf,\n} from './wind';\nimport { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';\nimport type { WsThanosOptions } from './ws-thanos.options';\n\n/** a short cross fade hides small differences between capture and element */\nconst FULL_COVERAGE_FADE_OUT_MS = 200;\n\n/**\n * The longest step the simulation takes: after a stalled frame or a hidden tab\n * the particles carry on instead of jumping away.\n */\nconst MAX_FRAME_SEC = 0.1;\n\ninterface RunningEffect {\n renderer: ParticleRenderer;\n sound?: SnapSound;\n}\n\n/** the captured element, its css size and the device pixels per css pixel */\ninterface Capture {\n imageData: ImageData;\n width: number;\n height: number;\n scale: number;\n}\n\n/** resolves once the browser has nothing else to do */\nfunction idle(window: Window): Promise<void> {\n return new Promise((resolve) =>\n typeof window.requestIdleCallback === 'function'\n ? window.requestIdleCallback(() => resolve())\n : window.setTimeout(resolve, 200)\n );\n}\n\n@Injectable({ providedIn: 'root' })\nexport class WsThanosService {\n private readonly thanosOptions: WsThanosOptions = inject(\n WS_THANOS_OPTIONS_TOKEN\n );\n private readonly document = inject(DOCUMENT);\n private readonly snapSound = inject(SnapSoundService);\n /** shared by all effects, browsers only allow a few WebGL contexts */\n private readonly gpuBudget = new GpuContextBudget();\n private preloaded?: Promise<void>;\n\n /**\n * start the vaporize-effect.\n * @param options override the provided options for this element\n */\n public vaporize(\n elem: HTMLElement,\n options?: Partial<WsThanosOptions>\n ): Observable<AnimationState> {\n // nothing is prepared before someone subscribes\n return defer(() =>\n this.vaporizeIntern(elem, { ...this.thanosOptions, ...options })\n );\n }\n\n /**\n * Snap a tiny invisible element once, so the first real snap starts without a stutter:\n * html2canvas, reading back its canvas and compiling the WebGL shaders are slow the first time.\n * Runs once while the browser is idle, the directive starts it unless `preload` is off.\n */\n public preload(): Promise<void> {\n this.preloaded ??= this.preloadIntern().catch((error) =>\n console.debug('ws-thanos: preload failed', error)\n );\n return this.preloaded;\n }\n\n private async preloadIntern(): Promise<void> {\n const { body, defaultView: window } = this.document;\n if (window == null || body == null) {\n return;\n }\n await idle(window);\n\n const elem = this.document.createElement('div');\n elem.textContent = 'ws';\n elem.setAttribute('aria-hidden', 'true');\n // draws what cards usually draw: text, a gradient, rounded borders and a shadow.\n // html2canvas ignores clip-path, so it captures the element while the page shows nothing\n elem.style.cssText = [\n 'position:fixed;left:0;top:0;width:32px;height:32px',\n 'clip-path:inset(50%);pointer-events:none',\n 'border:2px solid #888;border-radius:8px;box-shadow:0 2px 4px #0004',\n 'background:linear-gradient(#fff,#888)',\n ].join(';');\n body.appendChild(elem);\n\n const {\n maxParticleCount,\n maxCapturePixels,\n particleAcceleration,\n windDirection,\n } = this.thanosOptions;\n let rendererChoice: ParticleRendererChoice | undefined;\n let renderer: ParticleRenderer | undefined;\n try {\n // the scale does not matter for a warm up\n const capture = await this.capture(elem, () => 1);\n await idle(window);\n // take a WebGL context only now, not while waiting for idle time\n rendererChoice = this.chooseRenderer(maxParticleCount, maxCapturePixels);\n // dust skips the crack shader, the other variants compile every shader\n ({ renderer } = this.createRenderer(rendererChoice, capture, {\n particleAcceleration,\n crumble: 'shards',\n wind: windOf(windDirection),\n margins: effectMargins(windDirection),\n }));\n renderer.render({\n deltaTSec: 0,\n animationT: 0,\n maxWidth: renderer.canvas.width,\n maxHeight: renderer.canvas.height,\n });\n } finally {\n renderer?.dispose();\n elem.remove();\n rendererChoice?.release();\n }\n }\n\n private vaporizeIntern(\n elem: HTMLElement,\n options: WsThanosOptions\n ): Observable<AnimationState> {\n const {\n animationLength,\n maxParticleCount,\n maxCapturePixels,\n particleAcceleration,\n crumble,\n windDirection,\n } = options;\n\n elem.style.opacity = elem.style.opacity || '1';\n\n const rendererChoice = this.chooseRenderer(\n maxParticleCount,\n maxCapturePixels\n );\n let effect: RunningEffect | undefined;\n\n const wind = windOf(windDirection);\n const margins = effectMargins(windDirection);\n const capture = this.capture(elem, (width, height) =>\n rendererChoice.captureScale(width, height, effectScale(margins))\n );\n return from(capture).pipe(\n map((capture): RunningEffect => {\n const { renderer, particles } = this.createRenderer(\n rendererChoice,\n capture,\n { particleAcceleration, crumble, wind, margins }\n );\n\n this.placeEffectCanvas(\n elem,\n renderer.canvas,\n capture.width,\n capture.height,\n margins\n );\n renderer.canvas.dataset['wsThanosRenderer'] = renderer.kind;\n renderer.canvas.dataset['wsThanosCrumble'] = renderer.crumble;\n // when every pixel became a particle, the particles replace the element right away\n const fadeOutMs = particles.sampled\n ? Math.floor(animationLength * 0.8)\n : FULL_COVERAGE_FADE_OUT_MS;\n elem.style.transition = `opacity ${fadeOutMs}ms ease-out`;\n elem.style.opacity = '0';\n\n const { sound, soundVolume } = options;\n effect = {\n renderer,\n sound: sound\n ? this.snapSound.play(\n animationLength,\n soundVolume,\n renderer.crumble\n )\n : undefined,\n };\n return effect;\n }),\n switchMap(({ renderer, sound }) => {\n let previous = 0;\n // one step per frame the screen shows, at whatever rate it refreshes\n return animationFrames().pipe(\n map(({ elapsed }): AnimationState => {\n const deltaTSec = Math.min(\n (elapsed - previous) / 1000,\n MAX_FRAME_SEC\n );\n previous = elapsed;\n return {\n deltaTSec,\n animationT: elapsed / animationLength,\n maxWidth: renderer.canvas.width,\n maxHeight: renderer.canvas.height,\n };\n }),\n tap((animationState) => renderer.render(animationState)),\n takeWhile((animationState) => animationState.animationT <= 1),\n // a completed effect lets the sound blow away, a cancelled one stops it\n tap({ unsubscribe: () => sound?.stop(), error: () => sound?.stop() })\n );\n }),\n finalize(() => {\n effect?.renderer.dispose();\n rendererChoice.release();\n })\n );\n }\n\n private chooseRenderer(\n maxParticleCount: number,\n maxCapturePixels: number | undefined\n ): ParticleRendererChoice {\n return chooseParticleRenderer({\n maxParticleCount,\n maxCapturePixels,\n devicePixelRatio: this.document.defaultView?.devicePixelRatio ?? 1,\n createCanvas: () => this.document.createElement('canvas'),\n gpuBudget: this.gpuBudget,\n });\n }\n\n /** draw the element into an image at the scale the renderer needs */\n private async capture(\n elem: HTMLElement,\n captureScale: (width: number, height: number) => number\n ): Promise<Capture> {\n const window = this.document.defaultView;\n const { width, height } = elem.getBoundingClientRect();\n const scale = captureScale(width, height);\n // a canvas that is read back is drawn on the CPU: a GPU canvas would compile\n // the shaders for everything html2canvas draws on the first read back\n const canvas = this.document.createElement('canvas');\n canvas.width = Math.floor(Math.ceil(width) * scale);\n canvas.height = Math.floor(Math.ceil(height) * scale);\n const context = canvas.getContext('2d', { willReadFrequently: true });\n if (context == null) {\n throw new Error('Could not get a 2d context for the capture');\n }\n await html2canvas(elem, {\n canvas,\n backgroundColor: null,\n scale,\n allowTaint: true,\n windowHeight: window?.innerHeight,\n windowWidth: window?.innerWidth,\n scrollY: -(window?.scrollY ?? 0),\n onclone: (_document, clonedElem) =>\n fitImagesForCapture(clonedElem, scale),\n });\n const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n return { imageData, width, height, scale };\n }\n\n /** the effect canvas reaches past the element where the wind blows the dust */\n private createRenderer(\n rendererChoice: ParticleRendererChoice,\n { imageData, scale }: Capture,\n {\n particleAcceleration,\n crumble,\n wind,\n margins,\n }: Pick<WsThanosOptions, 'particleAcceleration' | 'crumble'> & {\n wind: Wind;\n margins: EffectMargins;\n }\n ): { renderer: ParticleRenderer; particles: Particles } {\n const { width, height } = imageData;\n const offsetX = Math.round(width * margins.left);\n const offsetY = Math.round(height * margins.top);\n const bottom = Math.round(height * margins.bottom);\n const particles = createParticles(\n imageData,\n rendererChoice.maxParticleCount,\n { x: offsetX, y: offsetY }\n );\n const renderer = rendererChoice.create(particles, {\n width: offsetX + width + Math.round(width * margins.right),\n height: offsetY + height + bottom,\n // particles move in device pixels, keep their speed in css pixels\n particleAcceleration: particleAcceleration * scale,\n pixelScale: scale,\n crumble,\n seed: Math.random() * 1000,\n wind,\n front: frontGeometry(particles.bounds, wind, { top: offsetY, bottom }),\n });\n return { renderer, particles };\n }\n\n /** the effect canvas covers the element and the margins around it */\n private placeEffectCanvas(\n elem: HTMLElement,\n canvas: HTMLCanvasElement,\n width: number,\n height: number,\n margins: EffectMargins\n ): void {\n const { left, top } = this.offsetInParent(elem);\n const scale = effectScale(margins);\n canvas.style.position = 'absolute';\n canvas.style.left = `${left - width * margins.left}px`;\n canvas.style.top = `${top - height * margins.top}px`;\n canvas.style.width = `${width * scale.width}px`;\n canvas.style.height = `${height * scale.height}px`;\n canvas.style.zIndex = '2000';\n canvas.style.pointerEvents = 'none';\n elem.insertAdjacentElement('beforebegin', canvas);\n }\n\n /** position of the element inside its parent, where absolute children are placed */\n private offsetInParent(elem: HTMLElement): { left: number; top: number } {\n const parent = elem.parentElement;\n if (parent == null) {\n return { left: 0, top: 0 };\n }\n parent.style.position = parent.style.position || 'relative';\n // absolute positions are relative to the parent's padding box\n const elemRect = elem.getBoundingClientRect();\n const parentRect = parent.getBoundingClientRect();\n return {\n left:\n elemRect.left - parentRect.left - parent.clientLeft + parent.scrollLeft,\n top: elemRect.top - parentRect.top - parent.clientTop + parent.scrollTop,\n };\n }\n}\n"]}
@@ -1,15 +1,14 @@
1
1
  import { AnimationState } from './animation.state';
2
2
  import { ParticleRenderer, ParticleRendererParams } from './particle-renderer';
3
3
  import { Particles } from './particles';
4
- import { SimplexNoise } from './simplex-noise';
4
+ import { FlowField } from './flow-field';
5
5
  /** the CPU can't keep up with more particles than this */
6
6
  export declare const CANVAS_MAX_PARTICLE_COUNT = 400000;
7
7
  export interface UpdateParticlesParams {
8
8
  particles: Particles;
9
9
  animationState: AnimationState;
10
- particleAcceleration: number;
11
- noise: SimplexNoise;
12
- seed: number;
10
+ params: Pick<ParticleRendererParams, 'particleAcceleration' | 'seed' | 'wind' | 'front'>;
11
+ flowField: FlowField;
13
12
  }
14
13
  /** Fallback renderer that simulates on the CPU and draws with a 2d context. */
15
14
  export declare class CanvasParticleRenderer implements ParticleRenderer {
@@ -19,11 +18,14 @@ export declare class CanvasParticleRenderer implements ParticleRenderer {
19
18
  readonly kind = "canvas";
20
19
  /** the CPU can't afford to simulate cracks and shards */
21
20
  readonly crumble = "dust";
22
- private readonly noise;
21
+ private readonly flowField;
23
22
  private readonly context;
23
+ /** drawn into and put on the canvas every frame */
24
+ private readonly image;
24
25
  constructor(canvas: HTMLCanvasElement, particles: Particles, params: ParticleRendererParams);
25
26
  render(animationState: AnimationState): void;
26
27
  dispose(): void;
27
28
  }
28
- export declare function updateParticlesOnCpu({ particles: { state, count, maxParticleX, minParticleY }, animationState: { deltaTSec, animationT, maxWidth, maxHeight }, particleAcceleration, noise, seed, }: UpdateParticlesParams): void;
29
- export declare function drawParticlesOnCpu(context: CanvasRenderingContext2D, { state, colors, count }: Particles): void;
29
+ export declare function updateParticlesOnCpu({ particles: { state, count }, animationState: { deltaTSec, animationT, maxWidth, maxHeight }, params: { particleAcceleration, seed, wind, front: frontGeometry }, flowField, }: UpdateParticlesParams): void;
30
+ /** paints the particles into image and puts it on the canvas, image has the canvas size */
31
+ export declare function drawParticlesOnCpu(context: CanvasRenderingContext2D, image: ImageData, { state, colors, count }: Particles): void;
@@ -1,9 +1,8 @@
1
- /** the effect canvas is wider and taller than the element so particles have room to fly */
2
- export declare const EFFECT_WIDTH_SCALE = 2;
3
- export declare const EFFECT_HEIGHT_SCALE = 5;
1
+ import type { EffectScale } from './wind';
4
2
  export interface CaptureScaleParams {
5
3
  width: number;
6
4
  height: number;
5
+ effectScale: EffectScale;
7
6
  devicePixelRatio: number;
8
7
  maxCanvasSize: number;
9
8
  /** the most pixels to capture the element with, no limit when unset */
@@ -14,9 +13,11 @@ export interface CaptureScaleParams {
14
13
  * Infinity without a limit: a missing, zero, negative or NaN one.
15
14
  */
16
15
  export declare function capturePixelsScale(width: number, height: number, maxCapturePixels?: number): number;
16
+ /** scale that keeps the effect canvas around an element of the given css size a canvas browsers create */
17
+ export declare function effectCanvasScale(width: number, height: number, effectScale: EffectScale): number;
17
18
  /**
18
19
  * Scale used to capture the element: device pixels for crisp particles,
19
- * but small enough that the effect canvas fits the device's max canvas size
20
+ * but small enough that the effect canvas fits the device's max canvas size and area
20
21
  * and the element stays within maxCapturePixels, below css pixels if needed.
21
22
  */
22
- export declare function computeCaptureScale({ width, height, devicePixelRatio, maxCanvasSize, maxCapturePixels, }: CaptureScaleParams): number;
23
+ export declare function computeCaptureScale({ width, height, effectScale, devicePixelRatio, maxCanvasSize, maxCapturePixels, }: CaptureScaleParams): number;
@@ -1,5 +1,6 @@
1
1
  import { ParticleRenderer, ParticleRendererParams } from './particle-renderer';
2
2
  import { Particles } from './particles';
3
+ import type { EffectScale } from './wind';
3
4
  /** counts the effects that hold a WebGL context */
4
5
  export declare class GpuContextBudget {
5
6
  private readonly max;
@@ -19,8 +20,8 @@ export interface ChooseParticleRendererParams {
19
20
  export interface ParticleRendererChoice {
20
21
  /** how many particles this renderer can handle */
21
22
  readonly maxParticleCount: number;
22
- /** scale to capture an element of the given css size */
23
- captureScale(width: number, height: number): number;
23
+ /** scale to capture an element of the given css size, see computeCaptureScale() */
24
+ captureScale(width: number, height: number, effectScale: EffectScale): number;
24
25
  create(particles: Particles, params: ParticleRendererParams): ParticleRenderer;
25
26
  /** free the reserved GPU context, call it once the effect is over */
26
27
  release(): void;
@@ -0,0 +1,24 @@
1
+ import { SimplexNoise } from './simplex-noise';
2
+ import { Wind } from './wind';
3
+ /**
4
+ * Pixels between two samples of the flow field. The noise changes over about a hundred pixels,
5
+ * so interpolating between samples looks the same as computing it for every particle.
6
+ */
7
+ export declare const FLOW_FIELD_CELL = 8;
8
+ /**
9
+ * The noise field released particles flow along, sampled once on a grid over the effect canvas:
10
+ * it only depends on the position, and looking it up is much cheaper than computing noise
11
+ * for every particle in every frame. Each grid point holds the push a particle gets
12
+ * in a frame at 60 Hz, already turned into the wind.
13
+ */
14
+ export declare class FlowField {
15
+ private readonly columns;
16
+ private readonly rows;
17
+ private readonly cellsPerPixel;
18
+ /** x and y of the push per grid point */
19
+ private readonly values;
20
+ /** @param noise only its raw3D() is used, frequency and range come from FLOW_FIELD */
21
+ constructor(width: number, height: number, seed: number, { cos, sin }?: Wind, cell?: number, noise?: SimplexNoise);
22
+ /** writes the push at x, y into out, interpolated between the grid points */
23
+ sample(x: number, y: number, out: Float32Array): void;
24
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The simulation was tuned at this frame rate. Whatever is applied once per frame
3
+ * is scaled by the frame's length relative to it, so the effect looks the same on a 120 Hz screen.
4
+ */
5
+ export declare const FRAME_RATE = 60;
6
+ /**
7
+ * the fade for one frame of the given length, fade is the fade for a frame at FRAME_RATE.
8
+ * A fade below 0, past the end of the animation, fades out completely.
9
+ */
10
+ export declare function frameFade(fade: number, deltaTSec: number): number;
@@ -1,4 +1,6 @@
1
1
  import { AnimationState } from './animation.state';
2
+ import { FrontGeometry } from './vaporizing-front';
3
+ import { Wind } from './wind';
2
4
  import { WsThanosCrumble } from './ws-thanos.options';
3
5
  /** moves and draws the particles of one vaporizing element */
4
6
  export interface ParticleRenderer {
@@ -23,4 +25,8 @@ export interface ParticleRendererParams {
23
25
  crumble: WsThanosCrumble;
24
26
  /** random seed that makes every snap look different */
25
27
  seed: number;
28
+ /** where the dust blows to */
29
+ wind: Wind;
30
+ /** where the vaporizing front starts and ends */
31
+ front: FrontGeometry;
26
32
  }
@@ -13,23 +13,31 @@ export declare enum ParticleStateIndex {
13
13
  RELEASED_AT = 7
14
14
  }
15
15
  export declare const PARTICLE_STATE_LENGTH = 8;
16
+ /** box around the visible particles in effect canvas pixels, edges included */
17
+ export interface ParticleBounds {
18
+ left: number;
19
+ top: number;
20
+ right: number;
21
+ bottom: number;
22
+ }
16
23
  export interface Particles {
17
24
  count: number;
18
25
  /** position, velocity, acceleration, alpha (0-255) and release time per particle */
19
26
  state: Float32Array;
20
27
  /** rgba (0-255) per particle */
21
28
  colors: Uint8Array;
22
- /** right most particle x of the captured image, the vaporizing front starts here */
23
- maxParticleX: number;
24
- /** top most particle y of the captured image */
25
- minParticleY: number;
29
+ /** where the particles are at the start, the vaporizing front sweeps through this box */
30
+ bounds: ParticleBounds;
26
31
  /** true when only a random sample of the visible pixels became particles */
27
32
  sampled: boolean;
28
33
  }
29
34
  /**
30
35
  * Creates one particle per visible pixel, randomly sampled down to maxParticleCount.
31
- * Particles are placed at the bottom of an effect canvas of the given height.
36
+ * The element's top left corner sits at offset in the effect canvas.
32
37
  */
33
- export declare function createParticles(imageData: ImageData, maxParticleCount: number, effectHeight: number): Particles;
38
+ export declare function createParticles(imageData: ImageData, maxParticleCount: number, offset: {
39
+ x: number;
40
+ y: number;
41
+ }): Particles;
34
42
  /** a random sample of at most maxCount particles, e.g. when the CPU takes over from the GPU */
35
43
  export declare function limitParticles(particles: Particles, maxCount: number): Particles;
@@ -1,5 +1,7 @@
1
+ import type { ParticleBounds } from './particles';
2
+ import type { Wind } from './wind';
1
3
  /**
2
- * The vaporizing front sweeps from the top right to the bottom left of the element.
4
+ * The vaporizing front sweeps against the wind through the element.
3
5
  * Shared by the CPU and the GPU simulation, the GPU gets the values below as uniforms
4
6
  * and the constants interpolated into its shader.
5
7
  */
@@ -20,15 +22,35 @@ export declare const FLOW_FIELD: {
20
22
  readonly xSeedOffset: 33.23;
21
23
  readonly ySeedDivisor: 13.23;
22
24
  };
25
+ /** where the front starts and how far it reaches, in effect canvas pixels */
26
+ export interface FrontGeometry {
27
+ /** upwind point of the element, the front sweeps towards it */
28
+ originX: number;
29
+ originY: number;
30
+ /** distance from the origin at which the front starts */
31
+ reach: number;
32
+ }
23
33
  export interface VaporizingFront {
24
34
  /** drives the fray waves */
25
35
  time: number;
26
- /** particles further than this (squared) from the bottom left corner break off */
36
+ originX: number;
37
+ originY: number;
38
+ /** particles further than this (squared) from the origin break off */
27
39
  radiusPow: number;
28
40
  /** alpha factor that fades the particles out very late */
29
41
  fade: number;
30
42
  }
43
+ /**
44
+ * The front starts at the downwind side and sweeps against the wind to the upwind point of the element,
45
+ * for the default wind from the top right to the bottom left corner.
46
+ * It starts out as far as the room above or below the element reaches,
47
+ * so the element cracks first and only later blows away.
48
+ */
49
+ export declare function frontGeometry(bounds: ParticleBounds, wind: Pick<Wind, 'x' | 'y'>, margins: {
50
+ top: number;
51
+ bottom: number;
52
+ }): FrontGeometry;
31
53
  /** where the front is at the given animation progress */
32
- export declare function vaporizingFront(animationT: number, maxParticleX: number, minParticleY: number, height: number): VaporizingFront;
54
+ export declare function vaporizingFront(animationT: number, { originX, originY, reach }: FrontGeometry): VaporizingFront;
33
55
  /** has the front passed the particle at x, y. No tan() waves: their poles break off whole rows */
34
- export declare function isBehindFront(x: number, y: number, height: number, deltaTSec: number, seed: number, { time, radiusPow }: VaporizingFront): boolean;
56
+ export declare function isBehindFront(x: number, y: number, deltaTSec: number, seed: number, { time, originX, originY, radiusPow }: VaporizingFront): boolean;