diff --git a/frontend/src/app.module.ts b/frontend/src/app.module.ts index 4a21d90..42e459a 100644 --- a/frontend/src/app.module.ts +++ b/frontend/src/app.module.ts @@ -16,7 +16,7 @@ import { AppComponent } from './components/app.component' import { MainComponent } from './components/main.component' import { ConfigModalComponent } from './components/configModal.component' import { SettingsModalComponent } from './components/settingsModal.component' -import { HomeComponent, HomeComponentPreloadResolver } from './components/home.component' +import { HomeComponent } from './components/home.component' import { LoginComponent } from './components/login.component' import { ConnectionListComponent } from './components/connectionList.component' import { UpgradeModalComponent } from './components/upgradeModal.component' @@ -33,7 +33,6 @@ const ROUTES = [ component: HomeComponent, resolve: { instanceInfo: InstanceInfoResolver, - preload: HomeComponentPreloadResolver, }, children: [ { diff --git a/frontend/src/components/home.component.ts b/frontend/src/components/home.component.ts index 9bb19f5..c8e00d9 100644 --- a/frontend/src/components/home.component.ts +++ b/frontend/src/components/home.component.ts @@ -1,6 +1,7 @@ -import { Component, Injectable } from '@angular/core' -import { ActivatedRoute, Router, Resolve } from '@angular/router' +import { Component } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' import { faCoffee, faDownload, faSignInAlt } from '@fortawesome/free-solid-svg-icons' +import type { Waves } from '../homepage/vanta/vanta.waves.js' import { InstanceInfo } from '../api' @@ -32,6 +33,8 @@ export class HomeComponent { instanceInfo: InstanceInfo + background: Waves|undefined + constructor ( public route: ActivatedRoute, public router: Router, @@ -42,14 +45,9 @@ export class HomeComponent { } } - static async preload () { - const three = await import(/* webpackChunkName: "gfx" */ 'three') - window['THREE'] = three - await import(/* webpackChunkName: "gfx" */ 'vanta/src/vanta.waves.js') - } - async ngAfterViewInit (): Promise { - window['VANTA'].WAVES({ + const { Waves } = await import(/* webpackChunkName: "gfx" */ '../homepage/vanta/vanta.waves.js') + this.background = new Waves({ el: 'body', mouseControls: true, touchControls: true, @@ -61,11 +59,8 @@ export class HomeComponent { color: 0x70f }) } -} -@Injectable({ providedIn: 'root' }) -export class HomeComponentPreloadResolver implements Resolve> { - resolve () { - return HomeComponent.preload() + ngOnDestroy () { + this.background?.destroy() } } diff --git a/frontend/src/components/homeFeatures.component.scss b/frontend/src/components/homeFeatures.component.scss index 53a165a..b346804 100644 --- a/frontend/src/components/homeFeatures.component.scss +++ b/frontend/src/components/homeFeatures.component.scss @@ -1,3 +1,8 @@ :host { font-size: 14px; } + +.card-img-top { + aspect-ratio: 2; + object-fit: cover; +} diff --git a/frontend/src/homepage/vanta/_base.js b/frontend/src/homepage/vanta/_base.js new file mode 100644 index 0000000..19e016d --- /dev/null +++ b/frontend/src/homepage/vanta/_base.js @@ -0,0 +1,400 @@ +import {extend, mobileCheck, q, color2Hex} from 'vanta/src/helpers.js' +// const DEBUGMODE = window.location.toString().indexOf('VANTADEBUG') !== -1 + +const win = typeof window == 'object' +if (win && !window.VANTA) window.VANTA = {} +const VANTA = (win && window.VANTA) || {} +VANTA.register = (name, Effect) => { + return VANTA[name] = (opts) => new Effect(opts) +} +VANTA.version = '0.5.21' + +export {VANTA} + +import { AxisHelper, js, MOUSE, OrbitControls, Scene, WebGLRenderer } from 'three/src/Three' +// const ORBITCONTROLS = { +// enableZoom: false, +// userPanSpeed: 3, +// userRotateSpeed: 2.0, +// maxPolarAngle: Math.PI * 0.8, // (pi/2 is pure horizontal) +// mouseButtons: { +// ORBIT: MOUSE.LEFT, +// ZOOM: null, +// PAN: null +// } +// } +// if (DEBUGMODE) { +// extend(ORBITCONTROLS, { +// enableZoom: true, +// zoomSpeed: 4, +// minDistance: 100, +// maxDistance: 4500 +// }) +// } + +// Namespace for errors +const error = function() { + Array.prototype.unshift.call(arguments, '[VANTA]') + return console.error.apply(this, arguments) +} + +VANTA.VantaBase = class VantaBase { + constructor(userOptions = {}) { + if (!win) return false + VANTA.current = this + this.windowMouseMoveWrapper = this.windowMouseMoveWrapper.bind(this) + this.windowTouchWrapper = this.windowTouchWrapper.bind(this) + this.windowGyroWrapper = this.windowGyroWrapper.bind(this) + this.resize = this.resize.bind(this) + this.animationLoop = this.animationLoop.bind(this) + this.restart = this.restart.bind(this) + + const defaultOptions = (typeof this.getDefaultOptions === 'function') ? this.getDefaultOptions() : this.defaultOptions + this.options = extend({ + mouseControls: true, + touchControls: true, + gyroControls: false, + minHeight: 200, + minWidth: 200, + scale: 1, + scaleMobile: 1, + }, defaultOptions) + + if (userOptions instanceof HTMLElement || typeof userOptions === 'string') { + userOptions = {el: userOptions} + } + extend(this.options, userOptions) + + // Set element + this.el = this.options.el + if (this.el == null) { + error("Instance needs \"el\" param!") + } else if (!(this.options.el instanceof HTMLElement)) { + const selector = this.el + this.el = q(selector) + if (!this.el) { + error("Cannot find element", selector) + return + } + } + + this.prepareEl() + this.initThree() + this.setSize() // Init needs size + + try { + this.init() + } catch (e) { + // FALLBACK - just use color + error('Init error', e) + if (this.renderer && this.renderer.domElement) { + this.el.removeChild(this.renderer.domElement) + } + if (this.options.backgroundColor) { + console.log('[VANTA] Falling back to backgroundColor') + this.el.style.background = color2Hex(this.options.backgroundColor) + } + return + } + + // After init + this.initMouse() // Triggers mouse, which needs to be called after init + this.resize() + this.animationLoop() + + // Event listeners + const ad = window.addEventListener + ad('resize', this.resize) + window.requestAnimationFrame(this.resize) // Force a resize after the first frame + + // Add event listeners on window, because this element may be below other elements, which would block the element's own mousemove event + if (this.options.mouseControls) { + ad('scroll', this.windowMouseMoveWrapper) + ad('mousemove', this.windowMouseMoveWrapper) + } + if (this.options.touchControls) { + ad('touchstart', this.windowTouchWrapper) + ad('touchmove', this.windowTouchWrapper) + } + if (this.options.gyroControls) { + ad('deviceorientation', this.windowGyroWrapper) + } + } + + setOptions(userOptions={}){ + extend(this.options, userOptions) + this.triggerMouseMove() + } + + prepareEl() { + let i, child + // wrapInner for text nodes, so text nodes can be put into foreground + if (typeof Node !== 'undefined' && Node.TEXT_NODE) { + for (i = 0; i < this.el.childNodes.length; i++) { + const n = this.el.childNodes[i] + if (n.nodeType === Node.TEXT_NODE) { + const s = document.createElement('span') + s.textContent = n.textContent + n.parentElement.insertBefore(s, n) + n.remove() + } + } + } + // Set foreground elements + for (i = 0; i < this.el.children.length; i++) { + child = this.el.children[i] + if (getComputedStyle(child).position === 'static') { + child.style.position = 'relative' + } + if (getComputedStyle(child).zIndex === 'auto') { + child.style.zIndex = 1 + } + } + // Set canvas and container style + if (getComputedStyle(this.el).position === 'static') { + this.el.style.position = 'relative' + } + } + + applyCanvasStyles(canvasEl, opts={}){ + extend(canvasEl.style, { + position: 'absolute', + zIndex: 0, + top: 0, + left: 0, + background: '' + }) + extend(canvasEl.style, opts) + canvasEl.classList.add('vanta-canvas') + } + + initThree() { + if (!WebGLRenderer) { + console.warn("[VANTA] No THREE defined on window") + return + } + // Set renderer + this.renderer = new WebGLRenderer({ + alpha: true, + antialias: true + }) + this.el.appendChild(this.renderer.domElement) + this.applyCanvasStyles(this.renderer.domElement) + if (isNaN(this.options.backgroundAlpha)) { + this.options.backgroundAlpha = 1 + } + + this.scene = new Scene() + } + + getCanvasElement() { + if (this.renderer) { + return this.renderer.domElement // js + } + if (this.p5renderer) { + return this.p5renderer.canvas // p5 + } + } + + getCanvasRect() { + const canvas = this.getCanvasElement() + if (!canvas) return false + return canvas.getBoundingClientRect() + } + + windowMouseMoveWrapper(e){ + const rect = this.getCanvasRect() + if (!rect) return false + const x = e.clientX - rect.left + const y = e.clientY - rect.top + if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { + this.mouseX = x + this.mouseY = y + if (!this.options.mouseEase) this.triggerMouseMove(x, y) + } + } + windowTouchWrapper(e){ + const rect = this.getCanvasRect() + if (!rect) return false + if (e.touches.length === 1) { + const x = e.touches[0].clientX - rect.left + const y = e.touches[0].clientY - rect.top + if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { + this.mouseX = x + this.mouseY = y + if (!this.options.mouseEase) this.triggerMouseMove(x, y) + } + } + } + windowGyroWrapper(e){ + const rect = this.getCanvasRect() + if (!rect) return false + const x = Math.round(e.alpha * 2) - rect.left + const y = Math.round(e.beta * 2) - rect.top + if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { + this.mouseX = x + this.mouseY = y + if (!this.options.mouseEase) this.triggerMouseMove(x, y) + } + } + + triggerMouseMove(x, y) { + if (x === undefined && y === undefined) { // trigger at current position + if (this.options.mouseEase) { + x = this.mouseEaseX + y = this.mouseEaseY + } else { + x = this.mouseX + y = this.mouseY + } + } + if (this.uniforms) { + this.uniforms.iMouse.value.x = x / this.scale // pixel values + this.uniforms.iMouse.value.y = y / this.scale // pixel values + } + const xNorm = x / this.width // 0 to 1 + const yNorm = y / this.height // 0 to 1 + typeof this.onMouseMove === "function" ? this.onMouseMove(xNorm, yNorm) : void 0 + } + + setSize() { + this.scale || (this.scale = 1) + if (mobileCheck() && this.options.scaleMobile) { + this.scale = this.options.scaleMobile + } else if (this.options.scale) { + this.scale = this.options.scale + } + this.width = Math.max(this.el.offsetWidth, this.options.minWidth) + this.height = Math.max(this.el.offsetHeight, this.options.minHeight) + } + initMouse() { + // Init mouseX and mouseY + if ((!this.mouseX && !this.mouseY) || + (this.mouseX === this.options.minWidth/2 && this.mouseY === this.options.minHeight/2)) { + this.mouseX = this.width/2 + this.mouseY = this.height/2 + this.triggerMouseMove(this.mouseX, this.mouseY) + } + } + + resize() { + this.setSize() + if (this.camera) { + this.camera.aspect = this.width / this.height + if (typeof this.camera.updateProjectionMatrix === "function") { + this.camera.updateProjectionMatrix() + } + } + if (this.renderer) { + this.renderer.setSize(this.width, this.height) + this.renderer.setPixelRatio(window.devicePixelRatio / this.scale) + } + typeof this.onResize === "function" ? this.onResize() : void 0 + } + + isOnScreen() { + const elHeight = this.el.offsetHeight + const elRect = this.el.getBoundingClientRect() + const scrollTop = (window.pageYOffset || + (document.documentElement || document.body.parentNode || document.body).scrollTop + ) + const offsetTop = elRect.top + scrollTop + const minScrollTop = offsetTop - window.innerHeight + const maxScrollTop = offsetTop + elHeight + return minScrollTop <= scrollTop && scrollTop <= maxScrollTop + } + + animationLoop() { + // Step time + this.t || (this.t = 0) + this.t += 1 + // Uniform time + this.t2 || (this.t2 = 0) + this.t2 += (this.options.speed || 1) + if (this.uniforms) { + this.uniforms.iTime.value = this.t2 * 0.016667 // iTime is in seconds + } + + if (this.options.mouseEase) { + this.mouseEaseX = this.mouseEaseX || this.mouseX || 0 + this.mouseEaseY = this.mouseEaseY || this.mouseY || 0 + if (Math.abs(this.mouseEaseX-this.mouseX) + Math.abs(this.mouseEaseY-this.mouseY) > 0.1) { + this.mouseEaseX += (this.mouseX - this.mouseEaseX) * 0.05 + this.mouseEaseY += (this.mouseY - this.mouseEaseY) * 0.05 + this.triggerMouseMove(this.mouseEaseX, this.mouseEaseY) + } + } + + // Only animate if element is within view + if (this.isOnScreen() || this.options.forceAnimate) { + if (typeof this.onUpdate === "function") { + this.onUpdate() + } + if (this.scene && this.camera) { + this.renderer.render(this.scene, this.camera) + this.renderer.setClearColor(this.options.backgroundColor, this.options.backgroundAlpha) + } + // if (this.stats) this.stats.update() + // if (this.renderStats) this.renderStats.update(this.renderer) + if (this.fps && this.fps.update) this.fps.update() + if (typeof this.afterRender === "function") this.afterRender() + } + return this.req = window.requestAnimationFrame(this.animationLoop) + } + + // setupControls() { + // if (DEBUGMODE && OrbitControls) { + // this.controls = new OrbitControls(this.camera, this.renderer.domElement) + // extend(this.controls, ORBITCONTROLS) + // return this.scene.add(new AxisHelper(100)) + // } + // } + + restart() { + // Restart the effect without destroying the renderer + if (this.scene) { + while (this.scene.children.length) { + this.scene.remove(this.scene.children[0]) + } + } + if (typeof this.onRestart === "function") { + this.onRestart() + } + this.init() + } + + init() { + if (typeof this.onInit === "function") { + this.onInit() + } + // this.setupControls() + } + + destroy() { + if (typeof this.onDestroy === "function") { + this.onDestroy() + } + const rm = window.removeEventListener + rm('touchstart', this.windowTouchWrapper) + rm('touchmove', this.windowTouchWrapper) + rm('scroll', this.windowMouseMoveWrapper) + rm('mousemove', this.windowMouseMoveWrapper) + rm('deviceorientation', this.windowGyroWrapper) + rm('resize', this.resize) + + window.cancelAnimationFrame(this.req) + if (this.renderer) { + if (this.renderer.domElement) { + this.el.removeChild(this.renderer.domElement) + } + this.renderer = null + this.scene = null + } + + if (VANTA.current === this) { + VANTA.current = null + } + } +} + +export default VANTA.VantaBase diff --git a/frontend/src/homepage/vanta/vanta.waves.js b/frontend/src/homepage/vanta/vanta.waves.js new file mode 100644 index 0000000..2f670a5 --- /dev/null +++ b/frontend/src/homepage/vanta/vanta.waves.js @@ -0,0 +1,193 @@ +import VantaBase, { VANTA } from './_base' +import { rn, ri, sample } from 'vanta/src/helpers.js' +import { Geometry, MeshPhongMaterial, Vector3, Face3, Mesh, AmbientLight, EdgesGeometry, LineBasicMaterial, LineSegments, PerspectiveCamera, PointLight, DoubleSide } from 'three/src/Three' +import { FaceColors } from 'three/src/Three.Legacy' + +const defaultOptions = { + color: 0x005588, + shininess: 30, + waveHeight: 15, + waveSpeed: 1, + zoom: 1 +} + +export class Waves extends VantaBase { + static initClass() { + this.prototype.ww = 100; + this.prototype.hh = 80; + this.prototype.waveNoise = 4; // Choppiness of water + } + constructor(userOptions) { + super(userOptions) + } + + getMaterial() { + const options = { + color: this.options.color, + shininess: this.options.shininess, + flatShading: true, + vertexColors: FaceColors, // Allow coloring individual faces + side: DoubleSide + }; + return new MeshPhongMaterial(options); + } + + onInit() { + let i, j; + const CELLSIZE = 18; + const material = this.getMaterial(); + const geometry = new Geometry(); + + // Add vertices + this.gg = []; + for (i=0; i<=this.ww; i++){ + this.gg[i] = []; + for (j=0; j<=this.hh; j++){ + const id = geometry.vertices.length; + const newVertex = new Vector3( + (i - (this.ww * 0.5)) * CELLSIZE, + rn(0, this.waveNoise) - 10, + ((this.hh * 0.5) - j) * CELLSIZE + ); + geometry.vertices.push(newVertex); + this.gg[i][j] = id; + } + } + + // Add faces + // a b + // c d <-- Looking from the bottom right point + for (i=1; i<=this.ww; i++){ + for (j=1; j<=this.hh; j++){ + let face1, face2 + const d = this.gg[i][j] + const b = this.gg[i][j-1] + const c = this.gg[i-1][j] + const a = this.gg[i-1][j-1] + if (ri(0,1)) { + face1 = new Face3( a, b, c ) + face2 = new Face3( b, c, d ) + } else { + face1 = new Face3( a, b, d ) + face2 = new Face3( a, c, d ) + } + geometry.faces.push( face1, face2 ) + } + } + + this.plane = new Mesh(geometry, material); + this.scene.add(this.plane); + + // WIREFRAME + // lightColor = 0x55aaee + // darkColor = 0x225577 + // thresholdAngle = 2 + // geo = new EdgesGeometry(geometry, thresholdAngle) + // mat = new LineBasicMaterial( { color: lightColor, linewidth: 2 } ) + // @wireframe = new LineSegments( geo, mat ) + // @scene.add( @wireframe ) + + // LIGHTS + const ambience = new AmbientLight( 0xffffff, 0.9 ); + this.scene.add(ambience); + + const pointLight = new PointLight( 0xffffff, 0.9 ); + pointLight.position.set(-100,250,-100); + this.scene.add(pointLight); + + // CAMERA + this.camera = new PerspectiveCamera( + 35, + this.width / this.height, + 50, 10000); + + const xOffset = -10; + const zOffset = -10; + this.cameraPosition = new Vector3( 250+xOffset, 200, 400+zOffset ); + this.cameraTarget = new Vector3( 150+xOffset, -30, 200+zOffset ); + this.camera.position.copy(this.cameraPosition); + this.scene.add(this.camera); + } + + onUpdate() { + // Update options + let diff; + this.plane.material.color.set(this.options.color); + this.plane.material.shininess = this.options.shininess; + this.camera.ox = this.cameraPosition.x / this.options.zoom; + this.camera.oy = this.cameraPosition.y / this.options.zoom; + this.camera.oz = this.cameraPosition.z / this.options.zoom; + + if (this.controls != null) { + this.controls.update(); + } + + const c = this.camera; + if (Math.abs(c.tx - c.position.x) > 0.01) { + diff = c.tx - c.position.x; + c.position.x += diff * 0.02; + } + if (Math.abs(c.ty - c.position.y) > 0.01) { + diff = c.ty - c.position.y; + c.position.y += diff * 0.02; + } + if (Math.abs(c.tz - c.position.z) > 0.01) { + diff = c.tz - c.position.z; + c.position.z += diff * 0.02; + } + + c.lookAt( this.cameraTarget ); + + // Fix flickering problems + // c.near = Math.max((c.position.y * 0.5) - 20, 1); + // c.updateMatrix(); + + // WAVES + for (let i = 0; i < this.plane.geometry.vertices.length; i++) { + const v = this.plane.geometry.vertices[i]; + if (!v.oy) { // INIT + v.oy = v.y; + } else { + const s = this.options.waveSpeed; + const crossChop = Math.sqrt(s) * Math.cos(-v.x - (v.z*0.7)); // + s * (i % 229) / 229 * 5 + const delta = Math.sin((((s*this.t*0.02) - (s*v.x*0.025)) + (s*v.z*0.015) + crossChop)); + const trochoidDelta = Math.pow(delta + 1, 2) / 4; + v.y = v.oy + (trochoidDelta * this.options.waveHeight); + } + } + + // @wireframe.geometry.vertices[i].y = v.y + + this.plane.geometry.dynamic = true; + this.plane.geometry.computeFaceNormals(); + this.plane.geometry.verticesNeedUpdate = true; + this.plane.geometry.normalsNeedUpdate = true; + + // @scene.remove( @wireframe ) + // geo = new EdgesGeometry(@plane.geometry) + // mat = new LineBasicMaterial( { color: 0x55aaee, linewidth: 2} ) + // @wireframe = new LineSegments( geo, mat ) + // @scene.add( @wireframe ) + + if (this.wireframe) { + this.wireframe.geometry.fromGeometry(this.plane.geometry); + this.wireframe.geometry.computeFaceNormals(); + } + } + + onMouseMove(x,y) { + const c = this.camera; + if (!c.oy) { + c.oy = c.position.y; + c.ox = c.position.x; + c.oz = c.position.z; + } + c.tx = c.ox + (((x-0.5) * 100) / this.options.zoom); + c.ty = c.oy + (((y-0.5) * -100) / this.options.zoom); + return c.tz = c.oz + (((x-0.5) * -50) / this.options.zoom); + } +} + +Waves.prototype.defaultOptions = defaultOptions +Waves.initClass() +export const WavesEffect = VANTA.register('WAVES', Waves) diff --git a/frontend/webpack.config.base.js b/frontend/webpack.config.base.js index 244d36b..c934be8 100644 --- a/frontend/webpack.config.base.js +++ b/frontend/webpack.config.base.js @@ -59,8 +59,5 @@ module.exports = { new webpack.DefinePlugin({ BACKEND_URL: JSON.stringify(process.env.BACKEND_URL || ''), }), - new webpack.ProvidePlugin({ - THREE: 'three', - }) ], } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index f939321..dc1bc5d 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -50,5 +50,5 @@ module.exports = { } if (process.env.BUNDLE_ANALYZER) { - module.exports[0].plugins.push(new BundleAnalyzerPlugin()) + module.exports.plugins.push(new BundleAnalyzerPlugin()) }