@wolsok/thanos 18.2.3 → 18.3.1

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.
package/README.md CHANGED
@@ -102,6 +102,13 @@ so the particles get coarser. `maxParticleCount` instead leaves out random pixel
102
102
  Both renderers follow it.
103
103
  The effect canvas has a `data-ws-thanos-renderer` attribute (`webgl` or `canvas`) showing which renderer is in use.
104
104
 
105
+ #### Images
106
+
107
+ html2canvas captures the element, and it stretches every `<img>` over its box.
108
+ ws-thanos draws images with `object-fit` and `object-position` the way the browser shows them before html2canvas captures them,
109
+ so logos and photos keep their shape while they vaporize.
110
+ Images from another origin can't be read back and stay stretched; serve them from your origin or with CORS.
111
+
105
112
  #### Crumble variants
106
113
 
107
114
  The `crumble` option decides how the element breaks apart while it vaporizes:
@@ -115,6 +122,7 @@ The `crumble` option decides how the element breaks apart while it vaporizes:
115
122
 
116
123
  The variants need WebGL2, the canvas fallback always turns into `'dust'`.
117
124
  The effect canvas shows the variant in use in its `data-ws-thanos-crumble` attribute.
125
+ `WS_THANOS_CRUMBLES` lists all variants, e.g. to pick a random one for every snap.
118
126
 
119
127
  #### Options per element
120
128
 
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Rect the browser draws an image into for the css object-fit and object-position,
3
+ * relative to the image's content box.
4
+ */
5
+ export function objectFitRect(objectFit, objectPosition, box, image) {
6
+ const size = fittedSize(objectFit, box, image);
7
+ const [x, y] = parseObjectPosition(objectPosition);
8
+ return {
9
+ ...size,
10
+ x: positionOffset(x, box.width - size.width),
11
+ y: positionOffset(y, box.height - size.height),
12
+ };
13
+ }
14
+ function fittedSize(objectFit, box, image) {
15
+ const containScale = Math.min(box.width / image.width, box.height / image.height);
16
+ switch (objectFit) {
17
+ case 'contain':
18
+ return scaled(image, containScale);
19
+ case 'cover':
20
+ return scaled(image, Math.max(box.width / image.width, box.height / image.height));
21
+ case 'none':
22
+ return image;
23
+ case 'scale-down':
24
+ return scaled(image, Math.min(1, containScale));
25
+ default:
26
+ return box;
27
+ }
28
+ }
29
+ function scaled({ width, height }, scale) {
30
+ return { width: width * scale, height: height * scale };
31
+ }
32
+ /** the computed object-position, e.g. "50% 50%" or "10px 20%" */
33
+ function parseObjectPosition(objectPosition) {
34
+ const [x = '50%', y = '50%'] = objectPosition.trim().split(/\s+/);
35
+ return [x, y];
36
+ }
37
+ const POSITION_KEYWORDS = {
38
+ left: '0%',
39
+ top: '0%',
40
+ center: '50%',
41
+ right: '100%',
42
+ bottom: '100%',
43
+ };
44
+ /** a percentage places the image along the free space, a length shifts it */
45
+ function positionOffset(value, freeSpace) {
46
+ const position = POSITION_KEYWORDS[value] ?? value;
47
+ const number = parseFloat(position);
48
+ if (Number.isNaN(number)) {
49
+ return freeSpace / 2;
50
+ }
51
+ return position.endsWith('%') ? (freeSpace * number) / 100 : number;
52
+ }
53
+ /**
54
+ * html2canvas stretches every image over its content box, it ignores object-fit and
55
+ * object-position. This draws each image of the cloned element the way the browser shows it,
56
+ * at the capture scale, and swaps it in, so html2canvas only has to stretch an image of the box size.
57
+ * Images that are not loaded or are from another origin stay as they are.
58
+ */
59
+ export async function fitImagesForCapture(clonedElem, scale) {
60
+ const images = Array.from(clonedElem.querySelectorAll('img'));
61
+ // the clone lives in an iframe, instanceof would check another window's class
62
+ if (clonedElem.tagName === 'IMG') {
63
+ images.push(clonedElem);
64
+ }
65
+ await Promise.all(images.map((img) => fitImage(img, scale)));
66
+ }
67
+ /** an image still loading gets this long, so a stalled request can't hold the snap back */
68
+ const DECODE_TIMEOUT_MS = 1000;
69
+ /** waits for the image to be ready to draw, throws when it fails or takes too long */
70
+ async function decode(img) {
71
+ let timeout;
72
+ try {
73
+ await Promise.race([
74
+ img.decode(),
75
+ new Promise((_, reject) => {
76
+ timeout = setTimeout(() => reject(new Error('image took too long to load')), DECODE_TIMEOUT_MS);
77
+ }),
78
+ ]);
79
+ }
80
+ catch (error) {
81
+ // some browsers reject decode() for svg images that are loaded fine
82
+ if (!(img.complete && img.naturalWidth > 0)) {
83
+ throw error;
84
+ }
85
+ }
86
+ finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+ /** the size html2canvas draws the image into: its border box without borders and padding */
91
+ function contentBox(img, style) {
92
+ const { width, height } = img.getBoundingClientRect();
93
+ const px = (...values) => values.reduce((sum, value) => sum + (parseFloat(value) || 0), 0);
94
+ return {
95
+ width: width -
96
+ px(style.borderLeftWidth, style.borderRightWidth, style.paddingLeft, style.paddingRight),
97
+ height: height -
98
+ px(style.borderTopWidth, style.borderBottomWidth, style.paddingTop, style.paddingBottom),
99
+ };
100
+ }
101
+ async function fitImage(img, scale) {
102
+ const style = img.ownerDocument.defaultView?.getComputedStyle(img);
103
+ if (style == null || (style.objectFit || 'fill') === 'fill') {
104
+ return;
105
+ }
106
+ // a hidden image has no box, it isn't captured and doesn't need to load
107
+ const box = contentBox(img, style);
108
+ if (!(box.width > 0 && box.height > 0)) {
109
+ return;
110
+ }
111
+ try {
112
+ await decode(img);
113
+ const image = { width: img.naturalWidth, height: img.naturalHeight };
114
+ if (!(image.width > 0 && image.height > 0)) {
115
+ return;
116
+ }
117
+ const canvas = img.ownerDocument.createElement('canvas');
118
+ canvas.width = Math.max(1, Math.round(box.width * scale));
119
+ canvas.height = Math.max(1, Math.round(box.height * scale));
120
+ const context = canvas.getContext('2d');
121
+ if (context == null) {
122
+ return;
123
+ }
124
+ const rect = objectFitRect(style.objectFit, style.objectPosition, box, image);
125
+ context.drawImage(img, rect.x * scale, rect.y * scale, rect.width * scale, rect.height * scale);
126
+ // throws for an image from another origin, that one stays stretched
127
+ const fitted = canvas.toDataURL();
128
+ // the sources of a picture would win over the image's own src
129
+ if (img.parentElement?.tagName === 'PICTURE') {
130
+ img.parentElement
131
+ .querySelectorAll('source')
132
+ .forEach((source) => source.remove());
133
+ }
134
+ img.srcset = '';
135
+ img.src = fitted;
136
+ await decode(img);
137
+ }
138
+ catch (error) {
139
+ console.debug('ws-thanos: could not fit image for the capture', error);
140
+ }
141
+ }
142
+ //# sourceMappingURL=fit-images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fit-images.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/fit-images.ts"],"names":[],"mappings":"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","sourcesContent":["export interface Size {\n width: number;\n height: number;\n}\n\n/** where the image is drawn inside its content box, can reach outside of it for cover */\nexport interface FitRect extends Size {\n x: number;\n y: number;\n}\n\n/**\n * Rect the browser draws an image into for the css object-fit and object-position,\n * relative to the image's content box.\n */\nexport function objectFitRect(\n objectFit: string,\n objectPosition: string,\n box: Size,\n image: Size\n): FitRect {\n const size = fittedSize(objectFit, box, image);\n const [x, y] = parseObjectPosition(objectPosition);\n return {\n ...size,\n x: positionOffset(x, box.width - size.width),\n y: positionOffset(y, box.height - size.height),\n };\n}\n\nfunction fittedSize(objectFit: string, box: Size, image: Size): Size {\n const containScale = Math.min(\n box.width / image.width,\n box.height / image.height\n );\n switch (objectFit) {\n case 'contain':\n return scaled(image, containScale);\n case 'cover':\n return scaled(\n image,\n Math.max(box.width / image.width, box.height / image.height)\n );\n case 'none':\n return image;\n case 'scale-down':\n return scaled(image, Math.min(1, containScale));\n default:\n return box;\n }\n}\n\nfunction scaled({ width, height }: Size, scale: number): Size {\n return { width: width * scale, height: height * scale };\n}\n\n/** the computed object-position, e.g. \"50% 50%\" or \"10px 20%\" */\nfunction parseObjectPosition(objectPosition: string): [string, string] {\n const [x = '50%', y = '50%'] = objectPosition.trim().split(/\\s+/);\n return [x, y];\n}\n\nconst POSITION_KEYWORDS: Record<string, string> = {\n left: '0%',\n top: '0%',\n center: '50%',\n right: '100%',\n bottom: '100%',\n};\n\n/** a percentage places the image along the free space, a length shifts it */\nfunction positionOffset(value: string, freeSpace: number): number {\n const position = POSITION_KEYWORDS[value] ?? value;\n const number = parseFloat(position);\n if (Number.isNaN(number)) {\n return freeSpace / 2;\n }\n return position.endsWith('%') ? (freeSpace * number) / 100 : number;\n}\n\n/**\n * html2canvas stretches every image over its content box, it ignores object-fit and\n * object-position. This draws each image of the cloned element the way the browser shows it,\n * at the capture scale, and swaps it in, so html2canvas only has to stretch an image of the box size.\n * Images that are not loaded or are from another origin stay as they are.\n */\nexport async function fitImagesForCapture(\n clonedElem: HTMLElement,\n scale: number\n): Promise<void> {\n const images = Array.from(clonedElem.querySelectorAll('img'));\n // the clone lives in an iframe, instanceof would check another window's class\n if (clonedElem.tagName === 'IMG') {\n images.push(clonedElem as HTMLImageElement);\n }\n await Promise.all(images.map((img) => fitImage(img, scale)));\n}\n\n/** an image still loading gets this long, so a stalled request can't hold the snap back */\nconst DECODE_TIMEOUT_MS = 1000;\n\n/** waits for the image to be ready to draw, throws when it fails or takes too long */\nasync function decode(img: HTMLImageElement): Promise<void> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n img.decode(),\n new Promise<never>((_, reject) => {\n timeout = setTimeout(\n () => reject(new Error('image took too long to load')),\n DECODE_TIMEOUT_MS\n );\n }),\n ]);\n } catch (error) {\n // some browsers reject decode() for svg images that are loaded fine\n if (!(img.complete && img.naturalWidth > 0)) {\n throw error;\n }\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/** the size html2canvas draws the image into: its border box without borders and padding */\nfunction contentBox(img: HTMLImageElement, style: CSSStyleDeclaration): Size {\n const { width, height } = img.getBoundingClientRect();\n const px = (...values: string[]): number =>\n values.reduce((sum, value) => sum + (parseFloat(value) || 0), 0);\n return {\n width:\n width -\n px(\n style.borderLeftWidth,\n style.borderRightWidth,\n style.paddingLeft,\n style.paddingRight\n ),\n height:\n height -\n px(\n style.borderTopWidth,\n style.borderBottomWidth,\n style.paddingTop,\n style.paddingBottom\n ),\n };\n}\n\nasync function fitImage(img: HTMLImageElement, scale: number): Promise<void> {\n const style = img.ownerDocument.defaultView?.getComputedStyle(img);\n if (style == null || (style.objectFit || 'fill') === 'fill') {\n return;\n }\n // a hidden image has no box, it isn't captured and doesn't need to load\n const box = contentBox(img, style);\n if (!(box.width > 0 && box.height > 0)) {\n return;\n }\n try {\n await decode(img);\n const image = { width: img.naturalWidth, height: img.naturalHeight };\n if (!(image.width > 0 && image.height > 0)) {\n return;\n }\n const canvas = img.ownerDocument.createElement('canvas');\n canvas.width = Math.max(1, Math.round(box.width * scale));\n canvas.height = Math.max(1, Math.round(box.height * scale));\n const context = canvas.getContext('2d');\n if (context == null) {\n return;\n }\n const rect = objectFitRect(\n style.objectFit,\n style.objectPosition,\n box,\n image\n );\n context.drawImage(\n img,\n rect.x * scale,\n rect.y * scale,\n rect.width * scale,\n rect.height * scale\n );\n // throws for an image from another origin, that one stays stretched\n const fitted = canvas.toDataURL();\n // the sources of a picture would win over the image's own src\n if (img.parentElement?.tagName === 'PICTURE') {\n img.parentElement\n .querySelectorAll('source')\n .forEach((source) => source.remove());\n }\n img.srcset = '';\n img.src = fitted;\n await decode(img);\n } catch (error) {\n console.debug('ws-thanos: could not fit image for the capture', error);\n }\n}\n"]}
@@ -1 +1,14 @@
1
+ /**
2
+ * How the element breaks apart (needs WebGL2, the canvas fallback always uses 'dust'):
3
+ * - dust: particles blow away along the vaporizing front
4
+ * - cracks: cracks spread through the element before it turns to dust
5
+ * - shards: cracks split the element into shards that shift and tilt before they turn to dust
6
+ * - chunks: whole chunks break off, jump away and then crumble to dust
7
+ */
8
+ export const WS_THANOS_CRUMBLES = [
9
+ 'dust',
10
+ 'cracks',
11
+ 'shards',
12
+ 'chunks',
13
+ ];
1
14
  //# sourceMappingURL=ws-thanos.options.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws-thanos.options.js","sourceRoot":"","sources":["../../../../../../libs/public/ws-thanos/src/lib/ws-thanos.options.ts"],"names":[],"mappings":"","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 type WsThanosCrumble = 'dust' | 'cracks' | 'shards' | 'chunks';\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 /** warm up the effect while the browser is idle, so the first snap does not stutter */\n preload: boolean;\n}\n"]}
@@ -2,6 +2,7 @@ import { DOCUMENT, Injectable, inject } from '@angular/core';
2
2
  import { default as html2canvas } from 'html2canvas';
3
3
  import { animationFrameScheduler, defer, finalize, from, interval, map, switchMap, takeWhile, tap, timeInterval, } from 'rxjs';
4
4
  import { EFFECT_HEIGHT_SCALE, EFFECT_WIDTH_SCALE } from './capture-scale';
5
+ import { fitImagesForCapture } from './fit-images';
5
6
  import { chooseParticleRenderer, GpuContextBudget, } from './choose-particle-renderer';
6
7
  import { createParticles } from './particles';
7
8
  import { SnapSoundService } from './snap-sound.service';
@@ -156,6 +157,7 @@ export class WsThanosService {
156
157
  windowHeight: window?.innerHeight,
157
158
  windowWidth: window?.innerWidth,
158
159
  scrollY: -(window?.scrollY ?? 0),
160
+ onclone: (_document, clonedElem) => fitImagesForCapture(clonedElem, scale),
159
161
  });
160
162
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
161
163
  return { imageData, width, height, scale };
@@ -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 {\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 });\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 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"]}
@@ -0,0 +1,21 @@
1
+ export interface Size {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ /** where the image is drawn inside its content box, can reach outside of it for cover */
6
+ export interface FitRect extends Size {
7
+ x: number;
8
+ y: number;
9
+ }
10
+ /**
11
+ * Rect the browser draws an image into for the css object-fit and object-position,
12
+ * relative to the image's content box.
13
+ */
14
+ export declare function objectFitRect(objectFit: string, objectPosition: string, box: Size, image: Size): FitRect;
15
+ /**
16
+ * html2canvas stretches every image over its content box, it ignores object-fit and
17
+ * object-position. This draws each image of the cloned element the way the browser shows it,
18
+ * at the capture scale, and swaps it in, so html2canvas only has to stretch an image of the box size.
19
+ * Images that are not loaded or are from another origin stay as they are.
20
+ */
21
+ export declare function fitImagesForCapture(clonedElem: HTMLElement, scale: number): Promise<void>;
@@ -5,7 +5,9 @@
5
5
  * - shards: cracks split the element into shards that shift and tilt before they turn to dust
6
6
  * - chunks: whole chunks break off, jump away and then crumble to dust
7
7
  */
8
- export type WsThanosCrumble = 'dust' | 'cracks' | 'shards' | 'chunks';
8
+ export declare const WS_THANOS_CRUMBLES: readonly ["dust", "cracks", "shards", "chunks"];
9
+ /** one of {@link WS_THANOS_CRUMBLES} */
10
+ export type WsThanosCrumble = (typeof WS_THANOS_CRUMBLES)[number];
9
11
  export interface WsThanosOptions {
10
12
  animationLength: number;
11
13
  maxParticleCount: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolsok/thanos",
3
- "version": "18.2.3",
3
+ "version": "18.3.1",
4
4
  "description": "Wolf Soko's implementation of a cool Thanos snapping vaporizing effect for destroying dom elements",
5
5
  "bugs": {
6
6
  "url": "https://github.com/WolfSoko/wol-sok-mono/issues"