import { PlaneGeometry, CylinderGeometry, CanvasTexture, Group, Mesh, MeshStandardMaterial, MeshBasicMaterial, DoubleSide } from "three"; import { Text } from "troika-three-text"; class DashBoard { #points = 0; constructor(engine) { let svg = p=>` ${p.hint || ''} Points `; let img = new Image(), url, levelProgress; let canvas = document.createElement('canvas'); let ctx = canvas.getContext('2d'); let texture = new CanvasTexture(canvas) let updating = false; let params = {} let occupied = false; img.addEventListener('load', function () { ctx.drawImage(img, 0, 0, engine.w, engine.h); URL.revokeObjectURL(url); texture.needsUpdate = true; updating = false; }) const dash = new Group(), hud = new Group(), hudTarget = new Group(); hud.visible = false; let hudAnimation, hudPlane, textPlane; this.group = dash; dash.add(hud); hud.add(hudTarget) hudTarget.position.set(0,0,0.52); dash.visible = false; const k = 1.55; const dashWidth = engine.aspect * k, dashHeight = k; const dashGeometry = new PlaneGeometry(dashWidth, dashHeight); const dashMesh = new Mesh(dashGeometry, new MeshBasicMaterial({ transparent: true, map: texture })) dash.add(dashMesh); engine.scene.add(dash); const loadingPlane = new Mesh( new PlaneGeometry(dashWidth, dashHeight), new MeshStandardMaterial({ color:0xffffff, opacity:0, transparent:true, roughness:0, metalness:0.1 }) ); const loadingProgress = new ProgressBar(engine); loadingProgress.object.scale.set(dashWidth*0.8, 0.05*dashHeight, 0.05*dashHeight) loadingProgress.object.position.set(-dashWidth/2 + dashWidth*0.1, 0, 0) loadingPlane.add(loadingProgress.object); dash.add(loadingPlane); (async()=>{ let map = await engine.loadTexture('/static/textures/hud.png', ''); hudPlane = new Mesh( new PlaneGeometry(dashWidth * 0.96, dashHeight * 0.9), new MeshBasicMaterial({ map, opacity: 0.37, transparent:true }) ); hudPlane.position.z = -0.07 * dashHeight; hudPlane.position.y = -0.011 * dashHeight hud.add(hudPlane) textPlane = new Mesh( new PlaneGeometry(dashWidth * 0.86, 0.15 * dashHeight), new MeshBasicMaterial({ map, opacity: 0.52, transparent:true }) ); textPlane.position.z = -0.002; textPlane.position.y = -0.33 * dashHeight textPlane.visible = false; dash.add(textPlane) // fix #44 textPlane.material.depthTest = false; //hudPlane.material.depthTest = false; })() const text = new Text() Object.assign(text, { text:``, fontSize: 0.033 * dashHeight, lineHeight: 1 * dashHeight, maxWidth: dashWidth * 0.8, textAlign: 'center', font: '/static/fonts/Montserrat-Regular.ttf', anchorX: 'center', anchorY: 0.03 * dashHeight, depthOffset: 0.1, color: 0x000000, clipRect: [-dashWidth * 0.4, -0.12 * dashHeight, dashWidth * 0.4, 0] }) text.sync(); text.position.set(0, -0.27 * dashHeight, 0.001); text.material.depthTest = false; dash.add(text); const pointsText = new Text() Object.assign(pointsText, { text:``, fontSize: 0.044, font: '/static/fonts/Montserrat-Bold.ttf', outlineColor: 0x113377, outlineWidth: '3%', anchorX: 'center', }) pointsText.position.set(0.86 * dashWidth/2, 0.47 * dashHeight, -0.001); dash.add(pointsText); engine.addEventListener('beforeRender', ()=>{ dash.quaternion.copy(engine.camera.quaternion) dash.position.copy(engine.camera.position) //dash.translateZ(-1.2 * engine.camera.zoom); dash.translateZ(-0.75/Math.tan(engine.camera.fov/2 * Math.PI/180) * engine.camera.zoom); }) this.updateText = function(t, textScrolledCallback){ textPlane.visible = !!t; engine.motionQueue.clear(text); text.text = t; text.anchorY = 0.03 * dashHeight; text.sync(()=>{ let dMax = text.textRenderInfo.blockBounds[3] - text.textRenderInfo.blockBounds[1]; let dh = dMax + text.clipRect[1]; if (dh > 0){ let updateFn = ()=>{ //text.sync(); if (text.textRenderInfo.blockBounds[1]> text.clipRect[1] * 1.33 ){ textScrolledCallback?.(true); textScrolledCallback = null; } } engine.motionQueue.add({ o: text, a: { anchorY: - dMax }, t: dMax*99 / dashHeight, u: updateFn }) }else{ textScrolledCallback?.(false) } }) } levelProgress = new ProgressBar(engine) dash.add(levelProgress.object); levelProgress.object.position.set(-dashWidth/2 + dashWidth/30, 0.45 * dashHeight, -0.001) levelProgress.object.scale.set(dashWidth/3, 0.02 * dashHeight, 0.02 * dashHeight) this.levelProgress = levelProgress; this.addPoints = function(p){ this.#points += p; engine.motionQueue.add({ o: pointsText, a: {rotation:{y: Math.PI}, scale:{x:1.5, y:1.5, z:1.5}}, t: 0.25, f:()=>{ pointsText.text = this.#points; pointsText.sync(); engine.motionQueue.add({ o: pointsText, a: {rotation:{y: Math.PI*2}, scale:{x:1, y:1, z:1}}, t: 0.5, f: ()=>{pointsText.rotation.y = 0;} }) } }) } this.enable = ()=>{ dash.visible = true; } this.disable = ()=>{ dash.visible = false; } this.reset = ()=>{ this.levelProgress.update(0); this.updateText(''); } //dashPlacement, rotate = false, plane = false this.attach = (object, opts = {})=>{ hud.visible = true; hudPlane.visible = !!opts.plane; if (opts.plane){ hudPlane.scale.set(0, .1, 1); hudPlane.material.opacity = 0.5; engine.motionQueue.add([{ o:hudPlane, a:{material:{opacity:0.73}}, t:.4, d:.8 },{ o:hudPlane, a:{scale:{x:1}}, t:.4 },{ o:hudPlane, a:{scale:{y:1}}, t:.4, d:.4, }]) } if (occupied) return false; object._hud = { parent: object.parent, placement: { position: object.position.clone(), quaternion: object.quaternion.clone(), scale: object.scale.clone() } } hudTarget.attach(object); occupied = true; let result = new Promise((resolve, reject)=>{ engine.motionQueue.add({ o: object, a: opts.placement || { quaternion: { x:0, y:0, z:0, w:0 }, position: { x:0, y:0, z:0 }, scale: { x: 1, y: 1, z: 1 } }, t: opts.skipTransition ? 0 : 1, f: resolve }) }) if (opts.rotate){ engine.motionQueue.add(hudAnimation = { o: hudTarget, a: { rotation: { y: 2*Math.PI } }, t: 4, r: true, d: 1 }) } return result; } this.detach = (object, opts = {})=>{ engine.motionQueue.remove(hudAnimation); object._hud.parent?.attach(object); hud.rotation.y = 0; hud.visible = false; engine.motionQueue.add({ o: object, a: object._hud.placement, t: opts.skipTransition ? 0 : 1 }); delete object._hud; occupied = false; hudAnimation = null; } this.loading = function(progress, tt){ loadingPlane.visible = progress > 0 && progress < 1; loadingProgress.update(progress, tt) } this.loading(0,0); } get active(){ return this.group.visible; } set active(v){ this.group.visible = v; } get points(){ return this.#points; } } class ProgressBar{ constructor(engine, params = {}){ this.object = new Group(); const geometry = new CylinderGeometry( 0.5, 0.5, 1, 3, 1, false, 0, Math.PI ); const staticCylinder = new Mesh( geometry, new MeshStandardMaterial({ roughness: 0, metalness:0.1, transparent: true, opacity:0.52, color: 0x55ff00, side: DoubleSide }) ); staticCylinder.rotation.set(-Math.PI/2, 0, Math.PI/2,) staticCylinder.position.x = 0.5; this.object.add( staticCylinder ); const progressCylinder = new Mesh( geometry, new MeshStandardMaterial({ roughness: 0, metalness:0.1, transparent: true, opacity:0.77, color: 0x11ff00 }) ); progressCylinder.rotation.set(Math.PI/2, 0, Math.PI/2,) this.object.add( progressCylinder ); this.update = function(value, transitionTime = 0.5){ progressCylinder.visible = !!value; engine.motionQueue.clear(progressCylinder); engine.motionQueue.add({ o: progressCylinder, a: { scale: {y: value}, position: {x: value / 2} }, t: transitionTime }) } this.update(0) } } export { DashBoard };