diff --git a/package-lock.json b/package-lock.json index b71dbe9e1608560a4162ddb75d111bbda7e427ac..e86a9d3b62f005678908a1a9c21432007fc42f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -659,6 +659,11 @@ "resolved": "https://registry.npmjs.org/an-array/-/an-array-1.0.0.tgz", "integrity": "sha1-wSWlu4JXd4419LT2qpx9D6nkJmU=" }, + "animejs": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/animejs/-/animejs-2.2.0.tgz", + "integrity": "sha1-Ne79/FNbgZScnLBvCz5gwC5v3IA=" + }, "ansi-colors": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.0.5.tgz", diff --git a/package.json b/package.json index 40a1584087063e2ddac674079394694a76783491..55b8f2ad4fb44f4ec92a3dbfe8f55adfbdbf4de3 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", "aframe-teleport-controls": "github:mozillareality/aframe-teleport-controls#hubs/master", + "animejs": "^2.2.0", "classnames": "^2.2.5", "copy-to-clipboard": "^3.0.8", "deepmerge": "^2.1.1", diff --git a/src/assets/hud/presence-count.png b/src/assets/hud/presence-count.png new file mode 100755 index 0000000000000000000000000000000000000000..7efe68711cdf61d0501f1dcef77d8f20b9f7272a Binary files /dev/null and b/src/assets/hud/presence-count.png differ diff --git a/src/assets/sfx/Chiptone_Settings/settings_SpawnBuildup.cpt b/src/assets/sfx/Chiptone_Settings/settings_SpawnBuildup.cpt new file mode 100755 index 0000000000000000000000000000000000000000..fb72bb1c0cdb868587f492a8862cc9f0d3cef85d Binary files /dev/null and b/src/assets/sfx/Chiptone_Settings/settings_SpawnBuildup.cpt differ diff --git a/src/assets/sfx/Chiptone_Settings/settings_TeleportArc.cpt b/src/assets/sfx/Chiptone_Settings/settings_TeleportArc.cpt new file mode 100755 index 0000000000000000000000000000000000000000..5d3430538890207b67d52f1555e747c2ab0186c6 Binary files /dev/null and b/src/assets/sfx/Chiptone_Settings/settings_TeleportArc.cpt differ diff --git a/src/assets/sfx/Chiptone_Settings/settings_tick.cpt b/src/assets/sfx/Chiptone_Settings/settings_tick.cpt new file mode 100755 index 0000000000000000000000000000000000000000..4135f4a621b44b84fde074dc40ac5f3341b6ae89 Binary files /dev/null and b/src/assets/sfx/Chiptone_Settings/settings_tick.cpt differ diff --git a/src/assets/sfx/Eb_blip.wav b/src/assets/sfx/Eb_blip.wav index 7428237df05a6655ba8a01a88878d43045967794..c8e8194d21e133ee2fb19b2ddc3e3b372cd89c4b 100644 Binary files a/src/assets/sfx/Eb_blip.wav and b/src/assets/sfx/Eb_blip.wav differ diff --git a/src/assets/sfx/pop.wav b/src/assets/sfx/pop.wav new file mode 100755 index 0000000000000000000000000000000000000000..b0fc22210c5114f8cc786e09d5816750cc667063 Binary files /dev/null and b/src/assets/sfx/pop.wav differ diff --git a/src/assets/sfx/suspense.wav b/src/assets/sfx/suspense.wav new file mode 100755 index 0000000000000000000000000000000000000000..3b9043984412fe96c8949a6927a051f66738e50b Binary files /dev/null and b/src/assets/sfx/suspense.wav differ diff --git a/src/assets/sfx/tack.wav b/src/assets/sfx/tack.wav new file mode 100755 index 0000000000000000000000000000000000000000..1f16efbdbbcf02548c16a4803da7f329948b4bec Binary files /dev/null and b/src/assets/sfx/tack.wav differ diff --git a/src/assets/sfx/teleportArc.wav b/src/assets/sfx/teleportArc.wav new file mode 100755 index 0000000000000000000000000000000000000000..cfedc3100094cc377ba7a9a0acb3c965a043819f Binary files /dev/null and b/src/assets/sfx/teleportArc.wav differ diff --git a/src/assets/sfx/tick.wav b/src/assets/sfx/tick.wav new file mode 100755 index 0000000000000000000000000000000000000000..c91e1b66c27a96bfcecdd95df64982f7209a66ca Binary files /dev/null and b/src/assets/sfx/tick.wav differ diff --git a/src/assets/sfx/welcome.wav b/src/assets/sfx/welcome.wav new file mode 100755 index 0000000000000000000000000000000000000000..9a85364db26dc7761926170547704ec023cc8f31 Binary files /dev/null and b/src/assets/sfx/welcome.wav differ diff --git a/src/assets/stylesheets/chat-command-help.scss b/src/assets/stylesheets/chat-command-help.scss new file mode 100644 index 0000000000000000000000000000000000000000..c8f0b42a5eedcbeaff94f06065cd87676b6f2aad --- /dev/null +++ b/src/assets/stylesheets/chat-command-help.scss @@ -0,0 +1,30 @@ +@import 'shared.scss'; + +:local(.command-help) { + background-color: $darker-grey; + color: $light-text; + position: absolute; + display: flex; + flex-direction: column; + width: 75%; + left: 0; + bottom: 58px; + pointer-events: auto; + padding: 8px 1.25em; + border-radius: 16px; + font-size: 0.8em; + + :local(.entry) { + @extend %default-font; + margin: 4px; + + display: flex; + justify-content: space-between; + + :local(.command) { + font-weight: bold; + white-space: nowrap; + margin-right: 8px; + } + } +} diff --git a/src/assets/stylesheets/spoke.scss b/src/assets/stylesheets/spoke.scss index 0219502e7a00014e7cbb07530ca3a78e06353b73..c253c221c30fd0878be3e31750f5feaacc2c6607 100644 --- a/src/assets/stylesheets/spoke.scss +++ b/src/assets/stylesheets/spoke.scss @@ -166,10 +166,17 @@ body { @extend %action-button; background-color: $darker-grey; margin: auto; - margin-top: 64px; padding: 0px 82px; } +:local(.tutorial-buttons) { + margin-top: 64px; + + button { + margin: 32px auto; + } +} + :local(.close-video) { margin-top: 12px; } diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 819a5a04649cf73d1cc12f9d8b64f6cd1b5ababd..643897d544fcb7e992c4263356d00fca979d0151 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -51,6 +51,7 @@ "audio.granted-next": "Next", "exit.subtitle.exited": "Your session has ended. Refresh your browser to start a new one.", "exit.subtitle.closed": "This room is no longer available.", + "exit.subtitle.left": "You have left the room.", "exit.subtitle.full": "This room is full, please try again later.", "exit.subtitle.connect_error": "Unable to connect to this room, please try again later.", "exit.subtitle.version_mismatch": "The version you deployed is not available yet. Your browser will refresh in 5 seconds.", @@ -59,6 +60,7 @@ "autoexit.subtitle": "You have started another session.", "autoexit.cancel": "CANCEL", "presence.entered_room": "entered the room.", + "presence.entered_lobby": "entered the lobby.", "presence.join_lobby": "joined the lobby.", "presence.leave": "left.", "presence.name_change": "is now known as", @@ -110,6 +112,14 @@ "spoke.download_unsupported": "View Releases", "spoke.browse_all_versions": "Browse All Versions", "spoke.close": "Close", - "spoke.play_button": "Learn Spoke in 5 Minutes" + "spoke.beginner_tutorial_button": "Learn Spoke in 5 Minutes", + "spoke.advanced_tutorial_button": "Advanced Spoke in 10 minutes", + "spoke.play_button": "Learn Spoke in 5 Minutes", + "commands.fly": "Toggle fly mode.", + "commands.bigger": "Increase your avatar's size.", + "commands.smaller": "Decrease your avatar's size.", + "commands.help": "Show help.", + "commands.leave": "Disconnect from the room.", + "commands.duck": "The duck tested well. Quack." } } diff --git a/src/components/animation.js b/src/components/animation.js new file mode 100644 index 0000000000000000000000000000000000000000..f0a709b9565a61b2e1a8c66d896fe336fb3ae471 --- /dev/null +++ b/src/components/animation.js @@ -0,0 +1,643 @@ +// Taken from A-Frame 0.9.0 master, TODO remove + +const anime = require("animejs"); +const components = AFRAME.components; +const registerComponent = AFRAME.registerComponent; +const utils = AFRAME.utils; + +const colorHelperFrom = new THREE.Color(); +const colorHelperTo = new THREE.Color(); + +const getComponentProperty = utils.entity.getComponentProperty; +const setComponentProperty = utils.entity.setComponentProperty; +const splitCache = {}; + +const TYPE_COLOR = "color"; +const PROP_POSITION = "position"; +const PROP_ROTATION = "rotation"; +const PROP_SCALE = "scale"; +const STRING_COMPONENTS = "components"; +const STRING_OBJECT3D = "object3D"; + +/** + * Given property name, check schema to see what type we are animating. + * We just care whether the property is a vector. + */ +function getPropertyType(el, property) { + const split = property.split("."); + const componentName = split[0]; + const propertyName = split[1]; + const component = el.components[componentName] || components[componentName]; + + // Primitives. + if (!component) { + return null; + } + + // Dynamic schema. We only care about vectors anyways. + if (propertyName && !component.schema[propertyName]) { + return null; + } + + // Multi-prop. + if (propertyName) { + return component.schema[propertyName].type; + } + + // Single-prop. + return component.schema.type; +} + +/** + * Convert object to radians. + */ +function toRadians(obj) { + obj.x = THREE.Math.degToRad(obj.x); + obj.y = THREE.Math.degToRad(obj.y); + obj.z = THREE.Math.degToRad(obj.z); +} + +function addEventListeners(el, eventNames, handler) { + let i; + for (i = 0; i < eventNames.length; i++) { + el.addEventListener(eventNames[i], handler); + } +} + +function removeEventListeners(el, eventNames, handler) { + let i; + for (i = 0; i < eventNames.length; i++) { + el.removeEventListener(eventNames[i], handler); + } +} + +function splitDot(path) { + if (path in splitCache) { + return splitCache[path]; + } + splitCache[path] = path.split("."); + return splitCache[path]; +} + +function getRawProperty(el, path) { + let i; + let value; + const split = splitDot(path); + value = el; + for (i = 0; i < split.length; i++) { + value = value[split[i]]; + } + return value; +} + +function setRawProperty(el, path, value, type) { + let i; + + if (path.startsWith("object3D.rotation")) { + value = THREE.Math.degToRad(value); + } + + // Walk. + const split = splitDot(path); + let targetValue = el; + for (i = 0; i < split.length - 1; i++) { + targetValue = targetValue[split[i]]; + } + const propertyName = split[split.length - 1]; + + // Raw color. + if (type === TYPE_COLOR) { + if ("r" in targetValue[propertyName]) { + targetValue[propertyName].r = value.r; + targetValue[propertyName].g = value.g; + targetValue[propertyName].b = value.b; + } else { + targetValue[propertyName].x = value.r; + targetValue[propertyName].y = value.g; + targetValue[propertyName].z = value.b; + } + return; + } + + targetValue[propertyName] = value; +} + +function isRawProperty(data) { + return data.isRawProperty || data.property.startsWith(STRING_COMPONENTS) || data.property.startsWith(STRING_OBJECT3D); +} +/** + * Animation component for A-Frame using anime.js. + * + * The component manually controls the tick by setting `autoplay: false` on anime.js and + * manually * calling `animation.tick()` in the tick handler. To pause or resume, we toggle a + * boolean * flag * `isAnimationPlaying`. + * + * anime.js animation config for tweenining Javascript objects and values works as: + * + * config = { + * targets: {foo: 0.0, bar: '#000'}, + * foo: 1.0, + * bar: '#FFF' + * } + * + * The above will tween each property in `targets`. The `to` values are set in the root of + * the config. + * + * @member {object} animation - anime.js instance. + * @member {boolean} animationIsPlaying - Control if animation is playing. + */ +module.exports.Component = registerComponent("animation", { + schema: { + autoplay: { default: true }, + delay: { default: 0 }, + dir: { default: "" }, + dur: { default: 1000 }, + easing: { default: "easeInQuad" }, + elasticity: { default: 400 }, + enabled: { default: true }, + from: { default: "" }, + loop: { + default: 0, + parse: function(value) { + // Boolean or integer. + if (value === true || value === "true") { + return true; + } + if (value === false || value === "false") { + return false; + } + return parseInt(value, 10); + } + }, + property: { default: "" }, + startEvents: { type: "array" }, + pauseEvents: { type: "array" }, + resumeEvents: { type: "array" }, + round: { default: false }, + to: { default: "" }, + type: { default: "" }, + isRawProperty: { default: false } + }, + + multiple: true, + + init: function() { + const self = this; + + this.eventDetail = { name: this.attrName }; + this.time = 0; + + this.animation = null; + this.animationIsPlaying = false; + this.onStartEvent = this.onStartEvent.bind(this); + this.beginAnimation = this.beginAnimation.bind(this); + this.pauseAnimation = this.pauseAnimation.bind(this); + this.resumeAnimation = this.resumeAnimation.bind(this); + + this.fromColor = {}; + this.toColor = {}; + this.targets = {}; + this.targetsArray = []; + + this.updateConfigForDefault = this.updateConfigForDefault.bind(this); + this.updateConfigForRawColor = this.updateConfigForRawColor.bind(this); + + this.config = { + complete: function() { + self.animationIsPlaying = false; + self.el.emit("animationcomplete", self.eventDetail, false); + if (self.id) { + self.el.emit("animationcomplete__" + self.id, self.eventDetail, false); + } + } + }; + }, + + update: function(oldData) { + const config = this.config; + const data = this.data; + + this.animationIsPlaying = false; + + if (oldData.enabled && !this.data.enabled) { + return; + } + + if (!data.property) { + return; + } + + // Base config. + config.autoplay = false; + config.direction = data.dir; + config.duration = data.dur; + config.easing = data.easing; + config.elasticity = data.elasticity; + config.loop = data.loop; + config.round = data.round; + + // Start new animation. + this.createAndStartAnimation(); + }, + + tick: function(t, dt) { + if (!this.animationIsPlaying) { + return; + } + this.time += dt; + this.animation.tick(this.time); + }, + + remove: function() { + this.pauseAnimation(); + this.removeEventListeners(); + }, + + pause: function() { + this.paused = true; + this.pausedWasPlaying = true; + this.pauseAnimation(); + this.removeEventListeners(); + }, + + /** + * `play` handler only for resuming scene. + */ + play: function() { + if (!this.paused) { + return; + } + this.paused = false; + this.addEventListeners(); + if (this.pausedWasPlaying) { + this.resumeAnimation(); + this.pausedWasPlaying = false; + } + }, + + /** + * Start animation from scratch. + */ + createAndStartAnimation: function() { + const data = this.data; + + this.updateConfig(); + this.animationIsPlaying = false; + this.animation = anime(this.config); + + this.removeEventListeners(); + this.addEventListeners(); + + // Wait for start events for animation. + if (!data.autoplay || (data.startEvents && data.startEvents.length)) { + return; + } + + // Delay animation. + if (data.delay) { + setTimeout(this.beginAnimation, data.delay); + return; + } + + // Play animation. + this.beginAnimation(); + }, + + /** + * This is before animation start (including from startEvents). + * Set to initial state (config.from, time = 0, seekTime = 0). + */ + beginAnimation: function() { + this.updateConfig(); + this.time = 0; + this.animationIsPlaying = true; + this.stopRelatedAnimations(); + this.el.emit("animationbegin", this.eventDetail); + }, + + pauseAnimation: function() { + this.animationIsPlaying = false; + }, + + resumeAnimation: function() { + this.animationIsPlaying = true; + }, + + /** + * startEvents callback. + */ + onStartEvent: function() { + if (!this.data.enabled) { + return; + } + + this.updateConfig(); + if (this.animation) { + this.animation.pause(); + } + this.animation = anime(this.config); + + // Include the delay before each start event. + if (this.data.delay) { + setTimeout(this.beginAnimation, this.data.delay); + return; + } + this.beginAnimation(); + }, + + /** + * rawProperty: true and type: color; + */ + updateConfigForRawColor: function() { + const config = this.config; + const data = this.data; + const el = this.el; + let from; + let key; + let to; + + if (this.waitComponentInitRawProperty(this.updateConfigForRawColor)) { + return; + } + + from = data.from === "" ? getRawProperty(el, data.property) : data.from; + to = data.to; + + // Use r/g/b vector for color type. + this.setColorConfig(from, to); + from = this.fromColor; + to = this.toColor; + + this.targetsArray.length = 0; + this.targetsArray.push(from); + config.targets = this.targetsArray; + for (key in to) { + config[key] = to[key]; + } + + config.update = (function() { + const lastValue = {}; + return function(anim) { + const value = anim.animatables[0].target; + // For animation timeline. + if (value.r === lastValue.r && value.g === lastValue.g && value.b === lastValue.b) { + return; + } + + setRawProperty(el, data.property, value, data.type); + }; + })(); + }, + + /** + * Stuff property into generic `property` key. + */ + updateConfigForDefault: function() { + const config = this.config; + const data = this.data; + const el = this.el; + let from; + let to; + + if (this.waitComponentInitRawProperty(this.updateConfigForDefault)) { + return; + } + + if (data.from === "") { + // Infer from. + from = isRawProperty(data) ? getRawProperty(el, data.property) : getComponentProperty(el, data.property); + } else { + // Explicit from. + from = data.from; + } + + to = data.to; + + const isNumber = !isNaN(from || to); + if (isNumber) { + from = parseFloat(from); + to = parseFloat(to); + } else { + from = from ? from.toString() : from; + to = to ? to.toString() : to; + } + + // Convert booleans to integer to allow boolean flipping. + const isBoolean = data.to === "true" || data.to === "false" || data.to === true || data.to === false; + if (isBoolean) { + from = data.from === "true" || data.from === true ? 1 : 0; + to = data.to === "true" || data.to === true ? 1 : 0; + } + + this.targets.aframeProperty = from; + config.targets = this.targets; + config.aframeProperty = to; + config.update = (function() { + let lastValue; + + return function(anim) { + let value; + value = anim.animatables[0].target.aframeProperty; + + // Need to do a last value check for animation timeline since all the tweening + // begins simultaenously even if the value has not changed. Also better for perf + // anyways. + if (value === lastValue) { + return; + } + lastValue = value; + + if (isBoolean) { + value = value >= 1; + } + + if (isRawProperty(data)) { + setRawProperty(el, data.property, value, data.type); + } else { + setComponentProperty(el, data.property, value); + } + }; + })(); + }, + + /** + * Extend x/y/z/w onto the config. + * Update vector by modifying object3D. + */ + updateConfigForVector: function() { + const config = this.config; + const data = this.data; + const el = this.el; + let key; + + // Parse coordinates. + const from = + data.from !== "" + ? utils.coordinates.parse(data.from) // If data.from defined, use that. + : getComponentProperty(el, data.property); // If data.from not defined, get on the fly. + const to = utils.coordinates.parse(data.to); + + if (data.property === PROP_ROTATION) { + toRadians(from); + toRadians(to); + } + + // Set to and from. + this.targetsArray.length = 0; + this.targetsArray.push(from); + config.targets = this.targetsArray; + for (key in to) { + config[key] = to[key]; + } + + // If animating object3D transformation, run more optimized updater. + if (data.property === PROP_POSITION || data.property === PROP_ROTATION || data.property === PROP_SCALE) { + config.update = (function() { + const lastValue = {}; + return function(anim) { + const value = anim.animatables[0].target; + + if (data.property === PROP_SCALE) { + value.x = Math.max(0.0001, value.x); + value.y = Math.max(0.0001, value.y); + value.z = Math.max(0.0001, value.z); + } + + // For animation timeline. + if (value.x === lastValue.x && value.y === lastValue.y && value.z === lastValue.z) { + return; + } + + lastValue.x = value.x; + lastValue.y = value.y; + lastValue.z = value.z; + + el.object3D[data.property].set(value.x, value.y, value.z); + }; + })(); + return; + } + + // Animating some vector. + config.update = (function() { + const lastValue = {}; + return function(anim) { + const value = anim.animations[0].target; + + // Animate rotation through radians. + // For animation timeline. + if (value.x === lastValue.x && value.y === lastValue.y && value.z === lastValue.z) { + return; + } + lastValue.x = value.x; + lastValue.y = value.y; + lastValue.z = value.z; + setComponentProperty(el, data.property, value); + }; + })(); + }, + + /** + * Update the config before each run. + */ + updateConfig: function() { + // Route config type. + const propType = getPropertyType(this.el, this.data.property); + if (isRawProperty(this.data) && this.data.type === TYPE_COLOR) { + this.updateConfigForRawColor(); + } else if (propType === "vec2" || propType === "vec3" || propType === "vec4") { + this.updateConfigForVector(); + } else { + this.updateConfigForDefault(); + } + }, + + /** + * Wait for component to initialize. + */ + waitComponentInitRawProperty: function(cb) { + const data = this.data; + const el = this.el; + const self = this; + + if (data.from !== "") { + return false; + } + + if (!data.property.startsWith(STRING_COMPONENTS)) { + return false; + } + + const componentName = splitDot(data.property)[1]; + if (el.components[componentName]) { + return false; + } + + el.addEventListener("componentinitialized", function wait(evt) { + if (evt.detail.name !== componentName) { + return; + } + cb(); + // Since the config was created async, create the animation now since we missed it + // earlier. + self.animation = anime(self.config); + el.removeEventListener("componentinitialized", wait); + }); + return true; + }, + + /** + * Make sure two animations on the same property don't fight each other. + * e.g., animation__mouseenter="property: material.opacity" + * animation__mouseleave="property: material.opacity" + */ + stopRelatedAnimations: function() { + let component; + let componentName; + for (componentName in this.el.components) { + component = this.el.components[componentName]; + if (componentName === this.attrName) { + continue; + } + if (component.name !== "animation") { + continue; + } + if (!component.animationIsPlaying) { + continue; + } + if (component.data.property !== this.data.property) { + continue; + } + component.animationIsPlaying = false; + } + }, + + addEventListeners: function() { + const data = this.data; + const el = this.el; + addEventListeners(el, data.startEvents, this.onStartEvent); + addEventListeners(el, data.pauseEvents, this.pauseAnimation); + addEventListeners(el, data.resumeEvents, this.resumeAnimation); + }, + + removeEventListeners: function() { + const data = this.data; + const el = this.el; + removeEventListeners(el, data.startEvents, this.onStartEvent); + removeEventListeners(el, data.pauseEvents, this.pauseAnimation); + removeEventListeners(el, data.resumeEvents, this.resumeAnimation); + }, + + setColorConfig: function(from, to) { + colorHelperFrom.set(from); + colorHelperTo.set(to); + from = this.fromColor; + to = this.toColor; + from.r = colorHelperFrom.r; + from.g = colorHelperFrom.g; + from.b = colorHelperFrom.b; + to.r = colorHelperTo.r; + to.g = colorHelperTo.g; + to.b = colorHelperTo.b; + } +}); diff --git a/src/components/character-controller.js b/src/components/character-controller.js index 083f47e195e7453136c958ecad62762be882c614..6eb743ec7ba515f19828a9709f44e8f1307f0455 100644 --- a/src/components/character-controller.js +++ b/src/components/character-controller.js @@ -3,6 +3,7 @@ const CLAMP_VELOCITY = 0.01; const MAX_DELTA = 0.2; const EPS = 10e-6; const MAX_WARNINGS = 10; +const PI_2 = Math.PI / 2; /** * Avatar movement controller that listens to move, rotate and teleportation events and moves the avatar accordingly. @@ -16,7 +17,8 @@ AFRAME.registerComponent("character-controller", { easing: { default: 10 }, pivot: { type: "selector" }, snapRotationDegrees: { default: THREE.Math.DEG2RAD * 45 }, - rotationSpeed: { default: -3 } + rotationSpeed: { default: -3 }, + fly: { default: false } }, init: function() { @@ -140,7 +142,7 @@ AFRAME.registerComponent("character-controller", { rotationInvMatrix.makeRotationAxis(rotationAxis, -root.rotation.y); pivotRotationMatrix.makeRotationAxis(rotationAxis, pivot.rotation.y); pivotRotationInvMatrix.makeRotationAxis(rotationAxis, -pivot.rotation.y); - this.updateVelocity(deltaSeconds); + this.updateVelocity(deltaSeconds, pivot); this.accelerationInput.set(0, 0, 0); const boost = userinput.get(paths.actions.boost) ? 2 : 1; @@ -178,7 +180,7 @@ AFRAME.registerComponent("character-controller", { this.pendingSnapRotationMatrix.identity(); // Revert to identity - if (this.velocity.lengthSq() > EPS) { + if (this.velocity.lengthSq() > EPS && !this.data.fly) { this.setPositionOnNavMesh(startPos, root.position, root); } }; @@ -221,13 +223,14 @@ AFRAME.registerComponent("character-controller", { pathfinder.clampStep(position, navPosition, this.navNode, this.navZone, this.navGroup, object3D.position); }, - updateVelocity: function(dt) { + updateVelocity: function(dt, pivot) { const data = this.data; const velocity = this.velocity; // If FPS too low, reset velocity. if (dt > MAX_DELTA) { velocity.x = 0; + velocity.y = 0; velocity.z = 0; return; } @@ -236,17 +239,24 @@ AFRAME.registerComponent("character-controller", { if (velocity.x !== 0) { velocity.x -= velocity.x * data.easing * dt; } - if (velocity.z !== 0) { - velocity.z -= velocity.z * data.easing * dt; - } if (velocity.y !== 0) { velocity.y -= velocity.y * data.easing * dt; } + if (velocity.z !== 0) { + velocity.z -= velocity.z * data.easing * dt; + } const dvx = data.groundAcc * dt * this.accelerationInput.x; const dvz = data.groundAcc * dt * -this.accelerationInput.z; velocity.x += dvx; - velocity.z += dvz; + + if (this.data.fly) { + const pitch = pivot.rotation.x / PI_2; + velocity.y += dvz * -pitch; + velocity.z += dvz * (1.0 - pitch); + } else { + velocity.z += dvz; + } const decay = 0.7; this.accelerationInput.x = this.accelerationInput.x * decay; @@ -255,7 +265,7 @@ AFRAME.registerComponent("character-controller", { if (Math.abs(velocity.x) < CLAMP_VELOCITY) { velocity.x = 0; } - if (Math.abs(velocity.y) < CLAMP_VELOCITY) { + if (this.data.fly && Math.abs(velocity.y) < CLAMP_VELOCITY) { velocity.y = 0; } if (Math.abs(velocity.z) < CLAMP_VELOCITY) { diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index 79289c07d7cffdcb3ab2af9cdceb0585bef5aeeb..cc90475b8ed00b7e1bd208e1e50fa5567f4c1372 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -98,7 +98,11 @@ AFRAME.registerComponent("cursor-controller", { const rightHandPose = userinput.get(paths.actions.rightHand.pose); this.data.cursor.object3D.visible = this.enabled && !!cursorPose; - this.el.setAttribute("line", "visible", this.enabled && !!rightHandPose); + const lineVisible = !!(this.enabled && rightHandPose); + + if (this.el.getAttribute("line").visible !== lineVisible) { + this.el.setAttribute("line", "visible", lineVisible); + } if (!this.enabled || !cursorPose) { return; @@ -129,11 +133,12 @@ AFRAME.registerComponent("cursor-controller", { cameraPos.y = cursor.object3D.position.y; cursor.object3D.lookAt(cameraPos); - this.data.cursor.setAttribute( - "material", - "color", - intersection || isGrabbing ? cursorColorHovered : cursorColorUnhovered - ); + const cursorColor = intersection || isGrabbing ? cursorColorHovered : cursorColorUnhovered; + + if (this.data.cursor.getAttribute("material").color !== cursorColor) { + this.data.cursor.setAttribute("material", "color", cursorColor); + } + if (this.el.components.line.data.visible) { this.el.setAttribute("line", { start: cursorPose.position.clone(), diff --git a/src/components/pin-networked-object-button.js b/src/components/pin-networked-object-button.js index 8f70dee186adc97ab16d49d9f2ca1616cd4f9f07..3b7ae88cc77d697499087033721eee24676fc8e0 100644 --- a/src/components/pin-networked-object-button.js +++ b/src/components/pin-networked-object-button.js @@ -61,6 +61,8 @@ AFRAME.registerComponent("pin-networked-object-button", { const isPinned = this.targetEl.getAttribute("pinnable") && this.targetEl.getAttribute("pinnable").pinned; this.labelEl.setAttribute("text", "value", isPinned ? "un-pin" : "pin"); + this.el.setAttribute("text-button", "backgroundColor", isPinned ? "#fff" : "#ff0520"); + this.el.setAttribute("text-button", "backgroundHoverColor", isPinned ? "#aaa" : "#cc0515"); this.el.parentNode.querySelectorAll(this.data.hideWhenPinnedSelector).forEach(hideEl => { hideEl.setAttribute("visible", !isPinned); diff --git a/src/components/pinnable.js b/src/components/pinnable.js index e719c033cde63a9f5d8c5ad025dafeeee2e845ae..a4870c831832701af00f6bfe7c23f739e5e78349 100644 --- a/src/components/pinnable.js +++ b/src/components/pinnable.js @@ -42,6 +42,27 @@ AFRAME.registerComponent("pinnable", { _fireEvents() { if (this.data.pinned) { this.el.emit("pinned", { el: this.el }); + + this.el.removeAttribute("animation__pin-start"); + this.el.removeAttribute("animation__pin-end"); + const currentScale = this.el.object3D.scale; + + this.el.setAttribute("animation__pin-start", { + property: "scale", + dur: 200, + from: { x: currentScale.x, y: currentScale.y, z: currentScale.z }, + to: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 }, + easing: "easeOutElastic" + }); + + this.el.setAttribute("animation__pin-end", { + property: "scale", + delay: 200, + dur: 200, + from: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 }, + to: { x: currentScale.x, y: currentScale.y, z: currentScale.z }, + easing: "easeOutElastic" + }); } else { this.el.emit("unpinned", { el: this.el }); } diff --git a/src/components/position-at-box-shape-border.js b/src/components/position-at-box-shape-border.js index 962433b6637c5fed3cf0373a31d1c893f62f87be..c013e6e2a80a05f0e88df795a88a00ebf225f074 100644 --- a/src/components/position-at-box-shape-border.js +++ b/src/components/position-at-box-shape-border.js @@ -69,14 +69,14 @@ AFRAME.registerComponent("position-at-box-shape-border", { } } - if (this.targetEl.getAttribute("visible") === false) return; - if (!this.el.getObject3D("mesh")) { return; } if (!this.halfExtents || this.mesh !== this.el.getObject3D("mesh") || this.shape !== this.el.components.shape) { this.mesh = this.el.getObject3D("mesh"); + this.shape = this.el.components.shape; + if (this.el.components.shape) { this.shape = this.el.components.shape; this.halfExtents.copy(this.shape.data.halfExtents); @@ -121,8 +121,25 @@ AFRAME.registerComponent("position-at-box-shape-border", { const distance = Math.sqrt(minSquareDistance); const scale = this.halfExtents[inverseHalfExtents[targetHalfExtentStr]] * distance; const targetScale = Math.min(2.0, Math.max(0.5, scale * tempParentWorldScale.x)); + const finalScale = targetScale / tempParentWorldScale.x; + + const isVisible = this.targetEl.getAttribute("visible"); + + if (isVisible && !this.wasVisible) { + this.targetEl.removeAttribute("animation__show"); + + this.targetEl.setAttribute("animation__show", { + property: "scale", + dur: 300, + from: { x: finalScale * 0.8, y: finalScale * 0.8, z: finalScale * 0.8 }, + to: { x: finalScale, y: finalScale, z: finalScale }, + easing: "easeOutElastic" + }); + } else { + this.target.scale.setScalar(finalScale); + } - this.target.scale.setScalar(targetScale / tempParentWorldScale.x); + this.wasVisible = isVisible; }; })() }); diff --git a/src/components/remove-networked-object-button.js b/src/components/remove-networked-object-button.js index e3f4aa2c020df42fb9603475441f09c53c1e47e6..d3663761b0c99d4df95f58c91b918849d933e958 100644 --- a/src/components/remove-networked-object-button.js +++ b/src/components/remove-networked-object-button.js @@ -3,7 +3,19 @@ AFRAME.registerComponent("remove-networked-object-button", { this.onClick = () => { if (!NAF.utils.isMine(this.targetEl) && !NAF.utils.takeOwnership(this.targetEl)) return; - this.targetEl.parentNode.removeChild(this.targetEl); + this.targetEl.setAttribute("animation__remove", { + property: "scale", + dur: 200, + to: { x: 0.01, y: 0.01, z: 0.01 }, + easing: "easeInQuad" + }); + + this.el.parentNode.removeAttribute("visible-while-frozen"); + this.el.parentNode.setAttribute("visible", false); + + this.targetEl.addEventListener("animationcomplete", () => { + this.targetEl.parentNode.removeChild(this.targetEl); + }); }; NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { diff --git a/src/components/scene-sound.js b/src/components/scene-sound.js index 249da3ebf130cca91a0ca6a544a3c33e9a3fc4b7..3454fa9ba6b9f50bcfea53d4e983964d47f14a27 100644 --- a/src/components/scene-sound.js +++ b/src/components/scene-sound.js @@ -5,11 +5,17 @@ AFRAME.registerComponent("scene-sound", { multiple: true, schema: { sound: { type: "string" }, - on: { type: "string" } + on: { type: "string" }, + off: { type: "string" } }, init() { const sound = this.el.components[`${this.attrName.replace("scene-", "")}`]; this.el.sceneEl.addEventListener(this.data.on, sound.playSound); + sound.stopSound = sound.stopSound.bind(sound); // wat + + if (this.data.off) { + this.el.sceneEl.addEventListener(this.data.off, sound.stopSound); + } } }); diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index 15c2f584750e1bbf5a4ea81c6c37699369925876..55fe0ab34400e4025ae531b96b7aab5b7abf1c7b 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -34,11 +34,10 @@ AFRAME.GLTFModelPlus.registerComponent( "shape", (() => { const euler = new THREE.Euler(); - const orientation = new THREE.Quaternion(); return (el, componentName, componentData) => { const { scale, rotation } = componentData; euler.set(rotation.x, rotation.y, rotation.z); - orientation.setFromEuler(euler); + const orientation = new THREE.Quaternion().setFromEuler(euler); el.setAttribute(componentName, { shape: "box", offset: componentData.position, @@ -80,6 +79,7 @@ AFRAME.GLTFModelPlus.registerComponent("media", "media", (el, componentName, com el.setAttribute("networked", { template: "#interactable-media", owner: "scene", + persistent: true, networkId: componentData.id }); } diff --git a/src/hub.html b/src/hub.html index a16a834139a53a5991a055f443a1d5bcb41043f7..6b469e1351330ac006b602b9f8481ecb58c402e4 100644 --- a/src/hub.html +++ b/src/hub.html @@ -54,6 +54,7 @@ <img id="spawn-pen-hover" crossorigin="anonymous" src="./assets/hud/spawn_pen-hover.png"> <img id="spawn-camera" crossorigin="anonymous" src="./assets/hud/spawn_camera.png"> <img id="spawn-camera-hover" crossorigin="anonymous" src="./assets/hud/spawn_camera-hover.png"> + <img id="presence-count" crossorigin="anonymous" src="./assets/hud/presence-count.png"> <a-asset-item id="botdefault" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotDefault_Avatar-9f71f8ff22.gltf"></a-asset-item> <a-asset-item id="botbobo" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotBobo_Avatar-f9740a010b.gltf"></a-asset-item> @@ -68,29 +69,34 @@ <a-asset-item id="quack" src="./assets/sfx/quack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> <a-asset-item id="specialquack" src="./assets/sfx/specialquack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-teleport_start" src="./assets/sfx/D_teleportStart.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-teleport_end" src="./assets/sfx/D_teleportEnd.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-snap_rotate" src="./assets/sfx/quickTurn.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-quack" src="./assets/sfx/quack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-teleport_start" src="./assets/sfx/teleportArc.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-teleport_end" src="./assets/sfx/quickTurn.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-snap_rotate" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> <a-asset-item id="sound_asset-media_loaded" src="./assets/sfx/A_bendUp.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-hud_hover_start" src="./assets/sfx/Eb_blip.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-hover" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="DISABLED_sound_asset-hover_off" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-cursor_distance_change_blocked" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-cursor_distance_changed" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-hud_click" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-toggle_mute" src="./assets/sfx/Fs_Mute.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-toggle_freeze" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-freeze" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-thaw" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-toggle_space_bubble" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-media_loading" src="./assets/sfx/suspense.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-hud_hover_start" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-grab" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-grab_off" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-pinned" src="./assets/sfx/tack.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-cursor_distance_change_blocked" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-cursor_distance_changed" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-hud_click" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-toggle_mute" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-toggle_freeze" src="./assets/sfx/Eb_blip.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-freeze" src="./assets/sfx/Eb_blip.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-thaw" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-toggle_space_bubble" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> <a-asset-item id="sound_asset-spawn_pen" src="./assets/sfx/PenSpawn.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-increase_pen_radius" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-decrease_pen_radius" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-next_pen_color" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-prev_pen_color" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-increase_pen_radius" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-decrease_pen_radius" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-next_pen_color" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-prev_pen_color" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> <a-asset-item id="sound_asset-start_draw" src="./assets/sfx/PenDraw1.wav" response-type="arraybuffer" preload="auto"></a-asset-item> - <a-asset-item id="sound_asset-stop_draw" src="./assets/sfx/tap_mellow.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-stop_draw" src="./assets/sfx/tick.wav" response-type="arraybuffer" preload="auto"></a-asset-item> <a-asset-item id="sound_asset-camera_tool_took_snapshot" src="./assets/sfx/PicSnapHey.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-welcome" src="./assets/sfx/welcome.wav" response-type="arraybuffer" preload="auto"></a-asset-item> + <a-asset-item id="sound_asset-chat" src="./assets/sfx/pop.wav" response-type="arraybuffer" preload="auto"></a-asset-item> <!-- Templates --> <template id="video-template"> @@ -125,8 +131,8 @@ <template data-name="Chest"> <a-entity personal-space-invader="radius: 0.2; useMaterial: true;" bone-visibility> <a-entity billboard> - <a-entity mixin="rounded-text-button" block-button visible-while-frozen="withinDistance: 3;" ui-class-while-frozen position="0 0 .35"> </a-entity> - <a-entity visible-while-frozen="withinDistance: 3;" text="value:Block; width:2.5; align:center;" position="0 0 0.36"></a-entity> + <a-entity mixin="rounded-text-button" block-button visible-while-frozen="withinDistance: 10;" ui-class-while-frozen position="0 0 .35"> </a-entity> + <a-entity visible-while-frozen="withinDistance: 10;" text="value:Block; width:2.5; align:center;" position="0 0 0.36"></a-entity> </a-entity> </a-entity> </template> @@ -164,6 +170,8 @@ class="interactable" super-networked-interactable="counter: #media-counter;" body="type: dynamic; shape: none; mass: 1;" + scale="0.5 0.5 0.5" + animation__spawn="property: scale; delay: 50; dur: 200; from: 0.5 0.5 0.5; to: 1 1 1; easing: easeInQuad" grabbable stretchable="useWorldPosition: true; usePhysics: never" hoverable @@ -174,13 +182,15 @@ destroy-at-extreme-distances set-yxz-order pinnable - sound__hover="src: #sound_asset-hover; on: hovered; poolSize: 1;" - sound__hoveroff ="src: #sound_asset-hover_off; on: unhovered; poolSize: 1;" - emit-state-change__hovered="state: hovered; transform: rising; event: hovered;" - emit-state-change__unhovered="state: hovered; transform: falling; event: unhovered;" + sound__grab="src: #sound_asset-grab; on: grabbed; poolSize: 1;" + sound__graboff ="src: #sound_asset-grab_off; on: ungrabbed; poolSize: 1;" + sound__pinned ="src: #sound_asset-pinned; on: pinned; poolSize: 1;" + emit-state-change__grabbed="state: grabbed; transform: rising; event: grabbed;" + emit-state-change__ungrabbed="state: grabbed; transform: falling; event: ungrabbed;" + emit-state-change__pinned="state: pinned; transform: rising; event: pinned;" > <a-entity class="interactable-ui" stop-event-propagation__grab-start="event: grab-start" stop-event-propagation__grab-end="event: grab-end"> - <a-entity class="freeze-menu" visible-while-frozen="withinDistance: 3;"> + <a-entity class="freeze-menu" visible-while-frozen="withinDistance: 10;"> <a-entity mixin="rounded-text-action-button" pin-networked-object-button="labelSelector:.pin-button-label; hideWhenPinnedSelector:.hide-when-pinned; uiSelector:.interactable-ui" position="0 0.125 0.01"> </a-entity> <a-entity class="pin-button-label" text=" value:pin; width:1.75; align:center;" text-raycast-hack position="0 0.125 0.02"></a-entity> <a-entity mixin="rounded-text-button" class="hide-when-pinned" remove-networked-object-button position="0 -0.125 0.01"> </a-entity> @@ -205,10 +215,10 @@ sound__stop_draw="src: #sound_asset-stop_draw; on: stop_draw; poolSize: 2;" sound__increase_pen_radius="src: #sound_asset-increase_pen_radius; on: increase_pen_radius; poolSize: 2;" sound__decrease_pen_radius="src: #sound_asset-decrease_pen_radius; on: decrease_pen_radius; poolSize: 2;" - sound__hover="src: #sound_asset-hover; on: hovered; poolSize: 1;" - sound__hoveroff ="src: #sound_asset-hover_off; on: unhovered; poolSize: 1;" - emit-state-change__hovered="state: hovered; transform: rising; event: hovered;" - emit-state-change__unhovered="state: hovered; transform: falling; event: unhovered;" + sound__grab="src: #sound_asset-grab; on: grabbed; poolSize: 1;" + sound__graboff ="src: #sound_asset-grab_off; on: ungrabbed; poolSize: 1;" + emit-state-change__grabbed="state: grabbed; transform: rising; event: grabbed;" + emit-state-change__ungrabbed="state: grabbed; transform: falling; event: ungrabbed;" > <a-sphere id="pen" @@ -220,7 +230,7 @@ segments-width="16" segments-height="12" ></a-sphere> - <a-entity class="delete-button" visible-while-frozen="withinDistance: 3;"> + <a-entity class="delete-button" visible-while-frozen="withinDistance: 10;"> <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> <a-entity text=" value:remove; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> </a-entity> @@ -234,6 +244,8 @@ hoverable stretchable camera-tool + animation__spawn="property: scale; delay: 50; dur: 200; from: 0.5 0.5 0.5; to: 1 1 1; easing: easeOutQuad" + scale="0.5 0.5 0.5" body="type: dynamic; shape: none; mass: 1;" shape="shape: box; halfExtents: 0.22 0.145 0.1; offset: 0 0.02 0" sticky-object="autoLockOnRelease: true; autoLockOnLoad: true; autoLockSpeedLimit: 0;" @@ -242,7 +254,7 @@ set-yxz-order auto-scale-cannon-physics-body > - <a-entity class="delete-button" visible-while-frozen="withinDistance: 3;"> + <a-entity class="delete-button" visible-while-frozen="withinDistance: 10;"> <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity> <a-entity text=" value:remove; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> </a-entity> @@ -292,8 +304,8 @@ haptic:#player-right-controller; textHoverColor: #fff; textColor: #fff; - backgroundHoverColor: #ff0434; - backgroundColor: #ff3464;" + backgroundHoverColor: #cc0515; + backgroundColor: #ff0520;" slice9=" width: 0.45; height: 0.2; @@ -382,9 +394,12 @@ vr-mode-toggle-playing__hud-controller > <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0"> - <a-rounded height="0.08" width="0.5" color="#000000" position="-0.20 0.125 0" radius="0.040" opacity="0.35" class="hud bg"></a-rounded> - <a-entity id="hud-hub-entry-link" text=" value:; width:1.1; align:center;" position="0.05 0.165 0"></a-entity> - <a-rounded height="0.13" width="0.59" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> + <a-rounded height="0.08" width="0.2" color="#000000" position="-0.30 0.125 0" radius="0.040" opacity="0.35" class="hud bg"></a-rounded> + <a-image scale="0.07 0.06 0.06" position="-0.23 0.165 0.001" src="#presence-count" material="alphaTest:0.1;"></a-image> + <a-entity id="hud-presence-count" text="value:; width:1.1; align:center;" position="-0.155 0.165 0"></a-entity> + <a-rounded height="0.08" width="0.5" color="#000000" position="-0.08 0.125 0" radius="0.040" opacity="0.35" class="hud bg"></a-rounded> + <a-entity id="hud-hub-entry-link" text="value:; width:1.1; align:center;" position="0.17 0.165 0"></a-entity> + <a-rounded height="0.13" width="0.59" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Mute Mic; activeTooltipText: Unmute Mic; image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" @@ -434,10 +449,10 @@ rotation pitch-yaw-rotator set-yxz-order - sound__teleport_start="positional: false; src: #sound_asset-teleport_start; on: nothing; poolSize: 2;" - scene-sound__teleport_start="on: right-teleport_down;" - sound__teleport_start2="positional: false; src: #sound_asset-teleport_start; on: nothing; poolSize: 2;" - scene-sound__teleport_start2="on: left-teleport_down;" + sound__teleport_start="positional: false; src: #sound_asset-teleport_start; on: nothing; poolSize: 2; loop: true;" + scene-sound__teleport_start="on: right-teleport_down; off: right-teleport_up;" + sound__teleport_start2="positional: false; src: #sound_asset-teleport_start; on: nothing; poolSize: 2; loop: true;" + scene-sound__teleport_start2="on: left-teleport_down; off: left-teleport_up;" sound__teleport_end="positional: false; src: #sound_asset-teleport_end; on: nothing; poolSize: 2;" scene-sound__teleport_end="on: right-teleport_up;" sound__teleport_end2="positional: false; src: #sound_asset-teleport_end; on: nothing; poolSize: 2;" @@ -446,10 +461,10 @@ scene-sound__snap_rotate_left="on: snap_rotate_left;" sound__snap_rotate_right="positional: false; src: #sound_asset-snap_rotate; on: nothing; poolSize: 3;" scene-sound__snap_rotate_right="on: snap_rotate_right;" - sound__model_loaded="positional: false; src: #sound_asset-media_loaded; on: nothing; poolSize: 2;" - scene-sound__model_loaded="on: model-loaded;" - sound__image_loaded="positional: false; src: #sound_asset-media_loaded; on: nothing; poolSize: 2;" - scene-sound__image_loaded="on: image-loaded;" + sound__media_loaded="positional: false; src: #sound_asset-media_loaded; on: nothing; poolSize: 2;" + scene-sound__media_loaded="on: media-loaded;" + sound__media_loading="positional: false; src: #sound_asset-media_loading; on: nothing; poolSize: 2; loop: true; " + scene-sound__media_loading="on: media-loading; off: media-loaded;" sound__hud_action_mute="positional: false; src: #sound_asset-toggle_mute; on: nothing; poolSize: 2;" scene-sound__hud_action_mute="on: action_mute;" sound__hud_action_freeze="positional: false; src: #sound_asset-toggle_freeze; on: nothing; poolSize: 2;" @@ -463,7 +478,6 @@ sound__hud_spawn_pen="positional: false; src: #sound_asset-spawn_pen; on: nothing; poolSize: 2;" scene-sound__hud_spawn_pen="on: spawn_pen;" sound__hud_hover_start="positional: false; src: #sound_asset-hud_hover_start; on: nothing; poolSize: 5;" - scene-sound__hud_hover_start="on: play_sound-hud_hover_start;" sound__hud_click="positional: false; src: #sound_asset-hud_click; on: nothing; poolSize: 1;" scene-sound__hud_click="on: hud_click;" sound__cursor_distance_changed="positional: false; src: #sound_asset-cursor_distance_changed; on: nothing; poolSize: 1;" @@ -472,6 +486,12 @@ scene-sound__cursor_distance_change_blocked="on: cursor-distance-change-blocked;" sound__camera_tool_took_snapshot="positional: false; src: #sound_asset-camera_tool_took_snapshot; on: nothing; poolSize: 1;" scene-sound__camera_tool_took_snapshot="on: camera_tool_took_snapshot;" + sound__welcome="positional: false; src: #sound_asset-welcome; on: nothing; poolSize: 2;" + scene-sound__welcome="on: entered;" + sound__log_chat_message="positional: false; src: #sound_asset-chat; on: nothing; poolSize: 2;" + scene-sound__log_chat_message="on: presence-log-chat;" + sound__quack="positional: false; src: #sound_asset-quack; on: nothing; poolSize: 2;" + scene-sound__quack="on: quack;" > <a-entity id="gaze-teleport" diff --git a/src/hub.js b/src/hub.js index 553f2a3a1fa72b5d5082d6ae391fcf2aefa165b0..a9ac40c324785aab8218a2d871b54e401365c0e5 100644 --- a/src/hub.js +++ b/src/hub.js @@ -67,6 +67,7 @@ import "./components/scene-sound"; import "./components/emit-state-change"; import "./components/action-to-event"; import "./components/stop-event-propagation"; +import "./components/animation"; import ReactDOM from "react-dom"; import React from "react"; @@ -76,6 +77,7 @@ import LinkChannel from "./utils/link-channel"; import { connectToReticulum } from "./utils/phoenix-utils"; import { disableiOSZoom } from "./utils/disable-ios-zoom"; import { proxiedUrlFor } from "./utils/media-utils"; +import MessageDispatch from "./message-dispatch"; import SceneEntryManager from "./scene-entry-manager"; import Subscriptions from "./subscriptions"; @@ -116,7 +118,6 @@ import "./components/cardboard-controls"; import "./components/cursor-controller"; import "./components/nav-mesh-helper"; -import "./systems/tunnel-effect"; import "./components/tools/pen"; import "./components/tools/networked-drawing"; @@ -213,7 +214,7 @@ function remountUI(props) { mountUI(uiProps); } -async function handleHubChannelJoined(entryManager, hubChannel, data) { +async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) { const scene = document.querySelector("a-scene"); if (NAF.connection.isConnected()) { @@ -251,7 +252,7 @@ async function handleHubChannelJoined(entryManager, hubChannel, data) { hubId: hub.hub_id, hubName: hub.name, hubEntryCode: hub.entry_code, - onSendMessage: hubChannel.sendMessage + onSendMessage: messageDispatch.dispatch }); document @@ -342,6 +343,7 @@ document.addEventListener("DOMContentLoaded", async () => { // If VR headset is activated, refreshing page will fire vrdisplayactivate // which puts A-Frame in VR mode, so exit VR mode whenever it is attempted // to be entered and we haven't entered the room yet. + console.log("Pre-emptively exiting VR mode."); scene.exitVR(); } }); @@ -386,6 +388,14 @@ document.addEventListener("DOMContentLoaded", async () => { if (availableVREntryTypes.isInHMD) { remountUI({ availableVREntryTypes, forcedVREntryType: "vr" }); + + if (/Oculus/.test(navigator.userAgent)) { + // HACK - The polyfill reports Cardboard as the primary VR display on startup out ahead of Oculus Go on Oculus Browser 5.5.0 beta. This display is cached by A-Frame, + // so we need to resolve that and get the real VRDisplay before entering as well. + const displays = await navigator.getVRDisplays(); + const vrDisplay = displays.length && displays[0]; + AFRAME.utils.device.getVRDisplay = () => vrDisplay; + } } else { remountUI({ availableVREntryTypes }); } @@ -430,32 +440,13 @@ document.addEventListener("DOMContentLoaded", async () => { const joinPayload = { profile: store.state.profile, push_subscription_endpoint: pushSubscriptionEndpoint, context }; const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload); - hubPhxChannel - .join() - .receive("ok", async data => { - hubChannel.setPhoenixChannel(hubPhxChannel); - subscriptions.setHubChannel(hubChannel); - subscriptions.setSubscribed(data.subscriptions.web_push); - remountUI({ initialIsSubscribed: subscriptions.isSubscribed() }); - await handleHubChannelJoined(entryManager, hubChannel, data); - }) - .receive("error", res => { - if (res.reason === "closed") { - entryManager.exitScene(); - remountUI({ roomUnavailableReason: "closed" }); - } - - console.error(res); - }); - - const hubPhxPresence = new Presence(hubPhxChannel); const presenceLogEntries = []; - const addToPresenceLog = entry => { entry.key = Date.now().toString(); presenceLogEntries.push(entry); remountUI({ presenceLogEntries }); + scene.emit(`presence-log-${entry.type}`); // Fade out and then remove setTimeout(() => { @@ -469,10 +460,35 @@ document.addEventListener("DOMContentLoaded", async () => { }, 20000); }; + const messageDispatch = new MessageDispatch(scene, entryManager, hubChannel, addToPresenceLog, remountUI); + + hubPhxChannel + .join() + .receive("ok", async data => { + hubChannel.setPhoenixChannel(hubPhxChannel); + subscriptions.setHubChannel(hubChannel); + subscriptions.setSubscribed(data.subscriptions.web_push); + remountUI({ initialIsSubscribed: subscriptions.isSubscribed() }); + await handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data); + }) + .receive("error", res => { + if (res.reason === "closed") { + entryManager.exitScene(); + remountUI({ roomUnavailableReason: "closed" }); + } + + console.error(res); + }); + + const hubPhxPresence = new Presence(hubPhxChannel); + let isInitialSync = true; + const vrHudPresenceCount = document.querySelector("#hud-presence-count"); hubPhxPresence.onSync(() => { remountUI({ presences: hubPhxPresence.state }); + const occupantCount = Object.entries(hubPhxPresence.state).length; + vrHudPresenceCount.setAttribute("text", "value", occupantCount.toString()); if (!isInitialSync) return; // Wire up join/leave event handlers after initial sync. diff --git a/src/message-dispatch.js b/src/message-dispatch.js new file mode 100644 index 0000000000000000000000000000000000000000..83babfd2bf7ff3faf2824beb5ac14d9ad96d4a1c --- /dev/null +++ b/src/message-dispatch.js @@ -0,0 +1,80 @@ +import { spawnChatMessage } from "./react-components/chat-message"; +const DUCK_URL = "https://asset-bundles-prod.reticulum.io/interactables/Ducky/DuckyMesh-438ff8e022.gltf"; + +// Handles user-entered messages +export default class MessageDispatch { + constructor(scene, entryManager, hubChannel, addToPresenceLog, remountUI) { + this.scene = scene; + this.entryManager = entryManager; + this.hubChannel = hubChannel; + this.addToPresenceLog = addToPresenceLog; + this.remountUI = remountUI; + } + + dispatch = message => { + if (message.startsWith("/")) { + this.dispatchCommand(message.substring(1)); + document.activeElement.blur(); // Commands should blur + } else { + this.hubChannel.sendMessage(message); + } + }; + + dispatchCommand = command => { + const entered = this.scene.is("entered"); + + switch (command) { + case "help": + // HACK for now, non-trivial to properly send this into React + document.querySelector(".help-button").click(); + return; + } + + if (!entered) { + this.addToPresenceLog({ type: "log", body: "You must enter the room to use this command." }); + return; + } + + const playerRig = document.querySelector("#player-rig"); + const scales = [0.0625, 0.125, 0.25, 0.5, 1.0, 1.5, 3, 5, 7.5, 12.5]; + const curScale = playerRig.object3D.scale; + + switch (command) { + case "fly": + if (playerRig.getAttribute("character-controller").fly !== true) { + playerRig.setAttribute("character-controller", "fly", true); + this.addToPresenceLog({ type: "log", body: "Fly mode enabled." }); + } else { + playerRig.setAttribute("character-controller", "fly", false); + this.addToPresenceLog({ type: "log", body: "Fly mode disabled." }); + } + break; + case "bigger": + for (let i = 0; i < scales.length; i++) { + if (scales[i] > curScale.x) { + playerRig.object3D.scale.set(scales[i], scales[i], scales[i]); + break; + } + } + + break; + case "smaller": + for (let i = scales.length - 1; i >= 0; i--) { + if (curScale.x > scales[i]) { + playerRig.object3D.scale.set(scales[i], scales[i], scales[i]); + break; + } + } + + break; + case "leave": + this.entryManager.exitScene(); + this.remountUI({ roomUnavailableReason: "left" }); + break; + case "duck": + spawnChatMessage(DUCK_URL); + this.scene.emit("quack"); + break; + } + }; +} diff --git a/src/react-components/chat-command-help.js b/src/react-components/chat-command-help.js new file mode 100644 index 0000000000000000000000000000000000000000..6a30a287380ea38764893ae4a4cb285a36bbd489 --- /dev/null +++ b/src/react-components/chat-command-help.js @@ -0,0 +1,30 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import styles from "../assets/stylesheets/chat-command-help.scss"; +import { FormattedMessage } from "react-intl"; + +export default class ChatCommandHelp extends Component { + static propTypes = { + matchingPrefix: PropTypes.string + }; + + render() { + const commands = ["help", "leave", "fly", "bigger", "smaller", "duck"]; + + return ( + <div className={styles.commandHelp}> + {commands.map( + c => + (this.props.matchingPrefix === "" || c.startsWith(this.props.matchingPrefix)) && ( + <div className={styles.entry} key={c}> + <div className={styles.command}>/{c}</div> + <div> + <FormattedMessage id={`commands.${c}`} /> + </div> + </div> + ) + )} + </div> + ); + } +} diff --git a/src/react-components/help-dialog.js b/src/react-components/help-dialog.js index 0e20c0aa98499e87f8e9fa4e52858d610d183f7f..d81b237045238396cd06f891156b9a7c0fa95875 100644 --- a/src/react-components/help-dialog.js +++ b/src/react-components/help-dialog.js @@ -19,8 +19,8 @@ export default class HelpDialog extends Component { </p> <p>When in a room, other avatars can see and hear you.</p> <p> - Use your controller's action button to teleport from place to place. If it has a trigger, use it to - pick up objects. + Use your controller's action button to teleport from place to place. If it has a grip, use it to pick + up objects. </p> <p> In VR, <b>look up</b> to find your menu. @@ -29,7 +29,7 @@ export default class HelpDialog extends Component { The <b>Mic Toggle</b> mutes your mic. </p> <p> - The <b>Pause/Resume Toggle</b> pauses all other avatars and lets you block others or remove objects. + The <b>Pause Toggle</b> pauses all other avatars and lets you block others or pin or remove objects. </p> <p className="dialog__box__contents__links"> <WithHoverSound> diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 521490271cab4cc98bbdfab4e682f23d4fef974d..bbb7e06e9dcbc01242b3a1917017953a702776d5 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -175,12 +175,6 @@ class HomeRoot extends Component { <div className={mainContentClassNames}> <div className={styles.headerContent}> <div className={styles.titleAndNav} onClick={() => (document.location = "/")}> - <WithHoverSound> - <div className={styles.hubs}>hubs</div> - </WithHoverSound> - <WithHoverSound> - <div className={styles.preview}>preview</div> - </WithHoverSound> <div className={styles.links}> <WithHoverSound> <a href="https://github.com/mozilla/hubs" rel="noreferrer noopener"> diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js index 40d18b9d34b63e0a1f4871cf75b9c6a63923ce55..70e6633b5fc7db4863af0f59bf4fcb945e544ab0 100644 --- a/src/react-components/presence-list.js +++ b/src/react-components/presence-list.js @@ -22,8 +22,8 @@ export default class PresenceList extends Component { const image = context && context.mobile ? PhoneImage : context && context.hmd ? HMDImage : DesktopImage; return ( - <WithHoverSound> - <div className={styles.row} key={sessionId}> + <WithHoverSound key={sessionId}> + <div className={styles.row}> <div className={styles.device}> <img src={image} /> </div> diff --git a/src/react-components/presence-log.js b/src/react-components/presence-log.js index 4426ac46e1e86d22e82b01de4f59152bb8b2184f..7eabb323aadd38e5eb3d37d5a669582a9f5699fc 100644 --- a/src/react-components/presence-log.js +++ b/src/react-components/presence-log.js @@ -56,7 +56,7 @@ export default class PresenceLog extends Component { maySpawn={e.maySpawn} /> ); - case "spawn": { + case "spawn": return ( <PhotoMessage key={e.key} @@ -67,7 +67,12 @@ export default class PresenceLog extends Component { hubId={this.props.hubId} /> ); - } + case "log": + return ( + <div key={e.key} className={classNames(entryClasses)}> + {e.body} + </div> + ); } }; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 77b0dfba4310c7986f9e4775cc38aa35c2449e8b..07a85c470178b2f04c270360b039fed303fc2be1 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -31,6 +31,7 @@ import CreateObjectDialog from "./create-object-dialog.js"; import PresenceLog from "./presence-log.js"; import PresenceList from "./presence-list.js"; import TwoDHUD from "./2d-hud"; +import ChatCommandHelp from "./chat-command-help"; import { spawnChatMessage } from "./chat-message"; import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; @@ -679,12 +680,12 @@ class UIRoot extends Component { <div> <FormattedMessage id={exitSubtitleId} /> <p /> - {this.props.roomUnavailableReason && ( + {this.props.roomUnavailableReason !== "left" && ( <div> You can also{" "} <WithHoverSound> - <a href="/">create a new room</a>. - </WithHoverSound> + <a href="/">create a new room</a> + </WithHoverSound>. </div> )} </div> @@ -1101,6 +1102,9 @@ class UIRoot extends Component { {entryFinished && ( <form onSubmit={this.sendMessage}> <div className={styles.messageEntryInRoom} style={{ height: pendingMessageFieldHeight }}> + {this.state.pendingMessage.startsWith("/") && ( + <ChatCommandHelp matchingPrefix={this.state.pendingMessage.substring(1)} /> + )} <textarea style={{ height: pendingMessageTextareaHeight }} className={classNames([ @@ -1209,7 +1213,7 @@ class UIRoot extends Component { )} <WithHoverSound> - <button onClick={() => this.showHelpDialog()} className={styles.helpIcon}> + <button onClick={() => this.showHelpDialog()} className={classNames([styles.helpIcon, "help-button"])}> <i> <FontAwesomeIcon icon={faQuestion} /> </i> diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js index 1f8e87d60fae179a802a9cf246cd79f1be09cead..ed34939f2e6ec0a38cd7858d7c2003eb1f48776e 100644 --- a/src/scene-entry-manager.js +++ b/src/scene-entry-manager.js @@ -51,6 +51,9 @@ export default class SceneEntryManager { } if (enterInVR) { + // HACK - A-Frame calls getVRDisplays at module load, we want to do it here to + // force gamepads to become live. + navigator.getVRDisplays(); this.scene.enterVR(); } else if (AFRAME.utils.device.isMobile()) { document.body.addEventListener("touchend", requestFullscreen); @@ -75,6 +78,8 @@ export default class SceneEntryManager { return; } + this.scene.setAttribute("motion-capture-replayer", "enabled", false); + if (mediaStream) { NAF.connection.adapter.setLocalMediaStream(mediaStream); } diff --git a/src/spoke.js b/src/spoke.js index d1f4fb3aa7322667d62faa8edf2b57adc296f48a..1764ba2eea6d2fc0088ed0e97bd14e7cf645e89d 100644 --- a/src/spoke.js +++ b/src/spoke.js @@ -44,7 +44,8 @@ class SpokeLanding extends Component { platform: getPlatform(), downloadClicked: false, downloadLinkForCurrentPlatform: {}, - showPlayer: false + showPlayer: false, + playerVideoId: "WmQKZJPhV7s" }; } @@ -189,9 +190,20 @@ class SpokeLanding extends Component { <FormattedMessage id="spoke.browse_all_versions" /> </a> )} - <button className={styles.playButton} onClick={() => this.setState({ showPlayer: true })}> - <FormattedMessage id="spoke.play_button" /> - </button> + <div className={styles.tutorialButtons}> + <button + className={styles.playButton} + onClick={() => this.setState({ showPlayer: true, playerVideoId: "WmQKZJPhV7s" })} + > + <FormattedMessage id="spoke.beginner_tutorial_button" /> + </button> + <button + className={styles.playButton} + onClick={() => this.setState({ showPlayer: true, playerVideoId: "1Yg5x4Plz_4" })} + > + <FormattedMessage id="spoke.advanced_tutorial_button" /> + </button> + </div> </div> </div> <div className={styles.heroVideo}> @@ -210,7 +222,7 @@ class SpokeLanding extends Component { <YouTube className={styles.playerVideo} opts={{ rel: 0 }} - videoId="WmQKZJPhV7s" + videoId={this.state.playerVideoId} onReady={e => e.target.playVideo()} /> {platform !== "unsupported" && ( diff --git a/src/systems/tunnel-effect.js b/src/systems/tunnel-effect.js index 88217e6757d8898c89009bef89b2a7c201b28b14..a254b7800f6fe1030df000cf00e3ef8fc8adf819 100644 --- a/src/systems/tunnel-effect.js +++ b/src/systems/tunnel-effect.js @@ -158,6 +158,8 @@ AFRAME.registerSystem("tunneleffect", { * use the render func of the effect composer when we need the postprocessing */ _bindRenderFunc: function() { - this.scene.renderer.render = this.postProcessingRenderFunc; + if (this.postProcessingRenderFunc) { + this.scene.renderer.render = this.postProcessingRenderFunc; + } } }); diff --git a/src/systems/userinput/devices/keyboard.js b/src/systems/userinput/devices/keyboard.js index 572e78596eb677e96e04a70c49f09465e4f0efc8..a2338567d0666a5477f622af969efa14e3e2297a 100644 --- a/src/systems/userinput/devices/keyboard.js +++ b/src/systems/userinput/devices/keyboard.js @@ -4,12 +4,13 @@ export class KeyboardDevice { this.keys = {}; this.events = []; - ["keydown", "keyup", "blur", "mouseout"].map(x => document.addEventListener(x, this.events.push.bind(this.events))); + ["keydown", "keyup"].map(x => document.addEventListener(x, this.events.push.bind(this.events))); + ["blur"].map(x => window.addEventListener(x, this.events.push.bind(this.events))); } write(frame) { this.events.forEach(event => { - if (event.type === "blur" || event.type === "mouseout") { + if (event.type === "blur") { this.keys = {}; return; } diff --git a/src/systems/userinput/userinput.js b/src/systems/userinput/userinput.js index 7bd37e7ee4cd65ad9c4e1ce8cddb814f9c836a4e..58136575df7246432debf21049b1c7e78b715603 100644 --- a/src/systems/userinput/userinput.js +++ b/src/systems/userinput/userinput.js @@ -70,98 +70,101 @@ AFRAME.registerSystem("userinput", { this.pendingSetChanges = []; this.activeDevices = new Set([new MouseDevice(), new AppAwareMouseDevice(), new KeyboardDevice(), new HudDevice()]); + if (AFRAME.utils.device.isMobile()) { + this.activeDevices.add(new AppAwareTouchscreenDevice()); + } + this.registeredMappings = new Set([keyboardDebuggingBindings]); this.xformStates = new Map(); - let connectedGamepadBindings; + const vrGamepadMappings = new Map(); + vrGamepadMappings.set(ViveControllerDevice, viveUserBindings); + vrGamepadMappings.set(OculusTouchControllerDevice, oculusTouchUserBindings); + vrGamepadMappings.set(OculusGoControllerDevice, oculusGoUserBindings); + vrGamepadMappings.set(DaydreamControllerDevice, daydreamUserBindings); - const appAwareTouchscreenDevice = new AppAwareTouchscreenDevice(); + const nonVRGamepadMappings = new Map(); + nonVRGamepadMappings.set(XboxControllerDevice, xboxControllerUserBindings); + nonVRGamepadMappings.set(GamepadDevice, gamepadBindings); + + const updateBindingsForVRMode = () => { + const inVRMode = this.el.sceneEl.is("vr-mode"); + const isMobile = AFRAME.utils.device.isMobile(); - const disableNonGamepadBindings = () => { - if (AFRAME.utils.device.isMobile()) { - this.activeDevices.delete(appAwareTouchscreenDevice); - this.registeredMappings.delete(touchscreenUserBindings); + if (inVRMode) { + console.log("Using VR bindings."); + this.registeredMappings.delete(isMobile ? touchscreenUserBindings : keyboardMouseUserBindings); + // add mappings for all active VR input devices + for (const activeDevice of this.activeDevices) { + const mapping = vrGamepadMappings.get(activeDevice.constructor); + mapping && this.registeredMappings.add(mapping); + } } else { - this.registeredMappings.delete(keyboardMouseUserBindings); + console.log("Using Non-VR bindings."); + // remove mappings for all active VR input devices + for (const activeDevice of this.activeDevices) { + this.registeredMappings.delete(vrGamepadMappings.get(activeDevice.constructor)); + } + this.registeredMappings.add(isMobile ? touchscreenUserBindings : keyboardMouseUserBindings); } - }; - const enableNonGamepadBindings = () => { - if (AFRAME.utils.device.isMobile()) { - this.activeDevices.add(appAwareTouchscreenDevice); - this.registeredMappings.add(touchscreenUserBindings); - } else { - this.registeredMappings.add(keyboardMouseUserBindings); + for (const activeDevice of this.activeDevices) { + const mapping = nonVRGamepadMappings.get(activeDevice.constructor); + mapping && this.registeredMappings.add(mapping); } }; - const updateBindingsForVRMode = () => { - const inVRMode = this.el.sceneEl.is("vr-mode"); - - if (inVRMode) { - disableNonGamepadBindings(); - this.registeredMappings.add(connectedGamepadBindings); + const gamepadConnected = e => { + let gamepadDevice; + for (const activeDevice of this.activeDevices) { + if (activeDevice.gamepad && activeDevice.gamepad.index === e.gamepad.index) { + console.warn("connected already fired for gamepad", e.gamepad); + return; // multiple connect events without a disconnect event + } + } + if (e.gamepad.id === "OpenVR Gamepad") { + gamepadDevice = new ViveControllerDevice(e.gamepad); + } else if (e.gamepad.id.startsWith("Oculus Touch")) { + gamepadDevice = new OculusTouchControllerDevice(e.gamepad); + } else if (e.gamepad.id === "Oculus Go Controller") { + gamepadDevice = new OculusGoControllerDevice(e.gamepad); + } else if (e.gamepad.id === "Daydream Controller") { + gamepadDevice = new DaydreamControllerDevice(e.gamepad); + } else if (e.gamepad.id.includes("Xbox")) { + gamepadDevice = new XboxControllerDevice(e.gamepad); } else { - enableNonGamepadBindings(); - this.registeredMappings.delete(connectedGamepadBindings); + gamepadDevice = new GamepadDevice(e.gamepad); } + + this.activeDevices.add(gamepadDevice); + + updateBindingsForVRMode(); }; - this.el.sceneEl.addEventListener("enter-vr", updateBindingsForVRMode); - this.el.sceneEl.addEventListener("exit-vr", updateBindingsForVRMode); - updateBindingsForVRMode(); - window.addEventListener( - "gamepadconnected", - e => { - let gamepadDevice; - const entered = this.el.sceneEl.is("entered"); - for (let i = 0; i < this.activeDevices.length; i++) { - const activeDevice = this.activeDevices[i]; - if (activeDevice.gamepad && activeDevice.gamepad === e.gamepad) { - console.warn("ignoring gamepad", e.gamepad); - return; // multiple connect events without a disconnect event - } - } - if (e.gamepad.id === "OpenVR Gamepad") { - gamepadDevice = new ViveControllerDevice(e.gamepad); - connectedGamepadBindings = viveUserBindings; - } else if (e.gamepad.id.startsWith("Oculus Touch")) { - gamepadDevice = new OculusTouchControllerDevice(e.gamepad); - connectedGamepadBindings = oculusTouchUserBindings; - } else if (e.gamepad.id === "Oculus Go Controller") { - gamepadDevice = new OculusGoControllerDevice(e.gamepad); - connectedGamepadBindings = oculusGoUserBindings; - } else if (e.gamepad.id === "Daydream Controller") { - gamepadDevice = new DaydreamControllerDevice(e.gamepad); - connectedGamepadBindings = daydreamUserBindings; - } else if (e.gamepad.id.includes("Xbox")) { - gamepadDevice = new XboxControllerDevice(e.gamepad); - connectedGamepadBindings = xboxControllerUserBindings; - } else { - gamepadDevice = new GamepadDevice(e.gamepad); - connectedGamepadBindings = gamepadBindings; + const gamepadDisconnected = e => { + for (const device of this.activeDevices) { + if (device.gamepad && device.gamepad.index === e.gamepad.index) { + this.registeredMappings.delete( + vrGamepadMappings.get(device.constructor) || nonVRGamepadMappings.get(device.constructor) + ); + this.activeDevices.delete(device); + return; } + } - if (entered) { - this.registeredMappings.add(connectedGamepadBindings); - } + updateBindingsForVRMode(); + }; - this.activeDevices.add(gamepadDevice); - }, - false - ); - window.addEventListener( - "gamepaddisconnected", - e => { - for (const device of this.activeDevices) { - if (device.gamepad === e.gamepad) { - this.activeDevices.delete(device); - return; - } - } - }, - false - ); + window.addEventListener("gamepadconnected", gamepadConnected, false); + window.addEventListener("gamepaddisconnected", gamepadDisconnected, false); + for (const gamepad of navigator.getGamepads()) { + gamepad && gamepadConnected({ gamepad }); + } + + this.el.sceneEl.addEventListener("enter-vr", updateBindingsForVRMode); + this.el.sceneEl.addEventListener("exit-vr", updateBindingsForVRMode); + + updateBindingsForVRMode(); }, tick() { @@ -197,6 +200,7 @@ AFRAME.registerSystem("userinput", { console.log("frame", this.frame); console.log("sets", this.activeSets); console.log("bindings", this.activeBindings); + console.log("mappings", this.registeredMappings); console.log("devices", this.activeDevices); console.log("xformStates", this.xformStates); } diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index 7a05a2c725b9ade88eb21bdd0df6e075459f8920..20bce73736f73f7275b5363cb8dec652767b4b3d 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -108,6 +108,31 @@ export const addMedia = (src, template, contentOrigin, resolve = false, resize = entity.setAttribute("media-loader", { resize, resolve, src: typeof src === "string" ? src : "" }); scene.appendChild(entity); + const fireLoadingTimeout = setTimeout(() => { + scene.emit("media-loading", { src: src }); + }, 100); + + ["model-loaded", "video-loaded", "image-loaded"].forEach(eventName => { + entity.addEventListener(eventName, () => { + clearTimeout(fireLoadingTimeout); + + if (!entity.classList.contains("pen")) { + entity.object3D.scale.setScalar(0.5); + + entity.setAttribute("animation__spawn-start", { + property: "scale", + delay: 50, + dur: 300, + from: { x: 0.5, y: 0.5, z: 0.5 }, + to: { x: 1.0, y: 1.0, z: 1.0 }, + easing: "easeOutElastic" + }); + } + + scene.emit("media-loaded", { src: src }); + }); + }); + const orientation = new Promise(function(resolve) { if (src instanceof File) { getOrientation(src, x => { @@ -156,6 +181,7 @@ export function injectCustomShaderChunks(obj) { // hover/toggle state, so for now just skip these while we figure out a more correct // solution. if (object.el.classList.contains("ui")) return; + if (object.el.getAttribute("text-button")) return; object.material = object.material.clone(); object.material.onBeforeCompile = shader => {