UNPKG

ngx-acuw

Version:

Angular components using WEBGL (threejs)

1,190 lines (1,144 loc) 139 kB
import * as i0 from '@angular/core'; import { Component, ChangeDetectionStrategy, ViewChild, Input, HostListener, NgModule, EventEmitter, Output, Directive, ContentChildren } from '@angular/core'; import { Texture, WebGLRenderer, Scene, Clock, Vector2, Raycaster, PerspectiveCamera, TextureLoader, LinearFilter, RGBAFormat, RawShaderMaterial, InstancedBufferGeometry, BufferAttribute, InstancedBufferAttribute, Mesh, Object3D, PlaneGeometry, MeshBasicMaterial, PlaneBufferGeometry, ShaderMaterial, DoubleSide, Vector4, Quaternion, Euler, Group, Vector3 } from 'three'; import { Observable, interval, Subscription, Subject, of, zip, NEVER } from 'rxjs'; import * as i1$1 from '@angular/animations'; import { trigger, transition, style, animate, query, stagger, state } from '@angular/animations'; import * as i2 from '@angular/common'; import { CommonModule } from '@angular/common'; import { switchMap, delay, tap } from 'rxjs/operators'; import { ComponentPortal } from '@angular/cdk/portal'; import * as i1 from '@angular/cdk/overlay'; import { OverlayModule } from '@angular/cdk/overlay'; import { CSS3DRenderer, CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer'; class TouchTexture { constructor() { this.size = 64; this.maxAge = 120; this.radius = 0.15; this.trail = new Array(); this.initTexture(); } /** * Initializes the texture for the touch area */ initTexture() { this.canvas = document.createElement('canvas'); this.canvas.width = this.canvas.height = this.size; this.ctx = this.canvas.getContext('2d'); this.ctx.fillStyle = 'black'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.texture = new Texture(this.canvas); this.canvas.id = 'touchTexture'; this.canvas.style.width = this.canvas.style.height = `${this.canvas.width}px`; } /** * Updates the trail */ update() { this.clear(); // age points this.trail.forEach((point, i) => { point.age++; // remove old if (point.age > this.maxAge) { this.trail.splice(i, 1); } }); this.trail.forEach((point, i) => { this.drawTouch(point); }); this.texture.needsUpdate = true; } clear() { this.ctx.fillStyle = 'black'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } addTouch(px, py) { let force = 0; const last = this.trail[this.trail.length - 1]; if (last) { const dx = last.x - px; const dy = last.y - py; const dd = dx * dx + dy * dy; force = Math.min(dd * 10000, 1); } this.trail.push({ x: px, y: py, age: 0, force }); } drawTouch(point) { const pos = { x: point.x * this.size, y: (1 - point.y) * this.size }; let intensity = 1; if (point.age < this.maxAge * 0.3) { intensity = this.easeOutSine(point.age / (this.maxAge * 0.3), 0, 1, 1); } else { intensity = this.easeOutSine(1 - (point.age - this.maxAge * 0.3) / (this.maxAge * 0.7), 0, 1, 1); } intensity *= point.force; const radius = this.size * this.radius * intensity; const grd = this.ctx.createRadialGradient(pos.x, pos.y, radius * 0.25, pos.x, pos.y, radius); grd.addColorStop(0, `rgba(255, 255, 255, 0.2)`); grd.addColorStop(1, 'rgba(0, 0, 0, 0.0)'); this.ctx.beginPath(); this.ctx.fillStyle = grd; this.ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2); this.ctx.fill(); } easeOutSine(t, b, c, d) { return c * Math.sin(t / d * (Math.PI / 2)) + b; } } class Shaders { constructor() { this.particleVertex = ` // @author brunoimbrizi / http://brunoimbrizi.com precision highp float; #define GLSLIFY 1 attribute float pindex; attribute vec3 position; attribute vec3 offset; attribute vec2 uv; attribute float angle; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform float uTime; uniform float uRandom; uniform float uDepth; uniform float uSize; uniform vec2 uTextureSize; uniform sampler2D uTexture; uniform sampler2D uTouch; varying vec2 vPUv; varying vec2 vUv; // // Description : Array and textureless GLSL 2D simplex noise function. // Author : Ian McEwan, Ashima Arts. // Maintainer : ijm // Lastmod : 20110822 (ijm) // License : Copyright (C) 2011 Ashima Arts. All rights reserved. // Distributed under the MIT License. See LICENSE file. // https://github.com/ashima/webgl-noise // vec3 mod289_1_0(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec2 mod289_1_0(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec3 permute_1_1(vec3 x) { return mod289_1_0(((x*34.0)+1.0)*x); } float snoise_1_2(vec2 v) { const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0 0.366025403784439, // 0.5*(sqrt(3.0)-1.0) -0.577350269189626, // -1.0 + 2.0 * C.x 0.024390243902439); // 1.0 / 41.0 // First corner vec2 i = floor(v + dot(v, C.yy) ); vec2 x0 = v - i + dot(i, C.xx); // Other corners vec2 i1; //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0 //i1.y = 1.0 - i1.x; i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); // x0 = x0 - 0.0 + 0.0 * C.xx ; // x1 = x0 - i1 + 1.0 * C.xx ; // x2 = x0 - 1.0 + 2.0 * C.xx ; vec4 x12 = x0.xyxy + C.xxzz; x12.xy -= i1; // Permutations i = mod289_1_0(i); // Avoid truncation effects in permutation vec3 p = permute_1_1( permute_1_1( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); m = m*m ; m = m*m ; // Gradients: 41 points uniformly over a line, mapped onto a diamond. // The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287) vec3 x = 2.0 * fract(p * C.www) - 1.0; vec3 h = abs(x) - 0.5; vec3 ox = floor(x + 0.5); vec3 a0 = x - ox; // Normalise gradients implicitly by scaling m // Approximation of: m *= inversesqrt( a0*a0 + h*h ); m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); // Compute final noise value at P vec3 g; g.x = a0.x * x0.x + h.x * x0.y; g.yz = a0.yz * x12.xz + h.yz * x12.yw; return 130.0 * dot(m, g); } float random(float n) { return fract(sin(n) * 43758.5453123); } void main() { vUv = uv; // particle uv vec2 puv = offset.xy / uTextureSize; vPUv = puv; // pixel color vec4 colA = texture2D(uTexture, puv); float grey = colA.r * 0.21 + colA.g * 0.71 + colA.b * 0.07; // displacement vec3 displaced = offset; // randomise displaced.xy += vec2(random(pindex) - 0.5, random(offset.x + pindex) - 0.5) * uRandom; float rndz = (random(pindex) + snoise_1_2(vec2(pindex * 0.1, uTime * 0.1))); displaced.z += rndz * (random(pindex) * 2.0 * uDepth); // center displaced.xy -= uTextureSize * 0.5; // touch float t = texture2D(uTouch, puv).r; displaced.z += t * 20.0 * rndz; displaced.x += cos(angle) * t * 20.0 * rndz; displaced.y += sin(angle) * t * 20.0 * rndz; // particle size float psize = (snoise_1_2(vec2(uTime, pindex) * 0.5) + 2.0); psize *= max(grey, 0.2); psize *= uSize; // final position vec4 mvPosition = modelViewMatrix * vec4(displaced, 1.0); mvPosition.xyz += position * psize; vec4 finalPosition = projectionMatrix * mvPosition; gl_Position = finalPosition; } `; this.particleFragment = ` // @author brunoimbrizi / http://brunoimbrizi.com precision highp float; #define GLSLIFY 1 uniform sampler2D uTexture; varying vec2 vPUv; varying vec2 vUv; void main() { vec4 color = vec4(0.0); vec2 uv = vUv; vec2 puv = vPUv; // pixel color vec4 colA = texture2D(uTexture, puv); // greyscale float grey = colA.r * 0.21 + colA.g * 0.71 + colA.b * 0.07; //vec4 colB = vec4(grey, grey, grey, 1.0); vec4 colB = vec4(colA.r, colA.g, colA.b, 1.0); // circle float border = 0.3; float radius = 0.5; float dist = radius - distance(uv, vec2(0.5)); float t = smoothstep(0.0, border, dist); // final color color = colB; color.a = t; gl_FragColor = color; } `; } } var RxjsTween; (function (RxjsTween) { function createTween(easingFunction, b, c, d, s) { return new Observable((observer) => { let startTime; const sample = (time) => { startTime = startTime || time; const t = time - startTime; if (t < d) { if (Array.isArray(b) && Array.isArray(c)) { const tweenVals = new Array(); for (let idx = 0; idx < b.length; idx++) { tweenVals.push(easingFunction(t, b[idx], c[idx], d, s)); } observer.next(tweenVals); } else { observer.next(easingFunction(t, b, c, d, s)); } // Request the animation frame again requestAnimationFrame(sample); } else { // End value reached if (Array.isArray(b) && Array.isArray(c)) { const tweenVals = new Array(); for (let idx = 0; idx < b.length; idx++) { tweenVals.push(c[idx]); } // Emitt end value of arry observer.next(tweenVals); } else { // Emitt end value observer.next(c); } // Complete the observable observer.complete(); } }; // Initially request the animation frame requestAnimationFrame(sample); }); } RxjsTween.createTween = createTween; function linear(t, b, pc, d) { const c = pc - b; return c * t / d + b; } RxjsTween.linear = linear; function easeInOutQuad(t, b, pc, d) { const c = pc - b; if ((t /= d / 2) < 1) { return c / 2 * t * t + b; } else { return -c / 2 * ((--t) * (t - 2) - 1) + b; } } RxjsTween.easeInOutQuad = easeInOutQuad; })(RxjsTween || (RxjsTween = {})); class PerformanceMonitorComponent { constructor(changeDetector) { this.changeDetector = changeDetector; this.fps = -1; this.fpsMin = -1; this.fpsMax = -1; this.framesCnt = 0; this.timeLastFpsCalc = 0; this.fpsHistory = new Array(); this.maxHistoryLength = 60; } /** * Method to be called at the end of a frame */ end() { // When called the first time, set the current time and return if (this.timeLastFpsCalc === 0) { this.timeLastFpsCalc = Date.now(); return; } // Increase the frames counter this.framesCnt++; // Get the milliseconds elapsed since January 1, 1970 for the current frame const currentFrameTime = Date.now(); // Calculate the FPS only every second if (currentFrameTime >= this.timeLastFpsCalc + 1000) { // Calculate the frames per second this.fps = this.framesCnt / (currentFrameTime - this.timeLastFpsCalc) * 1000; // Calculate the min. frames per second if (this.fpsMin === -1 || this.fps < this.fpsMin) { this.fpsMin = this.fps; } // Calculate the max. frames per second if (this.fpsMax === -1 || this.fps > this.fpsMax) { this.fpsMax = this.fps; } // Trigger the change detection this.changeDetector.detectChanges(); // Set the elapsed time to the timeLastFpsCalc this.timeLastFpsCalc = currentFrameTime; // Reset the frames counter this.framesCnt = 0; // Add fps to the history array this.fpsHistory.push(this.fps); if (this.fpsHistory.length >= this.maxHistoryLength) { this.fpsHistory.shift(); } // Create / Update chart const canvasEl = this.chart.nativeElement; const ctx = canvasEl.getContext('2d'); if (ctx) { ctx.fillStyle = 'rgb(30, 30, 30)'; ctx.fillRect(0, 0, canvasEl.width, canvasEl.height); ctx.strokeStyle = 'rgb(255, 255, 255)'; ctx.fillStyle = 'rgb(255, 255, 255)'; ctx.beginPath(); ctx.moveTo(0, canvasEl.height); for (let idx = 0; idx <= this.fpsHistory.length; idx++) { ctx.lineTo(canvasEl.width / this.maxHistoryLength * idx, canvasEl.height - (this.fpsHistory[idx] / this.fpsMax * canvasEl.height)); if (idx === this.fpsHistory.length - 1) { ctx.lineTo(canvasEl.width / this.maxHistoryLength * idx, canvasEl.height); } } ctx.fill(); } } } } PerformanceMonitorComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: PerformanceMonitorComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); PerformanceMonitorComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.1.1", type: PerformanceMonitorComponent, selector: "acuw-performance-monitor", viewQueries: [{ propertyName: "chart", first: true, predicate: ["chart"], descendants: true }], ngImport: i0, template: "<div id=\"pm-container\">\r\n <div id=\"fps-display-container\">\r\n <span id=\"fps-display\">FPS: {{ fps != -1 ? (fps | number: '1.0-0') : '-' }}</span>\r\n <div id=\"min-max-display\">\r\n <span id=\"max-fps-display\">{{ fpsMax != -1 ? (fpsMax | number: '1.0-0') : '-' }} max</span>\r\n <span id=\"min-fps-display\">{{ fpsMin != -1 ? (fpsMin | number: '1.0-0') : '-' }} min</span>\r\n </div>\r\n </div>\r\n <canvas #chart width=\"90\" height=\"40\"></canvas>\r\n</div>", styles: [":host{--position: absolute;--color: rgb(255, 255, 255);--background-color: rgba(0, 0, 0, .8);--top: 5px;--left: 5px;--width: 100px;--padding: 5px;--display: flex;--flex-direction: column;--align-items: center}#pm-container{position:var(--position);color:var(--color);background-color:var(--background-color);top:5px;left:5px;width:100px;padding:5px;display:var(--display);flex-direction:var(--flex-direction);align-items:var(--align-items)}#fps-display-container{display:flex;flex-direction:row;align-items:center}#min-max-display{display:flex;flex-direction:column;font-size:10px;padding-left:5px}\n"], pipes: { "number": i2.DecimalPipe }, changeDetection: i0.ChangeDetectionStrategy.OnPush }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: PerformanceMonitorComponent, decorators: [{ type: Component, args: [{ selector: 'acuw-performance-monitor', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div id=\"pm-container\">\r\n <div id=\"fps-display-container\">\r\n <span id=\"fps-display\">FPS: {{ fps != -1 ? (fps | number: '1.0-0') : '-' }}</span>\r\n <div id=\"min-max-display\">\r\n <span id=\"max-fps-display\">{{ fpsMax != -1 ? (fpsMax | number: '1.0-0') : '-' }} max</span>\r\n <span id=\"min-fps-display\">{{ fpsMin != -1 ? (fpsMin | number: '1.0-0') : '-' }} min</span>\r\n </div>\r\n </div>\r\n <canvas #chart width=\"90\" height=\"40\"></canvas>\r\n</div>", styles: [":host{--position: absolute;--color: rgb(255, 255, 255);--background-color: rgba(0, 0, 0, .8);--top: 5px;--left: 5px;--width: 100px;--padding: 5px;--display: flex;--flex-direction: column;--align-items: center}#pm-container{position:var(--position);color:var(--color);background-color:var(--background-color);top:5px;left:5px;width:100px;padding:5px;display:var(--display);flex-direction:var(--flex-direction);align-items:var(--align-items)}#fps-display-container{display:flex;flex-direction:row;align-items:center}#min-max-display{display:flex;flex-direction:column;font-size:10px;padding-left:5px}\n"] }] }], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }]; }, propDecorators: { chart: [{ type: ViewChild, args: ['chart'] }] } }); class ImageAsParticlesComponent { constructor(ngZone) { this.ngZone = ngZone; // Declare variables this.renderer = new WebGLRenderer({ antialias: true, alpha: true, }); this.scene = new Scene(); this.clock = new Clock(true); this.texture = new Texture(); this.width = 0; this.height = 0; this.touch = new TouchTexture(); this.mouse = new Vector2(); this.raycaster = new Raycaster(); this.pImageUrl = ''; this.pImageChanging = false; this.gestureInfo$ = interval(2000); this.gestureInfoSubscription = new Subscription(); this.showTouchGestureInfo = false; this.justifyContent = 'center'; this.alignItems = 'center'; this.backgroundColor = '#000000'; this.imageWidth = '100%'; this.imageHeight = '100%'; this.animationEnabled = true; this.showPerformanceMonitor = false; } // Inputs set imageUrl(imageUrl) { this.pImageUrl = imageUrl; if (this.pImageChanging === true) { return; } if (this.mesh != null) { this.pImageChanging = true; this.triggerImageChange(); } } get imageUrl() { return this.pImageUrl; } set horizontalAlignment(horizontalAlignment) { switch (horizontalAlignment) { case 'start': this.justifyContent = 'flex-start'; break; case 'center': this.justifyContent = 'center'; break; case 'end': this.justifyContent = 'flex-end'; break; default: this.justifyContent = 'center'; break; } } get horizontalAlignment() { return this.justifyContent; } set verticalAlignment(verticalAlignment) { switch (verticalAlignment) { case 'top': this.alignItems = 'flex-start'; break; case 'center': this.alignItems = 'center'; break; case 'bottom': this.alignItems = 'flex-end'; break; default: this.alignItems = 'center'; break; } } get verticalAlignment() { return this.alignItems; } ngAfterViewInit() { if (this.pImageUrl === '') { return; } const canvasWidth = this.canvasRef.nativeElement.clientWidth; const canvasHeight = this.canvasRef.nativeElement.clientHeight; // Set camera this.camera = new PerspectiveCamera(50, canvasWidth / canvasHeight, 1, 10000); this.camera.position.z = 300; // Init particles this.initParticles(this.pImageUrl); // Init renderer this.renderer.setSize(canvasWidth - 1, canvasHeight); this.canvasRef.nativeElement.appendChild(this.renderer.domElement); // Start animation this.animate(); } ngOnDestroy() { this.scene.clear(); this.renderer.clear(); this.texture.dispose(); this.renderer.dispose(); } /** * Creates the particles depending on the image and initializes the touch canvas * @param url url of the image */ initParticles(url) { const loader = new TextureLoader(); loader.load(url, (texture) => { this.texture = texture; this.texture.minFilter = LinearFilter; this.texture.magFilter = LinearFilter; this.texture.format = RGBAFormat; this.width = texture.image.width; this.height = texture.image.height; this.initPoints(true); this.initHitArea(); this.initTouch(); this.resize(); this.show(); }); } /** * Initializes the points * @param discard discard pixels darker than threshold #22 */ initPoints(discard) { const numPoints = this.width * this.height; let numVisible = numPoints; let threshold = 0; let originalColors = new Float32Array(); if (discard) { // discard pixels darker than threshold #22 numVisible = 0; threshold = 34; const img = this.texture.image; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = this.width; canvas.height = this.height; if (ctx != null) { ctx.scale(1, -1); ctx.drawImage(img, 0, 0, this.width, this.height * -1); const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); originalColors = Float32Array.from(imgData.data); for (let i = 0; i < numPoints; i++) { if (originalColors[i * 4 + 0] > threshold) { numVisible++; } } } } const uniforms = { uTime: { value: 0 }, uRandom: { value: 1.0 }, uDepth: { value: 2.0 }, uSize: { value: 0.0 }, uTextureSize: { value: new Vector2(this.width, this.height) }, uTexture: { value: this.texture }, uTouch: { value: null }, }; const shaders = new Shaders(); const material = new RawShaderMaterial({ uniforms, vertexShader: shaders.particleVertex, fragmentShader: shaders.particleFragment, depthTest: false, transparent: true, // blending: THREE.AdditiveBlending }); const geometry = new InstancedBufferGeometry(); // positions const positions = new BufferAttribute(new Float32Array(4 * 3), 3); positions.setXYZ(0, -0.5, 0.5, 0.0); positions.setXYZ(1, 0.5, 0.5, 0.0); positions.setXYZ(2, -0.5, -0.5, 0.0); positions.setXYZ(3, 0.5, -0.5, 0.0); geometry.setAttribute('position', positions); // uvs const uvs = new BufferAttribute(new Float32Array(4 * 2), 2); uvs.setXY(0, 0.0, 0.0); uvs.setXY(1, 1.0, 0.0); uvs.setXY(2, 0.0, 1.0); uvs.setXY(3, 1.0, 1.0); geometry.setAttribute('uv', uvs); // index geometry.setIndex(new BufferAttribute(new Uint16Array([0, 2, 1, 2, 3, 1]), 1)); const indices = new Uint16Array(numVisible); const offsets = new Float32Array(numVisible * 3); const angles = new Float32Array(numVisible); for (let i = 0, j = 0; i < numPoints; i++) { if (discard && originalColors[i * 4 + 0] <= threshold) { continue; } offsets[j * 3 + 0] = i % this.width; offsets[j * 3 + 1] = Math.floor(i / this.width); indices[j] = i; angles[j] = Math.random() * Math.PI; j++; } geometry.setAttribute('pindex', new InstancedBufferAttribute(indices, 1, false)); geometry.setAttribute('offset', new InstancedBufferAttribute(offsets, 3, false)); geometry.setAttribute('angle', new InstancedBufferAttribute(angles, 1, false)); this.mesh = new Mesh(geometry, material); const object3d = new Object3D(); object3d.add(this.mesh); this.scene.add(object3d); } /** * Initializes the touch area */ initTouch() { this.mesh.material.uniforms.uTouch.value = this.touch.texture; } /** * Initializes the hit area */ initHitArea() { const geometry = new PlaneGeometry(this.width, this.height, 1, 1); const material = new MeshBasicMaterial({ color: 0xffffff, wireframe: true, depthTest: false, }); material.visible = false; this.hitArea = new Mesh(geometry, material); this.mesh.add(this.hitArea); } /** * animation for showing the particles * @param time time of animation in ms */ show(time = 1000) { // Tween in this.ngZone.runOutsideAngular(() => { RxjsTween.createTween(RxjsTween.easeInOutQuad, [0.5, 0.0, 70.0], [1.5, 2.0, 4.0], time).subscribe((val) => { this.mesh.material.uniforms.uSize.value = val[0]; this.mesh.material.uniforms.uRandom.value = val[1]; this.mesh.material.uniforms.uDepth.value = val[2]; }, () => { }, () => { this.pImageChanging = false; }); }); } /** * animation for tween out the particles and destroy everything * @param time time of animation in ms */ triggerImageChange(time = 1000) { const uSizeStart = this.mesh.material.uniforms.uSize .value; const uRandomStart = this.mesh.material.uniforms .uRandom.value; const uDepth = this.mesh.material.uniforms.uDepth .value; this.ngZone.runOutsideAngular(() => { // Tween out RxjsTween.createTween(RxjsTween.easeInOutQuad, [uSizeStart, uRandomStart, uDepth], [0.0, 5.0, -20.0], time).subscribe((val) => { this.mesh.material.uniforms.uSize.value = val[0]; this.mesh.material.uniforms.uRandom.value = val[1]; this.mesh.material.uniforms.uDepth.value = val[2]; }, () => { }, () => { if (this.mesh != null) { if (this.mesh.parent != null) { this.mesh.parent.remove(this.mesh); } this.mesh.geometry.dispose(); this.mesh.material.dispose(); } if (this.hitArea != null) { if (this.hitArea.parent != null) { this.hitArea.parent.remove(this.hitArea); } this.hitArea.geometry.dispose(); this.hitArea.material.dispose(); } this.initParticles(this.pImageUrl); this.pImageChanging = false; }); }); } /** * Method for triggering the animation */ animate() { this.ngZone.runOutsideAngular(() => { window.requestAnimationFrame(() => this.animate()); if (this.animationEnabled === true) { const delta = this.clock.getDelta(); if (this.mesh != null) { if (this.touch) { this.touch.update(); } this.mesh.material.uniforms.uTime.value += delta; } this.renderer.render(this.scene, this.camera); } if (this.performanceMonitor && this.showPerformanceMonitor) { this.performanceMonitor.end(); } }); } /** * Handle mouse move event * @param event mouse event */ onMouseMove(event) { // getBoundingClientRect retruns the distance in pixels of the top left corner of the element // to the top left corner of the viewport const domRect = this.canvasRef.nativeElement.getBoundingClientRect(); // get the offset distance between the canvas, which contains the particles, to the outer container element const canvasEl = this.canvasRef.nativeElement .children[0]; // Calculate the relative mouse position this.mouse.x = ((event.clientX - domRect.left - canvasEl.offsetLeft) / canvasEl.clientWidth) * 2 - 1; this.mouse.y = (-(event.clientY - domRect.top - canvasEl.offsetTop) / canvasEl.clientHeight) * 2 + 1; // console.info('raw: x= ' + event.clientX + ' , y= ' + event.clientY); // console.info('normalized: x= ' + this.mouse.x + ' , y= ' + this.mouse.y); this.raycaster.setFromCamera(this.mouse, this.camera); if (this.hitArea === undefined) { return; } const intersects = this.raycaster.intersectObject(this.hitArea); if (intersects !== undefined && intersects.length > 0 && this.touch && intersects[0].uv !== undefined) { this.touch.addTouch(intersects[0].uv.x, intersects[0].uv.y); } } /** * Handle touch move envent * @param event mouse event */ onTouchMove(event) { if (event.touches.length < 2) { this.showTouchGestureInfo = true; this.gestureInfoSubscription.unsubscribe(); this.gestureInfoSubscription = this.gestureInfo$.subscribe({ next: () => { this.showTouchGestureInfo = false; this.gestureInfoSubscription.unsubscribe(); }, }); return; } event.preventDefault(); // getBoundingClientRect retruns the distance in pixels of the top left corner of the element // to the top left corner of the viewport const domRect = this.canvasRef.nativeElement.getBoundingClientRect(); // get the offset distance between the canvas, which contains the particles, to the outer container element const canvasEl = this.canvasRef.nativeElement .children[0]; // Calculate the relative mouse position this.mouse.x = ((event.touches[0].clientX - domRect.left - canvasEl.offsetLeft) / canvasEl.clientWidth) * 2 - 1; this.mouse.y = (-(event.touches[0].clientY - domRect.top - canvasEl.offsetTop) / canvasEl.clientHeight) * 2 + 1; this.raycaster.setFromCamera(this.mouse, this.camera); const intersects = this.raycaster.intersectObject(this.hitArea); if (intersects !== undefined && intersects.length > 0 && this.touch && intersects[0].uv !== undefined) { this.touch.addTouch(intersects[0].uv.x, intersects[0].uv.y); } } resize() { if (this.height !== undefined) { this.camera.aspect = this.canvasRef.nativeElement.clientWidth / this.canvasRef.nativeElement.clientHeight; this.camera.updateProjectionMatrix(); const fovHeight = 2 * Math.tan((this.camera.fov * Math.PI) / 180 / 2) * this.camera.position.z; const scale = fovHeight / this.height; this.mesh.scale.set(scale, scale, 1); // this.hitArea.scale.set(scale, scale, 1); if (this.renderer !== undefined) { const width = this.imageWidth == null ? this.canvasRef.nativeElement.clientWidth : this.distanceAsNumber(this.imageWidth, this.canvasRef.nativeElement.clientWidth); const height = this.imageHeight == null ? this.canvasRef.nativeElement.clientHeight : this.distanceAsNumber(this.imageHeight, this.canvasRef.nativeElement.clientHeight); this.renderer.setSize(width, height); } } } distanceAsNumber(distance, parentDistance) { let returnVal = 0; if (distance.includes('px')) { returnVal = Number.parseInt(distance.replace('px', ''), 10); } else if (distance.includes('%')) { returnVal = (Number.parseInt(distance.replace('%', ''), 10) / 100) * parentDistance; } else { returnVal = Number.parseInt(distance, 10); } return returnVal; } } ImageAsParticlesComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: ImageAsParticlesComponent, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); ImageAsParticlesComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.1.1", type: ImageAsParticlesComponent, selector: "lib-image-as-particles", inputs: { imageUrl: "imageUrl", backgroundColor: "backgroundColor", imageWidth: "imageWidth", imageHeight: "imageHeight", horizontalAlignment: "horizontalAlignment", verticalAlignment: "verticalAlignment", animationEnabled: "animationEnabled", showPerformanceMonitor: "showPerformanceMonitor" }, host: { listeners: { "window:resize": "resize()" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["container"], descendants: true }, { propertyName: "performanceMonitor", first: true, predicate: ["performanceMonitor"], descendants: true }], ngImport: i0, template: ` <div #container class="threejs-container" [style.background-color]="backgroundColor" [style.justify-content]="justifyContent" [style.align-items]="alignItems" (mousemove)="onMouseMove($event)" (touchmove)="onTouchMove($event)" ></div> <div *ngIf="showTouchGestureInfo == true" class="touch-gesture-info" [@showHideGestureInformation] > <div> <span>Use two fingers for touch animation</span> <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="white" width="18px" height="18px" > <g><rect fill="none" height="24" width="24" x="0" /></g> <g> <g> <g> <path d="M9,11.24V7.5C9,6.12,10.12,5,11.5,5S14,6.12,14,7.5v3.74c1.21-0.81,2-2.18,2-3.74C16,5.01,13.99,3,11.5,3S7,5.01,7,7.5 C7,9.06,7.79,10.43,9,11.24z M18.84,15.87l-4.54-2.26c-0.17-0.07-0.35-0.11-0.54-0.11H13v-6C13,6.67,12.33,6,11.5,6 S10,6.67,10,7.5v10.74c-3.6-0.76-3.54-0.75-3.67-0.75c-0.31,0-0.59,0.13-0.79,0.33l-0.79,0.8l4.94,4.94 C9.96,23.83,10.34,24,10.75,24h6.79c0.75,0,1.33-0.55,1.44-1.28l0.75-5.27c0.01-0.07,0.02-0.14,0.02-0.2 C19.75,16.63,19.37,16.09,18.84,15.87z" /> </g> </g> </g> </svg> </div> </div> <acuw-performance-monitor *ngIf="showPerformanceMonitor" #performanceMonitor ></acuw-performance-monitor> `, isInline: true, styles: [".threejs-container{position:relative;display:flex;justify-content:center;align-items:center;width:100%;height:100%;background-color:#222}.touch-gesture-info{position:absolute;width:100%;display:flex;flex-direction:row;justify-content:center;top:20px;color:#fff}.touch-gesture-info div{background-color:#0000004d;display:flex;flex-direction:row;padding:6px 10px;border-radius:5px}\n"], components: [{ type: PerformanceMonitorComponent, selector: "acuw-performance-monitor" }], directives: [{ type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], animations: [ trigger('showHideGestureInformation', [ transition(':enter', [ style({ opacity: '0' }), animate('300ms ease-in', style({ opacity: '1' })), ]), transition(':leave', [ style({ opacity: '1' }), animate('300ms ease-in', style({ opacity: '0' })), ]), ]), ] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: ImageAsParticlesComponent, decorators: [{ type: Component, args: [{ selector: 'lib-image-as-particles', template: ` <div #container class="threejs-container" [style.background-color]="backgroundColor" [style.justify-content]="justifyContent" [style.align-items]="alignItems" (mousemove)="onMouseMove($event)" (touchmove)="onTouchMove($event)" ></div> <div *ngIf="showTouchGestureInfo == true" class="touch-gesture-info" [@showHideGestureInformation] > <div> <span>Use two fingers for touch animation</span> <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="white" width="18px" height="18px" > <g><rect fill="none" height="24" width="24" x="0" /></g> <g> <g> <g> <path d="M9,11.24V7.5C9,6.12,10.12,5,11.5,5S14,6.12,14,7.5v3.74c1.21-0.81,2-2.18,2-3.74C16,5.01,13.99,3,11.5,3S7,5.01,7,7.5 C7,9.06,7.79,10.43,9,11.24z M18.84,15.87l-4.54-2.26c-0.17-0.07-0.35-0.11-0.54-0.11H13v-6C13,6.67,12.33,6,11.5,6 S10,6.67,10,7.5v10.74c-3.6-0.76-3.54-0.75-3.67-0.75c-0.31,0-0.59,0.13-0.79,0.33l-0.79,0.8l4.94,4.94 C9.96,23.83,10.34,24,10.75,24h6.79c0.75,0,1.33-0.55,1.44-1.28l0.75-5.27c0.01-0.07,0.02-0.14,0.02-0.2 C19.75,16.63,19.37,16.09,18.84,15.87z" /> </g> </g> </g> </svg> </div> </div> <acuw-performance-monitor *ngIf="showPerformanceMonitor" #performanceMonitor ></acuw-performance-monitor> `, styles: [ ` .threejs-container { position: relative; display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background-color: #222222; } .touch-gesture-info { position: absolute; width: 100%; display: flex; flex-direction: row; justify-content: center; top: 20px; color: white; } .touch-gesture-info div { background-color: rgba(0, 0, 0, 0.3); display: flex; flex-direction: row; padding: 6px 10px 6px 10px; border-radius: 5px; } `, ], animations: [ trigger('showHideGestureInformation', [ transition(':enter', [ style({ opacity: '0' }), animate('300ms ease-in', style({ opacity: '1' })), ]), transition(':leave', [ style({ opacity: '1' }), animate('300ms ease-in', style({ opacity: '0' })), ]), ]), ], }] }], ctorParameters: function () { return [{ type: i0.NgZone }]; }, propDecorators: { imageUrl: [{ type: Input }], backgroundColor: [{ type: Input }], imageWidth: [{ type: Input }], imageHeight: [{ type: Input }], horizontalAlignment: [{ type: Input }], verticalAlignment: [{ type: Input }], animationEnabled: [{ type: Input }], showPerformanceMonitor: [{ type: Input }], canvasRef: [{ type: ViewChild, args: ['container'] }], performanceMonitor: [{ type: ViewChild, args: ['performanceMonitor'] }], resize: [{ type: HostListener, args: ['window:resize'] }] } }); class PerformanceMonitorModule { } PerformanceMonitorModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: PerformanceMonitorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); PerformanceMonitorModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: PerformanceMonitorModule, declarations: [PerformanceMonitorComponent], imports: [CommonModule], exports: [PerformanceMonitorComponent] }); PerformanceMonitorModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: PerformanceMonitorModule, imports: [[ CommonModule ]] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: PerformanceMonitorModule, decorators: [{ type: NgModule, args: [{ declarations: [ PerformanceMonitorComponent ], imports: [ CommonModule ], exports: [ PerformanceMonitorComponent ] }] }] }); class ImageAsParticlesModule { } ImageAsParticlesModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: ImageAsParticlesModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); ImageAsParticlesModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: ImageAsParticlesModule, declarations: [ImageAsParticlesComponent], imports: [CommonModule, PerformanceMonitorModule], exports: [ImageAsParticlesComponent] }); ImageAsParticlesModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: ImageAsParticlesModule, imports: [[ CommonModule, PerformanceMonitorModule ]] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.1.1", ngImport: i0, type: ImageAsParticlesModule, decorators: [{ type: NgModule, args: [{ declarations: [ImageAsParticlesComponent], imports: [ CommonModule, PerformanceMonitorModule ], exports: [ImageAsParticlesComponent] }] }] }); var Direction; (function (Direction) { Direction[Direction["forward"] = 0] = "forward"; Direction[Direction["backward"] = 1] = "backward"; })(Direction || (Direction = {})); class ImageTransitionShaders { constructor() { this.vertex = `varying vec2 vUv;void main() {vUv = uv;gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );}`; this.splitTransitionFrag = ` uniform float progress; uniform float intensity; uniform sampler2D texture1; uniform sampler2D texture2; uniform vec4 resolution1; uniform vec4 resolution2; varying vec2 vUv; mat2 rotate(float a) { float s = sin(a); float c = cos(a); return mat2(c, -s, s, c); } void main() { vec2 newUV1 = (vUv - vec2(0.5,0.5))*resolution1.zw + vec2(0.5,0.5); vec2 newUV2 = (vUv - vec2(0.5,0.5))*resolution2.zw + vec2(0.5,0.5); vec2 uvDivided1 = fract(newUV1*vec2(intensity,1.)); vec2 uvDivided2 = fract(newUV2*vec2(intensity,1.)); vec2 uvDisplaced1 = newUV1 + rotate(3.1415926/4.)*uvDivided1*progress*0.1; vec2 uvDisplaced2 = newUV2 + rotate(3.1415926/4.)*uvDivided2*(1. - progress)*0.1; vec4 t1 = texture2D(texture1,uvDisplaced1); vec4 t2 = texture2D(texture2,uvDisplaced2); // Use black background color // Top right vec2 tr1 = step(newUV1, vec2(1.0, 1.0)); vec2 tr2 = step(newUV2, vec2(1.0, 1.0)); float pct1 = tr1.x * tr1.y; float pct2 = tr2.x * tr2.y; // Bottom left vec2 bl1 = step(vec2(0.0, 0.0), newUV1); vec2 bl2 = step(vec2(0.0, 0.0), newUV2); pct1 *= bl1.x * bl1.y; pct2 *= bl2.x * bl2.y; vec4 t1wb = t1 * vec4(pct1,pct1,pct1,1.0); vec4 t2wb = t2 * vec4(pct2,pct2,pct2,1.0); gl_FragColor = mix(t1wb, t2wb, progress); } `; this.fadeFrag = ` uniform float progress; uniform sampler2D texture1; uniform sampler2D texture2; uniform vec4 resolution1; uniform vec4 resolution2; varying vec2 vUv; mat2 rotate(float a) { float s = sin(a); float c = cos(a); return mat2(c, -s, s, c); } void main() { vec2 newUV1 = (vUv - vec2(0.5,0.5))*resolution1.zw + vec2(0.5,0.5); vec2 newUV2 = (vUv - vec2(0.5,0.5))*resolution2.zw + vec2(0.5,0.5); vec2 uvDisplaced1 = newUV1 + vec2(1.0,0)*progress*0.1; vec2 uvDisplaced2 = newUV2 + vec2(1.0,0)*(1. - progress)*0.1; vec4 t1 = texture2D(texture1,uvDisplaced1); vec4 t2 = texture2D(texture2,uvDisplaced2); // Use black background color // Top right vec2 tr1 = step(newUV1, vec2(1.0, 1.0)); vec2 tr2 = step(newUV2, vec2(1.0, 1.0)); float pct1 = tr1.x * tr1.y; float pct2 = tr2.x * tr2.y; // Bottom left vec2 bl1 = step(vec2(0.0, 0.0), newUV1); vec2 bl2 = step(vec2(0.0, 0.0), newUV2); pct1 *= bl1.x * bl1.y; pct2 *= bl2.x * bl2.y; vec4 t1wb = t1 * vec4(pct1,pct1,pct1,1.0); vec4 t2wb = t2 * vec4(pct2,pct2,pct2,1.0); gl_FragColor = mix(t1wb, t2wb, progress); } `; this.noiseFrag = ` uniform float time; uniform float progress; uniform float width; uniform float scaleX; uniform float scaleY; uniform sampler2D texture1; uniform sampler2D texture2; uniform sampler2D displacement; uniform vec4 resolution1; uniform vec4 resolution2; varying vec2 vUv; varying vec4 vPosition; // Classic Perlin 3D Noise // by Stefan Gustavson // vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} vec4 fade(vec4 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);} float cnoise(vec4 P){ ; vec4 Pi0 = floor(P); // Integer part for indexing vec4 Pi1 = Pi0 + 1.0; // Integer part + 1 Pi0 = mod(Pi0, 289.0); Pi1 = mod(Pi1, 289.0); vec4 Pf0 = fract(P); // Fractional part for interpolation vec4 Pf1 = Pf0 - 1.0; // Fractional part - 1.0 vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); vec4 iy = vec4(Pi0.yy, Pi1.yy); vec4 iz0 = vec4(Pi0.zzzz); vec4 iz1 = vec4(Pi1.zzzz); vec4 iw0 = vec4(Pi0.wwww); vec4 iw1 = vec4(Pi1.wwww); vec4 ixy = permute(permute(ix) + iy); vec4 ixy0 = permute(ixy + iz0); vec4 ixy1 = permute(ixy + iz1); vec4 ixy00 = permute(ixy0 + iw0); vec4 ixy01 = permute(ixy0 + iw1); vec4 ixy10 = permute(ixy1 + iw0); vec4 ixy11 = permute(ixy1 + iw1); vec4 gx00 = ixy00 / 7.0; vec4 gy00 = floor(gx00) / 7.0; vec4 gz00 = floor(gy00) / 6.0; gx00 = fract(gx00) - 0.5; gy00 = fract(gy00) - 0.5; gz00 = fract(gz00) - 0.5; vec4 gw00 = vec4(0.75) - abs(gx00) - abs(gy00) - abs(gz00); vec4 sw00 = step(gw00, vec4(0.0)); gx00