DEV Community

Cover image for Finding Stars and Affirmations in the Sky with Three.js for Ayra Starr
Lee Martin
Lee Martin

Posted on • Originally published at leemartin.com

Finding Stars and Affirmations in the Sky with Three.js for Ayra Starr

The Afrobeats revolution was well underway at Mavin Records long before Universal Music Group purchased a majority stake in the label, thanks to the meteoric rise of artists like Rema and Ayra Starr.

Ayra describes herself as a “celestial being” and her music as “heavenly.” Fans describe her lyrics as a series of affirmations meant to uplift and provide resiliency in the face of life’s hardships. I had the opportunity to build upon these themes for Ayra, alongside her new single “Commas,” and the outcome is a unique affirmations app for her fans.

Fans are encouraged to visit affirmations.ayrastarr.com daily to receive words of encouragement. Leaning into the celestial aspects of Ayra’s being, these daily affirmations are revealed in the user’s sky, like a message from heaven. As the reveal occurs, it is also recorded as a video, and the user is then provided a unique shareable piece of content consisting of Ayra’s words and their beautiful sky.

Here's how we used Three.js to place and find affirmations in the sky.

Development

I've developed a lot of web apps which use Three.js to create a light AR experience in the user's sky. From the Jack White Twilight Receiver to the Pop Smoke Tracklist Reveal, it's a simple but powerful mechanic. Each time I've developed one of these, I dreamed of a little wayfinder component (inspired by Sky Guide) which helps the user find the things we've placed in the sky but never actually got around to developing it. Since this particular app for Ayra was straight-forward, I put in the extra work to unlock this new piece of UX for this and future projects. I'm just going to focus on this core UX but please check out previous dev blogs on web AR projects for a more indepth look at how I build these apps.

Placing Star in Sky

The star in our sky is simply a Three.js Sprite with a star image texture. We place it in a random place in the sky using setFromSphericalCoords by choosing any random degree higher than the horizon and any degree around the user. I've adjusted the position a little so it isn't too close to the horizon.

// Texture
const texture = new THREE.TextureLoader().load("/images/star.png")

// Color space
texture.colorSpace = THREE.SRGBColorSpace

// Material
const material = new THREE.SpriteMaterial({
  depthTest: false,
  map: texture,
  opacity: 0.5,
  transparent: true
})

// Star
star = new THREE.Sprite(material)

// Spherical position
let vector = new THREE.Vector3().setFromSphericalCoords(10, THREE.MathUtils.degToRad(Math.random() * 90), THREE.MathUtils.degToRad(Math.random()* 360))

// Copy position
star.position.copy(vector)

// Add to scene
scene.add(star)
Enter fullscreen mode Exit fullscreen mode

Finding Star in Sky

In order to allow users to use their device as a controller to adjust the position of the camera and find stars, I use the depreciated DeviceOrientationControls by patching it back into Three. In order for DeviceOrientationControls to function, we need access the user to grant access to their device's orientation. I attempt to gain access to this, alongside their camera, during a previous step of the UX using a custom composable I wrote for this purpose. You can see that permission step in the mockup video above. Once this permission is granted, we can initialize our DeviceOrienationControls with a single line.

// Device Orientation Controls
const controls = new DeviceOrientationControls(camera)
Enter fullscreen mode Exit fullscreen mode

Just make sure to update the controls in your render step using controls.update(). Now the user can point their device at the sky and find the star but how do we know they are pointing at it? That's where a Raycaster comes in.

Targeting Star

On our app, we want to prevent users from revealing an affirmation unless they find and target the star in the sky. In order to determine if the user is currently pointing at the star, we can use a Raycaster. The raycaster, as it sounds, points a ray from the camera to a point on the screen. In our case, we're just interested in the center of the screen so a default (0, 0) point should work fine. Let's initialize both.

// Raycaster
const raycaster = new THREE.Raycaster()

// Pointer
const pointer = new THREE.Vector3()
Enter fullscreen mode Exit fullscreen mode

Now, back in our render step. We can update the raycaster with the latest camera position and use the intersectObject method to determine if the raycaster intersects our star. Then you can do "something." In our case, we activate the ability to reveal the associated affirmation in the sky.

// Update ray
raycaster.setFromCamera(pointer, camera)

// Check for intersections
const intersects = raycaster.intersectObject(star)

// If intersects
if (intersects.length) {
  // Targeting star

} else {
  // Not targeting star

}
Enter fullscreen mode Exit fullscreen mode

Now, let's look at the new piece of UX: the wayfinder.

Star Wayfinder

As I mentioned, I've always wanted to add a little 2D wayfinder to help direct users to the position of objects in the sky but I couldn't quite wrap my head around turning a 3D direction into a 2D element. After a lot of research and trial and error, I landed on a rather simple solution that works well. What we do is clone the star's position and then use the camera to convert it to local space. We can then calculate the directional angle in degrees using atan2 and adjust it by -90°. We then have a degree angle we can use with CSS to rotate the wayfinder element.

// Clone star position
let local = star.position.clone()

// Convert from world space to camera's local space
camera.worldToLocal(local)

// Calculate angle in degrees
let angleDeg = Math.atan2(-local.y - 0, local.x - 0) * 180 / Math.PI + 180

// Adjust by 90 degrees
angleDeg -= 90

// Rotate compass
document.querySelector("#wayfinder").style.transform = `rotate(${angleDeg}deg)` 
Enter fullscreen mode Exit fullscreen mode

Be on the lookout for an evolution of this component in future projects.

Top comments (0)