Blog Post|#3d#engineering

An A-Frame State Management Example

Samantha Shackelford
Samantha ShackelfordFriday, January 6, 2023
An A-Frame State Management Example


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:!/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("}'** ></a-entity>

Save the music info to state

In order to easily save state, we imported the 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=""></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


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 =; 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; } } });


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.


Share this post


Related Posts:

Interested in working with us?

Give us some details about your project, and our team will be in touch with how we can help.

Get in Touch