├── src
├── assets
│ ├── locales
│ │ ├── jp.json
│ │ └── locale_configs.js
│ ├── sfx
│ │ ├── pop.mp3
│ │ ├── click.wav
│ │ ├── emoji.mp3
│ │ ├── fart1.mp3
│ │ ├── fart2.mp3
│ │ ├── fart3.mp3
│ │ ├── fart4.mp3
│ │ ├── fart5.mp3
│ │ ├── quack.mp3
│ │ ├── tack.mp3
│ │ ├── tick.mp3
│ │ ├── fart-big.mp3
│ │ ├── freeze.mp3
│ │ ├── vacuum.mp3
│ │ ├── welcome.mp3
│ │ ├── click-alt.wav
│ │ ├── click-down.wav
│ │ ├── launcher1.mp3
│ │ ├── launcher2.mp3
│ │ ├── launcher3.mp3
│ │ ├── launcher4.mp3
│ │ ├── launcher5.mp3
│ │ ├── particles.mp3
│ │ ├── quick-turn.mp3
│ │ ├── quiet-pop.mp3
│ │ ├── tap-mellow.mp3
│ │ ├── launcher-big.mp3
│ │ └── special-quack.mp3
│ ├── images
│ │ ├── uv.jpg
│ │ ├── audio.png
│ │ ├── water.png
│ │ ├── water1.png
│ │ ├── app-logo.png
│ │ ├── avatar-sheet.png
│ │ ├── company-logo.png
│ │ ├── cubemap
│ │ │ ├── negx.jpg
│ │ │ ├── negy.jpg
│ │ │ ├── negz.jpg
│ │ │ ├── posx.jpg
│ │ │ ├── posy.jpg
│ │ │ └── posz.jpg
│ │ ├── editor-logo.png
│ │ ├── landing-logo.png
│ │ ├── media-error.gif
│ │ ├── warning_icon.png
│ │ ├── avatar-sheet.basis
│ │ ├── loading-particle.png
│ │ ├── avatar
│ │ │ ├── viseme-12.svg
│ │ │ ├── viseme-11.svg
│ │ │ ├── viseme-0.svg
│ │ │ ├── viseme-6.svg
│ │ │ ├── avatar.svgi
│ │ │ ├── eyes-4.svg
│ │ │ ├── viseme-1.svg
│ │ │ ├── viseme-2.svg
│ │ │ ├── viseme-9.svg
│ │ │ ├── viseme-8.svg
│ │ │ └── viseme-7.svg
│ │ ├── cursor.svg
│ │ ├── home-hero-background-unbranded.png
│ │ ├── icons
│ │ │ ├── sphere.svgi
│ │ │ ├── box.svgi
│ │ │ ├── security.svgi
│ │ │ ├── security-shadow.svgi
│ │ │ ├── add.svgi
│ │ │ ├── expand-down.svgi
│ │ │ ├── expand-up.svgi
│ │ │ ├── add-shadow.svgi
│ │ │ ├── dots-vertical.svgi
│ │ │ ├── dots-horizontal.svgi
│ │ │ ├── dots-horizontal-overlay-shadow.svgi
│ │ │ ├── notifications.svgi
│ │ │ ├── notifications-shadow.svgi
│ │ │ ├── menu-shadow.svgi
│ │ │ ├── builder-off.svgi
│ │ │ ├── invite.svgi
│ │ │ ├── next-page.svgi
│ │ │ ├── prev-page.svgi
│ │ │ ├── presets.svgi
│ │ │ ├── trash.svgi
│ │ │ ├── page.svgi
│ │ │ ├── upload.svgi
│ │ │ ├── back-shadow.svgi
│ │ │ ├── call-out.svgi
│ │ │ ├── switcher.svg
│ │ │ ├── screen.svgi
│ │ │ ├── fill.svgi
│ │ │ ├── check.svgi
│ │ │ ├── check-big.svgi
│ │ │ ├── cube.svgi
│ │ │ ├── edit.svgi
│ │ │ ├── scenes-off.svgi
│ │ │ ├── edit-shadow.svgi
│ │ │ ├── builder-on.svgi
│ │ │ ├── go-to.svgi
│ │ │ ├── important.svgi
│ │ │ ├── heart.svgi
│ │ │ ├── cancel.svgi
│ │ │ ├── sun-shadow.svgi
│ │ │ ├── github-off.svgi
│ │ │ ├── restore.svgi
│ │ │ ├── mic-unmuted.svgi
│ │ │ ├── thumb-label.svg
│ │ │ ├── call-end.svgi
│ │ │ ├── pick.svgi
│ │ │ ├── github-on.svgi
│ │ │ ├── mic-muted.svgi
│ │ │ └── discord-space-icon.svgi
│ │ └── jel-loading-shadow.svg
│ ├── hud
│ │ ├── button.9.png
│ │ └── action_button.9.png
│ ├── models
│ │ ├── face.vox
│ │ ├── chiclet.glb
│ │ ├── grass1.vox
│ │ ├── DuckyMesh.glb
│ │ ├── screen.blend
│ │ ├── chiclet-flip.glb
│ │ └── DefaultAvatar.glb
│ ├── waternormals.jpg
│ ├── fonts
│ │ └── Roboto-msdf.png
│ ├── video-overlay
│ │ ├── pause.png
│ │ ├── play.png
│ │ ├── play-hover.png
│ │ └── pause-hover.png
│ └── stylesheets
│ │ └── root.scss
├── systems
│ ├── userinput
│ │ ├── bindings
│ │ │ ├── generic-gamepad.js
│ │ │ ├── utils.js
│ │ │ ├── keyboard-debugging.js
│ │ │ └── cardboard-user.js
│ │ ├── devices
│ │ │ ├── hud.js
│ │ │ ├── copy-sitting-to-standing-transform.js
│ │ │ ├── gamepad.js
│ │ │ └── touchscreen
│ │ │ │ └── assignments.js
│ │ ├── array-backed-set.js
│ │ ├── pose.js
│ │ └── arm-model.js
│ ├── permissions.js
│ ├── listed-media.js
│ ├── cursor-pose-tracking.js
│ ├── sprites
│ │ ├── sprite.vert
│ │ └── sprite.frag
│ ├── enter-vr-button-system.js
│ ├── frame-scheduler.js
│ ├── scene-preview-camera-system.js
│ └── interaction-sfx-system.js
├── loaders
│ └── basis_transcoder.wasm
├── utils
│ ├── next-tick.js
│ ├── layout-utils.js
│ ├── animation.js
│ ├── crypto-utils.js
│ ├── logging.js
│ ├── presence-utils.js
│ ├── qs_truthy.js
│ ├── threejs-avoid-disposing-programs.js
│ ├── sharedbuffergeometrymanager.js
│ ├── set-utils.js
│ ├── threejs-video-texture-pause.js
│ ├── disable-ios-zoom.js
│ ├── get-current-player-height.js
│ ├── concurrent-load-detector.js
│ ├── scene-graph.js
│ ├── async-utils.js
│ ├── space-channel.js
│ ├── dyna-channel.js
│ ├── dpad.js
│ ├── fullscreen.js
│ ├── threejs-world-update.js
│ ├── image-bitmap-utils.js
│ ├── account-channel.js
│ ├── pdf-pool.js
│ ├── media-highlight-frag.glsl
│ ├── promisify-worker.js
│ ├── membership-utils.js
│ ├── crypto.js
│ ├── permissions-utils.js
│ └── focus-utils.js
├── terra
│ ├── blocks
│ │ ├── models
│ │ │ ├── feature.js
│ │ │ ├── glass.js
│ │ │ ├── dirt.js
│ │ │ └── block.js
│ │ ├── textures
│ │ │ └── block.js
│ │ └── index.js
│ └── constants.js
├── components
│ ├── set-yxz-order.js
│ ├── disable-frustum-culling.js
│ ├── visibility-by-path.js
│ ├── media-stream.js
│ ├── track-pose.js
│ ├── scene-components.js
│ ├── scale-in-screen-space.js
│ ├── set-max-resolution.js
│ ├── networked-avatar.js
│ ├── action-to-remove.js
│ ├── replay.js
│ ├── animation-mixer.js
│ ├── billboard.js
│ ├── set-active-camera.js
│ ├── scalable-when-grabbed.js
│ ├── inspect-button.js
│ ├── owned-object-limiter.js
│ ├── heightfield.js
│ ├── action-to-event.js
│ ├── tags.js
│ ├── quack.js
│ ├── remove-networked-object-button.js
│ ├── unmute-video-button.js
│ ├── destroy-at-extreme-distances.js
│ ├── layers.js
│ ├── duck.js
│ ├── virtual-gamepad-controls.css
│ ├── set-unowned-body-kinematic.js
│ ├── periodic-full-syncs.js
│ ├── environment-map.js
│ ├── bone-visibility.js
│ └── networked-counter.js
├── ui
│ ├── loading-panel.stories.js
│ ├── emoji-picker.stories.js
│ ├── panel-section-header.js
│ ├── chat-input-panel.stories.js
│ ├── name-input-panel.stories.js
│ ├── layer-pager.stories.js
│ ├── invite-panel.stories.js
│ ├── popup-panel.js
│ ├── color-picker.stories.js
│ ├── permissions-popup.stories.js
│ ├── environment-settings-popup.stories.js
│ ├── create-select.stories.js
│ ├── emoji-equip.stories.js
│ ├── side-panels.js
│ ├── wrapped-intl-provider.js
│ ├── floating-text-input.js
│ ├── equipped-emoji-icon.js
│ ├── key-tips.stories.js
│ ├── profile-editor-popup.stories.js
│ ├── atom-trail.stories.js
│ ├── create-embed-input-panel.stories.js
│ ├── equipped-color-icon.js
│ ├── action-button.stories.js
│ ├── tooltip.js
│ ├── avatar-swatch.stories.js
│ ├── input
│ │ └── useInstallPWA.js
│ ├── create-embed-popup.js
│ ├── chat-input-popup.js
│ ├── create-select-popup.js
│ ├── folder-access-request-panel.js
│ ├── segment-control.stories.js
│ ├── emoji-popup.js
│ ├── popup-menu.stories.js
│ └── loading-spinner.js
├── objects
│ └── chiclet-geometry.js
├── gltf-component-mappings.js
├── workers
│ ├── vox-mesher.worker.js
│ └── sketchfab-zip.worker.js
├── styles.js
├── worklets
│ └── audio-forward.worklet.js
├── App.js
└── constants.js
├── .npmrc
├── .tool-versions
├── .prettierignore
├── .eslintignore
├── .prettierrc.json
├── bin
└── compress-basis-assets.sh
├── .stylelintrc
├── babel.config.js
├── .storybook
├── preview-head.html
├── preview.js
└── main.js
├── doc
├── meta-dataflow.txt
└── spritesheet-generation.md
├── svox.fbs
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── .htmlhintrc
├── README.md
├── .eslintrc.js
├── .gitignore
└── .defaults.env
/src/assets/locales/jp.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps = true
2 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 16.17.0
2 | yarn 1.22.19
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | *.gltf
3 | src/vendor/
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | !.eslintrc.js
2 | src/vendor/*
3 | scripts/bot/node_modules/
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "parser": "babylon"
4 | }
5 |
--------------------------------------------------------------------------------
/src/systems/userinput/bindings/generic-gamepad.js:
--------------------------------------------------------------------------------
1 | export const gamepadBindings = {};
2 |
--------------------------------------------------------------------------------
/src/assets/sfx/pop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/pop.mp3
--------------------------------------------------------------------------------
/src/assets/images/uv.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/uv.jpg
--------------------------------------------------------------------------------
/src/assets/sfx/click.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/click.wav
--------------------------------------------------------------------------------
/src/assets/sfx/emoji.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/emoji.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/fart1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/fart1.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/fart2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/fart2.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/fart3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/fart3.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/fart4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/fart4.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/fart5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/fart5.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/quack.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/quack.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/tack.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/tack.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/tick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/tick.mp3
--------------------------------------------------------------------------------
/src/assets/hud/button.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/hud/button.9.png
--------------------------------------------------------------------------------
/src/assets/images/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/audio.png
--------------------------------------------------------------------------------
/src/assets/images/water.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/water.png
--------------------------------------------------------------------------------
/src/assets/models/face.vox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/models/face.vox
--------------------------------------------------------------------------------
/src/assets/sfx/fart-big.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/fart-big.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/freeze.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/freeze.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/vacuum.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/vacuum.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/welcome.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/welcome.mp3
--------------------------------------------------------------------------------
/src/assets/waternormals.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/waternormals.jpg
--------------------------------------------------------------------------------
/src/assets/images/water1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/water1.png
--------------------------------------------------------------------------------
/src/assets/models/chiclet.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/models/chiclet.glb
--------------------------------------------------------------------------------
/src/assets/models/grass1.vox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/models/grass1.vox
--------------------------------------------------------------------------------
/src/assets/sfx/click-alt.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/click-alt.wav
--------------------------------------------------------------------------------
/src/assets/sfx/click-down.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/click-down.wav
--------------------------------------------------------------------------------
/src/assets/sfx/launcher1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/launcher1.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/launcher2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/launcher2.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/launcher3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/launcher3.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/launcher4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/launcher4.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/launcher5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/launcher5.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/particles.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/particles.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/quick-turn.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/quick-turn.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/quiet-pop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/quiet-pop.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/tap-mellow.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/tap-mellow.mp3
--------------------------------------------------------------------------------
/src/assets/fonts/Roboto-msdf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/fonts/Roboto-msdf.png
--------------------------------------------------------------------------------
/src/assets/images/app-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/app-logo.png
--------------------------------------------------------------------------------
/src/assets/models/DuckyMesh.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/models/DuckyMesh.glb
--------------------------------------------------------------------------------
/src/assets/models/screen.blend:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/models/screen.blend
--------------------------------------------------------------------------------
/src/assets/sfx/launcher-big.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/launcher-big.mp3
--------------------------------------------------------------------------------
/src/assets/sfx/special-quack.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/sfx/special-quack.mp3
--------------------------------------------------------------------------------
/bin/compress-basis-assets.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | pushd src/assets/images
3 | basisu -mipmap -q 224 avatar-sheet.png
4 | popd
5 |
--------------------------------------------------------------------------------
/src/assets/hud/action_button.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/hud/action_button.9.png
--------------------------------------------------------------------------------
/src/assets/images/avatar-sheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/avatar-sheet.png
--------------------------------------------------------------------------------
/src/assets/images/company-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/company-logo.png
--------------------------------------------------------------------------------
/src/assets/images/cubemap/negx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/cubemap/negx.jpg
--------------------------------------------------------------------------------
/src/assets/images/cubemap/negy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/cubemap/negy.jpg
--------------------------------------------------------------------------------
/src/assets/images/cubemap/negz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/cubemap/negz.jpg
--------------------------------------------------------------------------------
/src/assets/images/cubemap/posx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/cubemap/posx.jpg
--------------------------------------------------------------------------------
/src/assets/images/cubemap/posy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/cubemap/posy.jpg
--------------------------------------------------------------------------------
/src/assets/images/cubemap/posz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/cubemap/posz.jpg
--------------------------------------------------------------------------------
/src/assets/images/editor-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/editor-logo.png
--------------------------------------------------------------------------------
/src/assets/images/landing-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/landing-logo.png
--------------------------------------------------------------------------------
/src/assets/images/media-error.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/media-error.gif
--------------------------------------------------------------------------------
/src/assets/images/warning_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/warning_icon.png
--------------------------------------------------------------------------------
/src/assets/models/chiclet-flip.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/models/chiclet-flip.glb
--------------------------------------------------------------------------------
/src/assets/video-overlay/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/video-overlay/pause.png
--------------------------------------------------------------------------------
/src/assets/video-overlay/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/video-overlay/play.png
--------------------------------------------------------------------------------
/src/loaders/basis_transcoder.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/loaders/basis_transcoder.wasm
--------------------------------------------------------------------------------
/src/assets/images/avatar-sheet.basis:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/avatar-sheet.basis
--------------------------------------------------------------------------------
/src/assets/models/DefaultAvatar.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/models/DefaultAvatar.glb
--------------------------------------------------------------------------------
/src/assets/images/loading-particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/loading-particle.png
--------------------------------------------------------------------------------
/src/assets/video-overlay/play-hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/video-overlay/play-hover.png
--------------------------------------------------------------------------------
/src/assets/video-overlay/pause-hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/video-overlay/pause-hover.png
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-12.svg:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/src/utils/next-tick.js:
--------------------------------------------------------------------------------
1 | export default function nextTick() {
2 | return new Promise(resolve => {
3 | setTimeout(resolve, 0);
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/src/terra/blocks/models/feature.js:
--------------------------------------------------------------------------------
1 | import block from "./block";
2 |
3 | export default {
4 | ...block,
5 | name: "Feature",
6 | textures: {}
7 | };
8 |
--------------------------------------------------------------------------------
/src/assets/images/cursor.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/home-hero-background-unbranded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webspace-sdk/webspace-engine/HEAD/src/assets/images/home-hero-background-unbranded.png
--------------------------------------------------------------------------------
/src/components/set-yxz-order.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("set-yxz-order", {
2 | init: function() {
3 | this.el.object3D.rotation.order = "YXZ";
4 | }
5 | });
6 |
--------------------------------------------------------------------------------
/src/terra/blocks/models/glass.js:
--------------------------------------------------------------------------------
1 | import block from "./block";
2 |
3 | export default {
4 | ...block,
5 | name: "Glass",
6 | isTransparent: true,
7 | textures: {}
8 | };
9 |
--------------------------------------------------------------------------------
/src/components/disable-frustum-culling.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("disable-frustum-culling", {
2 | init() {
3 | this.el.object3D.traverse(o => (o.frustumCulled = false));
4 | }
5 | });
6 |
--------------------------------------------------------------------------------
/src/terra/blocks/models/dirt.js:
--------------------------------------------------------------------------------
1 | import block from "./block";
2 |
3 | export default {
4 | ...block,
5 | name: "Dirt",
6 | textures: {
7 | block: "block.js"
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/assets/images/icons/sphere.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icons/box.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-11.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/utils/layout-utils.js:
--------------------------------------------------------------------------------
1 | export function outerHeight(el) {
2 | let height = el.offsetHeight;
3 | const style = getComputedStyle(el);
4 |
5 | height += parseInt(style.marginTop) + parseInt(style.marginBottom);
6 | return height;
7 | }
8 |
--------------------------------------------------------------------------------
/src/assets/images/icons/security.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/icons/security-shadow.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/icons/add.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/icons/expand-down.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icons/expand-up.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icons/add-shadow.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/icons/dots-vertical.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/animation.js:
--------------------------------------------------------------------------------
1 | export function addAnimationComponents(modelEl) {
2 | if (!modelEl.components["animation-mixer"]) {
3 | return;
4 | }
5 |
6 | if (!modelEl.querySelector("[loop-animation]")) {
7 | modelEl.setAttribute("loop-animation", "");
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/crypto-utils.js:
--------------------------------------------------------------------------------
1 | export async function toHexDigest(str) {
2 | const hashData = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
3 | const hashArray = Array.from(new Uint8Array(hashData));
4 | return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-0.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icons/dots-horizontal.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/systems/permissions.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerSystem("permissions", {
2 | onPermissionsUpdated(handler) {
3 | window.APP.atomAccessManager.addEventListener("permissions_updated", handler);
4 | },
5 | can(permissionName) {
6 | return !!window.APP.atomAccessManager.hubCan(permissionName);
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-6.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/avatar.svgi:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/icons/dots-horizontal-overlay-shadow.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-recommended-scss",
3 | "rules": {
4 | "indentation": 2,
5 | "selector-pseudo-class-no-unknown": [true, { "ignorePseudoClasses": ["local"] }],
6 | "selector-type-no-unknown": [true, { "ignoreTypes": ["/^a-/"] }],
7 | "no-descending-specificity": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/systems/userinput/bindings/utils.js:
--------------------------------------------------------------------------------
1 | export const addSetsToBindings = mapping => {
2 | for (const setName in mapping) {
3 | for (const binding of mapping[setName]) {
4 | if (!binding.sets) {
5 | binding.sets = [];
6 | }
7 | binding.sets.push(setName);
8 | }
9 | }
10 | return mapping;
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/logging.js:
--------------------------------------------------------------------------------
1 | // A-Frame blows away any npm debug log filters so this allow the user to set the log filter
2 | // via the query string.
3 | import debug from "debug";
4 |
5 | const qs = new URLSearchParams(location.search);
6 | const logFilter = qs.get("log_filter");
7 |
8 | if (logFilter) {
9 | debug.enable(logFilter);
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/presence-utils.js:
--------------------------------------------------------------------------------
1 | export function getCurrentPresence() {
2 | if (!NAF.connection.presence?.states) return;
3 |
4 | for (const state of NAF.connection.presence.states.values()) {
5 | const clientId = state.client_id;
6 | if (clientId && NAF.clientId === clientId) return state;
7 | }
8 |
9 | return null;
10 | }
11 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/eyes-4.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/icons/notifications.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/jel-loading-shadow.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/images/icons/notifications-shadow.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/qs_truthy.js:
--------------------------------------------------------------------------------
1 | const qs = new URLSearchParams(location.search);
2 |
3 | export default function qsTruthy(param) {
4 | const val = qs.get(param);
5 | // if the param exists but is not set (e.g. "?foo&bar"), its value is the empty string.
6 | return val === "" || /1|on|true|yes/i.test(val);
7 | }
8 |
9 | export function qsGet(param) {
10 | return qs.get(param);
11 | }
12 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = api => {
2 | api.cache(true);
3 |
4 | return {
5 | presets: ["@babel/react", ["@babel/preset-env", { useBuiltIns: false }]],
6 | plugins: [
7 | ["react-intl", { messagesDir: "./public/messages", enforceDescriptions: false }],
8 | "transform-react-jsx-img-import",
9 | "babel-plugin-styled-components"
10 | ]
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/src/assets/images/icons/menu-shadow.svgi:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/icons/builder-off.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icons/invite.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/icons/next-page.svgi:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/icons/prev-page.svgi:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/icons/presets.svgi:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/components/visibility-by-path.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("visibility-by-path", {
2 | schema: {
3 | path: { type: "string" }
4 | },
5 | tick() {
6 | const userinput = AFRAME.scenes[0].systems.userinput;
7 | const shouldBeVisible = !!userinput.get(this.data.path);
8 | if (this.el.object3D.visible !== shouldBeVisible) {
9 | this.el.setAttribute("visible", shouldBeVisible);
10 | }
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/src/ui/loading-panel.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import LoadingPanel from "./loading-panel";
3 |
4 | export const Loading = () => (
5 |
13 |
14 |
15 | );
16 |
17 | export default {
18 | title: "Loading Panel"
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/media-stream.js:
--------------------------------------------------------------------------------
1 | // This component is added to media-video entities that are sharing the video
2 | // stream from this client.
3 | AFRAME.registerComponent("media-stream", {
4 | remove() {
5 | const streams = DOM_ROOT.querySelectorAll("[media-stream]");
6 | if (streams.length > 0) return;
7 |
8 | // No entities left, end the stream.
9 | this.el.sceneEl.emit("action_end_video_sharing");
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/src/assets/images/icons/trash.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/threejs-avoid-disposing-programs.js:
--------------------------------------------------------------------------------
1 | // For Webspaces, we do not want to dispose any programs, to avoid complilation mid-session.
2 | export default function patchThreeNoProgramDispose(renderer) {
3 | const programs = renderer.info.programs;
4 |
5 | const push = programs.push.bind(programs);
6 |
7 | // Hijack the array.push to increment the used times :P
8 | programs.push = function(o) {
9 | o.usedTimes++;
10 | return push(o);
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/assets/images/icons/page.svgi:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/systems/userinput/devices/hud.js:
--------------------------------------------------------------------------------
1 | import { paths } from "../paths";
2 |
3 | export class HudDevice {
4 | constructor() {
5 | this.events = [];
6 | DOM_ROOT.querySelector("a-scene").addEventListener("penButtonPressed", this.events.push.bind(this.events));
7 | }
8 |
9 | write(frame) {
10 | frame.setValueType(paths.device.hud.penButton, this.events.length !== 0);
11 | while (this.events.length) {
12 | this.events.pop();
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/images/icons/upload.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/ui/emoji-picker.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import EmojiPicker from "./emoji-picker";
3 |
4 | export const Basic = () => (
5 |
14 | console.log(name)} />
15 |
16 | );
17 |
18 | export default {
19 | title: "Emoji Picker"
20 | };
21 |
--------------------------------------------------------------------------------
/src/assets/images/icons/back-shadow.svgi:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/utils/sharedbuffergeometrymanager.js:
--------------------------------------------------------------------------------
1 | import SharedBufferGeometry from "./sharedbuffergeometry";
2 |
3 | export default class SharedBufferGeometryManager {
4 | constructor() {
5 | this.sharedBuffers = {};
6 | }
7 |
8 | addSharedBuffer(name, material, primitiveMode, maxBufferSize) {
9 | this.sharedBuffers[name] = new SharedBufferGeometry(material, primitiveMode, maxBufferSize);
10 | }
11 |
12 | getSharedBuffer(name) {
13 | return this.sharedBuffers[name];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/images/icons/call-out.svgi:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/root.scss:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | line-height: 1;
4 |
5 | font-family: Lato, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
6 | }
7 |
8 | body {
9 | background-color: #333;
10 | margin: 0;
11 | overflow: hidden;
12 | }
13 |
14 | body:focus {
15 | outline: none;
16 | }
17 |
18 | body::before {
19 | content: "sm";
20 | display: none;
21 | }
22 |
23 | a-assets img {
24 | display: none;
25 | }
26 |
--------------------------------------------------------------------------------
/src/assets/images/icons/switcher.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/utils/set-utils.js:
--------------------------------------------------------------------------------
1 | export function hasIntersection(a, b) {
2 | for (const e of a) {
3 | if (b.has(e)) return true;
4 | }
5 |
6 | return false;
7 | }
8 |
9 | // Is a full subset of a b
10 | export function isSubset(a, b) {
11 | for (const e of a) {
12 | if (!b.has(e)) return false;
13 | }
14 |
15 | return true;
16 | }
17 |
18 | export function isSetEqual(a, b) {
19 | if (a.size !== b.size) return false;
20 | for (const x of a) if (!b.has(x)) return false;
21 | return true;
22 | }
23 |
--------------------------------------------------------------------------------
/src/systems/userinput/bindings/keyboard-debugging.js:
--------------------------------------------------------------------------------
1 | import { paths } from "../paths";
2 | import { sets } from "../sets";
3 | import { xforms } from "./xforms";
4 | import { addSetsToBindings } from "./utils";
5 |
6 | export const keyboardDebuggingBindings = addSetsToBindings({
7 | [sets.global]: [
8 | {
9 | src: {
10 | value: paths.device.keyboard.key("l")
11 | },
12 | dest: {
13 | value: paths.actions.logDebugFrame
14 | },
15 | xform: xforms.rising
16 | }
17 | ]
18 | });
19 |
--------------------------------------------------------------------------------
/src/assets/locales/locale_configs.js:
--------------------------------------------------------------------------------
1 | // Certain locales are duplicates such as "zh-cn" and "zh" or "en-us" and "en".
2 | // For non "en" (since it's the default), use this mapping to define a fallback,
3 | // which will only be used if the specified locale file doesn't exist.
4 |
5 | export const AVAILABLE_LOCALES = {
6 | en: "English" //,
7 | // zh: "简体中文",
8 | // jp: "日本語"
9 | };
10 |
11 | export const FALLBACK_LOCALES = {
12 | //"zh-cn": "zh",
13 | //"zh-hans": "zh",
14 | //"zh-hans-cn": "zh",
15 | //ja: "jp"
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/track-pose.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("track-pose", {
2 | schema: {
3 | path: { type: "string" }
4 | },
5 |
6 | play() {
7 | if (!this.didRegister) {
8 | this.didRegister = true;
9 | this.el.sceneEl.systems["hubs-systems"].cursorPoseTrackingSystem.register(this.el.object3D, this.data.path);
10 | }
11 | },
12 | remove() {
13 | if (this.didRegister) {
14 | this.el.sceneEl.systems["hubs-systems"].cursorPoseTrackingSystem.unregister(this.el.object3D);
15 | }
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/scene-components.js:
--------------------------------------------------------------------------------
1 | import "./animation";
2 | import "./animation-mixer";
3 | import "./audio-feedback";
4 | import "./duck";
5 | import "./gltf-model-plus";
6 | import "./heightfield";
7 | import "./layers";
8 | import "./loop-animation";
9 | import "./media-loader";
10 | import "./point-light";
11 | import "./quack";
12 | import "./scene-preview-camera";
13 | import "./spawn-controller";
14 | import "./spot-light";
15 | import "./floaty-object";
16 | import "./super-spawner";
17 | import "./environment-map";
18 | import "./particle-emitter";
19 |
--------------------------------------------------------------------------------
/doc/meta-dataflow.txt:
--------------------------------------------------------------------------------
1 | hub metadata update
2 | channel.updateHubMeta -> broadcasts message -> pushes to DOM -> mutate observer -> hub metadata source -> fires hub_meta_refresh -> updates UI
3 |
4 | space metadata update
5 | dynachannel.updateSpace -> flush local updates -> writes tree (to filesystem) *and* broadcasts full HTML to peers via update_nav -> listens for update_nav -> updates local tree document -> rebuilds tree data -> fires treedata_updated event -> space metadata source -> fetch from tree index DOM (now updated to new document) -> fires space_meta_refresh -> updates UI
6 |
7 |
--------------------------------------------------------------------------------
/src/ui/panel-section-header.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export default styled.div`
4 | color: var(--panel-header-text-color);
5 | font-size: var(--panel-header-text-size);
6 | font-weight: var(--panel-header-text-weight);
7 | text-transform: uppercase;
8 | margin: 32px 16px 16px 16px;
9 | display: flex;
10 | align-items: center;
11 |
12 | &:first-child {
13 | margin-top: 0;
14 | }
15 |
16 | & .show-on-hover {
17 | visibility: hidden;
18 | }
19 |
20 | &:hover .show-on-hover {
21 | visibility: visible;
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/src/ui/chat-input-panel.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ChatInputPanel from "./chat-input-panel";
3 | import sharedStyles from "../../assets/stylesheets/shared.scss";
4 | import classNames from "classnames";
5 |
6 | export const Normal = () => (
7 |
12 | );
13 |
14 | export default {
15 | title: "Chat Input Panel"
16 | };
17 |
--------------------------------------------------------------------------------
/src/ui/name-input-panel.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import NameInputPanel from "./name-input-panel";
3 | import sharedStyles from "../../assets/stylesheets/shared.scss";
4 | import classNames from "classnames";
5 |
6 | export const Normal = () => (
7 |
12 | );
13 |
14 | export default {
15 | title: "Name Input Panel"
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/threejs-video-texture-pause.js:
--------------------------------------------------------------------------------
1 | // Monkey patches three.js to stop doing texture uploads for paused videos
2 | THREE.VideoTexture.prototype.update = function() {
3 | const video = this.image;
4 | const paused = video.paused;
5 |
6 | // Don't transfer textures from paused videos.
7 | if (paused && this.wasPaused) return;
8 |
9 | if (video.readyState >= video.HAVE_CURRENT_DATA) {
10 | if (paused) {
11 | this.wasPaused = true;
12 | } else if (this.wasPaused) {
13 | this.wasPaused = false;
14 | }
15 |
16 | this.needsUpdate = true;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/ui/layer-pager.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import LayerPager from "./layer-pager";
3 |
4 | export const MidPage = () => (
5 |
15 | console.log(p)} />
16 |
17 | );
18 |
19 | export default {
20 | title: "Layer Pager"
21 | };
22 |
--------------------------------------------------------------------------------
/src/systems/listed-media.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerSystem("listed-media", {
2 | init: function() {
3 | this.els = [];
4 | },
5 |
6 | register: function(el) {
7 | this.els.push(el);
8 | this.el.emit("listed_media_changed");
9 | },
10 |
11 | unregister: function(el) {
12 | this.els.splice(this.els.indexOf(el), 1);
13 | this.el.emit("listed_media_changed");
14 | }
15 | });
16 |
17 | AFRAME.registerComponent("listed-media", {
18 | init: function() {
19 | this.system.register(this.el);
20 | },
21 |
22 | remove: function() {
23 | this.system.unregister(this.el);
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/scale-in-screen-space.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("scale-in-screen-space", {
2 | schema: {
3 | baseScale: { type: "vec3", default: { x: 1, y: 1, z: 1 } },
4 | addedScale: { type: "vec3", default: { x: 1, y: 1, z: 1 } }
5 | },
6 |
7 | play() {
8 | if (!this.didRegister) {
9 | this.didRegister = true;
10 | this.el.sceneEl.systems["hubs-systems"].scaleInScreenSpaceSystem.register(this);
11 | }
12 | },
13 | remove() {
14 | if (this.didRegister) {
15 | this.el.sceneEl.systems["hubs-systems"].scaleInScreenSpaceSystem.unregister(this);
16 | }
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/utils/disable-ios-zoom.js:
--------------------------------------------------------------------------------
1 | import { detectOS } from "detect-browser";
2 |
3 | export function disableiOSZoom() {
4 | if (detectOS(navigator.userAgent) !== "iOS") return;
5 |
6 | let lastTouchAtMs = 0;
7 |
8 | document.addEventListener("touchmove", ev => {
9 | if (ev.scale === 1) return;
10 |
11 | ev.preventDefault();
12 | });
13 |
14 | document.addEventListener("touchend", ev => {
15 | const now = new Date().getTime();
16 | const isDoubleTouch = now - lastTouchAtMs <= 300;
17 | lastTouchAtMs = now;
18 |
19 | if (isDoubleTouch) {
20 | ev.preventDefault();
21 | }
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/get-current-player-height.js:
--------------------------------------------------------------------------------
1 | export const getCurrentPlayerHeight = (function() {
2 | let avatarPOV;
3 | let avatarRig;
4 | return function getCurrentPlayerHeight(world) {
5 | avatarPOV = avatarPOV || DOM_ROOT.getElementById("avatar-pov-node");
6 | avatarRig = avatarRig || DOM_ROOT.getElementById("avatar-rig");
7 | avatarRig.object3D.updateMatrices();
8 | avatarPOV.object3D.updateMatrices();
9 | if (world) {
10 | return avatarPOV.object3D.matrixWorld.elements[13] - avatarRig.object3D.matrixWorld.elements[13];
11 | }
12 | return avatarPOV.object3D.matrix.elements[13];
13 | };
14 | })();
15 |
--------------------------------------------------------------------------------
/src/assets/images/icons/screen.svgi:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/icons/fill.svgi:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/systems/userinput/devices/copy-sitting-to-standing-transform.js:
--------------------------------------------------------------------------------
1 | export const copySittingToStandingTransform = function copySittingToStandingTransform(matrix) {
2 | navigator.getVRDisplays &&
3 | navigator.getVRDisplays().then(displays => {
4 | if (displays[0] && displays[0].stageParameters && displays[0].stageParameters.sittingToStandingTransform) {
5 | matrix.fromArray(displays[0].stageParameters.sittingToStandingTransform);
6 | } else {
7 | setTimeout(() => {
8 | copySittingToStandingTransform(matrix);
9 | }, 1000); // Try again when the display is ready
10 | }
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/svox.fbs:
--------------------------------------------------------------------------------
1 | namespace VOX;
2 |
3 | enum StackAxis : byte { UP = 0, DOWN = 1, FORWARD = 2, BACKWARD = 3, ALONG = 4, AGAINST = 5 }
4 |
5 | table SVoxChunk {
6 | size_x:byte;
7 | size_y:byte;
8 | size_z:byte;
9 | bits_per_index:byte;
10 | palette:[ubyte] (required);
11 | indices:[ubyte] (required);
12 | }
13 |
14 | table SVox {
15 | header:[ubyte] (required);
16 | name:string;
17 | version:int = 0;
18 | revision:int = 0;
19 | scale:float = 1.0;
20 | stack_axis:StackAxis = UP;
21 | stack_snap_position:bool = false;
22 | stack_snap_scale:bool = false;
23 | frames:[SVoxChunk] (required);
24 | }
25 |
26 | root_type SVox;
27 |
--------------------------------------------------------------------------------
/src/objects/chiclet-geometry.js:
--------------------------------------------------------------------------------
1 | import glb from "!!url-loader!../assets/models/chiclet.glb";
2 | import glbFlipped from "!!url-loader!../assets/models/chiclet-flip.glb";
3 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
4 |
5 | export const chicletGeometry = new Promise(res => {
6 | new GLTFLoader(new THREE.LoadingManager()).load(glb, async gltf => {
7 | res(gltf.scene.children[2].geometry);
8 | });
9 | });
10 |
11 | export const chicletGeometryFlipped = new Promise(res => {
12 | new GLTFLoader(new THREE.LoadingManager()).load(glbFlipped, async gltf => {
13 | res(gltf.scene.children[2].geometry);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/utils/concurrent-load-detector.js:
--------------------------------------------------------------------------------
1 | // Stores a key in localStorage and registeres a listener for the same key being set (by another tab).
2 | // When detected, fire a "concurrentload" event.
3 | export default function detectConcurrentLoad(instanceKey = "global") {
4 | const key = `___concurrent_load_detector_${instanceKey}`;
5 | localStorage.setItem(key, JSON.stringify({ started_at: new Date() }));
6 | const onStorageEvent = e => {
7 | if (e.key !== key) return;
8 | window.dispatchEvent(new CustomEvent("concurrentload"));
9 | window.removeEventListener("storage", onStorageEvent);
10 | };
11 | window.addEventListener("storage", onStorageEvent);
12 | }
13 |
--------------------------------------------------------------------------------
/src/ui/invite-panel.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import InvitePanel from "./invite-panel";
3 | import sharedStyles from "../../assets/stylesheets/shared.scss";
4 | import classNames from "classnames";
5 |
6 | export const Normal = () => (
7 |
11 | {
13 | console.log("Fetch");
14 | return "https://jel.app/invite";
15 | }}
16 | />
17 |
18 | );
19 |
20 | export default {
21 | title: "Invite Panel"
22 | };
23 |
--------------------------------------------------------------------------------
/src/assets/images/icons/check.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icons/check-big.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/ui/popup-panel.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | // Note we only use this for invite popout from left panel so border radii assume left side.
4 |
5 | const PopupPanel = styled.div`
6 | color: var(--panel-text-color);
7 | background-color: var(--panel-background-color);
8 | font-size: var(--panel-text-size);
9 | font-weight: var(--panel-text-weight);
10 | display: flex;
11 | flex-direction: column;
12 | align-items: flex-start;
13 | justify-content: flex-start;
14 | border-radius: 0px 4px 4px 0px;
15 | box-shadow: 12px 12px 28px var(--menu-shadow-color);
16 | pointer-events: none;
17 | padding: 16px 0px;
18 | `;
19 |
20 | export { PopupPanel as default };
21 |
--------------------------------------------------------------------------------
/src/components/set-max-resolution.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("set-max-resolution", {
2 | init() {
3 | const store = window.APP.store;
4 | this.onStoreChanged = () => {
5 | const width =
6 | store.state.preferences.maxResolutionWidth === undefined ? 1920 : store.state.preferences.maxResolutionWidth;
7 | const height =
8 | store.state.preferences.maxResolutionHeight === undefined ? 1920 : store.state.preferences.maxResolutionHeight;
9 | this.el.sceneEl.maxCanvasSize = { width, height };
10 | this.el.sceneEl.resize();
11 | };
12 | this.onStoreChanged();
13 | store.addEventListener("statechanged-preferences", this.onStoreChanged);
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/src/systems/userinput/bindings/cardboard-user.js:
--------------------------------------------------------------------------------
1 | import { paths } from "../paths";
2 | import { sets } from "../sets";
3 | import { xforms } from "./xforms";
4 | import { addSetsToBindings } from "./utils";
5 |
6 | export const cardboardUserBindings = addSetsToBindings({
7 | [sets.global]: [
8 | {
9 | src: { value: paths.device.touchscreen.isTouching },
10 | dest: {
11 | value: paths.actions.startGazeTeleport
12 | },
13 | xform: xforms.rising
14 | },
15 | {
16 | src: { value: paths.device.touchscreen.isTouching },
17 | dest: {
18 | value: paths.actions.stopGazeTeleport
19 | },
20 | xform: xforms.falling
21 | }
22 | ]
23 | });
24 |
--------------------------------------------------------------------------------
/src/assets/images/icons/cube.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for Hubs!
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/gltf-component-mappings.js:
--------------------------------------------------------------------------------
1 | import "./components/gltf-model-plus";
2 |
3 | function registerRootSceneComponent(componentName) {
4 | AFRAME.GLTFModelPlus.registerComponent(componentName, componentName, (el, componentName, componentData) => {
5 | const sceneEl = AFRAME.scenes[0];
6 |
7 | sceneEl.setAttribute(componentName, componentData);
8 | });
9 | }
10 |
11 | // TODO not sure why this is needed, changes water color significantly
12 | registerRootSceneComponent("background");
13 |
14 | AFRAME.GLTFModelPlus.registerComponent("duck", "duck", el => {
15 | el.setAttribute("duck", "");
16 | el.setAttribute("quack", { quackPercentage: 0.1 });
17 | });
18 | AFRAME.GLTFModelPlus.registerComponent("quack", "quack");
19 |
--------------------------------------------------------------------------------
/src/systems/userinput/array-backed-set.js:
--------------------------------------------------------------------------------
1 | // You might prefer to use this over a Set because iterating over a set allocates memory
2 | export function ArrayBackedSet(initialItems) {
3 | return {
4 | items: initialItems || [],
5 | has(item) {
6 | return this.items.indexOf(item) !== -1;
7 | },
8 | add(item) {
9 | if (!this.has(item)) {
10 | this.items.push(item);
11 | }
12 | },
13 | delete(item) {
14 | const index = this.items.indexOf(item);
15 | if (index !== -1) {
16 | this.items[index] = this.items[this.items.length - 1];
17 | this.items.pop();
18 | }
19 | },
20 | clear() {
21 | this.items.length = 0;
22 | }
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/networked-avatar.js:
--------------------------------------------------------------------------------
1 | import { restartPeriodicSyncs } from "./periodic-full-syncs";
2 |
3 | /**
4 | * Stores networked avatar state.
5 | * @namespace avatar
6 | * @component networked-avatar
7 | */
8 | AFRAME.registerComponent("networked-avatar", {
9 | schema: {
10 | left_hand_pose: { default: 0 },
11 | right_hand_pose: { default: 0 },
12 | relative_motion: { default: 0 },
13 | // True when avatar should be expressing body language of executing a jump
14 | is_jumping: { default: false }
15 | },
16 |
17 | init() {
18 | // Whenever a networked avatar is spawned, we need to begin periodic
19 | // syncs to ensure we spawn on their side.
20 | restartPeriodicSyncs();
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/src/terra/constants.js:
--------------------------------------------------------------------------------
1 | export const VOXEL_SIZE = 1 / 8;
2 | export const VOXELS_PER_CHUNK = 64;
3 | export const CHUNK_WORLD_SIZE = VOXELS_PER_CHUNK * VOXEL_SIZE;
4 | export const WORLD_CHUNK_SIZE = 8;
5 | export const WORLD_MAX_COORD = (WORLD_CHUNK_SIZE * CHUNK_WORLD_SIZE) / 2;
6 | export const WORLD_MIN_COORD = -WORLD_MAX_COORD;
7 | export const WORLD_SIZE = (WORLD_MAX_COORD - WORLD_MIN_COORD) * CHUNK_WORLD_SIZE;
8 | export const WATER_LEVEL = 4;
9 | export const VOXEL_PALETTE_NONE = 0;
10 | export const VOXEL_PALETTE_GROUND = 1;
11 | export const VOXEL_PALETTE_EDGE = 2;
12 | export const VOXEL_PALETTE_LEAVES = 3;
13 | export const VOXEL_PALETTE_BARK = 4;
14 | export const VOXEL_PALETTE_ROCK = 5;
15 | export const VOXEL_PALETTE_GRASS = 6;
16 |
--------------------------------------------------------------------------------
/src/components/action-to-remove.js:
--------------------------------------------------------------------------------
1 | import { isMine, getNetworkedEntity } from "../utils/ownership-utils";
2 |
3 | AFRAME.registerComponent("action-to-remove", {
4 | multiple: true,
5 |
6 | schema: {
7 | path: { type: "string" },
8 | requireOwnership: { type: "boolean", default: true }
9 | },
10 |
11 | init() {
12 | getNetworkedEntity(this.el).then(networkedEl => {
13 | this.networkedEl = networkedEl;
14 | });
15 | },
16 |
17 | tick() {
18 | const userinput = AFRAME.scenes[0].systems.userinput;
19 | if (!userinput.get(this.data.path)) return;
20 | if (this.data.requireOwnership && this.networkedEl && !isMine(this.networkedEl)) return;
21 |
22 | this.el.parentNode.removeChild(this.el);
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/src/utils/scene-graph.js:
--------------------------------------------------------------------------------
1 | export function findAncestorWithComponent(entity, componentName) {
2 | while (entity && !(entity.components && entity.components[componentName])) {
3 | entity = entity.parentNode;
4 | }
5 | return entity;
6 | }
7 |
8 | export function findComponentsInNearestAncestor(entity, componentName) {
9 | const components = [];
10 | while (entity) {
11 | if (entity.components) {
12 | for (const c in entity.components) {
13 | if (entity.components[c].name === componentName) {
14 | components.push(entity.components[c]);
15 | }
16 | }
17 | }
18 | if (components.length) {
19 | return components;
20 | }
21 | entity = entity.parentNode;
22 | }
23 | return components;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/replay.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("replay", {
2 | init: function() {
3 | this.playhead = 0;
4 | this.poseIndex = 0;
5 | },
6 |
7 | tick: function(t, dt) {
8 | let overflow = false;
9 | while (!overflow && this.playhead >= this.poses[this.poseIndex].timestamp) {
10 | this.el.setAttribute("position", this.poses[this.poseIndex].position);
11 | this.el.setAttribute("rotation", this.poses[this.poseIndex].rotation);
12 | this.el.object3D.matrixNeedsUpdate = true;
13 | this.poseIndex += 1;
14 | overflow = this.poseIndex === this.poses.length;
15 | }
16 |
17 | this.playhead += dt;
18 |
19 | if (overflow) {
20 | this.playhead = 0;
21 | this.poseIndex = 0;
22 | }
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/src/utils/async-utils.js:
--------------------------------------------------------------------------------
1 | export const waitForEvent = function(eventName, eventObj) {
2 | return new Promise(resolve => {
3 | eventObj.addEventListener(eventName, resolve, { once: true });
4 | });
5 | };
6 |
7 | export const waitForShadowDOMContentLoaded = function() {
8 | if (window.DOM_ROOT?._ready) {
9 | return Promise.resolve(null);
10 | } else {
11 | return waitForEvent("shadow-root-ready", document);
12 | }
13 | };
14 |
15 | export const waitForDOMContentLoaded = function(doc = document, win = window) {
16 | if (doc.readyState === "complete" || doc.readyState === "loaded" || doc.readyState === "interactive") {
17 | return Promise.resolve(null);
18 | } else {
19 | return waitForEvent("DOMContentLoaded", win);
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/src/systems/cursor-pose-tracking.js:
--------------------------------------------------------------------------------
1 | export class CursorPoseTrackingSystem {
2 | constructor() {
3 | this.pairs = [];
4 | }
5 | register(object3D, path) {
6 | this.pairs.push({ object3D, path });
7 | }
8 | unregister(object3D) {
9 | this.pairs = this.pairs.filter(p => p.object3D !== object3D);
10 | }
11 | tick() {
12 | for (let i = 0; i < this.pairs.length; i++) {
13 | const matrix = AFRAME.scenes[0].systems.userinput.get(this.pairs[i].path);
14 | if (matrix) {
15 | const o = this.pairs[i].object3D;
16 | o.matrix.copy(matrix);
17 | o.matrix.decompose(o.position, o.quaternion, o.scale);
18 | o.matrixIsModified = true;
19 | o.matrixWorldNeedsUpdate = true;
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/space-channel.js:
--------------------------------------------------------------------------------
1 | import { EventTarget } from "event-target-shim";
2 |
3 | export default class SpaceChannel extends EventTarget {
4 | constructor(store) {
5 | super();
6 | this.store = store;
7 | }
8 |
9 | updateVoxMeta = (voxId, vox) => {
10 | const { atomAccessManager } = window.APP;
11 | if (!atomAccessManager.voxCan("edit_vox", voxId)) return;
12 |
13 | this.broadcastMessage({ vox_id: voxId, ...vox }, "update_vox_meta");
14 | };
15 |
16 | broadcastMessage = (body, type, toSessionId) => {
17 | if (!body) return;
18 | const payload = { body };
19 | if (toSessionId) {
20 | payload.to_session_id = toSessionId;
21 | }
22 |
23 | NAF.connection.broadcastCustomDataGuaranteed(type, payload);
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/.htmlhintrc:
--------------------------------------------------------------------------------
1 | {
2 | "alt-require": false,
3 | "attr-lowercase": true,
4 | "attr-no-duplication": true,
5 | "attr-unsafe-chars": true,
6 | "attr-value-double-quotes": true,
7 | "attr-value-not-empty": false,
8 | "doctype-first": true,
9 | "doctype-html5": true,
10 | "head-script-disabled": false,
11 | "href-abs-or-rel": false,
12 | "id-class-ad-disabled": false,
13 | "id-class-value": false,
14 | "id-unique": true,
15 | "inline-script-disabled": false,
16 | "inline-style-disabled": false,
17 | "space-tab-mixed-disabled": "space2",
18 | "spec-char-escape": true,
19 | "src-not-empty": true,
20 | "style-disabled": false,
21 | "tag-pair": true,
22 | "tag-self-close": false,
23 | "tagname-lowercase": true,
24 | "title-require": true
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Webspace Engine
2 |
3 | This is the engine for [Webspaces](https://webspaces.space), which allows you to create 3D worlds using just HTML.
4 |
5 | If you aren't interested in working on the **Webspace Engine** but wish to create your own webspaces, please visit the [**webspace for Webspaces**](https://webspaces.space)
6 |
7 | ## Getting started
8 |
9 | Clone the repo and run:
10 |
11 | ```
12 | npm ci
13 | npm run local
14 | ```
15 |
16 | Then, to create a new webspace, save this to a new HTML file in a new folder:
17 | ```
18 |
19 |
20 |
21 |
22 | ```
23 |
24 | And open it up in chrome via File->Open. You will then be able to edit the HTML file as a 3D world.
25 |
--------------------------------------------------------------------------------
/src/systems/userinput/devices/gamepad.js:
--------------------------------------------------------------------------------
1 | import { paths } from "../paths";
2 |
3 | export class GamepadDevice {
4 | constructor(gamepad) {
5 | this.gamepad = gamepad;
6 | }
7 |
8 | write(frame) {
9 | if (this.gamepad.connected) {
10 | this.gamepad.buttons.forEach((button, i) => {
11 | const buttonPath = paths.device.gamepad(this.gamepad.index).button(i);
12 | frame.setValueType(buttonPath.pressed, !!button.pressed);
13 | frame.setValueType(buttonPath.touched, !!button.touched);
14 | frame.setValueType(buttonPath.value, button.value);
15 | });
16 | this.gamepad.axes.forEach((axis, i) => {
17 | frame.setValueType(paths.device.gamepad(this.gamepad.index).axis(i), axis);
18 | });
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ui/color-picker.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import sharedStyles from "../../assets/stylesheets/shared.scss";
3 | import classNames from "classnames";
4 |
5 | import ColorPicker from "./color-picker";
6 |
7 | export const Normal = () => {
8 | return (
9 |
19 |
20 | console.log(e)} />
21 |
22 |
23 | );
24 | };
25 |
26 | export default {
27 | title: "Color Picker"
28 | };
29 |
--------------------------------------------------------------------------------
/src/systems/sprites/sprite.vert:
--------------------------------------------------------------------------------
1 | attribute vec4 mvCol0;
2 | attribute vec4 mvCol1;
3 | attribute vec4 mvCol2;
4 | attribute vec4 mvCol3;
5 | attribute vec3 position;
6 | attribute vec2 a_uvs;
7 | varying vec2 v_uvs;
8 | attribute float a_hubs_EnableSweepingEffect;
9 | varying float v_hubs_EnableSweepingEffect;
10 | attribute vec2 a_hubs_SweepParams;
11 | varying vec2 v_hubs_SweepParams;
12 | varying vec3 hubs_WorldPosition;
13 |
14 | void main() {
15 | mat4 mv = mat4(mvCol0, mvCol1, mvCol2, mvCol3);
16 | gl_Position = projectionMatrix * modelViewMatrix * mv * vec4(position, 1.0);
17 | v_uvs = a_uvs;
18 | v_hubs_EnableSweepingEffect = a_hubs_EnableSweepingEffect;
19 | v_hubs_SweepParams = a_hubs_SweepParams;
20 |
21 | vec4 wt = vec4(position, 1.0);
22 | hubs_WorldPosition = (mv * wt).xyz;
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/dyna-channel.js:
--------------------------------------------------------------------------------
1 | import { EventTarget } from "event-target-shim";
2 |
3 | export default class DynaChannel extends EventTarget {
4 | constructor(store) {
5 | super();
6 | this.store = store;
7 | this.flushSpaceMetaTimeout = null;
8 | }
9 |
10 | updateSpace = (spaceId, newSpaceFields) => {
11 | const { spaceMetadata, atomAccessManager } = window.APP;
12 | const canUpdateSpaceMeta = atomAccessManager.spaceCan("update_space_meta");
13 | if (!canUpdateSpaceMeta) return "unauthorized";
14 | spaceMetadata.localUpdate(spaceId, newSpaceFields);
15 |
16 | if (this.flushSpaceMetatimeout) clearTimeout(this.flushSpaceMetatimeout);
17 |
18 | this.flushSpaceMetaTimeout = setTimeout(() => {
19 | spaceMetadata.flushLocalUpdates();
20 | }, 3000);
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/assets/images/icons/edit.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/animation-mixer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Instantiates and updates a THREE.AnimationMixer on an entity.
3 | * @component animation-mixer
4 | */
5 |
6 | const components = [];
7 | export class AnimationMixerSystem {
8 | tick(dt) {
9 | for (let i = 0; i < components.length; i++) {
10 | const cmp = components[i];
11 | if (cmp.mixer) {
12 | cmp.mixer.update(dt / 1000);
13 | }
14 | }
15 | }
16 | }
17 |
18 | AFRAME.registerComponent("animation-mixer", {
19 | initMixer(animations) {
20 | this.mixer = new THREE.AnimationMixer(this.el.object3D);
21 | this.el.object3D.animations = animations;
22 | this.animations = animations;
23 | },
24 | play() {
25 | components.push(this);
26 | },
27 | pause() {
28 | components.splice(components.indexOf(this), 1);
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/src/components/billboard.js:
--------------------------------------------------------------------------------
1 | // Billboard component that only updates visible objects and only those in the camera view on mobile VR.
2 | AFRAME.registerComponent("billboard", {
3 | init: function() {
4 | this.target = new THREE.Vector3();
5 | this._updateBillboard = this._updateBillboard.bind(this);
6 | },
7 |
8 | tick() {
9 | this._updateBillboard();
10 | },
11 |
12 | _updateBillboard: function() {
13 | if (!this.el.object3D.visible) return;
14 |
15 | const camera = this.el.sceneEl.camera;
16 | const object3D = this.el.object3D;
17 |
18 | if (camera) {
19 | // Set the camera world position as the target.
20 | this.target.setFromMatrixPosition(camera.matrixWorld);
21 | object3D.lookAt(this.target);
22 | object3D.matrixNeedsUpdate = true;
23 | }
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/src/assets/images/icons/scenes-off.svgi:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/images/icons/edit-shadow.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/set-active-camera.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("set-active-camera", {
2 | init() {
3 | // This is a hack needed to set the active camera because sometimes A-Frame will create the default camera and set it to the active camera on startup because the query selector to find an existing camera in the scene fails.
4 |
5 | const cameraSystem = this.el.sceneEl.systems.camera;
6 | const currentActiveCamera = cameraSystem.activeCameraEl;
7 |
8 | if (currentActiveCamera !== this.el) {
9 | cameraSystem.setActiveCamera(this.el);
10 |
11 | // Also A-Frame fails to delete the default camera :P
12 | currentActiveCamera.parentNode.removeChild(currentActiveCamera);
13 |
14 | // Look controls leaves behind this CSS class.
15 | this.el.sceneEl.canvas.classList.remove("a-grab-cursor");
16 | }
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/ui/permissions-popup.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import HubPermissionsPopup from "./hub-permissions-popup";
3 | import sharedStyles from "../../assets/stylesheets/shared.scss";
4 | import AtomMetadata, { ATOM_TYPES } from "../utils/atom-metadata";
5 |
6 | const metadata = new AtomMetadata(ATOM_TYPES.HUB);
7 | metadata._metadata.set("abc123", { roles: { space: "viewer" } });
8 |
9 | export const Hub = () => {
10 | useEffect(() => document.querySelector(`.${sharedStyles.showWhenPopped}`).focus());
11 |
12 | return (
13 |
14 |
20 |
21 | );
22 | };
23 |
24 | export default {
25 | title: "Permissions Popup"
26 | };
27 |
--------------------------------------------------------------------------------
/src/assets/images/icons/builder-on.svgi:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/icons/go-to.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/terra/blocks/textures/block.js:
--------------------------------------------------------------------------------
1 | // TODO this seems like dead code
2 | const block = {
3 | width: 16,
4 | height: 16,
5 | colorType: 6,
6 | data: Array(16 * 16 * 4) // RGBA
7 | };
8 |
9 | for (let y = 0; y < block.height; y += 1) {
10 | for (let x = 0; x < block.width; x += 1) {
11 | let light = 0.9 + Math.random() * 0.05;
12 | if (x === 0 || x === block.width - 1 || y === 0 || y === block.width - 1) {
13 | light *= 0.9;
14 | } else if (x === 1 || x === block.width - 2 || y === 1 || y === block.width - 2) {
15 | light *= 1.2;
16 | }
17 | light = Math.floor(Math.min(Math.max(light, 0), 1) * 0xff);
18 | const i = (y * block.width + x) * 4;
19 | block.data[i] = light;
20 | block.data[i + 1] = light;
21 | block.data[i + 2] = light;
22 | block.data[i + 3] = 0xff;
23 | }
24 | }
25 |
26 | export default block;
27 |
--------------------------------------------------------------------------------
/src/components/scalable-when-grabbed.js:
--------------------------------------------------------------------------------
1 | import { paths } from "../systems/userinput/paths";
2 |
3 | const SENSITIVITY = 2.5;
4 |
5 | AFRAME.registerComponent("scalable-when-grabbed", {
6 | tick: function() {
7 | const userinput = AFRAME.scenes[0].systems.userinput;
8 | const interaction = AFRAME.scenes[0].systems.interaction;
9 | let deltaScale;
10 | if (interaction.state.rightRemote.held === this.el) {
11 | deltaScale = userinput.get(paths.actions.cursor.right.scaleGrabbedGrabbable);
12 | }
13 | if (interaction.state.leftRemote.held === this.el) {
14 | deltaScale = userinput.get(paths.actions.cursor.left.scaleGrabbedGrabbable);
15 | }
16 | if (!deltaScale) return;
17 |
18 | this.el.object3D.scale.addScalar(SENSITIVITY * deltaScale).clampScalar(0.1, 100);
19 | this.el.object3D.matrixNeedsUpdate = true;
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/src/workers/vox-mesher.worker.js:
--------------------------------------------------------------------------------
1 | import { Voxels, SvoxMeshGenerator, ModelReader, Buffers as SvoxBuffers } from "smoothvoxels";
2 |
3 | // Based on testing with a bunch of models
4 | const svoxBuffers = new SvoxBuffers(375000);
5 |
6 | const EMPTY_OBJECT = {};
7 |
8 | self.onmessage = ({
9 | data: {
10 | id,
11 | payload: { voxId, iFrame, modelString, voxelPackage }
12 | }
13 | }) => {
14 | const model = ModelReader.readFromString(modelString, EMPTY_OBJECT, true /* skip voxels */);
15 | model.voxels = new Voxels(...voxelPackage);
16 |
17 | const svoxMesh = SvoxMeshGenerator.generate(model, svoxBuffers);
18 | self.postMessage({ id, result: { voxId, iFrame, svoxMesh } }, [
19 | svoxMesh.positions.buffer,
20 | svoxMesh.normals.buffer,
21 | svoxMesh.colors.buffer,
22 | svoxMesh.indices.buffer,
23 | svoxMesh.uvs.buffer
24 | ]);
25 | };
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve Hubs
4 | title: ''
5 | labels: bug, needs triage
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Description**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Hardware**
27 | - Device: [e.g. Desktop, phone, VR headset]
28 | - OS: [e.g. Windows, iOS, Linux]
29 | - Browser [e.g. Firefox]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "babel-eslint",
3 | env: {
4 | browser: true,
5 | es6: true,
6 | node: true
7 | },
8 | globals: {
9 | SharedArrayBuffer: true,
10 | THREE: true,
11 | AFRAME: true,
12 | NAF: true,
13 | SAF: true,
14 | DecompressionStream: true,
15 | TransformStream: true,
16 | SYSTEMS: true,
17 | DOM_ROOT: true,
18 | UI: true
19 | },
20 | plugins: ["prettier", "react"],
21 | rules: {
22 | "prettier/prettier": "error",
23 | "prefer-const": "error",
24 | "no-use-before-define": "error",
25 | "no-var": "error",
26 | "no-throw-literal": "error",
27 | // Light console usage is useful but remove debug logs before merging to master.
28 | "no-console": "off"
29 | },
30 | extends: ["prettier", "plugin:react/recommended", "eslint:recommended", "plugin:react-hooks/recommended"]
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/dpad.js:
--------------------------------------------------------------------------------
1 | export function angleTo4Direction(angle) {
2 | angle = (angle * THREE.MathUtils.RAD2DEG + 180 + 45) % 360;
3 | if (angle > 0 && angle < 90) {
4 | return "north";
5 | } else if (angle >= 90 && angle < 180) {
6 | return "west";
7 | } else if (angle >= 180 && angle < 270) {
8 | return "south";
9 | } else {
10 | return "east";
11 | }
12 | }
13 |
14 | export function angleTo8Direction(angle) {
15 | angle = (angle * THREE.MathUtils.RAD2DEG + 180 + 45) % 360;
16 | let direction = "";
17 | if ((angle >= 0 && angle < 120) || angle >= 330) {
18 | direction += "north";
19 | }
20 | if (angle >= 150 && angle < 300) {
21 | direction += "south";
22 | }
23 | if (angle >= 60 && angle < 210) {
24 | direction += "west";
25 | }
26 | if ((angle >= 240 && angle < 360) || angle < 30) {
27 | direction += "east";
28 | }
29 | return direction;
30 | }
31 |
--------------------------------------------------------------------------------
/src/assets/images/icons/important.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/inspect-button.js:
--------------------------------------------------------------------------------
1 | import { getInspectable } from "../systems/camera-system";
2 |
3 | AFRAME.registerComponent("inspect-button", {
4 | tick() {
5 | if (!this.initializedInTick) {
6 | // initialize in tick so that parent's `tags` component has been initialized
7 | this.initializedInTick = true;
8 |
9 | this.inspectable = getInspectable(this.el);
10 | if (!this.inspectable) {
11 | console.error("You put an inspect button but I could not find what you want to inspect.", this.el);
12 | return;
13 | }
14 | this.el.object3D.addEventListener("holdable-button-down", () => {
15 | this.el.sceneEl.systems["hubs-systems"].cameraSystem.inspect(this.inspectable.object3D);
16 | });
17 | this.el.object3D.addEventListener("holdable-button-up", () => {
18 | this.el.sceneEl.systems["hubs-systems"].cameraSystem.uninspect();
19 | });
20 | }
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/src/utils/fullscreen.js:
--------------------------------------------------------------------------------
1 | import screenfull from "screenfull";
2 |
3 | let hasEnteredFullScreenThisSession = false;
4 |
5 | function shouldShowFullScreen() {
6 | // Disable full screen on iOS, since Safari's fullscreen mode does not let you prevent native pinch-to-zoom gestures.
7 | return (
8 | (AFRAME.utils.device.isMobile() || AFRAME.utils.device.isMobileVR()) &&
9 | !AFRAME.utils.device.isIOS() &&
10 | screenfull.enabled
11 | );
12 | }
13 |
14 | export function willRequireUserGesture() {
15 | return !screenfull.isFullscreen && shouldShowFullScreen();
16 | }
17 |
18 | export async function showFullScreenIfAvailable() {
19 | if (shouldShowFullScreen()) {
20 | hasEnteredFullScreenThisSession = true;
21 | await screenfull.request();
22 | }
23 | }
24 |
25 | export async function showFullScreenIfWasFullScreen() {
26 | if (hasEnteredFullScreenThisSession && !screenfull.isFullscreen) {
27 | await screenfull.request();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/ui/environment-settings-popup.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import EnvironmentSettingsPopup from "./environment-settings-popup";
3 | import sharedStyles from "../../assets/stylesheets/shared.scss";
4 | import AtomMetadata, { ATOM_TYPES } from "../utils/atom-metadata";
5 |
6 | const metadata = new AtomMetadata(ATOM_TYPES.HUB);
7 | metadata._metadata.set("abc123", { roles: { space: "viewer" }, world: {} });
8 |
9 | export const Settings = () => {
10 | useEffect(() => document.querySelector(`.${sharedStyles.showWhenPopped}`).focus());
11 |
12 | return (
13 |
14 | {}}
20 | onColorChangeComplete={() => {}}
21 | />
22 |
23 | );
24 | };
25 |
26 | export default {
27 | title: "Environment Settings Popup"
28 | };
29 |
--------------------------------------------------------------------------------
/src/ui/create-select.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import "../assets/stylesheets/create-select.scss";
3 | import CreateSelect from "./create-select";
4 |
5 | export const Basic = () => {
6 | const ref = useRef();
7 |
8 | setTimeout(() => {
9 | console.log(ref.current);
10 | ref.current.focus();
11 | }, 500);
12 |
13 | return (
14 |
24 |
25 | console.log("exec", a)} />
26 |
27 |
28 | );
29 | };
30 |
31 | export default {
32 | title: "Create Select"
33 | };
34 |
--------------------------------------------------------------------------------
/src/utils/threejs-world-update.js:
--------------------------------------------------------------------------------
1 | // When world matrices are updated, we flip 8 bits to one and then they can
2 | // be consumed by various subsystems.
3 | export const WORLD_MATRIX_CONSUMERS = {
4 | PHYSICS: 0,
5 | BEAMS: 1,
6 | VOX: 2,
7 | AVATARS: 3,
8 | DOM_SERIALIZER: 4
9 | };
10 |
11 | // New function to set the matrix properly
12 | THREE.Object3D.prototype.setMatrix = function(matrix) {
13 | this.matrixWorldNeedsUpdate = true;
14 | this.matrix.copy(matrix);
15 | this.matrix.decompose(this.position, this.quaternion, this.scale);
16 | };
17 |
18 | // Pass a system (like WORLD_MATRIX_CONSUMERS.PHYSICS) to get true or false
19 | // if the world matrix has changed since last consumption.
20 | THREE.Object3D.prototype.consumeIfDirtyWorldMatrix = function(system) {
21 | const mask = 0x1 << system;
22 |
23 | if ((this.worldMatrixConsumerFlags & mask) === 0) {
24 | this.worldMatrixConsumerFlags |= mask;
25 | return true;
26 | }
27 |
28 | return false;
29 | };
30 |
--------------------------------------------------------------------------------
/src/ui/emoji-equip.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import sharedStyles from "../../assets/stylesheets/shared.scss";
3 | import classNames from "classnames";
4 |
5 | import EmojiEquip from "./emoji-equip";
6 |
7 | window.APP.store.update({
8 | equips: {
9 | launcher: "😀",
10 | launcherSlot1: "😀",
11 | launcherSlot2: "👍",
12 | launcherSlot3: "👏",
13 | launcherSlot4: "❤",
14 | launcherSlot5: "😂",
15 | launcherSlot6: "🤔",
16 | launcherSlot7: "😍",
17 | launcherSlot8: "😘",
18 | launcherSlot9: "🥺",
19 | launcherSlot10: "😭"
20 | }
21 | });
22 |
23 | export const Normal = () => (
24 |
28 | console.log("Center clicked")} />
29 |
30 | );
31 |
32 | export default {
33 | title: "Emoji Equip"
34 | };
35 |
--------------------------------------------------------------------------------
/src/ui/side-panels.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import LeftPanel from "./left-panel";
4 | import RightPanel from "./right-panel";
5 |
6 | const Wrap = styled.div`
7 | color: var(--panel-text-color);
8 | background-color: var(--panel-background-color);
9 | font-size: var(--panel-text-size);
10 | font-weight: var(--panel-text-weight);
11 | position: absolute;
12 | width: 100%;
13 | height: 100%;
14 | left: 0;
15 | top: 0;
16 | z-index: 2;
17 | pointer-events: none;
18 | display: flex;
19 | justify-content: space-between;
20 | overflow: hidden;
21 | user-select: none;
22 |
23 | #webspace-ui:focus-within & {
24 | pointer-events: auto;
25 | }
26 | `;
27 |
28 | function SidePanels(props) {
29 | return (
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | SidePanels.propTypes = {};
38 |
39 | export default SidePanels;
40 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-1.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/components/owned-object-limiter.js:
--------------------------------------------------------------------------------
1 | import { isSynchronized, isMine } from "../utils/ownership-utils";
2 |
3 | /* global AFRAME performance */
4 | AFRAME.registerComponent("owned-object-limiter", {
5 | schema: {
6 | counter: { type: "selector" }
7 | },
8 |
9 | init() {
10 | this.counter = this.data.counter.components["networked-counter"];
11 | },
12 |
13 | tick() {
14 | this._syncCounterRegistration();
15 | const isHeld = this.el.sceneEl.systems.interaction.isHeld(this.el);
16 | if (!isHeld && this.wasHeld && this.counter.timestamps.has(this.el)) {
17 | this.counter.timestamps.set(this.el, performance.now());
18 | }
19 | this.wasHeld = isHeld;
20 | },
21 |
22 | remove() {
23 | this.counter.deregister(this.el);
24 | },
25 |
26 | _syncCounterRegistration() {
27 | if (!isSynchronized(this.el)) return;
28 |
29 | if (isMine(this.el)) {
30 | this.counter.register(this.el);
31 | } else {
32 | this.counter.deregister(this.el);
33 | }
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/src/ui/wrapped-intl-provider.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { IntlProvider } from "react-intl";
4 | import { getLocale, getMessages } from "../utils/i18n";
5 |
6 | export class WrappedIntlProvider extends React.Component {
7 | static propTypes = {
8 | children: PropTypes.node.isRequired
9 | };
10 |
11 | state = {
12 | locale: getLocale(),
13 | messages: getMessages()
14 | };
15 |
16 | updateLocale = () => {
17 | this.setState({ locale: getLocale(), messages: getMessages() });
18 | };
19 |
20 | componentDidMount() {
21 | this.updateLocale();
22 | document.body.addEventListener("locale-updated", this.updateLocale);
23 | }
24 |
25 | componentWillUnmount() {
26 | document.body.removeEventListener("locale-updated", this.updateLocale);
27 | }
28 |
29 | render() {
30 | return (
31 |
32 | {this.props.children}
33 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/heightfield.js:
--------------------------------------------------------------------------------
1 | /* global CANNON */
2 | AFRAME.registerComponent("heightfield", {
3 | init() {
4 | this.el.addEventListener("componentinitialized", e => {
5 | if (e.detail.name === "static-body") {
6 | this.generateAndAddHeightfield(this.el.components["static-body"]);
7 | }
8 | });
9 | this.el.setAttribute("static-body", { shape: "none", mass: 0 });
10 | },
11 | generateAndAddHeightfield(body) {
12 | const { offset, distance, data } = this.data;
13 |
14 | const orientation = new CANNON.Quaternion();
15 | orientation.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
16 |
17 | const rotation = new CANNON.Quaternion();
18 | rotation.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), -Math.PI / 2);
19 | rotation.mult(orientation, orientation);
20 |
21 | const cannonOffset = new CANNON.Vec3(offset.x, offset.y, offset.z);
22 |
23 | const shape = new CANNON.Heightfield(data, { elementSize: distance });
24 |
25 | body.addShape(shape, cannonOffset, orientation);
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useAccessibleOutlineStyle } from "../src/ui/input/useAccessibleOutlineStyle";
3 | import { WrappedIntlProvider } from "../src/ui/wrapped-intl-provider";
4 | import "../src/hubs/react-components/styles/global.scss";
5 | import Store from "../src/storage/store";
6 | import AccountChannel from "../src/utils/account-channel";
7 | import SpaceChannel from "../src/utils/space-channel";
8 | import { EventTarget } from "event-target-shim";
9 |
10 | class Scene extends EventTarget {
11 |
12 | }
13 |
14 | const store = new Store();
15 |
16 | window.APP = { store, accountChannel: new AccountChannel(),
17 | spaceChannel: new SpaceChannel(store), scene: new Scene() }
18 |
19 | window.SYSTEMS = { voxSystem: {} };
20 |
21 | const Layout = ({ children }) => {
22 | useAccessibleOutlineStyle();
23 | return <>{children}>;
24 | };
25 |
26 | export const decorators = [
27 | Story => (
28 |
29 |
30 |
31 |
32 |
33 | )
34 | ];
35 |
--------------------------------------------------------------------------------
/src/workers/sketchfab-zip.worker.js:
--------------------------------------------------------------------------------
1 | import ZipLoader from "zip-loader";
2 |
3 | async function fetchZipAndGetBlobs(src) {
4 | const zip = new ZipLoader(src);
5 | return zip.load().then(() => {
6 | // Rewrite any url references in the GLTF to blob urls
7 | const fileMap = {};
8 | for (const fileName in zip.files) {
9 | fileMap[fileName] = zip.extractAsBlobUrl(fileName);
10 | }
11 |
12 | const gltfJson = JSON.parse(zip.extractAsText("scene.gltf"));
13 | gltfJson.buffers && gltfJson.buffers.forEach(b => (b.uri = fileMap[b.uri]));
14 | gltfJson.images && gltfJson.images.forEach(i => (i.uri = fileMap[i.uri]));
15 | fileMap["scene.gtlf"] = URL.createObjectURL(new Blob([JSON.stringify(gltfJson, null, 2)], { type: "text/plain" }));
16 |
17 | return fileMap;
18 | });
19 | }
20 |
21 | self.onmessage = async msg => {
22 | try {
23 | const result = await fetchZipAndGetBlobs(msg.data.payload);
24 | self.postMessage({ id: msg.data.id, result });
25 | } catch (e) {
26 | self.postMessage({ id: msg.data.id, err: e.message });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/action-to-event.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent("action-to-event", {
2 | multiple: true,
3 |
4 | schema: {
5 | path: { type: "string" },
6 | event: { type: "string" },
7 | withPermission: { type: "string" }
8 | },
9 |
10 | init() {
11 | this.needsPermission = !!this.data.withPermission;
12 | this.updatePermissions = () => {
13 | if (!this.needsPermission || !window.APP.atomAccessManager) return;
14 | this.hasPermission = window.APP.atomAccessManager.hubCan(this.data.withPermission);
15 | };
16 | },
17 |
18 | play() {
19 | this.el.sceneEl.systems.permissions.onPermissionsUpdated(this.updatePermissions);
20 | this.updatePermissions();
21 | },
22 |
23 | pause() {
24 | window.APP.atomAccessManager.removeEventListener("permissions_updated", this.updatePermissions);
25 | },
26 |
27 | tick() {
28 | if (this.needsPermission && !this.hasPermission) return;
29 | const userinput = AFRAME.scenes[0].systems.userinput;
30 | if (userinput.get(this.data.path)) {
31 | this.el.emit(this.data.event);
32 | }
33 | }
34 | });
35 |
--------------------------------------------------------------------------------
/src/ui/floating-text-input.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const FloatingTextPanelElement = styled.div`
4 | background-color: var(--menu-background-color);
5 | min-width: 512px;
6 | height: fit-content;
7 | display: flex;
8 | flex-direction: row;
9 | align-items: flex-start;
10 | justify-content: flex-start;
11 | border-radius: 6px;
12 | border: 1px solid var(--menu-border-color);
13 | box-shadow: 0px 12px 28px var(--menu-shadow-color);
14 | padding: 6px;
15 | `;
16 |
17 | export const FloatingTextWrap = styled.div`
18 | flex: 1;
19 | padding: 2px 4px;
20 | border-radius: 4px;
21 | border: 0;
22 | background: var(--text-input-background-color);
23 | box-shadow: inset 0px 0px 2px var(--menu-background-color);
24 | `;
25 |
26 | export const FloatingTextElement = styled.input`
27 | width: 100%;
28 | border: 0;
29 | color: var(--text-input-text-color);
30 | font-size: var(--text-input-text-size);
31 | font-weight: var(--text-input-text-weight);
32 | padding: 4px;
33 |
34 | &::placeholder {
35 | color: var(--text-input-placeholder-color);
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/src/styles.js:
--------------------------------------------------------------------------------
1 | import NORMALIZE_CSS from "./assets/stylesheets/normalize.scss";
2 | import GLOBAL_CSS from "./assets/stylesheets/global.scss";
3 | import ATOM_TREE from "./assets/stylesheets/atom-tree.scss";
4 | import SPACE_TREE from "./assets/stylesheets/space-tree.scss";
5 | import CREATE_SELECT from "./assets/stylesheets/create-select.scss";
6 | import THEME from "./assets/stylesheets/theme.scss";
7 | import AFRAME_CSS from "aframe/src/style/aframe.css";
8 | import TIPPY_CSS from "tippy.js/dist/tippy.css";
9 | import QUILL_PRE from "./assets/stylesheets/quill-pre.scss";
10 | import QUILL_CORE from "quill/dist/quill.core.css";
11 | import QUILL_BUBBLE from "quill/dist/quill.bubble.css";
12 | import QUILL_EMOJI from "quill-emoji/dist/quill-emoji.css";
13 | import QUILL_HIGHLIGHT from "highlight.js/scss/github.scss";
14 |
15 | export const SHADOW_DOM_STYLES = `
16 | ${AFRAME_CSS}
17 | ${THEME}
18 | ${NORMALIZE_CSS}
19 | ${GLOBAL_CSS}
20 | ${TIPPY_CSS}
21 | ${QUILL_PRE}
22 | ${QUILL_CORE}
23 | ${QUILL_BUBBLE}
24 | ${QUILL_EMOJI}
25 | ${QUILL_HIGHLIGHT}
26 | ${ATOM_TREE}
27 | ${SPACE_TREE}
28 | ${CREATE_SELECT}
29 | `;
30 |
--------------------------------------------------------------------------------
/src/ui/equipped-emoji-icon.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import styled from "styled-components";
3 | import { BigIconButton } from "./icon-button";
4 | import { imageUrlForEmoji } from "../utils/media-url-utils";
5 |
6 | const EquippedEmojiImage = styled.img`
7 | width: 100%;
8 | height: 100%;
9 | opacity: 50%;
10 | width: 26px;
11 | height: 26px;
12 | `;
13 |
14 | export default function EquippedEmojiIcon() {
15 | const { store } = window.APP;
16 | const [equippedEmoji, setEquippedEmoji] = useState(store.state.equips.launcher);
17 | const equippedEmojiImageUrl = imageUrlForEmoji(equippedEmoji);
18 |
19 | // Equipped emoji
20 | useEffect(
21 | () => {
22 | const handler = () => setEquippedEmoji(store.state.equips.launcher);
23 | store.addEventListener("statechanged-equips", handler);
24 | return () => store.removeEventListener("statechanged-equips", handler);
25 | },
26 | [store, setEquippedEmoji]
27 | );
28 |
29 | return (
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/assets/images/icons/heart.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icons/cancel.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/tags.js:
--------------------------------------------------------------------------------
1 | export function isTagged(el, tag) {
2 | return el && el.components && el.components.tags && el.components.tags.data[tag];
3 | }
4 |
5 | AFRAME.registerComponent("tags", {
6 | schema: {
7 | isHandCollisionTarget: { default: false },
8 | isHoldable: { default: false },
9 | offersHandConstraint: { default: false },
10 | offersRemoteConstraint: { default: false },
11 | togglesHoveredActionSet: { default: false },
12 | singleActionButton: { default: false },
13 | holdableButton: { default: false },
14 | isPen: { default: false },
15 | isHoverMenuChild: { default: false },
16 | isStatic: { default: false },
17 | inspectable: { default: false }
18 | },
19 | update() {
20 | if (this.didUpdateOnce) {
21 | console.warn("Do not edit tags with .setAttribute");
22 | }
23 | this.didUpdateOnce = true;
24 | },
25 |
26 | remove() {
27 | const interaction = this.el.sceneEl.systems.interaction;
28 | if (interaction.isHeld(this.el)) {
29 | interaction.release(this.el);
30 | this.el.sceneEl.systems["hubs-systems"].constraintsSystem.release(this.el);
31 | }
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/ui/key-tips.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from "react";
2 | import sharedStyles from "../../assets/stylesheets/shared.scss";
3 | import classNames from "classnames";
4 | import KeyTips, { KEY_TIP_TYPES } from "./key-tips";
5 |
6 | let curTipType = -1;
7 |
8 | export const IdlePanels = () => {
9 | const ref = useRef();
10 |
11 | useEffect(
12 | () => {
13 | const interval = setInterval(() => {
14 | curTipType = (curTipType + 1) % KEY_TIP_TYPES.length;
15 | ref.current.setAttribute("data-show-tips", KEY_TIP_TYPES[curTipType]);
16 | }, 1000);
17 |
18 | return () => clearInterval(interval);
19 | },
20 | [ref]
21 | );
22 |
23 | return (
24 |
37 | );
38 | };
39 |
40 | export default {
41 | title: "Key Tips"
42 | };
43 |
--------------------------------------------------------------------------------
/src/assets/images/icons/sun-shadow.svgi:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/systems/enter-vr-button-system.js:
--------------------------------------------------------------------------------
1 | import configs from "../utils/configs";
2 |
3 | let uiRoot;
4 | export class EnterVRButtonSystem {
5 | constructor(scene) {
6 | this.scene = scene;
7 | this.wasGhost = false;
8 | }
9 | enableButton() {
10 | this.scene.setAttribute("vr-mode-ui", "enabled", true);
11 | this.scene.components["vr-mode-ui"].enterVREl.style.display = "block";
12 | this.scene.components["vr-mode-ui"].enterVREl.style.bottom = "80px";
13 | }
14 | disableButton() {
15 | this.scene.components["vr-mode-ui"].enterVREl.style.display = "none";
16 | }
17 | tick() {
18 | if (this.scene.is("entered")) {
19 | return;
20 | }
21 | uiRoot = uiRoot || DOM_ROOT.getElementById("ui-root");
22 | const enable =
23 | configs.feature("enable_lobby_ghosts") &&
24 | uiRoot &&
25 | uiRoot.firstChild &&
26 | (uiRoot.firstChild.classList.contains("isGhost") && !uiRoot.firstChild.classList.contains("hide"));
27 | if (enable && !this.enabled) {
28 | this.enableButton();
29 | } else if (this.enabled && !enable) {
30 | this.disableButton();
31 | }
32 | this.enabled = enable;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-2.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/ui/profile-editor-popup.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import ProfileEditorPopup, { PROFILE_EDITOR_MODES } from "./profile-editor-popup";
3 | import sharedStyles from "../../assets/stylesheets/shared.scss";
4 |
5 | export const Unverified = () => {
6 | useEffect(() => document.querySelector(`.${sharedStyles.showWhenPopped}`).focus());
7 |
8 | return (
9 |
12 | );
13 | };
14 |
15 | export const Verifying = () => {
16 | useEffect(() => document.querySelector(`.${sharedStyles.showWhenPopped}`).focus());
17 |
18 | return (
19 |
22 | );
23 | };
24 |
25 | export const Verified = () => {
26 | useEffect(() => document.querySelector(`.${sharedStyles.showWhenPopped}`).focus());
27 |
28 | return (
29 |
32 | );
33 | };
34 |
35 | export default {
36 | title: "Profile Editor Popup"
37 | };
38 |
--------------------------------------------------------------------------------
/src/utils/image-bitmap-utils.js:
--------------------------------------------------------------------------------
1 | export const createImageBitmap =
2 | window.createImageBitmap ||
3 | async function(data) {
4 | return new Promise(resolve => {
5 | // https://dev.to/nektro/createimagebitmap-polyfill-for-safari-and-edge-228
6 | // https://gist.github.com/MonsieurV/fb640c29084c171b4444184858a91bc7
7 |
8 | let srcUrl;
9 | if (data instanceof Blob) {
10 | srcUrl = URL.createObjectURL(data);
11 | } else {
12 | const canvas = document.createElement("canvas");
13 | const ctx = canvas.getContext("2d");
14 | canvas.width = data.width;
15 | canvas.height = data.height;
16 | ctx.putImageData(data, 0, 0);
17 | srcUrl = canvas.toDataURL();
18 | }
19 | const img = document.createElement("img");
20 |
21 | img.addEventListener("load", () => resolve(img));
22 |
23 | img.src = srcUrl;
24 | });
25 | };
26 |
27 | export function disposeImageBitmap(imageBitmap) {
28 | if (imageBitmap instanceof HTMLImageElement && imageBitmap.src.startsWith("blob:")) {
29 | URL.revokeObjectURL(imageBitmap.src);
30 | } else {
31 | imageBitmap.close && imageBitmap.close();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/ui/atom-trail.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AtomTrail from "./atom-trail";
3 | import AtomMetadata, { ATOM_TYPES } from "../utils/atom-metadata";
4 |
5 | const metadata = new AtomMetadata(ATOM_TYPES.HUB);
6 | metadata._metadata.set("QxRKdNF", { displayName: "Test Name" });
7 | metadata._metadata.set("JRrZerh", { displayName: "Test Very Long Name That Keeps Going and Going" });
8 | metadata._metadata.set("QcAVkAR", { displayName: "This is is the one you are on" });
9 | metadata._metadata.set("ARbzxCd", { displayName: "You should not see me" });
10 |
11 | export const TrailMulti = () => (
12 |
22 |
console.log(hubId)}
26 | hubCan={() => true}
27 | />
28 |
29 | );
30 |
31 | export default {
32 | title: "AtomTrail"
33 | };
34 |
--------------------------------------------------------------------------------
/src/ui/create-embed-input-panel.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import CreateEmbedInputPanel from "./create-embed-input-panel";
3 | import sharedStyles from "../../assets/stylesheets/shared.scss";
4 | import classNames from "classnames";
5 |
6 | export const Normal = () => (
7 |
8 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 |
31 | export default {
32 | title: "Create Embed Input Panel"
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/quack.js:
--------------------------------------------------------------------------------
1 | import { SOUND_QUACK, SOUND_SPECIAL_QUACK } from "../systems/sound-effects-system";
2 |
3 | AFRAME.registerComponent("quack", {
4 | schema: {
5 | quackPercentage: { default: 1 },
6 | specialQuackPercentage: { default: 0.01 }
7 | },
8 |
9 | init: function() {
10 | this.wasInteracting = false;
11 | NAF.utils.getNetworkedEntity(this.el).then(networkedEntity => {
12 | this.networkedEntity = networkedEntity;
13 | });
14 | },
15 |
16 | tick: function() {
17 | const interaction = AFRAME.scenes[0].systems.interaction;
18 | const isInteracting = interaction.isHeld(this.networkedEntity || this.el);
19 |
20 | if (isInteracting && !this.wasInteracting) {
21 | this.quack();
22 | }
23 |
24 | this.wasInteracting = isInteracting;
25 | },
26 |
27 | quack: function() {
28 | const rand = Math.random();
29 | if (rand < this.data.specialQuackPercentage) {
30 | this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_SPECIAL_QUACK);
31 | } else if (rand < this.data.quackPercentage) {
32 | this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_QUACK);
33 | }
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/src/utils/account-channel.js:
--------------------------------------------------------------------------------
1 | import { EventTarget } from "event-target-shim";
2 |
3 | export default class AccountChannel extends EventTarget {
4 | constructor(store) {
5 | super();
6 | this.memberships = [];
7 | this.hubSettings = [];
8 | this.store = store;
9 | }
10 |
11 | // publishVox = (
12 | // voxId,
13 | // collection,
14 | // category,
15 | // stackAxis,
16 | // stackSnapPosition,
17 | // stackSnapScale,
18 | // scale,
19 | // thumbFileId,
20 | // previewFileId
21 | // ) => {
22 | // return new Promise(res => {
23 | // this.channel
24 | // .push("publish_vox", {
25 | // vox_id: voxId,
26 | // collection,
27 | // category,
28 | // stack_axis: stackAxis,
29 | // stack_snap_position: stackSnapPosition,
30 | // stack_snap_scale: stackSnapScale,
31 | // scale,
32 | // thumb_file_id: thumbFileId,
33 | // preview_file_id: previewFileId
34 | // })
35 | // .receive("ok", async ({ published_to_vox_id: publishedVoxId }) => {
36 | // res(publishedVoxId);
37 | // });
38 | // });
39 | // };
40 | }
41 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-9.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/remove-networked-object-button.js:
--------------------------------------------------------------------------------
1 | import { ensureOwnership, getNetworkedEntity } from "../utils/ownership-utils";
2 |
3 | AFRAME.registerComponent("remove-networked-object-button", {
4 | init() {
5 | this.onClick = () => {
6 | if (!ensureOwnership(this.targetEl)) return;
7 |
8 | // DEAD, see object-info-dialog for remove pattern
9 | /*this.targetEl.setAttribute("animation__remove", {
10 | property: "scale",
11 | dur: 200,
12 | to: { x: 0.01, y: 0.01, z: 0.01 },
13 | easing: "easeInQuad"
14 | });
15 |
16 | this.el.parentNode.removeAttribute("visibility-while-frozen");
17 | this.el.parentNode.setAttribute("visible", false);
18 |
19 | this.targetEl.addEventListener("animationcomplete", () => {
20 | takeOwnership(this.targetEl);
21 | this.targetEl.parentNode.removeChild(this.targetEl);
22 | });*/
23 | };
24 |
25 | getNetworkedEntity(this.el).then(networkedEl => {
26 | this.targetEl = networkedEl;
27 | });
28 | },
29 |
30 | play() {
31 | this.el.object3D.addEventListener("interact", this.onClick);
32 | },
33 |
34 | pause() {
35 | this.el.object3D.removeEventListener("interact", this.onClick);
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/src/assets/images/icons/github-off.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/terra/blocks/models/block.js:
--------------------------------------------------------------------------------
1 | const offset = { x: 0, y: 0, z: 0 };
2 | const size = { x: 8, y: 8 };
3 |
4 | const faces = ["top", "bottom", "south", "north", "west", "east"].reduce((faces, facing) => {
5 | faces[facing] = [
6 | {
7 | facing,
8 | texture: "block",
9 | offset,
10 | size
11 | }
12 | ];
13 | return faces;
14 | }, {});
15 |
16 | const isVisible = (type, neighbor) =>
17 | !type.hasCulling || !neighbor.hasCulling || (neighbor.isTransparent && (!type.isTransparent || type !== neighbor));
18 |
19 | const empty = [];
20 | export default {
21 | isVisible,
22 | faces: ({ neighbors, types, voxel }) => [
23 | ...(isVisible(types[voxel.type], types[neighbors.top.type]) ? faces.top : empty),
24 | ...(isVisible(types[voxel.type], types[neighbors.bottom.type]) ? faces.bottom : empty),
25 | ...(isVisible(types[voxel.type], types[neighbors.south.type]) ? faces.south : empty),
26 | ...(isVisible(types[voxel.type], types[neighbors.north.type]) ? faces.north : empty),
27 | ...(isVisible(types[voxel.type], types[neighbors.west.type]) ? faces.west : empty),
28 | ...(isVisible(types[voxel.type], types[neighbors.east.type]) ? faces.east : empty)
29 | ],
30 | hasAO: true,
31 | hasCulling: true
32 | };
33 |
--------------------------------------------------------------------------------
/src/worklets/audio-forward.worklet.js:
--------------------------------------------------------------------------------
1 | const PROCESSOR_NAME = "audio-forwarder";
2 | const EMPTY = [];
3 |
4 | class AudioForwarder extends AudioWorkletProcessor {
5 | constructor({ audioContext, processorOptions }) {
6 | super(audioContext, PROCESSOR_NAME, {
7 | numberOfInputs: 1,
8 | numberOfOutputs: 0,
9 | channelCount: 1
10 | });
11 |
12 | // The second frame buffer is offset by 1024 bytes to ensure
13 | // we can always grab one windows worth of data without a copy.
14 | this.frameData1 = new Float32Array(processorOptions.audioFrameBuffer1);
15 | this.frameData2 = new Float32Array(processorOptions.audioFrameBuffer2);
16 | this.offsetData = new Uint8Array(processorOptions.audioOffsetBuffer);
17 | this.offset = 0;
18 | }
19 | process(inputs) {
20 | const inbuf = inputs[0][0] || EMPTY; // Always 128 bytes per spec
21 | const { frameData1, frameData2 } = this;
22 |
23 | const dataOffset = this.offset * 128;
24 | frameData1.set(inbuf, dataOffset);
25 | frameData2.set(inbuf, (dataOffset + 1024) % 2048);
26 | this.offsetData[0] = this.offset;
27 | this.offset = (this.offset + 1) % 16;
28 |
29 | return true;
30 | }
31 | }
32 |
33 | registerProcessor(PROCESSOR_NAME, AudioForwarder);
34 |
--------------------------------------------------------------------------------
/src/systems/frame-scheduler.js:
--------------------------------------------------------------------------------
1 | // Given a function and a queue name, schedules things so a single function
2 | // from a given queue will be called per frame.
3 | AFRAME.registerSystem("frame-scheduler", {
4 | init() {
5 | // Registry a map from queue name to list of registered functions.
6 | this.registry = new Map();
7 | this.queues = [];
8 | this.frameIndex = 0;
9 | },
10 |
11 | schedule(func, queue) {
12 | if (!this.registry.has(queue)) {
13 | this.queues.push(queue);
14 | this.registry.set(queue, []);
15 | }
16 |
17 | this.registry.get(queue).push(func);
18 | },
19 |
20 | unschedule(func) {
21 | for (let i = 0; i < this.queues.length; i++) {
22 | const queue = this.queues[i];
23 | const entries = this.registry.get(queue);
24 | const idx = entries.indexOf(func);
25 |
26 | if (idx >= 0) {
27 | entries.splice(idx, 1);
28 | }
29 | }
30 | },
31 |
32 | tick: function() {
33 | for (let i = 0; i < this.queues.length; i++) {
34 | const queue = this.queues[i];
35 | const entries = this.registry.get(queue);
36 | if (entries.length === 0) continue;
37 |
38 | entries[this.frameIndex % entries.length]();
39 | }
40 |
41 | this.frameIndex++;
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/src/ui/equipped-color-icon.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import styled from "styled-components";
3 | import { BigIconButton } from "./icon-button";
4 | import { objRgbToCssRgb } from "../utils/dom-utils";
5 | import { storedColorToRgb } from "../storage/store";
6 |
7 | const EquippedColorSwatch = styled.div`
8 | width: 100%;
9 | height: 100%;
10 | width: 26px;
11 | height: 26px;
12 | border-radius: 4px;
13 | `;
14 |
15 | export default function EquippedColorIcon() {
16 | const { store } = window.APP;
17 | const [equippedColor, setEquippedColor] = useState(store.state.equips.color);
18 |
19 | const { r, g, b } = storedColorToRgb(equippedColor);
20 | const cssRgb = objRgbToCssRgb({ r: r / 255.0, g: g / 255.0, b: b / 255.0 });
21 |
22 | // Equipped emoji
23 | useEffect(
24 | () => {
25 | const handler = () => setEquippedColor(store.state.equips.color);
26 | store.addEventListener("statechanged-equips", handler);
27 | return () => store.removeEventListener("statechanged-equips", handler);
28 | },
29 | [store, setEquippedColor]
30 | );
31 |
32 | return (
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/ui/action-button.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import sharedStyles from "../../assets/stylesheets/shared.scss";
3 | import classNames from "classnames";
4 | import addIcon from "../assets/images/icons/add.svgi";
5 |
6 | import ActionButton from "./action-button";
7 | import SmallActionButton from "./small-action-button";
8 |
9 | export const Normal = () => (
10 |
13 | );
14 |
15 | export const Small = () => (
16 |
20 | Test Button
21 | Test Long Button Label
22 | Tiny
23 | With An Icon
24 |
25 | );
26 |
27 | export const WithIcon = () => (
28 |
31 | );
32 |
33 | export default {
34 | title: "Action Button"
35 | };
36 |
--------------------------------------------------------------------------------
/src/utils/pdf-pool.js:
--------------------------------------------------------------------------------
1 | import * as pdfjs from "pdfjs-dist";
2 |
3 | const pdfjsWorker = require("pdfjs-dist/build/pdf.worker.js");
4 | pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfjsWorker], { type: "text/javascript" }));
5 |
6 | const pdfs = [];
7 |
8 | export async function retainPdf(src) {
9 | for (const entry of pdfs) {
10 | if (entry.src === src) {
11 | entry.refs++;
12 | return await entry.getPdfPromise;
13 | }
14 | }
15 |
16 | let pdf;
17 | try {
18 | const promise = pdfjs.getDocument(src).promise;
19 | pdfs.push({ src, getPdfPromise: promise, refs: 1 });
20 | return await promise;
21 | } catch (e) {
22 | if (pdf) {
23 | pdf.destroy();
24 | }
25 |
26 | throw e;
27 | }
28 | }
29 |
30 | export async function releasePdf(pdf) {
31 | let removeAtIndex = -1;
32 |
33 | for (let i = 0; i < pdfs.length; i++) {
34 | const entry = pdfs[i];
35 | const entryPdf = await entry.getPdfPromise;
36 |
37 | if (entryPdf !== pdf) continue;
38 | removeAtIndex = i;
39 | entry.refs--;
40 | if (entry.refs <= 0) {
41 | entryPdf.destroy();
42 | removeAtIndex = i;
43 | }
44 |
45 | break;
46 | }
47 |
48 | if (removeAtIndex !== -1) {
49 | pdfs.splice(removeAtIndex, 1);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/unmute-video-button.js:
--------------------------------------------------------------------------------
1 | import { findAncestorWithComponent } from "../utils/scene-graph";
2 |
3 | AFRAME.registerComponent("unmute-video-button", {
4 | init() {
5 | this.onClick = () => {
6 | const videoEl = findAncestorWithComponent(this.el, "media-video");
7 | const mediaVideo = videoEl.components["media-video"];
8 | if (!mediaVideo || !mediaVideo.video || !mediaVideo.videoMutedAt) return;
9 |
10 | // iOS initially plays the sound and *then* mutes it, and sometimes a second video playing
11 | // can break all sound in the app. (Likely a Safari bug.) Adding a delay before the unmute
12 | // occurs seems to help with reducing this.
13 | if (performance.now() - mediaVideo.videoMutedAt < 3000) {
14 | return;
15 | }
16 |
17 | if (mediaVideo.video.muted) {
18 | mediaVideo.video.muted = false;
19 | this.el.setAttribute("visible", false);
20 | }
21 | };
22 | },
23 |
24 | play() {
25 | // Safari won't accept grab-start or even mousedown as user-initiated gestures,
26 | // so we have to use a global touchstart event here.
27 | document.addEventListener("touchstart", this.onClick);
28 | },
29 |
30 | pause() {
31 | document.removeEventListener("touchstart", this.onClick);
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # Ignore dist folder with webpack build output
61 | dist/
62 |
63 | .DS_Store
64 |
65 | certs/
66 | results/
67 | .ret.credentials
68 |
69 | .vscode
70 | .idea
71 |
--------------------------------------------------------------------------------
/src/systems/userinput/pose.js:
--------------------------------------------------------------------------------
1 | const THREE = typeof window !== "undefined" && window.THREE;
2 | const Vector3 = (THREE && THREE.Vector3) || function() {};
3 | const Quaternion = (THREE && THREE.Quaternion) || function() {};
4 | const forward = typeof window !== "undefined" && window.THREE && new THREE.Vector3(0, 0, -1);
5 | export function Pose() {
6 | return {
7 | position: new Vector3(),
8 | direction: new Vector3(),
9 | orientation: new Quaternion(),
10 | fromOriginAndDirection: function(origin, direction) {
11 | this.position = origin;
12 | this.direction = direction;
13 | this.orientation = this.orientation.setFromUnitVectors(forward, direction);
14 | return this;
15 | },
16 | fromCameraProjection: function(camera, normalizedX, normalizedY) {
17 | this.position.setFromMatrixPosition(camera.matrixWorld);
18 | this.direction
19 | .set(normalizedX, normalizedY, 0.5)
20 | .unproject(camera)
21 | .sub(this.position)
22 | .normalize();
23 | this.fromOriginAndDirection(this.position, this.direction);
24 | return this;
25 | },
26 | copy: function(pose) {
27 | this.position.copy(pose.position);
28 | this.direction.copy(pose.direction);
29 | this.orientation.copy(pose.orientation);
30 | }
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/media-highlight-frag.glsl:
--------------------------------------------------------------------------------
1 | if (hubs_HighlightInteractorOne || hubs_HighlightInteractorTwo || hubs_IsFrozen) {
2 | float ratio = 0.0;
3 | float size = hubs_SweepParams.t - hubs_SweepParams.s;
4 |
5 | if (hubs_EnableSweepingEffect) {
6 | float line = mod(hubs_Time / 500.0 * size, size * 3.0) + hubs_SweepParams.s - size / 3.0;
7 |
8 | if (hubs_WorldPosition.y < line) {
9 | // Highlight with a sweeping gradient.
10 | ratio = max(0.0, 1.0 - (line - hubs_WorldPosition.y) / (size * 1.5));
11 | }
12 | }
13 |
14 | // Highlight with a gradient falling off with distance.
15 | float pulse = 1.0 / (size + 0.2) * 8.0 + 1.0 / (size + 0.3) * 3.0 * (sin(hubs_Time / 1000.0) + 1.0);
16 |
17 | if (hubs_HighlightInteractorOne) {
18 | float dist1 = distance(hubs_WorldPosition, hubs_InteractorOnePos);
19 | ratio += -min(1.0, pow(dist1 * pulse, 3.0)) + 1.0;
20 | }
21 |
22 | if (hubs_HighlightInteractorTwo) {
23 | float dist2 = distance(hubs_WorldPosition, hubs_InteractorTwoPos);
24 | ratio += -min(1.0, pow(dist2 * pulse, 3.0)) + 1.0;
25 | }
26 |
27 | ratio = min(1.0, ratio);
28 |
29 | // Gamma corrected highlight color
30 | vec3 highlightColor = vec3(0.184, 0.499, 0.933);
31 |
32 | gl_FragColor.rgb = (gl_FragColor.rgb * (1.0 - ratio)) + (highlightColor * ratio);
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/destroy-at-extreme-distances.js:
--------------------------------------------------------------------------------
1 | import { getLastWorldPosition } from "../utils/three-utils";
2 | import { isSynchronized, isMine } from "../utils/ownership-utils";
3 |
4 | AFRAME.registerComponent("destroy-at-extreme-distances", {
5 | schema: {
6 | xMin: { default: -1000 },
7 | xMax: { default: 1000 },
8 | yMin: { default: -1000 },
9 | yMax: { default: 1000 },
10 | zMin: { default: -1000 },
11 | zMax: { default: 1000 }
12 | },
13 |
14 | init() {
15 | this._checkForDestroy = this._checkForDestroy.bind(this);
16 | this.el.sceneEl.systems["frame-scheduler"].schedule(this._checkForDestroy, "media-components");
17 | },
18 |
19 | remove() {
20 | this.el.sceneEl.systems["frame-scheduler"].unschedule(this._checkForDestroy, "media-components");
21 | },
22 |
23 | _checkForDestroy: (function() {
24 | const pos = new THREE.Vector3();
25 | return function() {
26 | const { xMin, xMax, yMin, yMax, zMin, zMax } = this.data;
27 | getLastWorldPosition(this.el.object3D, pos);
28 |
29 | if (pos.x < xMin || pos.x > xMax || pos.y < yMin || pos.y > yMax || pos.z < zMin || pos.z > zMax) {
30 | if (!isSynchronized(this.el) || isMine(this.el)) {
31 | this.el.parentNode.removeChild(this.el);
32 | }
33 | }
34 | };
35 | })()
36 | });
37 |
--------------------------------------------------------------------------------
/src/ui/tooltip.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Tippy from "@tippyjs/react";
4 |
5 | const TooltipStyled = styled(Tippy)`
6 | background: var(--tooltip-background-color);
7 | font-size: var(--tooltip-text-size);
8 | font-weight: var(--tooltip-text-weight);
9 | line-height: calc(var(--tooltip-text-size) + 4px);
10 | color: var(--tooltip-text-color);
11 |
12 | & .tippy-arrow {
13 | color: var(--tooltip-background-color);
14 | background: var(--tooltip-background-color);
15 | border-color: var(--tooltip-background-color);
16 | }
17 |
18 | &[data-animation="open"][data-state="hidden"] {
19 | opacity: 0;
20 | transform: translateY(2px) scale(0.95, 0.95);
21 | transition-property: opacity, transform;
22 | transition-duration: 75ms, 75ms !important;
23 | }
24 |
25 | &[data-animation="open"][data-state="visible"] {
26 | opacity: 1;
27 | transform: translateY(0px) scale(1, 1);
28 | transition-property: opacity, transform;
29 | transition-duration: 75ms, 75ms !important;
30 | }
31 |
32 | .panels-collapsed &.hide-when-expanded {
33 | display: none;
34 | }
35 | `;
36 |
37 | const Tooltip = function(props) {
38 | return ;
39 | };
40 |
41 | export default Tooltip;
42 |
--------------------------------------------------------------------------------
/src/components/layers.js:
--------------------------------------------------------------------------------
1 | export const Layers = {
2 | // Layers 0 - 2 reserverd by ThreeJS and AFrame.
3 | reflection: 3
4 | };
5 |
6 | /**
7 | * Sets layer flags on the underlying Object3D
8 | * @namespace environment
9 | * @component layers
10 | */
11 | AFRAME.registerComponent("layers", {
12 | schema: {
13 | reflection: { type: "boolean", default: false },
14 | inWorldHud: { type: "boolean", default: false },
15 | exclusive: { type: "boolean", default: false } // if true, only these layers will be set
16 | },
17 | init() {
18 | this.update = this.update.bind(this);
19 | this.el.addEventListener("model-loaded", this.update);
20 | },
21 | update(oldData) {
22 | const obj = this.el.object3D;
23 |
24 | if (this.data.exclusive) {
25 | obj.traverse(o => (o.layers.mask = 0));
26 | }
27 |
28 | for (const [name, layer] of Object.entries(Layers)) {
29 | const oldValue = oldData[name];
30 | const newValue = this.data[name];
31 |
32 | if (oldValue !== newValue) {
33 | if (newValue) {
34 | obj.traverse(o => o.layers.enable(layer));
35 | } else {
36 | obj.traverse(o => o.layers.disable(layer));
37 | }
38 | }
39 | }
40 | },
41 | remove() {
42 | this.el.removeEventListener("model-loaded", this.update);
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/src/ui/avatar-swatch.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import sharedStyles from "../../assets/stylesheets/shared.scss";
3 | import classNames from "classnames";
4 |
5 | import AvatarSwatch from "./avatar-swatch";
6 |
7 | let eyeIndex = 0;
8 | let mouthIndex = 0;
9 |
10 | export const Normal = () => {
11 | const swatchRef = useRef();
12 |
13 | useEffect(
14 | () => {
15 | const interval = setInterval(() => {
16 | eyeIndex = (eyeIndex + 1) % 8;
17 | mouthIndex = (mouthIndex + 1) % 12;
18 | swatchRef.current.setAttribute("data-eyes", eyeIndex);
19 | swatchRef.current.setAttribute("data-mouth", mouthIndex);
20 | swatchRef.current.setAttribute("style", "color: blue;");
21 | }, 150);
22 |
23 | return () => clearInterval(interval);
24 | },
25 | [swatchRef]
26 | );
27 |
28 | return (
29 |
41 | );
42 | };
43 |
44 | export default {
45 | title: "Avatar Swatch"
46 | };
47 |
--------------------------------------------------------------------------------
/src/ui/input/useInstallPWA.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback, useState, useRef } from "react";
2 | import { supportsLipSync } from "../../systems/audio-system";
3 |
4 | const browserSupport =
5 | "relList" in HTMLLinkElement.prototype &&
6 | document.createElement("link").relList.supports("manifest") &&
7 | "onbeforeinstallprompt" in window;
8 |
9 | export function useInstallPWA() {
10 | const installEventRef = useRef();
11 |
12 | const [available, setAvailable] = useState(false);
13 | const [installed, setInstalled] = useState(false);
14 |
15 | useEffect(() => {
16 | const onBeforeInstallPrompt = event => {
17 | event.preventDefault();
18 | installEventRef.current = event;
19 | setAvailable(supportsLipSync());
20 | };
21 |
22 | window.addEventListener("beforeinstallprompt", onBeforeInstallPrompt);
23 |
24 | return () => {
25 | window.removeEventListener("beforeinstallprompt", onBeforeInstallPrompt);
26 | };
27 | }, []);
28 |
29 | const installPWA = useCallback(async () => {
30 | installEventRef.current.prompt();
31 |
32 | const choiceResult = await installEventRef.current.userChoice;
33 |
34 | if (choiceResult.outcome === "accepted") {
35 | setInstalled(true);
36 | }
37 | }, []);
38 |
39 | return [browserSupport && available && !installed, installPWA];
40 | }
41 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import Store from "./storage/store";
2 | import MediaSearchStore from "./storage/media-search-store";
3 |
4 | export class App {
5 | constructor() {
6 | this.scene = null;
7 | this.quality = "low";
8 | this.store = new Store();
9 | this.mediaSearchStore = new MediaSearchStore();
10 |
11 | // Detail levels
12 | // 0 - Full
13 | // 1 - No reflections, simple sky, lower shadow map res
14 | // 2 - Disable shadows, no terrain detail meshes, disable FXAA
15 | // 3 - Also disable SSAO, used when software rendering
16 | //
17 | // Start at lowest detail level, so app boots quickly.
18 | this._detailLevel = 3;
19 | }
20 |
21 | setQuality(quality) {
22 | if (this.quality === quality) {
23 | return false;
24 | }
25 |
26 | this.quality = quality;
27 |
28 | if (this.scene) {
29 | this.scene.dispatchEvent(new CustomEvent("quality-changed", { detail: quality }));
30 | }
31 |
32 | return true;
33 | }
34 |
35 | get detailLevel() {
36 | return this._detailLevel;
37 | }
38 |
39 | set detailLevel(detailLevel) {
40 | this._detailLevel = detailLevel;
41 |
42 | if (typeof AFRAME !== "undefined") {
43 | const scene = AFRAME.scenes[0];
44 |
45 | if (scene) {
46 | scene.emit("detail-level-changed", {});
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/duck.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Floats a duck based on its scale.
3 | * @component duck
4 | */
5 | AFRAME.registerComponent("duck", {
6 | schema: {
7 | initialForce: { default: 0 },
8 | maxForce: { default: 6.5 },
9 | maxScale: { default: 5 }
10 | },
11 |
12 | init: function() {
13 | this.initialScale = this.el.object3D.scale.x;
14 | this.maxScale = this.data.maxScale * this.initialScale;
15 |
16 | NAF.utils.getNetworkedEntity(this.el).then(networkedEntity => {
17 | this.networkedEntity = networkedEntity;
18 |
19 | this.initialScale = this.networkedEntity.object3D.scale.x;
20 | this.maxScale = this.data.maxScale * this.initialScale;
21 | });
22 | },
23 |
24 | tick: function() {
25 | if (!this.networkedEntity || NAF.utils.isMine(this.networkedEntity)) {
26 | const entity = this.networkedEntity || this.el;
27 | const currentScale = entity.object3D.scale.x;
28 | const ratio = Math.min(1, (currentScale - this.initialScale) / (this.maxScale - this.initialScale));
29 | const force = ratio * this.data.maxForce;
30 | if (force > 0) {
31 | const angle = Math.random() * Math.PI * 2;
32 | const x = Math.cos(angle);
33 | const z = Math.sin(angle);
34 | entity.setAttribute("body-helper", { gravity: { x, y: force, z } });
35 | }
36 | }
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/src/components/virtual-gamepad-controls.css:
--------------------------------------------------------------------------------
1 | :local(.touchZone) {
2 | position: absolute;
3 | height: 20vh;
4 | bottom: 0;
5 | z-index: 1;
6 |
7 | /* Orientation selector fails here when keyboard pops up on shorter screens */
8 | @media(min-aspect-ratio: 15/9) {
9 | height: 30vh;
10 | }
11 | }
12 |
13 | :local(.touchZone.left) {
14 | left: 0;
15 | right: 55%;
16 | }
17 |
18 | :local(.touchZone.right) {
19 | left: 55%;
20 | right: 0;
21 | }
22 |
23 | :local(.mockJoystickContainer) {
24 | position: absolute;
25 | height: 0;
26 | left: 0;
27 | right: 0;
28 | bottom: 15vh;
29 | align-items: center;
30 | justify-content: space-around;
31 | display: none;
32 |
33 | /* Orientation selector fails here when keyboard pops up on shorter screens */
34 | @media (min-aspect-ratio: 15/9) {
35 | display: flex;
36 | }
37 | }
38 |
39 | :local(.mockJoystick) {
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 | width: 100px;
44 | height: 100px;
45 | background-color: rgba(255,255,255,0.5);
46 | border-top-left-radius: 50%;
47 | border-top-right-radius: 50%;
48 | border-bottom-right-radius: 50%;
49 | border-bottom-left-radius: 50%;
50 | }
51 |
52 | :local(.hidden) {
53 | background-color: transparent;
54 | }
55 |
56 | :local(.mockJoystick.inner) {
57 | width: 50px;
58 | height: 50px;
59 | }
60 |
--------------------------------------------------------------------------------
/src/utils/promisify-worker.js:
--------------------------------------------------------------------------------
1 | // Wrapper for a worker which accepts work items and responds with results.
2 | //
3 | // The worker must receive messages with data of the shape { id, payload }, and then respond
4 | // with messages containing matching IDs of the shape { id, result, err }.
5 | //
6 | // Returns a function that accepts a work item, sends it to the worker, and returns a promise that
7 | // will resolve with the result or reject with the error.
8 | //
9 | export function promisifyWorker(worker) {
10 | let nextItemId = 0;
11 | const outstanding = {}; // item ID: { resolve, reject }
12 |
13 | worker.onmessage = msg => {
14 | const { id, result, err } = msg.data;
15 | const handlers = outstanding[id];
16 | if (handlers == null) {
17 | console.error(`Unknown message with ID ${id} received from ${worker}.`);
18 | } else {
19 | delete outstanding[id];
20 | if (err != null) {
21 | handlers.reject(new Error(err));
22 | } else {
23 | handlers.resolve(result);
24 | }
25 | }
26 | };
27 |
28 | return function(data, transfer, args = {}) {
29 | const id = nextItemId++;
30 | const promise = new Promise((resolve, reject) => {
31 | outstanding[id] = { resolve, reject };
32 | });
33 | worker.postMessage(Object.assign(args, { id, payload: data }), transfer);
34 | return promise;
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/ui/create-embed-popup.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import React, { forwardRef } from "react";
3 | import ReactDOM from "react-dom";
4 | import CreateEmbedInputPanel from "./create-embed-input-panel";
5 | import { waitForShadowDOMContentLoaded } from "../utils/async-utils";
6 |
7 | let popupRoot = null;
8 | waitForShadowDOMContentLoaded().then(() => (popupRoot = DOM_ROOT.getElementById("popup-root")));
9 |
10 | const CreateEmbedPopup = forwardRef(({ styles, attributes, setPopperElement, onURLEntered, embedType }, ref) => {
11 | const popupInput = (
12 |
19 |
25 |
26 | );
27 |
28 | return ReactDOM.createPortal(popupInput, popupRoot);
29 | });
30 |
31 | CreateEmbedPopup.displayName = "CreateEmbedPopup";
32 | CreateEmbedPopup.propTypes = {
33 | styles: PropTypes.object,
34 | attributes: PropTypes.object,
35 | setPopperElement: PropTypes.func,
36 | embedType: PropTypes.string,
37 | onURLEntered: PropTypes.func
38 | };
39 |
40 | export default CreateEmbedPopup;
41 |
--------------------------------------------------------------------------------
/src/systems/scene-preview-camera-system.js:
--------------------------------------------------------------------------------
1 | import { CAMERA_MODE_INSPECT } from "./camera-system.js";
2 | import { setMatrixWorld } from "../utils/three-utils";
3 |
4 | let viewingCamera;
5 | let uiRoot;
6 | export class ScenePreviewCameraSystem {
7 | constructor() {
8 | this.entities = [];
9 | }
10 |
11 | register(el) {
12 | this.entities.push(el);
13 | }
14 |
15 | unregister(el) {
16 | this.entities.splice(this.entities.indexOf(el), 1);
17 | }
18 |
19 | tick() {
20 | viewingCamera = viewingCamera || DOM_ROOT.getElementById("viewing-camera");
21 | uiRoot = uiRoot || DOM_ROOT.getElementById("ui-root");
22 | const entered = viewingCamera && viewingCamera.sceneEl.is("entered");
23 | const isGhost = !entered && uiRoot && uiRoot.firstChild && uiRoot.firstChild.classList.contains("isGhost");
24 | for (let i = 0; i < this.entities.length; i++) {
25 | const el = this.entities[i];
26 | const hubsSystems = AFRAME.scenes[0].systems["hubs-systems"];
27 | if (el && (!hubsSystems || (hubsSystems.cameraSystem.mode !== CAMERA_MODE_INSPECT && !isGhost && !entered))) {
28 | el.components["scene-preview-camera"].tick2();
29 | if (hubsSystems && viewingCamera) {
30 | el.object3D.updateMatrices();
31 | setMatrixWorld(viewingCamera.object3DMap.camera, el.object3D.matrixWorld);
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/assets/images/icons/restore.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-8.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/terra/blocks/index.js:
--------------------------------------------------------------------------------
1 | import dirt from "./models/dirt";
2 | import glass from "./models/glass";
3 | import water from "./models/water";
4 | import feature from "./models/feature";
5 | import blockTexture from "./textures/block";
6 |
7 | const blockTypes = {
8 | air: 0,
9 | 0: { isTransparent: true, isVisible: () => true }
10 | };
11 |
12 | const models = {
13 | dirt,
14 | glass,
15 | water,
16 | feature
17 | };
18 |
19 | const LoadBlockTypes = () => {
20 | const types = ["dirt", "glass", "water", "feature"];
21 |
22 | const textures = [];
23 | types.forEach((type, i) => {
24 | const model = models[type];
25 | Object.keys(model.textures).forEach(id => {
26 | const texture = model.textures[id];
27 | let index = textures.findIndex(({ name }) => name === texture);
28 | if (index === -1) {
29 | index = textures.length;
30 | let image;
31 | if (texture === "block.js") {
32 | image = blockTexture;
33 | } else {
34 | console.error(`Texture: ${texture} format not supported.\n`);
35 | process.exit(1);
36 | }
37 | image.name = texture;
38 | textures.push(image);
39 | }
40 | model.textures[id] = index;
41 | });
42 | const index = i + 1;
43 | blockTypes[index] = model;
44 | blockTypes[type] = index;
45 | });
46 | return blockTypes;
47 | };
48 |
49 | export default LoadBlockTypes();
50 |
--------------------------------------------------------------------------------
/src/systems/userinput/devices/touchscreen/assignments.js:
--------------------------------------------------------------------------------
1 | export function touchIsAssigned(touch, assignments) {
2 | return (
3 | assignments.find(assignment => {
4 | return assignment.touch.identifier === touch.identifier;
5 | }) !== undefined
6 | );
7 | }
8 |
9 | export function jobIsAssigned(job, assignments) {
10 | return (
11 | assignments.find(assignment => {
12 | return assignment.job === job;
13 | }) !== undefined
14 | );
15 | }
16 |
17 | export function assign(touch, job, assignments) {
18 | if (touchIsAssigned(touch, assignments) || jobIsAssigned(job, assignments)) {
19 | console.error("cannot reassign touches or jobs. unassign first");
20 | return undefined;
21 | }
22 | const assignment = { job, touch };
23 | assignments.push(assignment);
24 | return assignment;
25 | }
26 |
27 | export function unassign(touch, job, assignments) {
28 | function match(assignment) {
29 | return assignment.touch.identifier === touch.identifier && assignment.job === job;
30 | }
31 | assignments.splice(assignments.findIndex(match), 1);
32 | }
33 |
34 | export function findByJob(job, assignments) {
35 | return assignments.find(assignment => {
36 | return assignment.job === job;
37 | });
38 | }
39 |
40 | export function findByTouch(touch, assignments) {
41 | return assignments.find(assignment => {
42 | return assignment.touch.identifier === touch.identifier;
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/src/ui/chat-input-popup.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import React, { forwardRef } from "react";
3 | import ReactDOM from "react-dom";
4 | import ChatInputPanel from "./chat-input-panel";
5 | import { waitForShadowDOMContentLoaded } from "../utils/async-utils";
6 |
7 | let popupRoot = null;
8 | waitForShadowDOMContentLoaded().then(() => (popupRoot = DOM_ROOT.getElementById("popup-root")));
9 |
10 | const ChatInputPopup = forwardRef(
11 | ({ styles, attributes, setPopperElement, onMessageEntered, onEntryComplete }, ref) => {
12 | const popupInput = (
13 |
20 |
26 |
27 | );
28 |
29 | return ReactDOM.createPortal(popupInput, popupRoot);
30 | }
31 | );
32 |
33 | ChatInputPopup.displayName = "ChatInputPopup";
34 | ChatInputPopup.propTypes = {
35 | styles: PropTypes.object,
36 | attributes: PropTypes.object,
37 | setPopperElement: PropTypes.func,
38 | onMessageEntered: PropTypes.func,
39 | onEntryComplete: PropTypes.func
40 | };
41 |
42 | export default ChatInputPopup;
43 |
--------------------------------------------------------------------------------
/src/assets/images/icons/mic-unmuted.svgi:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/set-unowned-body-kinematic.js:
--------------------------------------------------------------------------------
1 | const COLLISION_LAYERS = require("../constants").COLLISION_LAYERS;
2 | import { isSynchronized, isMine } from "../utils/ownership-utils";
3 |
4 | AFRAME.registerComponent("set-unowned-body-kinematic", {
5 | init() {
6 | this.setBodyKinematic = this.setBodyKinematic.bind(this);
7 | },
8 | play() {
9 | this.el.addEventListener("ownership-lost", this.setBodyKinematic);
10 |
11 | if (!this.didThisOnce) {
12 | // Do this in play instead of init so that the ammo-body and networked components are done
13 | this.didThisOnce = true;
14 |
15 | if (!isSynchronized(this.el) || !isMine(this.el)) {
16 | this.setBodyKinematic();
17 | }
18 | }
19 | },
20 | pause() {
21 | this.el.removeEventListener("ownership-lost", this.setBodyKinematic);
22 | },
23 | setBodyKinematic() {
24 | const collisionFilterGroup = this.el.components["body-helper"].data.collisionFilterGroup;
25 |
26 | if (collisionFilterGroup === COLLISION_LAYERS.INTERACTABLES) {
27 | this.el.setAttribute("body-helper", {
28 | type: "kinematic",
29 | collisionFilterMask: COLLISION_LAYERS.UNOWNED_INTERACTABLE
30 | });
31 | } else {
32 | this.el.setAttribute("body-helper", {
33 | type: "kinematic"
34 | });
35 | }
36 |
37 | if (this.el.components["floaty-object"]) {
38 | this.el.components["floaty-object"].locked = true;
39 | }
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/src/ui/create-select-popup.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import React, { forwardRef } from "react";
3 | import ReactDOM from "react-dom";
4 | import CreateSelect from "./create-select";
5 | import { waitForShadowDOMContentLoaded } from "../utils/async-utils";
6 |
7 | let popupRoot = null;
8 | waitForShadowDOMContentLoaded().then(() => (popupRoot = DOM_ROOT.getElementById("popup-root")));
9 |
10 | // Note that we can't use slide-down-when-popped transition
11 | // since it causes positioning artifacts in rc-select
12 | const CreateSelectPopup = forwardRef(
13 | ({ styles, attributes, popperElement, setPopperElement, onActionSelected }, ref) => {
14 | const popup = (
15 |
22 | popperElement} onActionSelected={onActionSelected} />
23 |
24 | );
25 |
26 | return ReactDOM.createPortal(popup, popupRoot);
27 | }
28 | );
29 |
30 | CreateSelectPopup.displayName = "CreateSelectPopup";
31 | CreateSelectPopup.propTypes = {
32 | styles: PropTypes.object,
33 | attributes: PropTypes.object,
34 | setPopperElement: PropTypes.func,
35 | popperElement: PropTypes.object,
36 | onActionSelected: PropTypes.func
37 | };
38 |
39 | export default CreateSelectPopup;
40 |
--------------------------------------------------------------------------------
/src/components/periodic-full-syncs.js:
--------------------------------------------------------------------------------
1 | const SYNC_DURATION_MS = 3000;
2 | const NUM_EXTRA_SYNCS = Infinity;
3 |
4 | // HACK this is a hacky component that is used to mitigate the situation where a first sync is missed on critical
5 | // networked elements. (At the time of this writing, specifically just the user's avatar.) The motivation
6 | // is that there have been a variety of issues resulting in missed avatar instantiation messages, and
7 | // this is meant to ensure we recover from those in the cases where they occur.
8 | //
9 | // This component, when added, will re-send a isFirstSync message for the networked object is it attached to
10 | // every SYNC_DURATION_MS milliseconds.
11 | AFRAME.registerComponent("periodic-full-syncs", {
12 | init() {
13 | this.reset();
14 | },
15 |
16 | tick() {
17 | if (this.syncCount > NUM_EXTRA_SYNCS) return;
18 |
19 | const now = performance.now();
20 |
21 | if (now - this.lastSync >= SYNC_DURATION_MS && this.el.components && this.el.components.networked) {
22 | this.lastSync = now;
23 | this.syncCount++;
24 |
25 | // Sends an undirected first sync message.
26 | this.el.components.networked.syncAll(null, true);
27 | }
28 | },
29 |
30 | reset() {
31 | this.lastSync = 0;
32 | this.syncCount = 0;
33 | }
34 | });
35 |
36 | export function restartPeriodicSyncs() {
37 | [...DOM_ROOT.querySelectorAll("[periodic-full-syncs]")].forEach(el => el.components["periodic-full-syncs"].reset());
38 | }
39 |
--------------------------------------------------------------------------------
/src/systems/sprites/sprite.frag:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | uniform sampler2D u_spritesheet;
4 |
5 | varying vec2 v_uvs;
6 |
7 | varying float v_hubs_EnableSweepingEffect;
8 | varying vec2 v_hubs_SweepParams;
9 | varying vec3 hubs_WorldPosition;
10 | uniform bool hubs_HighlightInteractorOne;
11 | uniform vec3 hubs_InteractorOnePos;
12 | uniform bool hubs_HighlightInteractorTwo;
13 | uniform vec3 hubs_InteractorTwoPos;
14 | uniform float hubs_Time;
15 |
16 | #define GAMMA_FACTOR 2.2
17 |
18 | vec4 LinearToGamma( in vec4 value, in float gammaFactor ) {
19 | return vec4( pow( value.rgb, vec3( 1.0 / gammaFactor ) ), value.a );
20 | }
21 |
22 | vec4 linearToOutputTexel( vec4 value ) {
23 | return LinearToGamma( value, float( GAMMA_FACTOR ) );
24 | }
25 |
26 | void main() {
27 | vec4 texColor = texture2D(u_spritesheet, v_uvs);
28 |
29 | float ratio = 0.0;
30 | if (v_hubs_EnableSweepingEffect > 0.9 ) {
31 | float size = v_hubs_SweepParams.t - v_hubs_SweepParams.s * 1.0;
32 | float line = mod(hubs_Time / 500.0 * size, size * 3.0) + v_hubs_SweepParams.s - size / 3.0;
33 | if (hubs_WorldPosition.y < line) {
34 | ratio = max(0.0, 1.0 - (line - hubs_WorldPosition.y) / (size * 1.5));
35 | }
36 |
37 | }
38 | ratio = min(1.0, ratio);
39 |
40 | vec4 highlightColor = linearToOutputTexel(vec4(0.184, 0.499, 0.933, 1.0));
41 | gl_FragColor = vec4((texColor.rgb * (1.0 - ratio)) + (highlightColor.rgb * ratio), texColor.a);
42 | }
43 |
--------------------------------------------------------------------------------
/src/ui/folder-access-request-panel.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PanelWrap, Info, Tip } from "./form-components";
3 | import { FormattedMessage } from "react-intl";
4 | import ActionButton from "./action-button";
5 | import PropTypes from "prop-types";
6 |
7 | const FolderAccessRequestPanel = ({ failedOriginState, onAccessClicked }) => {
8 | const supported = !!window.showDirectoryPicker;
9 |
10 | return (
11 |
12 | {!failedOriginState && (
13 |
14 |
17 |
18 | )}
19 |
20 |
29 |
30 | {supported && (
31 | onAccessClicked()}>
32 |
33 |
34 | )}
35 |
36 | );
37 | };
38 |
39 | FolderAccessRequestPanel.propTypes = {
40 | onAccessClicked: PropTypes.func,
41 | failedOriginState: PropTypes.number
42 | };
43 |
44 | export { FolderAccessRequestPanel as default };
45 |
--------------------------------------------------------------------------------
/src/utils/membership-utils.js:
--------------------------------------------------------------------------------
1 | export function membershipForSpaceId(spaceId, memberships) {
2 | if (!memberships) return null;
3 |
4 | for (let i = 0; i < memberships.length; i++) {
5 | const membership = memberships[i];
6 |
7 | if (membership.space.space_id === spaceId) {
8 | return membership;
9 | }
10 | }
11 |
12 | return null;
13 | }
14 |
15 | export function membershipSettingsForSpaceId(spaceId, memberships) {
16 | const membership = membershipForSpaceId(spaceId, memberships);
17 | if (!membership) return null;
18 |
19 | return {
20 | notifySpaceCopresence: membership.notify_space_copresence,
21 | notifyHubCopresence: membership.notify_hub_copresence,
22 | notifyCurrentWorldChatMode: membership.notify_current_world_chat_mode
23 | };
24 | }
25 |
26 | export function hubSettingsForHubId(hubId, hubSettings) {
27 | for (let i = 0; i < hubSettings.length; i++) {
28 | const s = hubSettings[i];
29 |
30 | if (s.hub.hub_id === hubId) {
31 | return {
32 | notifyJoins: s.notify_joins
33 | };
34 | }
35 | }
36 |
37 | return null;
38 | }
39 |
40 | export function homeHubForSpaceId(spaceId, memberships) {
41 | const m = membershipForSpaceId(spaceId, memberships);
42 | return m ? m.home_hub : null;
43 | }
44 |
45 | export function spaceForSpaceId(spaceId, memberships) {
46 | const m = membershipForSpaceId(spaceId, memberships);
47 | return m ? m.space : null;
48 | }
49 |
50 | export async function getInitialHubForSpaceId(/*spaceId*/) {}
51 |
--------------------------------------------------------------------------------
/src/components/environment-map.js:
--------------------------------------------------------------------------------
1 | import { forEachMaterial } from "../utils/material-utils";
2 | import cubeMapPosX from "../assets/images/cubemap/posx.jpg";
3 | import cubeMapNegX from "../assets/images/cubemap/negx.jpg";
4 | import cubeMapPosY from "../assets/images/cubemap/posy.jpg";
5 | import cubeMapNegY from "../assets/images/cubemap/negy.jpg";
6 | import cubeMapPosZ from "../assets/images/cubemap/posz.jpg";
7 | import cubeMapNegZ from "../assets/images/cubemap/negz.jpg";
8 |
9 | export async function createDefaultEnvironmentMap() {
10 | const urls = [cubeMapPosX, cubeMapNegX, cubeMapPosY, cubeMapNegY, cubeMapPosZ, cubeMapNegZ];
11 | const texture = await new Promise((resolve, reject) =>
12 | new THREE.CubeTextureLoader().load(urls, resolve, undefined, reject)
13 | );
14 | texture.format = THREE.RGBFormat;
15 | return texture;
16 | }
17 |
18 | AFRAME.registerComponent("environment-map", {
19 | init() {
20 | this.environmentMap = null;
21 |
22 | this.updateEnvironmentMap = this.updateEnvironmentMap.bind(this);
23 | },
24 |
25 | updateEnvironmentMap(environmentMap) {
26 | this.environmentMap = environmentMap;
27 | this.applyEnvironmentMap(this.el.object3D);
28 | },
29 |
30 | applyEnvironmentMap(object3D) {
31 | object3D.traverse(object => {
32 | forEachMaterial(object, material => {
33 | if (material.isMeshStandardMaterial) {
34 | material.envMap = this.environmentMap;
35 | material.needsUpdate = true;
36 | }
37 | });
38 | });
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/src/ui/segment-control.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import addIcon from "../assets/images/icons/add.svgi";
3 | import SegmentControl from "./segment-control";
4 |
5 | export const WithIcons = () => {
6 | const [selected, setSelected] = useState(0);
7 |
8 | return (
9 | setSelected(idx)}
14 | items={[
15 | { id: "addItem", iconSrc: addIcon, title: "Add Item" },
16 | { id: "removeItem", iconSrc: addIcon, title: "Remove Item" },
17 | { id: "lastItem", iconSrc: addIcon, title: "Remove Item" },
18 | { id: "addItem2", iconSrc: addIcon, title: "Add Item" },
19 | { id: "removeItem2", iconSrc: addIcon, title: "Remove Item" },
20 | { id: "lastItem2", iconSrc: addIcon, title: "Remove Item" }
21 | ]}
22 | />
23 | );
24 | };
25 |
26 | export const WithIconsSingle = () => {
27 | const [selected, setSelected] = useState(0);
28 |
29 | return (
30 | setSelected(idx)}
35 | items={[
36 | { id: "addItem", iconSrc: addIcon, title: "Add Item" },
37 | { id: "removeItem", iconSrc: addIcon, title: "Remove Item" },
38 | { id: "lastItem", iconSrc: addIcon, title: "Remove Item" }
39 | ]}
40 | />
41 | );
42 | };
43 |
44 | export default {
45 | title: "Segment Control"
46 | };
47 |
--------------------------------------------------------------------------------
/src/ui/emoji-popup.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import React, { useState, forwardRef, useCallback } from "react";
3 | import ReactDOM from "react-dom";
4 | import EmojiPicker from "./emoji-picker";
5 | import { waitForShadowDOMContentLoaded } from "../utils/async-utils";
6 |
7 | let popupRoot = null;
8 | waitForShadowDOMContentLoaded().then(() => (popupRoot = DOM_ROOT.getElementById("popup-root")));
9 |
10 | const EmojiPopup = forwardRef(({ styles, attributes, setPopperElement, onEmojiSelected }, ref) => {
11 | const [hasBeenFocused, setHasBeenFocused] = useState(false);
12 |
13 | const popupInput = (
14 | {
20 | if (!hasBeenFocused) setHasBeenFocused(true);
21 | },
22 | [hasBeenFocused]
23 | )}
24 | style={styles.popper}
25 | {...attributes.popper}
26 | >
27 |
33 |
34 | );
35 |
36 | return ReactDOM.createPortal(popupInput, popupRoot);
37 | });
38 |
39 | EmojiPopup.displayName = "EmojiPopup";
40 | EmojiPopup.propTypes = {
41 | styles: PropTypes.object,
42 | attributes: PropTypes.object,
43 | setPopperElement: PropTypes.func,
44 | onEmojiSelected: PropTypes.func
45 | };
46 |
47 | export default EmojiPopup;
48 |
--------------------------------------------------------------------------------
/src/assets/images/avatar/viseme-7.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/images/icons/thumb-label.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/systems/interaction-sfx-system.js:
--------------------------------------------------------------------------------
1 | import { paths } from "./userinput/paths";
2 | import { SOUND_HOVER_OR_GRAB, SOUND_RELEASE } from "./sound-effects-system";
3 |
4 | export class InteractionSfxSystem {
5 | constructor() {}
6 |
7 | tick(interaction, userinput, sfx) {
8 | const state = interaction.state;
9 | const previousState = interaction.previousState;
10 |
11 | if (state.leftHand.held !== previousState.leftHand.held) {
12 | sfx.playSoundOneShot(state.leftHand.held ? SOUND_HOVER_OR_GRAB : SOUND_RELEASE);
13 | }
14 |
15 | if (state.rightHand.held !== previousState.rightHand.held) {
16 | sfx.playSoundOneShot(state.rightHand.held ? SOUND_HOVER_OR_GRAB : SOUND_RELEASE);
17 | }
18 |
19 | if (state.rightRemote.held !== previousState.rightRemote.held) {
20 | sfx.playSoundOneShot(state.rightRemote.held ? SOUND_HOVER_OR_GRAB : SOUND_RELEASE);
21 | }
22 |
23 | if (userinput.get(paths.actions.logInteractionState)) {
24 | console.log(
25 | "Interaction System State\nleftHand held",
26 | state.leftHand.held,
27 | "\nleftHand hovered",
28 | state.leftHand.hovered,
29 | "\nrightHand held",
30 | state.rightHand.held,
31 | "\nrightHand hovered",
32 | state.rightHand.hovered,
33 | "\nrightRemote held",
34 | state.rightRemote.held,
35 | "\nrightRemote hovered",
36 | state.rightRemote.hovered,
37 | "\nleftRemote held",
38 | state.leftRemote.held,
39 | "\nleftRemote hovered",
40 | state.leftRemote.hovered
41 | );
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/ui/popup-menu.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PopupMenu, { PopupMenuItem } from "./popup-menu";
3 | import PopupPanelMenu, { PopupPanelMenuItem, PopupPanelMenuSectionHeader } from "./popup-panel-menu";
4 | import sharedStyles from "../../assets/stylesheets/shared.scss";
5 | import classNames from "classnames";
6 | import addIcon from "../assets/images/icons/add.svgi";
7 | import checkIcon from "../assets/images/icons/check.svgi";
8 |
9 | export const Normal = () => (
10 |
11 |
12 |
13 | Add Duplicate
14 | Export...
15 |
16 |
17 |
18 | );
19 |
20 | export const Panel = () => (
21 |
22 |
23 |
24 | Input Device
25 | AT202USB+ Analog Stereo
26 | ThinkPad Thunderbold 3 Dock USB Audio Multichannel
27 | Default Microphone
28 |
29 |
30 |
31 | );
32 |
33 | export default {
34 | title: "Popup Menu"
35 | };
36 |
--------------------------------------------------------------------------------
/src/assets/images/icons/call-end.svgi:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/utils/crypto.js:
--------------------------------------------------------------------------------
1 | export async function keyToJwk(key) {
2 | return await crypto.subtle.exportKey("jwk", key);
3 | }
4 |
5 | export async function keyToString(key) {
6 | return JSON.stringify(keyToJwk(key));
7 | }
8 |
9 | export async function jwkToKey(jwk, usages) {
10 | return await crypto.subtle.importKey("jwk", jwk, { name: "ECDSA", namedCurve: "P-256" }, true, usages);
11 | }
12 |
13 | export async function stringToKey(s, usages) {
14 | return await jwkToKey(JSON.parse(s), usages);
15 | }
16 |
17 | export async function signString(s, jwk) {
18 | const cryptoKey = await jwkToKey(jwk, ["sign"]);
19 | const data = new TextEncoder().encode(s);
20 | return await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-512" }, cryptoKey, data);
21 | }
22 |
23 | export async function verifyString(s, jwk, signature) {
24 | const cryptoKey = await jwkToKey(jwk, ["verify"]);
25 | return await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-512" }, cryptoKey, signature, s);
26 | }
27 |
28 | // This allows a single object to be passed encrypted from a receiver in a req -> response flow
29 |
30 | // Requestor generates a public key and private key, and should send the public key to receiver.
31 | export async function generateKeys() {
32 | const keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["verify", "sign"]);
33 | return { publicKeyJwk: await keyToJwk(keyPair.publicKey), privateKeyJwk: await keyToJwk(keyPair.privateKey) };
34 | }
35 |
36 | export async function hashString(s) {
37 | return new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)));
38 | }
39 |
--------------------------------------------------------------------------------
/src/systems/userinput/arm-model.js:
--------------------------------------------------------------------------------
1 | // Vector from eyes to elbow (divided by user height).
2 | const EYES_TO_ELBOW = { x: 0.175, y: -0.3, z: -0.03 };
3 | // Vector from eyes to elbow (divided by user height).
4 | const FOREARM = { x: 0, y: 0, z: -0.175 };
5 |
6 | export const applyArmModel = (function() {
7 | const controllerEuler = new THREE.Euler();
8 | const deltaControllerPosition = new THREE.Vector3();
9 | const controllerQuaternion = new THREE.Quaternion();
10 | const controllerPosition = new THREE.Vector3();
11 | return function applyArmModel(gamepadPose, hand, headObject3D, userHeight) {
12 | headObject3D.updateMatrices();
13 | controllerPosition.copy(headObject3D.position);
14 | controllerPosition.y = controllerPosition.y - userHeight;
15 | deltaControllerPosition.set(
16 | EYES_TO_ELBOW.x * (hand === "left" ? -1 : hand === "right" ? 1 : 0),
17 | EYES_TO_ELBOW.y,
18 | EYES_TO_ELBOW.z
19 | );
20 | deltaControllerPosition.multiplyScalar(userHeight);
21 | deltaControllerPosition.applyAxisAngle(headObject3D.up, headObject3D.rotation.y);
22 | controllerPosition.add(deltaControllerPosition);
23 | deltaControllerPosition.set(FOREARM.x, FOREARM.y, FOREARM.z);
24 | deltaControllerPosition.multiplyScalar(userHeight);
25 | controllerQuaternion.fromArray(gamepadPose.orientation);
26 | controllerEuler.setFromQuaternion(controllerQuaternion);
27 | controllerEuler.set(controllerEuler.x, controllerEuler.y, 0);
28 | deltaControllerPosition.applyEuler(controllerEuler);
29 | controllerPosition.add(deltaControllerPosition);
30 | return controllerPosition;
31 | };
32 | })();
33 |
--------------------------------------------------------------------------------
/src/assets/images/icons/pick.svgi:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/src/components/bone-visibility.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Scales an object to near-zero if the object is invisible. Useful for bones representing avatar body parts.
3 | * @namespace avatar
4 | * @component bone-visibility
5 | */
6 |
7 | const HIDDEN_SCALE = 0.00000001;
8 |
9 | const components = [];
10 | export class BoneVisibilitySystem {
11 | tick() {
12 | for (let i = 0; i < components.length; i++) {
13 | const cmp = components[i];
14 | const obj = cmp.el.object3D;
15 | const { visible, scale } = obj;
16 | const { updateWhileInvisible } = cmp.data;
17 | if (visible !== cmp.lastVisible || updateWhileInvisible) {
18 | if (visible && (scale.x !== 1 || scale.y !== 1 || scale.z !== 1)) {
19 | scale.setScalar(1);
20 | obj.matrixNeedsUpdate = true;
21 | } else if (!visible && (scale.x !== HIDDEN_SCALE || scale.y !== HIDDEN_SCALE || scale.z !== HIDDEN_SCALE)) {
22 | scale.setScalar(HIDDEN_SCALE);
23 | obj.matrixNeedsUpdate = true;
24 | }
25 |
26 | // Normally this object being invisible would cause it not to get updated even though the matrixNeedsUpdate flag is set, force it
27 | if (updateWhileInvisible && obj.matrixNeedsUpdate) {
28 | obj.updateMatrixWorld(true, true);
29 | }
30 | }
31 | cmp.lastVisible = visible;
32 | }
33 | }
34 | }
35 |
36 | AFRAME.registerComponent("bone-visibility", {
37 | schema: {
38 | updateWhileInvisible: { type: "boolean", default: false }
39 | },
40 | play() {
41 | components.push(this);
42 | },
43 | pause() {
44 | components.splice(components.indexOf(this), 1);
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/.defaults.env:
--------------------------------------------------------------------------------
1 | # To override these variables, create a .env file containing the overrides.
2 |
3 | # Domain for short links
4 | SHORTLINK_DOMAIN="hub.link"
5 |
6 | # The Reticulum backend to connect to. Used for storing information about active hubs.
7 | # See here for the server code: https://github.com/mozilla/reticulum
8 | RETICULUM_SERVER="hubs.local:4000"
9 |
10 | # CORS proxy.
11 | CORS_PROXY_SERVER="cors-proxy.jel.app,cors-proxy-1.jel.app,cors-proxy-2.jel.app,cors-proxy-3.jel.app,cors-proxy-4.jel.app,cors-proxy-5.jel.app,cors-proxy-6.jel.app,cors-proxy-7.jel.app,cors-proxy-8.jel.app,cors-proxy-9.jel.app"
12 |
13 | # The thumbnailing backend to connect to.
14 | # See here for the server code: https://github.com/MozillaReality/farspark or https://github.com/MozillaReality/nearspark
15 | THUMBNAIL_SERVER="nearspark-dev.reticulum.io"
16 |
17 | # The root URL under which Hubs expects environment GLTF bundles to be served.
18 | ASSET_BUNDLE_SERVER="https://asset-bundles-prod.reticulum.io"
19 |
20 | # Terra server serving terrains.
21 | TERRA_SERVER="arweave.net/BK4k7-aBjxWkQLANR7AmwHte2bROgUulsQuq5D6Zqd0"
22 |
23 | # Comma-separated list of domains which are known to not need CORS proxying
24 | NON_CORS_PROXY_DOMAINS="hubs.local,dev.reticulum.io"
25 |
26 | # The root URL under which Hubs expects static assets to be served.
27 | BASE_ASSETS_PATH=/
28 |
29 | # The default scene to use. Note the example scene id is only availible on dev.reticulum.io
30 | DEFAULT_SCENE_SID="JGLt8DP"
31 |
32 | MIXPANEL_TOKEN="9bc3e675ed764e6ecf48a440d932ebd8"
33 |
34 | # Uncomment to load the app config from the reticulum server in development.
35 | # Useful when testing the admin panel.
36 | # LOAD_APP_CONFIG=true
37 |
--------------------------------------------------------------------------------
/src/assets/images/icons/github-on.svgi:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/utils/permissions-utils.js:
--------------------------------------------------------------------------------
1 | export const ROLES = {
2 | NONE: 0,
3 | OWNER: 0x80,
4 | MEMBER: 0x01
5 | };
6 |
7 | // Brief overview of client authorization can be found in the wiki:
8 | // https://github.com/mozilla/hubs/wiki/Hubs-authorization
9 | export function canMove(el) {
10 | const isHoldableButton = el.components.tags && el.components.tags.data.holdableButton;
11 | const mediaLoader = el.components["media-loader"];
12 | const isMedia = !!mediaLoader;
13 | const canMove = window.APP.atomAccessManager.hubCan("spawn_and_move_media");
14 | const isLocked = mediaLoader && mediaLoader.data.locked;
15 | return isHoldableButton || (isMedia && canMove && !isLocked);
16 | }
17 |
18 | export function canCloneOrSnapshot(el) {
19 | const isHoldableButton = el.components.tags && el.components.tags.data.holdableButton;
20 | const mediaLoader = el.components["media-loader"];
21 | const isMedia = !!mediaLoader;
22 | const canSpawn = window.APP.atomAccessManager.hubCan("spawn_and_move_media");
23 | return isHoldableButton || (isMedia && canSpawn);
24 | }
25 |
26 | export function showHoverEffect(el) {
27 | const isMedia = !!el.components["media-loader"];
28 | return isMedia && canMove(el);
29 | }
30 |
31 | export function gatePermissionPredicate(predicate) {
32 | if (predicate) return true;
33 | const { atomAccessManager } = window.APP;
34 |
35 | if (atomAccessManager.saveChangesToOrigin && !atomAccessManager.isWritebackOpen) {
36 | AFRAME.scenes[0].emit("action_open_writeback");
37 | }
38 |
39 | return false;
40 | }
41 |
42 | export function gatePermission(permission) {
43 | const hasPerm = window.APP.atomAccessManager.hubCan(permission);
44 | return gatePermissionPredicate(hasPerm);
45 | }
46 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const COLLISION_LAYERS = {
2 | ALL: -1,
3 | NONE: 0,
4 | INTERACTABLES: 1,
5 | ENVIRONMENT: 2,
6 | AVATAR: 4,
7 | HANDS: 8,
8 | PROJECTILES: 16,
9 | BURSTS: 32,
10 | DEFAULT_INTERACTABLE: 1 | 2 | 4 | 8 | 16,
11 | UNOWNED_INTERACTABLE: 1 | 8 | 16,
12 | ENVIRONMENTAL_VOX: 1 | 16,
13 | DEFAULT_SPAWNER: 1 | 8
14 | };
15 |
16 | export const RENDER_ORDER = {
17 | LIGHTS: 0, // Render lights first, otherwise compiled programs may not define USE_SHADOWMAP
18 | HUD_BACKGROUND: 1,
19 | HUD_ICONS: 2,
20 | TERRAIN: 10,
21 | FIELD: 100,
22 | PHYSICS_DEBUG: 1000,
23 | VOX: 5000,
24 | MEDIA: 10000,
25 | MEDIA_NO_FXAA: 10010, // Render last because of stencil ops
26 | TOON: 20000, // Render last because of stencil ops
27 | INSTANCED_AVATAR: 21000, // Render last because of stencil ops
28 | INSTANCED_BEAM: 22000, // Render last because of stencil ops
29 | SKY: 100000,
30 | HELPERS: 200000,
31 | CURSOR: 300000,
32 | PICTURE_IN_PICTURE: 350000,
33 |
34 | // Transparent objects:
35 | WATER: 1
36 | };
37 |
38 | export const WORLD_COLOR_TYPES = ["ground", "edge", "leaves", "bark", "rock", "grass", "sky", "water"];
39 |
40 | export const BRUSH_TYPES = {
41 | VOXEL: 0,
42 | FACE: 1,
43 | BOX: 2,
44 | CENTER: 3,
45 | FILL: 4,
46 | PICK: 5
47 | };
48 |
49 | export const BRUSH_MODES = {
50 | ADD: 0,
51 | REMOVE: 1,
52 | PAINT: 2
53 | };
54 |
55 | export const BRUSH_SHAPES = {
56 | BOX: 0,
57 | SPHERE: 1
58 | };
59 |
60 | export const BRUSH_CRAWL_TYPES = {
61 | GEO: 0,
62 | COLOR: 1
63 | };
64 |
65 | export const BRUSH_CRAWL_EXTENTS = {
66 | NSEW: 0,
67 | ALL: 1
68 | };
69 |
70 | export const BRUSH_COLOR_FILL_MODE = {
71 | SELECTED: 0,
72 | EXISTING: 1
73 | };
74 |
--------------------------------------------------------------------------------
/doc/spritesheet-generation.md:
--------------------------------------------------------------------------------
1 | ## intro
2 | We use texture atlassing to improve rendering and loading efficiency. This document explains how to generate the spritesheets used by the SpriteSystem and the page's css.
3 |
4 | ## generating spritesheets
5 |
6 | After installing the project dependencies, spritesheets can be generated with the command `npm run spritesheet`. The exact parameters used by this script can be inspected in the `package.json` where it is defined. More information about the tool we use, `spritesheet-js`, can be found on the [github page](https://github.com/mozillareality/spritesheet.js/).
7 |
8 | The steps to generate a spritesheet are :
9 |
10 | 1. Move sprites you want in the sprite-system-spritesheet to `src/assets/images/sprites/`.
11 | 1. Move sprites you want in the css-spritesheet to `src/assets/images/css-sprites/`.
12 | 1. Type `npm run spritesheet`. This will generate
13 | `sprite-system-spritesheet.json` with `sprite-system-spritesheet.png` for the
14 | sprite system and
15 | `css-spritesheet.css` with `css-spritesheet.png` in the directory `src/assets/images/spritesheets/`.
16 |
17 | ## Notes
18 |
19 | The name of the sprite that is used in the generated `json` file is the same as its source filename; When a sprite component has the name `foo.png`, the sprite system looks for that name in the `json` file. It does not use the `foo.png` source file.
20 |
21 | The source images are exported from Figma. If you want to alter these images, it is probably best to do so in Figma, then re-export at the desired size. It is a good idea to stack all of the icons you want to export on top of one another in figma, because otherwise Figma produces surprisingly different results for similar icons when exporting at low resolution.
22 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | stories: ["../src/jel/react-components/**/*.stories.mdx", "../src/jel/react-components/*.stories.js", "../src/jel/utils/*.stories.js"],
5 | addons: ["@storybook/addon-links", "@storybook/addon-essentials", "storybook-addon-designs"],
6 | webpackFinal: async config => {
7 | config.module.rules.push({
8 | test: /\.scss$/,
9 | use: [
10 | "style-loader",
11 | {
12 | loader: "css-loader",
13 | options: {
14 | importLoaders: "1",
15 | localIdentName: "[name]__[local]___[hash:base64:5]",
16 | modules: false,
17 | camelCase: true
18 | }
19 | },
20 | "sass-loader"
21 | ],
22 | include: path.resolve(__dirname, "..", "src")
23 | });
24 |
25 | config.module.rules.push({
26 | test: /\.(png|jpg|gif|glb|ogg|mp3|mp4|wav|woff2|svg|webm)$/,
27 | use: {
28 | loader: "file-loader",
29 | options: {
30 | // move required assets to output dir and add a hash for cache busting
31 | name: "[path][name]-[hash].[ext]",
32 | // Make asset paths relative to /src
33 | context: path.join(__dirname, "src")
34 | }
35 | }
36 | });
37 |
38 | const svgLoaderRule = config.module.rules.find(rule => rule.test.test(".svg"));
39 | svgLoaderRule.exclude = /\.svg$/;
40 | config.module.rules.push({
41 | test: /\.svg$/,
42 | use: [
43 | "url-loader"
44 | ]
45 | });
46 |
47 | config.module.rules.push({
48 | test: /\.svgi$/,
49 | use: [
50 | "svg-inline-loader"
51 | ]
52 | });
53 |
54 | return config;
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/src/assets/images/icons/mic-muted.svgi:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/components/networked-counter.js:
--------------------------------------------------------------------------------
1 | import { isMine } from "../utils/ownership-utils";
2 | /* global AFRAME performance */
3 | /**
4 | * Limits networked interactables to a maximum number at any given time
5 | * @namespace network
6 | * @component networked-counter
7 | */
8 | AFRAME.registerComponent("networked-counter", {
9 | schema: {
10 | max: { default: 3 }
11 | },
12 |
13 | init() {
14 | this.timestamps = new Map();
15 | this.el.object3D.visible = false;
16 | },
17 |
18 | remove() {
19 | this.timestamps.clear();
20 | },
21 |
22 | count() {
23 | return this.timestamps.size;
24 | },
25 |
26 | register(el) {
27 | if (this.data.max <= 0 || this.timestamps.has(el)) return;
28 |
29 | this.timestamps.set(el, performance.now());
30 | if (this.timestamps.size > this.data.max) {
31 | this._destroyOldest();
32 | }
33 | },
34 |
35 | deregister(el) {
36 | if (!this.timestamps.has(el)) return;
37 | this.timestamps.delete(el);
38 | },
39 |
40 | _destroyOldest() {
41 | const interaction = this.el.sceneEl.systems.interaction;
42 | let oldestEl = null,
43 | minTs = Number.MAX_VALUE;
44 | this.timestamps.forEach((ts, el) => {
45 | if (ts < minTs && !interaction.isHeld(el) && isMine(el)) {
46 | oldestEl = el;
47 | minTs = ts;
48 | }
49 | });
50 | this._destroy(oldestEl);
51 | },
52 |
53 | _destroy(el) {
54 | // Pause the entity so that it won't just re-register itself immediately in owned-object-limiter.
55 | el.pause();
56 | // networked-interactable's remove will also call deregister, but it will happen async so we do it here as well.
57 | this.deregister(el);
58 | if (el.parentNode) {
59 | el.parentNode.removeChild(el);
60 | }
61 | }
62 | });
63 |
--------------------------------------------------------------------------------
/src/ui/loading-spinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const LoadingSpinner = styled.div`
5 | width: 24px;
6 | height: 24px;
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | `;
11 |
12 | const LoadingSpinnerElement = styled.div`
13 | &,
14 | &:after {
15 | border-radius: 50%;
16 | width: 1.5em;
17 | height: 1.5em;
18 | }
19 | & {
20 | margin: 9px auto;
21 | font-size: 10px;
22 | position: relative;
23 | text-indent: -9999em;
24 | border-top: 0.13em solid transparent;
25 | border-right: 0.13em solid transparent;
26 | border-bottom: 0.13em solid transparent;
27 | border-left: 0.13em solid var(--dialog-tip-text-color);
28 | -webkit-transform: translateZ(0);
29 | -ms-transform: translateZ(0);
30 | transform: translateZ(0);
31 | -webkit-animation: load8 1.1s infinite linear;
32 | animation: load8 1.1s infinite linear;
33 | }
34 | @-webkit-keyframes load8 {
35 | 0% {
36 | -webkit-transform: rotate(0deg);
37 | transform: rotate(0deg);
38 | }
39 | 100% {
40 | -webkit-transform: rotate(360deg);
41 | transform: rotate(360deg);
42 | }
43 | }
44 | @keyframes load8 {
45 | 0% {
46 | -webkit-transform: rotate(0deg);
47 | transform: rotate(0deg);
48 | }
49 | 100% {
50 | -webkit-transform: rotate(360deg);
51 | transform: rotate(360deg);
52 | }
53 | }
54 | `;
55 |
56 | const LoadingSpinnerExport = function(props) {
57 | return (
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | LoadingSpinnerExport.displayName = "LoadingSpinner";
65 |
66 | export default LoadingSpinnerExport;
67 |
--------------------------------------------------------------------------------
/src/utils/focus-utils.js:
--------------------------------------------------------------------------------
1 | import screenfull from "screenfull";
2 | import { showFullScreenIfWasFullScreen } from "./fullscreen";
3 | import { temporarilyReleaseCanvasCursorLock } from "./dom-utils";
4 | import { detect } from "detect-browser";
5 |
6 | const browser = detect();
7 | let isExitingFullscreenDueToFocus = false;
8 |
9 | // Utility function that handles a bunch of incidental stuff related to text fields:
10 | //
11 | // - On non-mobile platforms, selects the value on focus
12 | // - If full screen, exits/enters full screen because of firefox full screen issues
13 | export function handleTextFieldFocus(target, doNotSelect) {
14 | if (!window.AFRAME) return;
15 | const isMobile = AFRAME.utils.device.isMobile();
16 |
17 | if (screenfull.isFullscreen && !AFRAME.utils.device.isMobileVR() && browser.name === "firefox") {
18 | // This will prevent focus, but its the only way to avoid getting into a
19 | // weird "firefox reports full screen but actually not". You end up having to tap
20 | // twice to ultimately get the focus.
21 | //
22 | // We need to keep track of a bit here so that we don't re-full screen when
23 | // the text box is blurred by the browser.
24 |
25 | isExitingFullscreenDueToFocus = true;
26 | screenfull.exit().then(() => {
27 | target.focus();
28 | });
29 | }
30 |
31 | // Need to add a delay since this happens before the focus actually occurs.
32 | if (!isMobile && !doNotSelect) setTimeout(() => target.select(), 0);
33 |
34 | temporarilyReleaseCanvasCursorLock();
35 | }
36 |
37 | export function handleTextFieldBlur() {
38 | // This is the incidental blur event when exiting fullscreen mode on mobile
39 | if (isExitingFullscreenDueToFocus) {
40 | isExitingFullscreenDueToFocus = false;
41 | return;
42 | }
43 |
44 | showFullScreenIfWasFullScreen();
45 | }
46 |
--------------------------------------------------------------------------------
/src/assets/images/icons/discord-space-icon.svgi:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------