From 5ae970b04a964055c78bb7da791beda6345c589a Mon Sep 17 00:00:00 2001 From: "Diego F. Goberna" Date: Wed, 5 Dec 2018 21:01:39 +0100 Subject: [PATCH] game texts glow, tweaked ui buttons --- assets/shaders/stageAdditive.js | 50 ++ src/components/beat.js | 856 ++++++++++++++++++++++++++++++++ src/constants/colors.js | 21 + src/index.html | 7 +- 4 files changed, 932 insertions(+), 2 deletions(-) create mode 100644 assets/shaders/stageAdditive.js create mode 100644 src/components/beat.js create mode 100644 src/constants/colors.js diff --git a/assets/shaders/stageAdditive.js b/assets/shaders/stageAdditive.js new file mode 100644 index 0000000..ee0ba1d --- /dev/null +++ b/assets/shaders/stageAdditive.js @@ -0,0 +1,50 @@ +module.exports = { + vertexShader : ` + varying vec2 uvs; + varying vec3 worldPos; + void main() { + uvs.xy = uv.xy; + vec4 p = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + worldPos = (modelMatrix * vec4( position, 1.0 )).xyz; + gl_Position = p; + } + `, + + fragmentShader: ` + varying vec2 uvs; + varying vec3 worldPos; + uniform vec3 tunnelNeon; + uniform vec3 leftLaser; + uniform vec3 rightLaser; + uniform vec3 floorNeon; + uniform vec3 textGlow; + uniform sampler2D src; + + void main() { + float mask; + vec4 col = texture2D(src, uvs); + + // tunnel neon + mask = step(0.87, uvs.x) * step(0.5, uvs.y) * (1.0 - step(0.935, uvs.x)) * ( 1.0 - step(0.75, uvs.y)); + col.xyz = mix(col.xyz, col.xyz * tunnelNeon, mask); + + // floor & corridor neons + mask = step(0.935, uvs.x) * step(0.5, uvs.y) * ( 1.0 - step(0.75, uvs.y)); + col.xyz = mix(col.xyz, col.xyz * floorNeon, mask); + + // left laser + mask = step(0.5, uvs.x) * (1.0 - step(0.625, uvs.x)) * (1.0 - step(0.5, uvs.y)); + col.xyz = mix(col.xyz, col.xyz * leftLaser, mask); + + // right laser + mask = step(0.625, uvs.x) * (1.0 - step(0.75, uvs.x)) * (1.0 - step(0.5, uvs.y)); + col.xyz = mix(col.xyz, col.xyz * rightLaser, mask); + + // text glows + mask = step(0.87, uvs.x) * step(0.25, uvs.y) * (1.0 - step(0.935, uvs.x)) * ( 1.0 - step(0.5, uvs.y)); + col.xyz = mix(col.xyz, col.xyz * textGlow, mask); + + gl_FragColor = col; + } + ` +}; diff --git a/src/components/beat.js b/src/components/beat.js new file mode 100644 index 0000000..a3930fe --- /dev/null +++ b/src/components/beat.js @@ -0,0 +1,856 @@ +import {BEAT_WARMUP_OFFSET, BEAT_WARMUP_SPEED, BEAT_WARMUP_TIME} from '../constants/beat'; + +const auxObj3D = new THREE.Object3D(); +const collisionZThreshold = -1.65; +const BEAT_WARMUP_ROTATION_CHANGE = Math.PI / 5; +const BEAT_WARMUP_ROTATION_OFFSET = 0.4; +const BEAT_WARMUP_ROTATION_TIME = 750; +const ONCE = {once: true}; + +const SCORE_POOL = { + OK : 'pool__beatscoreok', + GOOD : 'pool__beatscoregood', + GREAT : 'pool__beatscoregreat', + SUPER : 'pool__beatscoresuper' +}; + +/** + * Bears, beats, Battlestar Galactica. + * Create beat from pool, collision detection, clipping planes, movement, scoring. + */ +AFRAME.registerComponent('beat', { + schema: { + anticipationPosition: {default: 0}, + color: {default: 'red', oneOf: ['red', 'blue']}, + cutDirection: {default: 'down'}, + debug: {default: false}, + horizontalPosition: {default: 'middleleft', oneOf: ['left', 'middleleft', 'middleright', 'right']}, + size: {default: 0.40}, + speed: {default: 1.0}, + type: {default: 'arrow', oneOf: ['arrow', 'dot', 'mine']}, + verticalPosition: {default: 'middle', oneOf: ['bottom', 'middle', 'top']}, + warmupPosition: {default: 0}, + }, + + materialColor: { + blue: '#08083E', + red: '#290404' + }, + + cutColor: { + blue: '#b3dcff', + red: '#ffb3ca' + }, + + models: { + arrow: 'beatObjTemplate', + dot: 'beatObjTemplate', + mine: 'mineObjTemplate' + }, + + signModels: { + arrowred: 'arrowRedObjTemplate', + arrowblue: 'arrowBlueObjTemplate', + dotred: 'dotRedObjTemplate', + dotblue: 'dotBlueObjTemplate' + }, + + orientations: [180, 0, 270, 90, 225, 135, 315, 45, 0], + + rotations: { + up: 180, + down: 0, + left: 270, + right: 90, + upleft: 225, + upright: 135, + downleft: 315, + downright: 45 + }, + + horizontalPositions: { + left: -0.75, + middleleft: -0.25, + middleright: 0.25, + right: 0.75 + }, + + verticalPositions: { + bottom: 0.70, + middle: 1.20, + top: 1.70 + }, + + init: function () { + this.backToPool = false; + this.beams = document.getElementById('beams').components.beams; + this.beatBoundingBox = new THREE.Box3(); + this.currentRotationWarmupTime = 0; + this.cutDirection = new THREE.Vector3(); + this.destroyed = false; + this.gravityVelocity = 0; + this.hitEventDetail = {}; + this.hitBoundingBox = new THREE.Box3(); + this.poolName = undefined; + this.returnToPoolTimer = 800; + this.rotationAxis = new THREE.Vector3(); + this.saberEls = this.el.sceneEl.querySelectorAll('[saber-controls]'); + this.scoreEl = null; + this.scoreElTime = undefined; + this.startPositionZ = undefined; + this.rightCutPlanePoints = [ + new THREE.Vector3(), + new THREE.Vector3(), + new THREE.Vector3() + ]; + this.leftCutPlanePoints = [ + new THREE.Vector3(), + new THREE.Vector3(), + new THREE.Vector3() + ]; + + this.mineParticles = document.getElementById('mineParticles'); + this.wrongElLeft = document.getElementById('wrongLeft'); + this.wrongElRight = document.getElementById('wrongRight'); + this.missElLeft = document.getElementById('missLeft'); + this.missElRight = document.getElementById('missRight'); + this.particles = document.getElementById('saberParticles'); + this.mineParticles = document.getElementById('mineParticles'); + + this.superCuts = document.querySelectorAll('.superCutFx'); + this.superCutIdx = 0; + + this.explodeEventDetail = {position: null, rotation: null}; + this.saberColors = {right: 'blue', left: 'red'}; + + this.onEndStroke = this.onEndStroke.bind(this); + + this.initBlock(); + this.initColliders(); + if (this.data.type === 'mine') { + this.initMineFragments(); + } else { + this.initFragments(); + }; + }, + + updatePosition: function () { + const el = this.el; + const data = this.data; + + el.object3D.position.set( + this.horizontalPositions[data.horizontalPosition], + this.verticalPositions[data.verticalPosition], + data.anticipationPosition + data.warmupPosition + ); + + el.object3D.rotation.z = THREE.Math.degToRad(this.rotations[data.cutDirection]); + }, + + update: function () { + this.updatePosition(); + this.updateBlock(); + this.updateFragments(); + + if (this.data.type === 'mine') { + this.poolName = `pool__beat-mine`; + } else { + this.poolName = `pool__beat-${this.data.type}-${this.data.color}`; + } + }, + + pause: function () { + this.el.object3D.visible = false; + if (this.data.type !== 'mine') { + this.partLeftEl.object3D.visible = false; + this.partRightEl.object3D.visible = false; + } + }, + + play: function () { + this.blockEl.object3D.visible = true; + this.destroyed = false; + this.el.object3D.visible = true; + }, + + tock: function (time, timeDelta) { + const el = this.el; + const data = this.data; + const position = el.object3D.position; + const rotation = el.object3D.rotation; + + if (this.destroyed) { + this.tockDestroyed(timeDelta); + // Check to remove score entity from pool. + } else { + // Only check collisions when close. + if (position.z > collisionZThreshold) { this.checkCollisions(); } + + // Move. + if (position.z < data.anticipationPosition) { + let newPositionZ = position.z + BEAT_WARMUP_SPEED * (timeDelta / 1000); + // Warm up / warp in. + if (newPositionZ < data.anticipationPosition) { + position.z = newPositionZ; + } else { + position.z = data.anticipationPosition; + this.beams.newBeam(this.data.color, position); + } + } else { + // Standard moving. + position.z += this.data.speed * (timeDelta / 1000); + rotation.z = this.startRotationZ; + } + + if (position.z > (data.anticipationPosition - BEAT_WARMUP_ROTATION_OFFSET) && + this.currentRotationWarmupTime < BEAT_WARMUP_ROTATION_TIME) { + const progress = AFRAME.ANIME.easings.easeOutBack( + this.currentRotationWarmupTime / BEAT_WARMUP_ROTATION_TIME); + el.object3D.rotation.z = this.rotationZStart + (progress * this.rotationZChange); + this.currentRotationWarmupTime += timeDelta; + } + + // Check. + this.backToPool = position.z >= 2; + if (this.backToPool) { this.missHit(); } + } + this.returnToPool(); + }, + + /** + * Called when summoned by beat-loader. + */ + onGenerate: function () { + this.startRotationZ = this.el.object3D.rotation.z; + + // Set up rotation warmup. + this.currentRotationWarmupTime = 0; + this.rotationZChange = BEAT_WARMUP_ROTATION_CHANGE; + if (Math.random > 0.5) { this.rotationZChange *= -1; } + this.el.object3D.rotation.z -= this.rotationZChange; + this.rotationZStart = this.el.object3D.rotation.z; + // Reset mine. + if (this.data.type == 'mine') { this.resetMineFragments(); } + }, + + initBlock: function () { + var el = this.el; + var blockEl = this.blockEl = document.createElement('a-entity'); + var signEl = this.signEl = document.createElement('a-entity'); + + blockEl.setAttribute('mixin', 'beatBlock'); + blockEl.setAttribute('mixin', 'beatSign'); + + // Small offset to prevent z-fighting when the blocks are far away + signEl.object3D.position.z += 0.02; + blockEl.appendChild(signEl); + el.appendChild(blockEl); + }, + + updateBlock: function () { + const blockEl = this.blockEl; + const signEl = this.signEl; + if (!blockEl) { return; } + + blockEl.setAttribute('material', { + metalness: 0.9, + roughness: 0.10, + sphericalEnvMap: '#envmapTexture', + color: this.materialColor[this.data.color] + }); + this.setObjModelFromTemplate(blockEl, this.models[this.data.type]); + + // Model is 0.29 size. We make it 1.0 so we can easily scale based on 1m size. + blockEl.object3D.scale.set(1, 1, 1); + blockEl.object3D.scale.multiplyScalar(3.45).multiplyScalar(this.data.size); + + if (this.data.type === 'mine') { + const model = blockEl.getObject3D('mesh'); + if (model) { + model.material = this.el.sceneEl.components['stage-colors'].mineMaterial; + } else { + blockEl.addEventListener('model-loaded', () => { + model.material = this.el.sceneEl.components['stage-colors'].mineMaterial; + }, ONCE); + } + } else { + signEl.setAttribute('materials', {name: 'stageAdditive'}); + this.setObjModelFromTemplate(signEl, this.signModels[this.data.type + this.data.color]); + } + }, + + initColliders: function () { + var data = this.data; + var hitColliderConfiguration; + var hitColliderEl; + + if (this.data.type === 'dot' || this.data.type === 'mine') { return; } + + // Hit colliders are 40% larger than the block. + hitColliderConfiguration = { + position: {x: 0, y: data.size / 2, z: 0}, + size: {width: data.size * 1.4, height: data.size / 3.0, depth: data.size* 1.4} + }; + + hitColliderEl = this.hitColliderEl = document.createElement('a-entity'); + hitColliderEl.setAttribute('geometry', { + primitive: 'box', + height: hitColliderConfiguration.size.height, + width: hitColliderConfiguration.size.width, + depth: hitColliderConfiguration.size.depth + }); + + hitColliderEl.object3D.position.copy(hitColliderConfiguration.position); + hitColliderEl.object3D.visible = false; + this.el.appendChild(hitColliderEl); + + if (data.debug) { + hitColliderEl.object3D.visible = true; + hitColliderEl.setAttribute('material', 'color', 'purple'); + } + }, + + initFragments: function () { + var cutEl; + var partEl; + + partEl = this.partLeftEl = document.createElement('a-entity'); + cutEl = this.cutLeftEl = document.createElement('a-entity'); + + partEl.appendChild(cutEl); + this.el.appendChild(partEl); + + partEl = this.partRightEl = document.createElement('a-entity'); + cutEl = this.cutRightEl = document.createElement('a-entity'); + + partEl.appendChild(cutEl); + this.el.appendChild(partEl); + + this.initCuttingClippingPlanes(); + }, + + initMineFragments: function () { + var fragment; + var fragments = this.el.sceneEl.systems['mine-fragments-loader'].fragments.children; + var material = this.el.sceneEl.components['stage-colors'].mineMaterial; + + this.randVec = new THREE.Vector3( + Math.random() * Math.PI, + Math.random() * Math.PI, + Math.random() * Math.PI); + + this.mineFragments = []; + this.mineBroken = document.createElement('a-entity'); + this.el.appendChild(this.mineBroken); + + for (var i = 0; i < fragments.length; i++) { + fragment = new THREE.Mesh(fragments[i].geometry, material); + fragment.speed = new THREE.Vector3(); + fragment.speed.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); + this.mineFragments.push(fragment); + this.mineBroken.object3D.add(fragment); + } + }, + + updateFragments: function () { + var cutLeftEl = this.cutLeftEl; + var cutRightEl = this.cutRightEl; + var partLeftEl = this.partLeftEl; + var partRightEl = this.partRightEl; + var fragment; + if (!partLeftEl) { return; } + if (this.data.type === 'mine') { + this.resetMineFragments(); + return; + } + + partLeftEl.setAttribute('material', { + metalness: 0.8, + roughness: 0.12, + sphericalEnvMap: '#envmapTexture', + color: this.materialColor[this.data.color], + side: 'double' + }); + this.setObjModelFromTemplate(partLeftEl, this.models.dot); + partLeftEl.object3D.visible = false; + + cutLeftEl.setAttribute('material', { + shader: 'flat', + color: this.data.cutColor, + side: 'double' + }); + this.setObjModelFromTemplate(cutLeftEl, this.models.dot); + + partRightEl.setAttribute('material', { + metalness: 0.8, + roughness: 0.12, + sphericalEnvMap: '#envmapTexture', + color: this.materialColor[this.data.color], + side: 'double' + }); + this.setObjModelFromTemplate(partRightEl, this.models.dot); + partRightEl.object3D.visible = false; + + cutRightEl.setAttribute('material', { + shader: 'flat', + color: this.data.cutColor, + side: 'double' + }); + this.setObjModelFromTemplate(cutRightEl, this.models.dot); + }, + + resetMineFragments: function () { + if (this.data.type !== 'mine') { return; } + for (let i = 0; i < this.mineFragments.length; i++) { + let fragment = this.mineFragments[i]; + fragment.visible = false; + fragment.position.set(0, 0, 0); + fragment.scale.set(1, 1, 1); + fragment.speed.set( + Math.random() * 5 - 2.5, + Math.random() * 5 - 2.5, + Math.random() * 5 - 2.5); + } + }, + + wrongHit: function (hand) { + var wrongEl = hand === 'left' ? this.wrongElLeft : this.wrongElRight; + if (!wrongEl) { return; } + wrongEl.object3D.position.copy(this.el.object3D.position); + wrongEl.object3D.position.y += 0.2; + wrongEl.object3D.position.z -= 0.5; + wrongEl.object3D.visible = true; + wrongEl.emit('beatwrong', null, true); + this.destroyed = true; + }, + + missHit: function (hand) { + var missEl = hand === 'left' ? this.missElLeft : this.missElRight; + if (!missEl || this.data.type === 'mine') { return; } + missEl.object3D.position.copy(this.el.object3D.position); + missEl.object3D.position.y += 0.2; + missEl.object3D.position.z -= 0.5; + missEl.object3D.visible = true; + missEl.emit('beatmiss', null, true); + if (AFRAME.utils.getUrlParameter('synctest')) { + console.log(this.el.sceneEl.components.song.getCurrentTime()); + } + }, + + destroyBeat: (function () { + var parallelPlaneMaterial = new THREE.MeshBasicMaterial({ + color: '#00008b', + side: THREE.DoubleSide + }); + var planeMaterial = new THREE.MeshBasicMaterial({color: 'grey', side: THREE.DoubleSide}); + var point1 = new THREE.Vector3(); + var point2 = new THREE.Vector3(); + var point3 = new THREE.Vector3(); + + return function (saberEl) { + var coplanarPoint; + var cutThickness = this.cutThickness = 0.02; + var direction = this.cutDirection; + var leftBorderInnerPlane = this.leftBorderInnerPlane; + var leftBorderOuterPlane = this.leftBorderOuterPlane; + var leftCutPlane = this.leftCutPlane; + var planeGeometry; + var planeMesh; + var rightBorderInnerPlane = this.rightBorderInnerPlane; + var rightBorderOuterPlane = this.rightBorderOuterPlane; + var rightCutPlane = this.rightCutPlane; + var trailPoints = saberEl.components.trail.saberTrajectory; + + point1.copy(trailPoints[0].top); + point2.copy(trailPoints[0].center); + point3.copy(trailPoints[trailPoints.length - 1].top); + direction.copy(point1).sub(point3); + + this.partRightEl.object3D.position.set(0, 0, 0); + this.partRightEl.object3D.rotation.set(0, 0, 0); + this.partRightEl.object3D.updateMatrixWorld(); + + this.partRightEl.object3D.worldToLocal(this.rightCutPlanePoints[0].copy(point1)); + this.partRightEl.object3D.worldToLocal(this.rightCutPlanePoints[1].copy(point2)); + this.partRightEl.object3D.worldToLocal(this.rightCutPlanePoints[2].copy(point3)); + + this.partLeftEl.object3D.position.set(0, 0, 0); + this.partLeftEl.object3D.rotation.set(0, 0, 0); + this.partLeftEl.object3D.updateMatrixWorld(); + + this.partLeftEl.object3D.worldToLocal(this.leftCutPlanePoints[0].copy(point3)); + this.partLeftEl.object3D.worldToLocal(this.leftCutPlanePoints[1].copy(point2)); + this.partLeftEl.object3D.worldToLocal(this.leftCutPlanePoints[2].copy(point1)); + + this.generateCutClippingPlanes(); + + if (this.data.debug) { + coplanarPoint = new THREE.Vector3(); + planeGeometry = new THREE.PlaneGeometry(4.0, 4.0, 1.0, 1.0); + + rightCutPlane.coplanarPoint(coplanarPoint); + planeGeometry.lookAt(rightCutPlane.normal); + planeGeometry.translate(coplanarPoint.x, coplanarPoint.y, coplanarPoint.z); + + planeMesh = new THREE.Mesh(planeGeometry, planeMaterial); + this.el.sceneEl.setObject3D('rightCutPlane', planeMesh); + + planeGeometry = new THREE.PlaneGeometry(4.0, 4.0, 1.0, 1.0); + + rightBorderOuterPlane.coplanarPoint(coplanarPoint); + planeGeometry.lookAt(rightBorderOuterPlane.normal); + planeGeometry.translate(coplanarPoint.x, coplanarPoint.y, coplanarPoint.z); + + const parallelPlaneMesh = new THREE.Mesh(planeGeometry, parallelPlaneMaterial); + this.el.sceneEl.setObject3D('planeParallel', parallelPlaneMesh); + } + + this.blockEl.object3D.visible = false; + + const partRightMaterial = this.partRightEl.getObject3D('mesh').material; + partRightMaterial.clippingPlanes = partRightMaterial.clippingPlanes || []; + partRightMaterial.clippingPlanes.length = 0; + partRightMaterial.clippingPlanes.push(rightCutPlane); + + const cutRightMaterial = this.cutRightEl.getObject3D('mesh').material; + cutRightMaterial.clippingPlanes = cutRightMaterial.clippingPlanes || []; + cutRightMaterial.clippingPlanes.length = 0; + cutRightMaterial.clippingPlanes.push(rightBorderOuterPlane); + cutRightMaterial.clippingPlanes.push(rightBorderInnerPlane); + + const partLeftMaterial = this.partLeftEl.getObject3D('mesh').material; + partLeftMaterial.clippingPlanes = partLeftMaterial.clippingPlanes || []; + partLeftMaterial.clippingPlanes.length = 0; + partLeftMaterial.clippingPlanes.push(leftCutPlane); + + const cutLeftMaterial = this.cutLeftEl.getObject3D('mesh').material; + cutLeftMaterial.clippingPlanes = cutLeftMaterial.clippingPlanes || []; + cutLeftMaterial.clippingPlanes.length = 0; + cutLeftMaterial.clippingPlanes.push(leftBorderInnerPlane); + cutLeftMaterial.clippingPlanes.push(leftBorderOuterPlane); + + this.partLeftEl.object3D.visible = true; + this.partRightEl.object3D.visible = true; + + this.el.sceneEl.renderer.localClippingEnabled = true; + this.destroyed = true; + this.gravityVelocity = 0.1; + + this.rotationAxis.copy(this.rightCutPlanePoints[0]).sub(this.rightCutPlanePoints[1]); + + this.returnToPoolTimer = 800; + + auxObj3D.up.copy(rightCutPlane.normal); + auxObj3D.lookAt(direction); + this.explodeEventDetail.position = this.el.object3D.position; + this.explodeEventDetail.rotation = auxObj3D.rotation; + this.particles.emit('explode', this.explodeEventDetail, false); + }; + })(), + + destroyMine: function () { + for (let i = 0; i < this.mineFragments.length; i++) { + this.mineFragments[i].visible = true; + } + + this.blockEl.object3D.visible = false; + this.destroyed = true; + this.gravityVelocity = 0.1; + this.returnToPoolTimer = 800; + + this.explodeEventDetail.position = this.el.object3D.position; + this.explodeEventDetail.rotation = this.randVec; + this.mineParticles.emit('explode', this.explodeEventDetail, false); + }, + + initCuttingClippingPlanes: function () { + this.leftCutPlanePointsWorld = [ + new THREE.Vector3(), + new THREE.Vector3(), + new THREE.Vector3() + ]; + this.rightCutPlanePointsWorld = [ + new THREE.Vector3(), + new THREE.Vector3(), + new THREE.Vector3() + ]; + + this.rightCutPlane = new THREE.Plane(); + this.rightBorderOuterPlane = new THREE.Plane(); + this.rightBorderInnerPlane = new THREE.Plane(); + + this.leftCutPlane = new THREE.Plane(); + this.leftBorderOuterPlane = new THREE.Plane(); + this.leftBorderInnerPlane = new THREE.Plane(); + }, + + generateCutClippingPlanes: function () { + var leftBorderInnerPlane = this.leftBorderInnerPlane; + var leftBorderOuterPlane = this.leftBorderOuterPlane; + var leftCutPlane = this.leftCutPlane; + var leftCutPlanePointsWorld = this.leftCutPlanePointsWorld; + var partLeftEl = this.partLeftEl; + var partRightEl = this.partRightEl; + var rightBorderInnerPlane = this.rightBorderInnerPlane; + var rightBorderOuterPlane = this.rightBorderOuterPlane; + var rightCutPlane = this.rightCutPlane; + var rightCutPlanePointsWorld = this.rightCutPlanePointsWorld; + + partRightEl.object3D.updateMatrixWorld(); + partRightEl.object3D.localToWorld( + rightCutPlanePointsWorld[0].copy(this.rightCutPlanePoints[0])); + partRightEl.object3D.localToWorld( + rightCutPlanePointsWorld[1].copy(this.rightCutPlanePoints[1])); + partRightEl.object3D.localToWorld( + rightCutPlanePointsWorld[2].copy(this.rightCutPlanePoints[2])); + + partLeftEl.object3D.updateMatrixWorld(); + partLeftEl.object3D.localToWorld( + leftCutPlanePointsWorld[0].copy(this.leftCutPlanePoints[0])); + partLeftEl.object3D.localToWorld( + leftCutPlanePointsWorld[1].copy(this.leftCutPlanePoints[1])); + partLeftEl.object3D.localToWorld( + leftCutPlanePointsWorld[2].copy(this.leftCutPlanePoints[2])); + + rightCutPlane.setFromCoplanarPoints( + rightCutPlanePointsWorld[0], rightCutPlanePointsWorld[1], rightCutPlanePointsWorld[2]); + rightBorderOuterPlane.set(rightCutPlane.normal, + rightCutPlane.constant + this.cutThickness); + + leftCutPlane.setFromCoplanarPoints( + leftCutPlanePointsWorld[0], leftCutPlanePointsWorld[1], leftCutPlanePointsWorld[2]); + leftBorderOuterPlane.set(leftCutPlane.normal, leftCutPlane.constant + this.cutThickness); + + rightBorderInnerPlane.setFromCoplanarPoints( + rightCutPlanePointsWorld[2], rightCutPlanePointsWorld[1], rightCutPlanePointsWorld[0]); + leftBorderInnerPlane.setFromCoplanarPoints( + leftCutPlanePointsWorld[2], leftCutPlanePointsWorld[1], leftCutPlanePointsWorld[0]); + }, + + returnToPool: function (force) { + if (!this.backToPool && !force) { return; } + this.el.sceneEl.components[this.poolName].returnEntity(this.el); + }, + + checkCollisions: function () { + const cutDirection = this.data.cutDirection; + const saberColors = this.saberColors; + const saberEls = this.saberEls; + const hitBoundingBox = this.hitColliderEl && this.hitBoundingBox.setFromObject( + this.hitColliderEl.getObject3D('mesh')); + const beatBoundingBox = this.beatBoundingBox.setFromObject( + this.blockEl.getObject3D('mesh')); + + for (let i = 0; i < saberEls.length; i++) { + let saberBoundingBox = saberEls[i].components['saber-controls'].boundingBox; + let saberControls; + let maxAngle; + + if (!saberBoundingBox) { break; } + + const hand = saberEls[i].getAttribute('saber-controls').hand; + + if (hitBoundingBox && saberBoundingBox.intersectsBox(hitBoundingBox)) { + if (saberEls[i].components['saber-controls'].swinging && + this.data.color === saberColors[hand]) { + saberControls = saberEls[i].components['saber-controls']; + this.hitHand = hand; + this.hitSaberEl = saberEls[i]; + this.hitSaberEl.addEventListener('strokeend', this.onEndStroke, ONCE); + if (cutDirection === 'up' || cutDirection === 'down') { + maxAngle = saberControls.maxAnglePlaneX; + } else if (cutDirection === 'left' || cutDirection === 'right') { + maxAngle = saberControls.maxAnglePlaneY; + } else { + maxAngle = saberControls.maxAnglePlaneXY; + } + this.angleBeforeHit = maxAngle; + saberControls.maxAnglePlaneX = 0; + saberControls.maxAnglePlaneY = 0; + saberControls.maxAnglePlaneXY = 0; + } else { + this.wrongHit(hand); + } + + // Notify for haptics. + this.el.emit(`beatcollide${hand}`, null, true); + + // Sound. + this.el.parentNode.components['beat-hit-sound'].playSound( + this.el, this.data.cutDirection); + + if (this.data.type === 'mine') { + this.destroyMine(); + } else { + this.destroyBeat(saberEls[i]); + } + break; + } + + if (saberBoundingBox.intersectsBox(beatBoundingBox)) { + // Notify for haptics. + this.el.emit(`beatcollide${hand}`, null, true); + + // Sound. + this.el.parentNode.components['beat-hit-sound'].playSound(this.el); + + if (this.data.type === 'mine') { + this.el.emit('minehit', null, true); + this.destroyMine(); + break; + } + + this.destroyBeat(saberEls[i]); + + if (this.data.type === 'dot' && saberEls[i].components['saber-controls'].swinging && + this.data.color === saberColors[hand]) { + this.hitSaberEl = saberEls[i]; + this.hitSaberEl.addEventListener('strokeend', this.onEndStroke, ONCE); + saberControls = saberEls[i].components['saber-controls']; + maxAngle = Math.max(saberControls.maxAnglePlaneX, saberControls.maxAnglePlaneY, + saberControls.maxAnglePlaneXY); + this.hitHand = hand; + this.angleBeforeHit = maxAngle; + saberControls.maxAnglePlaneX = 0; + saberControls.maxAnglePlaneY = 0; + saberControls.maxAnglePlaneXY = 0; + + } else { + this.wrongHit(hand); + } + break; + } + } + }, + + onEndStroke: function () { + var cutDirection = this.data.cutDirection; + var hitEventDetail = this.hitEventDetail; + var maxAngle; + var saberControls = this.hitSaberEl.components['saber-controls']; + var scoreText; + + // Harcoded temporarily. + const saberRotation = 3.14 / 12; + + if (cutDirection === 'up' || cutDirection === 'down') { + maxAngle = saberControls.maxAnglePlaneX; + } else if (cutDirection === 'left' || cutDirection === 'right') { + maxAngle = saberControls.maxAnglePlaneY; + } else { + maxAngle = saberControls.maxAnglePlaneXY; + } + + const angleBeforeHit = Math.max(0, (this.angleBeforeHit - saberRotation) * 180 / Math.PI); + const angleAfterHit = Math.max(0, (maxAngle - saberRotation) * 180 / Math.PI); + + let score = 0; + score += angleBeforeHit >= 85 ? 70 : (angleBeforeHit / 80) * 70; + score += angleAfterHit >= 60 ? 30 : (angleAfterHit / 60) * 30; + + hitEventDetail.score = score; + this.el.emit('beathit', hitEventDetail, true); + this.el.sceneEl.emit('textglowbold', null, false); + + let beatScorePool; + if (score < 60) { beatScorePool = SCORE_POOL.OK; } + else if (score < 80) { beatScorePool = SCORE_POOL.GOOD; } + else if (score < 100) { beatScorePool = SCORE_POOL.GREAT; } + else { + beatScorePool = SCORE_POOL.SUPER; + + this.superCuts[this.superCutIdx].components.supercutfx.createSuperCut(this.el.object3D.position); + this.superCutIdx = (this.superCutIdx + 1) % this.superCuts.length; + } + + const scoreEl = this.el.sceneEl.components[beatScorePool].requestEntity(); + if (scoreEl) { + scoreEl.object3D.position.copy(this.el.object3D.position); + scoreEl.play(); + scoreEl.emit('beatscorestart', null, false); + } + }, + + /** + * Destroyed animation. + */ + tockDestroyed: (function () { + var leftCutNormal = new THREE.Vector3(); + var leftRotation = 0; + var rightCutNormal = new THREE.Vector3(); + var rightRotation = 0; + var rotationStep = 2 * Math.PI / 150; + var fragment; + + return function (timeDelta) { + // Update gravity velocity. + this.gravityVelocity = getGravityVelocity(this.gravityVelocity, timeDelta); + this.el.object3D.position.y += this.gravityVelocity * (timeDelta / 1000); + + if (this.data.type == 'mine') { + for (var i = 0; i < this.mineFragments.length; i++) { + fragment = this.mineFragments[i]; + if (!fragment.visible) { continue; } + fragment.position.addScaledVector(fragment.speed, timeDelta / 1000); + fragment.scale.multiplyScalar(0.97) + if (fragment.scale.y < 0.1){ + fragment.visible = false; + } + } + return; + } + + rightCutNormal.copy(this.rightCutPlane.normal) + .multiplyScalar((this.data.speed / 2) * (timeDelta / 500)); + rightCutNormal.y = 0; // Y handled by gravity. + this.partRightEl.object3D.position.add(rightCutNormal); + this.partRightEl.object3D.setRotationFromAxisAngle(this.rotationAxis, rightRotation); + rightRotation = rightRotation >= 2 * Math.PI ? 0 : rightRotation + rotationStep; + + leftCutNormal.copy(this.leftCutPlane.normal) + .multiplyScalar((this.data.speed / 2) * (timeDelta / 500)); + leftCutNormal.y = 0; // Y handled by gravity. + this.partLeftEl.object3D.position.add(leftCutNormal); + this.partLeftEl.object3D.setRotationFromAxisAngle(this.rotationAxis, leftRotation); + leftRotation = leftRotation >= 2 * Math.PI ? 0 : leftRotation + rotationStep; + + this.generateCutClippingPlanes(); + + this.returnToPoolTimer -= timeDelta; + this.backToPool = this.returnToPoolTimer <= 0; + }; + })(), + + /** + * Load OBJ from already parsed and loaded OBJ template. + */ + setObjModelFromTemplate: (function () { + const geometries = {}; + + return function (el, templateId) { + if (!geometries[templateId]) { + const templateEl = document.getElementById(templateId); + if (templateEl.getObject3D('mesh')) { + geometries[templateId] = templateEl.getObject3D('mesh').children[0].geometry; + } else { + templateEl.addEventListener('model-loaded', () => { + geometries[templateId] = templateEl.getObject3D('mesh').children[0].geometry; + this.setObjModelFromTemplate(el, templateId); + }); + return; + } + } + + if (!el.getObject3D('mesh')) { el.setObject3D('mesh', new THREE.Mesh()); } + el.getObject3D('mesh').geometry = geometries[templateId]; + }; + })() +}); + +/** + * Get velocity given current velocity using gravity acceleration. + */ +function getGravityVelocity (velocity, timeDelta) { + const GRAVITY = -9.8; + return velocity + (GRAVITY * (timeDelta / 1000)); +} diff --git a/src/constants/colors.js b/src/constants/colors.js new file mode 100644 index 0000000..be186f9 --- /dev/null +++ b/src/constants/colors.js @@ -0,0 +1,21 @@ +module.exports = { + OFF: '#111', + RED: '#f00', + BLUE: '#00f', + + BG_OFF: '#222', + BG_BLUE: '#007AB8', + BG_BRIGHTBLUE: '#5FCCFF', + BG_RED: '#B80000', + BG_BRIGHTRED: '#FF4343', + + NEON_OFF: '#000', + NEON_BLUE: '#00f', + NEON_BRIGHTBLUE: '#aaf', + NEON_RED: '#f00', + NEON_BRIGHTRED: '#faa', + + TEXT_OFF: '#000', + TEXT_NORMAL: '#444', + TEXT_BOLD: '#888', +}; diff --git a/src/index.html b/src/index.html index 9f90207..385883b 100644 --- a/src/index.html +++ b/src/index.html @@ -18,7 +18,7 @@