An A-Frame State Management Example
Background
Thanks to Investment Time, a couple of coworkers and I embarked on creating a 3D Record Store! This project contained a lot of learning by doing since A-Frame/3D development was new to us. A-Frame is a framework built on top of three.js that makes creating 3D experiences for web much easier. The challenge that I'd like to focus on describing in this blog post is how to click on a record, display its data, and play its music.
I’ve created this stripped down version of the project on Glitch to contain only the elements necessary to demonstrate how we did this:
https://glitch.com/edit/#!/lime-balanced-dinghy
In the left panel of the Glitch project, you can see/edit the code and in the right panel you can interact with the output selecting an album to play its music.
Let’s get into it!
Enabling cursor click events with a camera
First, in order to enable navigating around the scene with our arrow keys and clicking with our 2D browser cursor we created a camera entity and applied a cursor
property with a value of rayOrigin: mouse
.
<!-- Mouse as Cursor --> <a-camera> <a-entity cursor="rayOrigin: mouse"></a-entity> </a-camera>
Add on-record-click method to Records
Each record on the shelf is its own A-Frame entity. On each of the record entities we attached a custom on-record-click
handler that we wrote (imported as a script at the top of the HTML file).
Passed through the on-record-click
handler is a jsonData
object which contains the information for the record: song, album, artist & sound.
<a-entity id="cover18" gltf-model="#record18" shadow="receive: true; cast: true" cursor-listener **on-record-click='jsonData: {"song": "Paper Ships", "album": "Dead Mans Bones", "artist": "Dead Mans Bones", "sound": "src:url(https://cdn.glitch.global/e8ce53d8-7f00-41aa-9601-6170b37afa32/paper-ships.m4a?v=1665756945121)"}'** ></a-entity>
Save the music info to state
In order to easily save state, we imported the aframe-state-component
(https://www.npmjs.com/package/aframe-state-component) package:
<!-- Aframe State Component: Manage application state and bind it to parts of the application to automatically react to state changes. --> <script src="https://unpkg.com/aframe-state-component@6.6.0/dist/aframe-state-component.min.js"></script>
When clicking on a record, the on-record-click
handler parses the JSON object and then calls the setCurrentSong
function (defined in state.js
) to store the current musicInfo
(song, album, artist, sound).
AFRAME.registerComponent("on-record-click", { init: function () { this.el.addEventListener("click", function (evt) { let json = this.components["on-record-click"].data.jsonData; if (json) { let musicInfo = JSON.parse(json); this.emit("setCurrentSong", musicInfo); } }); }, });
Notice the emit
function that passes the name of the setState handler (in this case “setCurrentSong”). This comes from the aframe-state-component
package we imported earlier
State.js
Where state is stored and setState handlers are defined in the state.js
file (shown below). Among those handler’s is the setCurrentSong
function that was just shown above to be passed through the emit
function when an album is clicked. Here you can see that the musicInfo
is passed along as well and is set to current state.
AFRAME.registerState({ initialState: { buttonText: 'Play Music', songIsSelected: false, currentSong: '', currentAlbum: '', currentArtist: '', currentSound: '' isMusicPlaying: false, }, handlers: { **setCurrentSong: function(state, musicInfo) { state.songIsSelected = true; state.currentSong = musicInfo.song; state.currentAlbum = musicInfo.album; state.currentArtist = musicInfo.artist; state.currentSound = musicInfo.sound; },** setMusicPlaying: function(state) { let newButtonText; state.isMusicPlaying = !state.isMusicPlaying if(state.isMusicPlaying) { newButtonText = 'Pause Music' } else { newButtonText = 'Play Music' }; state.buttonText = newButtonText; } } });
bind__
Thanks to aframe-state-component
we can also access state and bind__
its value to A-Frame entities to affect their render output.
This is the UI Panel entity we created in index.html:
<!-- UI PANEL --> <a-box id="menu" **bind__visible="isSongSelected"** position="1.77 1.57 -3.34" height="1.2" width="1" depth=".001" color="black" material="opacity: 0.6" > <!-- Current Song Title --> <a-entity **bind__text="value: currentSong"** position=".61 .36 .01" scale="2 2 2" ></a-entity> <!-- Current Artist --> <a-entity **bind__text="value: currentArtist; color: white"** scale="1.5 1.5 1.5" position=".36 .25 .01" ></a-entity> <!-- Current Album --> <a-entity **bind__text="value: currentAlbum; color: white"** position=".12 .149 .01" ></a-entity> <!-- Play/Pause Music Button --> <a-box on-button-click **bind__sound="currentSound" bind__visible="isSongSelected"** primitive="box" id="button" height=".2" width=".4" depth=".025" position="0 -0.15 0.01" color="pink" > <a-entity **bind__text="value: buttonText"** text="color: black" position=".38 0 .025" ></a-entity> </a-box> </a-box>
Notice that the current song title entity contains a bind__text
attribute which passes the currentSong
stored in state.js
to the value
of the text attribute. Thus rendering the text of the current song! The same applies to the current artist and album entities.
Resources
- Supermedium’s Collection of Aframe Components is a nifty list of packages that appear in a lot of example projects on a-frame’s site.
- Guide for building UI in A-Frame: https://glitch.com/~aframe-building-ui
- The aframe-state-component: https://www.npmjs.com/package/aframe-state-component