<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lee Martin</title>
    <description>The latest articles on DEV Community by Lee Martin (@leemartin).</description>
    <link>https://dev.to/leemartin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F107498%2F76258fd6-0264-489f-a153-cbff75322329.jpg</url>
      <title>DEV Community: Lee Martin</title>
      <link>https://dev.to/leemartin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leemartin"/>
    <language>en</language>
    <item>
      <title>State of Spotify Web API Report 2025</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Tue, 14 Oct 2025 12:31:37 +0000</pubDate>
      <link>https://dev.to/leemartin/the-state-of-spotify-web-api-report-2025-4gh3</link>
      <guid>https://dev.to/leemartin/the-state-of-spotify-web-api-report-2025-4gh3</guid>
      <description>&lt;p&gt;&lt;strong&gt;Understanding Spotify's new API restrictions and finding practical alternatives.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;On May 15, 2025, Spotify &lt;a href="https://developer.spotify.com/blog/2025-04-15-updating-the-criteria-for-web-api-extended-access" rel="noopener noreferrer"&gt;updated&lt;/a&gt; the criteria for &lt;a href="https://developer.spotify.com/documentation/web-api" rel="noopener noreferrer"&gt;Web API&lt;/a&gt; extended access, the process which determines if a Spotify app built on the Web API can come out of development mode (limited to 25 test users) and be offered publicly to a larger group of users. If you do not meet this steep new criteria, the Web API is now mostly limited to experimentation and personal use. As someone who has developed many artist campaigns and software projects using the Web API, I wanted to take a moment to talk about these changes. Naturally, I thought about complaining but... that doesn't really help anyone. Instead, I've decided to speak to how this platform has inspired my work in the past and share potential alternatives as we look to the future. This report is specifically about the &lt;a href="https://developer.spotify.com/documentation/web-api" rel="noopener noreferrer"&gt;Spotify Web API&lt;/a&gt;, &lt;a href="https://developer.spotify.com/documentation/web-playback-sdk" rel="noopener noreferrer"&gt;Web Playback SDK&lt;/a&gt;, and &lt;a href="https://developer.spotify.com/documentation/embeds" rel="noopener noreferrer"&gt;Embeds&lt;/a&gt;. It does not cover any other Spotify Platform products. In addition to suggestions for you, I have a few suggestions for Spotify and other companies offering Music APIs. Anyway, I hope you enjoy this report. Please share it if you find it useful and if you have any feedback, please get in touch.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://www.leemartin.com" rel="noopener noreferrer"&gt;Lee Martin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I do not work for Spotify and all opinions and suggestions expressed are my own.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Brief History
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;We, the computer geeks at Spotify, also love cool software. To that end, we’re trying to open up our platform to enable third party application development on top of the Spotify platform. These pages are our first baby steps toward that goal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had to use the &lt;a href="https://web.archive.org/web/20090512080900/http://developer.spotify.com/en/" rel="noopener noreferrer"&gt;Way Back Machine&lt;/a&gt; to find this quote on the Spotify Developer site from 2009. Through my participation in &lt;a href="https://en.wikipedia.org/wiki/Music_Hack_Day" rel="noopener noreferrer"&gt;Music Hack Day&lt;/a&gt;, I had the chance to meet some of the people who would eventually build this platform. I mention this to acknowledge that this API's existence was never guaranteed, it happened because people believed in API enablement and inspiration. The reason we mourn these changes is because it was really well done.&lt;/p&gt;

&lt;p&gt;My first major project using the Spotify Web API was a  &lt;a href="https://leemartin.dev/come-fly-the-funky-skies-with-khruangbin-7c08ea9930a3" rel="noopener noreferrer"&gt;playlist generator&lt;/a&gt; for the band Khruangbin. The idea behind the app was to help users generate playlists for an upcoming flight. Khruangbin provided a pool of funky tracks and the user provided their flight details and preferences. Coffee or tea? Aisle or window? These preferences were mapped to Spotify audio features and helped determine which songs from the pool should be used. For example, coffee drinkers got high energy tracks, while tea drinkers got low energy tracks. It was fun, useful, and only something the Spotify Web API could pull off.&lt;/p&gt;

&lt;p&gt;Since then, I've developed numerous activations and apps using the Web API and have had a lot of fun doing so. In fact, a &lt;a href="https://www.leemartin.com/queen-of-me" rel="noopener noreferrer"&gt;case study&lt;/a&gt; from a Shania Twain campaign back in 2023 is the &lt;a href="https://x.com/SpotifyPlatform" rel="noopener noreferrer"&gt;last thing&lt;/a&gt; the Spotify Developer platform's Twitter account ever shared. Things were looking up then but the times, they are a-changin'.&lt;/p&gt;

&lt;h2&gt;
  
  
  New Requirements
&lt;/h2&gt;

&lt;p&gt;In order to transition an app from development mode to extended quota mode, developers must submit their application for review. Previously, this review process was open to any individual but now, Spotify only accepts applications from established business entities, whose implementation meets a very specific set of &lt;a href="https://developer.spotify.com/documentation/web-api/concepts/quota-modes" rel="noopener noreferrer"&gt;requirements&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The business entity must be a legally registered business or organization. &lt;/li&gt;
&lt;li&gt;The business must be operating an active and launched service.&lt;/li&gt;
&lt;li&gt;The service must be maintaining a minimum of 250,000 monthly active users.&lt;/li&gt;
&lt;li&gt;The service must be available in key Spotify markets&lt;/li&gt;
&lt;li&gt;The service must prove commercial viability&lt;/li&gt;
&lt;li&gt;The service must adhere to Terms&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anything jump out at you? 250,000 MAUs. This means that you need to have a massively successful app &lt;em&gt;before&lt;/em&gt; seeking an integration with the Spotify Web API. In plain terms, that means your service should be successful without Spotify and &lt;em&gt;only then&lt;/em&gt; should you be aiming to integrate Spotify with it. In my opinion, if you're thinking about building a new service which relies on Spotify, you should think twice. Spotify continues:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Eligibility criteria may change over time. Spotify makes no promise or guarantee that extended access requests will get approved. &lt;/p&gt;

&lt;p&gt;We strongly advise that you start integrating the Web API into an application intended for an audience of more than 25 authenticated Spotify users only after Spotify has agreed to this application in writing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Spotify doesn't mince words. They have final say on approvals. Read the &lt;a href="https://developer.spotify.com/policy" rel="noopener noreferrer"&gt;Developer Policy&lt;/a&gt; thoroughly before making any investment.&lt;/p&gt;

&lt;p&gt;I've built Spotify powered activations for major artists but even they can't prove 250k MAU. Under these requirements, I would no longer be able to build Spotify Web API powered artist campaigns which require user authentication. And therein lies the issue: Spotify is ending support for those apps that live somewhere between a personal project and an enterprise project.&lt;/p&gt;

&lt;p&gt;I should mention that if your app doesn't require user authentication, you should be able to continue to use some aspects of the Spotify Web API using &lt;a href="https://developer.spotify.com/documentation/web-api/tutorials/client-credentials-flow" rel="noopener noreferrer"&gt;client credentials&lt;/a&gt; but you should still be aware of the development mode limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Alternatives
&lt;/h2&gt;

&lt;p&gt;If your application requires user authentication and is affected by the new requirements, you'll need to get creative or look elsewhere. Let me walk through key Web API features, how they inspired me, and what you can do instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search
&lt;/h3&gt;

&lt;p&gt;Web API &lt;a href="https://developer.spotify.com/documentation/web-api/reference/search" rel="noopener noreferrer"&gt;search&lt;/a&gt; requires authentication. That means if you're looking for a track, album, artist, or playlist, you'll need to be authenticated. Since you won't be able to search on behalf of a user, you can look towards using &lt;a href="https://developer.spotify.com/documentation/web-api/tutorials/client-credentials-flow" rel="noopener noreferrer"&gt;client credentials&lt;/a&gt; instead. In order to combat development mode rate limits, you'll also want to look at caching data.&lt;/p&gt;

&lt;p&gt;One viable alternative for music searching is the ancient &lt;a href="https://performance-partners.apple.com/search-api" rel="noopener noreferrer"&gt;iTunes Search API&lt;/a&gt;. This API does not require any authentication and will return a lot of the same basic metadata you'd expect from Spotify. In addition, most tracks will have an ISRC, which you can use to &lt;a href="https://leemartin.dev/how-to-match-tracks-between-spotify-and-apple-music-2d6b6159957e" rel="noopener noreferrer"&gt;cross-reference&lt;/a&gt; with the Spotify platform and other music APIs. Also, Apple Music still provides audio previews which Spotify deprecated last year.&lt;/p&gt;

&lt;p&gt;I once used the Web API search endpoint to develop a search feature into an early 2000s inspired &lt;a href="https://leemartin.dev/hey-there-nostalgia-30ada8728515" rel="noopener noreferrer"&gt;mixtape generator&lt;/a&gt; for the Plain White T's. By adjusting the year parameter to 2000-2010, the search would only return tracks from this era, leading to very nostalgic results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Likes and Follows
&lt;/h3&gt;

&lt;p&gt;Authentication is required to like a track or follow an artist using the Web API. Without it, your best recourse is simply linking to the associated pages on Spotify. Previously, Spotify had a follow button which was pre-authenticated but they &lt;a href="https://developer.spotify.com/blog/2021-10-15-follow-button-deprecation" rel="noopener noreferrer"&gt;deprecated&lt;/a&gt; it back in 2021. Since the Web API was suggested as an alternative, Spotify should reconsider embed solutions for these touchpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audio Features
&lt;/h3&gt;

&lt;p&gt;I &lt;a href="https://www.linkedin.com/posts/leepaulmartin_while-i-was-away-spotify-removed-some-features-activity-7272655083694100480-EjV0/" rel="noopener noreferrer"&gt;wrote&lt;/a&gt; about the deprecation of &lt;a href="https://developer.spotify.com/documentation/web-api/reference/get-audio-features" rel="noopener noreferrer"&gt;Audio Features&lt;/a&gt; last year. This was a massive blow to what made the Spotify Web API truly unique and now we know it was the sign of things to come. I used Audio Features for so many things including a &lt;a href="https://leemartin.dev/sad-boi-5457970fb8d5" rel="noopener noreferrer"&gt;Sadboi Detector&lt;/a&gt; I developed for Illenium. That application analyzed a user's listening history or a provided playlist and determined how sad it was by examining each track's Valence. Spotify deprecated this feature, alongside Recommendations and Related Artists, because they were concerned developers would use it to train AI models.&lt;/p&gt;

&lt;p&gt;While I haven't done much building around audio classification this year, I've been eyeing &lt;a href="https://cyanite.ai/" rel="noopener noreferrer"&gt;Cyanite&lt;/a&gt; as a potential alternative to Audio Features. Cyanite's business is analyzing the emotions of music and offers all of the measurements the Spotify Web API previously provided alongside even more data. Depending on your use case, audio classification in general is worth exploring as it becomes more mainstream with AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Playlist Creation
&lt;/h3&gt;

&lt;p&gt;I already mentioned one playlist generator for Khruangbin but I created multiple with the Web API, including another for Khruangbin during the pandemic called &lt;a href="https://leemartin.dev/sheltering-in-space-with-khruangbin-f185885089e6" rel="noopener noreferrer"&gt;Shelter in Space&lt;/a&gt;. Playlist generators work particularly well when an artist or curator can bring a particular vibe to the pool of potential songs.&lt;/p&gt;

&lt;p&gt;Since creating a playlist and adding tracks requires authentication, we need to look elsewhere for a solution. Depending on your use case, you may be able to manually create the playlist or a series of playlists and serve them up thematically. The Apple Music API is still very good at creating playlists and that can be accomplished using &lt;a href="https://js-cdn.music.apple.com/musickit/v3/docs/index.html?path=/story/introduction--page" rel="noopener noreferrer"&gt;MusicKit&lt;/a&gt; on the web.&lt;/p&gt;

&lt;p&gt;If still you want to create an application that builds playlists dynamically and saves them to Spotify, you may need a 3rd party solution. I haven't used or tested &lt;a href="https://www.tunemymusic.com/transfer/text-file-to-spotify?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;TuneMyMusic&lt;/a&gt;, but this service allows users to create a playlist from a text file. If your service can generate a text file of tracks, your users could import it via a 3rd party solution (or recreate it manually). I kinda recall building a text file export or playlist tracks into one of my client apps. Maybe it was the &lt;a href="https://leemartin.dev/turning-weather-into-music-with-dark-sky-and-spotify-for-tycho-f4f40aef97ed" rel="noopener noreferrer"&gt;Tycho Weather&lt;/a&gt; playlist generator?&lt;/p&gt;

&lt;p&gt;This raises an interesting point: there may be existing Spotify apps with extended quota access that can bridge the gap. However, thoroughly vet any 3rd party service before sending your users there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Playback
&lt;/h3&gt;

&lt;p&gt;Being a web developer for over two decades means I was able to see a lot of new features adopted by browsers and as soon as feature parity was met, I was able to roll them into my client projects. I felt the same way about the Music APIs and I kept a little &lt;a href="https://docs.google.com/spreadsheets/d/19fESz43BdObxWp0ysVKDOZwS_Ak6-5R4t5koJV6ZpXg/edit?usp=sharing" rel="noopener noreferrer"&gt;spreadsheet&lt;/a&gt; which tracked when Spotify, Apple Music, and Deezer inched closer to providing full track streaming via their API to authenticated users. It was a great day when all of these finally turned green.&lt;/p&gt;

&lt;p&gt;With this functionality, I built a Spotify and Apple Music powered &lt;a href="https://leemartin.dev/building-an-spotify-and-apple-music-powered-a-b-player-for-r-e-m-d283e35ec466" rel="noopener noreferrer"&gt;A/B player&lt;/a&gt; for R.E.M., which allowed fans to jump between the original and new mix of Monster seamlessly. I also built an app called &lt;a href="https://www.listeningparty.com" rel="noopener noreferrer"&gt;Listening Party&lt;/a&gt; which allows artists to throw parties around their music.&lt;/p&gt;

&lt;p&gt;Deezer has deprecated their custom player library and with these new Spotify requirements, that leaves Apple Music as the only viable option of the three for authenticated playback. However, you can also look into SoundCloud or just rolling something custom.&lt;/p&gt;

&lt;p&gt;Spotify also has an &lt;a href="https://developer.spotify.com/documentation/embeds" rel="noopener noreferrer"&gt;embed&lt;/a&gt; (potentially with an authenticated Spotify user) and a complimentary iFrame API, similar to YouTube. However, unlike YouTube, the iFrame API isn't very customizable. With the &lt;a href="https://developer.spotify.com/documentation/web-playback-sdk" rel="noopener noreferrer"&gt;Web Playback SDK&lt;/a&gt; now being out of reach, I recommend Spotify consider a chromeless iFrame API for more customizable integrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listening History
&lt;/h3&gt;

&lt;p&gt;One of the most popular features of the Spotify Web API is the ability to read the users &lt;a href="https://developer.spotify.com/documentation/web-api/reference/get-users-top-artists-and-tracks" rel="noopener noreferrer"&gt;top tracks or artists&lt;/a&gt; in three different time ranges: last 4 weeks, last 6 months, and all time. It's sort of a simplified version of Spotify Wrapped. I used this feature to generate Waffle House &lt;a href="https://www.leemartin.com/waffle-house" rel="noopener noreferrer"&gt;receipts&lt;/a&gt; for Jonas Brothers, mail &lt;a href="https://www.leemartin.com/greetings-from-stick-season" rel="noopener noreferrer"&gt;postcards&lt;/a&gt; to Noah Kahan fans, and grow a &lt;a href="https://www.leemartin.com/proof-of-life" rel="noopener noreferrer"&gt;virtual plant&lt;/a&gt; for Joy Oladokun, among countless other activations. Not having the ability to integrate this feature into an application that has more than 25 users is hard to stomach but there are ways forward.&lt;/p&gt;

&lt;p&gt;Apple Music offers an alternative. While not as detailed as Spotify's data, you can check what's in &lt;a href="https://developer.apple.com/documentation/applemusicapi/get-heavy-rotation-content" rel="noopener noreferrer"&gt;heavy rotation&lt;/a&gt; on a user's account. I also noticed a new endpoint called &lt;a href="https://developer.apple.com/documentation/applemusicapi/get-the-user's-replay-data" rel="noopener noreferrer"&gt;replay data&lt;/a&gt; which summarizes the data from Apple Music Replay, their answer to Spotify Wrapped. That's something the Spotify Web API never provided!&lt;/p&gt;

&lt;p&gt;What we really need is a platform-agnostic way to keep track of plays. Did someone say &lt;a href="https://www.last.fm/" rel="noopener noreferrer"&gt;scrobbling&lt;/a&gt;? God, I miss last.fm. By the way, did you know you can export your extended streaming history from Spotify? It takes up to 30 days but you can &lt;a href="https://www.spotify.com/us/account/privacy/" rel="noopener noreferrer"&gt;download your data&lt;/a&gt; in case you ever switch services or simply want to build something interesting around your listening history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Possibilities
&lt;/h2&gt;

&lt;p&gt;Unless Spotify changes their mind, we'll need to adapt to these limitations. I haven't developed any Spotify Web API powered apps this year because my clients didn't ask for any. Sign of the times? However, I did build an activation for Lil Poppa which suggests a song off his new album based on the results of an AI-powered therapy session. On this app we simply linked to the song on Spotify. No integration needed. I wrote a little bit about this and using AI to &lt;a href="https://netmaker.substack.com/p/enhancing-thematic-song-recommendation" rel="noopener noreferrer"&gt;bridge the gaps&lt;/a&gt; of these missing features.&lt;/p&gt;

&lt;p&gt;I've been using my YouTube &lt;a href="https://www.youtube.com/@LeeMartinNetmaker" rel="noopener noreferrer"&gt;channel&lt;/a&gt; to research new technologies like AI and automation for creative marketing in music. While researching, I noticed something interesting: Spotify has granted extended quota access to some AI and agentic platforms. This means you can develop agentic flows that interact with your Spotify account. This doesn't necessarily solve the issues outlined in this report &lt;em&gt;but&lt;/em&gt; it may offer a glimpse at a near future where Spotify and other DSPs are integrated with these platforms, allowing for custom applications.&lt;/p&gt;

&lt;p&gt;Check out my video &lt;a href="https://youtu.be/hP4r0q_b7BY?si=-Gi5-5IfBjzBkY2R" rel="noopener noreferrer"&gt;introduction&lt;/a&gt; to AI Agents to see how I use Spotify in a custom flow, and take a look at OpenAI's new &lt;a href="https://openai.com/index/introducing-apps-in-chatgpt/" rel="noopener noreferrer"&gt;Apps SDK&lt;/a&gt; which integrates Spotify. I'll be keeping an eye on these developments so please &lt;a href="https://www.youtube.com/@LeeMartinNetmaker" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; if you're interested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Relying on and investing ourselves into other platforms to achieve things online has always been a risky endeavor. Platforms change, adjust their terms, shut down, and all you're left with is whatever value you were able to extract and make defensible elsewhere. To be a web developer is to be malleable. You need to be constantly looking for new ways to achieve solutions to your problems. These changes by Spotify might be hard to process at the moment, but I see them as the latest in a series of hard lessons about web development. Instead of letting it get me down, I embrace the web's evolution and accept the challenge of looking for new solutions. I hope this report helps you, and I can't wait to see what you build. Thanks for reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  About
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.leemartin.com/" rel="noopener noreferrer"&gt;Lee Martin&lt;/a&gt; is a creative developer who has largely worked in music for the last two decades. He previously worked at Silva Artist Management, SoundCloud, and Songkick. Currently he is a freelancer who &lt;a href="https://www.leemartin.com" rel="noopener noreferrer"&gt;builds&lt;/a&gt; creative marketing campaigns for some of the biggest and smallest names in music. In addition, he maintains several software projects including &lt;a href="https://www.turn.audio" rel="noopener noreferrer"&gt;Turn&lt;/a&gt;, &lt;a href="https://www.vinylmockup.com" rel="noopener noreferrer"&gt;Vinyl Mockup&lt;/a&gt;, and &lt;a href="https://www.listeningparty.com" rel="noopener noreferrer"&gt;Listening Party&lt;/a&gt;. He publishes a newsletter called &lt;a href="https://netmaker.leemartin.com" rel="noopener noreferrer"&gt;Netmaker&lt;/a&gt; and is publicly exploring the world of AI/Automation on his YouTube &lt;a href="https://www.youtube.com/@LeeMartinNetmaker" rel="noopener noreferrer"&gt;channel&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>spotify</category>
      <category>api</category>
      <category>music</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Developing The Cosmic Selector Jukebox for Lord Huron</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Fri, 18 Jul 2025 12:29:47 +0000</pubDate>
      <link>https://dev.to/leemartin/developing-the-cosmic-selector-jukebox-for-lord-huron-mh1</link>
      <guid>https://dev.to/leemartin/developing-the-cosmic-selector-jukebox-for-lord-huron-mh1</guid>
      <description>&lt;p&gt;With our &lt;a href="https://www.leemartin.com/cosmic-coin" rel="noopener noreferrer"&gt;Cosmic Coins&lt;/a&gt; produced and in fans’ hands, it was time to turn our focus to The Cosmic Selector jukebox. Just to recap, we &lt;a href="https://www.leemartin.com/cosmic-coin" rel="noopener noreferrer"&gt;created&lt;/a&gt; physical NFC powered coins and it was our intention to let fans spend these coins to play clips from Lord Huron’s new album, The Cosmic Selector Vol. 1, on a virtual jukebox. This jukebox would live at &lt;a href="http://cosmicselector.com" rel="noopener noreferrer"&gt;cosmicselector.com&lt;/a&gt; and similar to a jukebox in a bar, any fan currently on the site could hear what was being played. In addition to creating a visual for the jukebox itself, this work would include some bespoke database design and determining how the queue and audio playback would function. Let’s start by taking a look at the Cosmic Selector visual.&lt;/p&gt;

&lt;h2&gt;
  
  
  3D Modeling
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1083496783" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;There was a goal to have the final visual of the jukebox exist as a sort of CCTV visual and there were many ways to get there. First, we had some existing teaser videos of the jukebox in various locations and we also had a photo shoot of the jukebox in various locales. However, both of these assets were flat 2D and that doesn’t pair very nicely with the responsive web, nor does it leave much room for dynamic choices. In my mind, if I could somehow create a 3D model of the jukebox, I would then be able to better control it dynamically and place it into various 3D environments. It’s just a couple of boxy lines, how hard could it be? Well, it turned out to be VERY hard to model. &lt;/p&gt;

&lt;p&gt;The Cosmic Selector’s foundation is a Wurlitzer 2710 from 1963. Yes, the 60s. That means there are some wild angles to it which makes understanding the shape and scale of it very difficult. At one point, I was tempted to buy the actual jukebox on Ebay but it was pickup only, and in Florida. After a few false starts in Blender, I ended up purchasing the original Wurlitzer 2710 manual (instead of the whole jukebox,) which included a schematic of the jukebox, and slowly started sketching the overall shape of it in my sketchbook. I also referenced many photos of the original jukebox and the band’s adjustments. After spending a lot of time with all these assets, I built up the confidence to get back into &lt;a href="https://www.blender.org/download/releases/4-3/" rel="noopener noreferrer"&gt;Blender&lt;/a&gt; and just slowly steered the design into the model you see above. I wasn’t trying to get it super detailed. In fact, I wanted the model footprint to be quite small (low poly) so the website continued to run quickly.&lt;br&gt;
Once I had the model to the place I wanted, I started working up the textures. Most of the jukebox is reflective metal, which you can accomplish in 3D design with the right material and an environment texture. For the other elements, I hand-crafted a few textures and pulled others straight from photos of the Cosmic Selector.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three.js Scene
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1102257490" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;With the jukebox modeled in &lt;a href="https://www.blender.org/download/releases/4-3/" rel="noopener noreferrer"&gt;Blender&lt;/a&gt;, I could now bring it into the website using &lt;a href="https://threejs.org/" rel="noopener noreferrer"&gt;Three.js&lt;/a&gt;. First, I exported the model in GLTF format. Then, I used the &lt;a href="https://threejs.org/docs/#examples/en/loaders/GLTFLoader" rel="noopener noreferrer"&gt;GLTFLoader&lt;/a&gt; to load it into my scene. However, it was void of an environment. I had all sorts of ideas of how to handle environments for this project, including using AI, which the band and I agreed to avoid. That’s when I saw this example for a &lt;a href="https://threejs.org/examples/?q=groun#webgl_materials_envmaps_groundprojected" rel="noopener noreferrer"&gt;groundprojected skybox&lt;/a&gt; and I thought maybe I could ground the jukebox in an HDRI environment. So, I grabbed a series of HDRI textures from &lt;a href="https://polyhaven.com/" rel="noopener noreferrer"&gt;Polyhaven&lt;/a&gt; and it ended up working nicely. &lt;/p&gt;

&lt;p&gt;In order to achieve the CCTV aesthetic, I used a series of &lt;a href="https://threejs.org/manual/?q=post#en/how-to-use-post-processing" rel="noopener noreferrer"&gt;post processing&lt;/a&gt; passes. First, a saturation pass was used to desaturate the scene. Next, I applied a subtle blur to lower the overall quality of the image. Then I added scanlines and a rolling TV effect to make it feel like a camera broadcast with a bad signal. Finally, I sprinkled a bit of static and vignette on top to complete the visual. This drastically changed the vibe of the scene but without sacrificing the performance of the site much.&lt;/p&gt;

&lt;p&gt;The final aspect I needed to work out was making some elements of the jukebox glow when it was playing. This was done by using another post processing pass: &lt;a href="https://threejs.org/examples/webgl_postprocessing_unreal_bloom.html" rel="noopener noreferrer"&gt;UnrealBloom&lt;/a&gt;. Through a process of &lt;a href="https://threejs.org/examples/?q=bloom#webgl_postprocessing_unreal_bloom_selective" rel="noopener noreferrer"&gt;selective&lt;/a&gt; blooming, I could make some elements of the jukebox glow and others not. In addition to this selective glow, I used &lt;a href="https://gsap.com/" rel="noopener noreferrer"&gt;Greensock&lt;/a&gt; to add a bit of flickering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Modeling
&lt;/h2&gt;

&lt;p&gt;Technically, as part of the coin &lt;a href="https://www.leemartin.com/cosmic-coin" rel="noopener noreferrer"&gt;work&lt;/a&gt;, I had already established a database for creating and storing coins. This was done using a &lt;a href="https://www.serverless.com/" rel="noopener noreferrer"&gt;Serverless&lt;/a&gt; instance and a &lt;a href="https://aws.amazon.com/dynamodb/" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt; database. In addition to a unique ID, we stored things like the coin color, medium, and how many plays were left on the coin. In addition to coins, we needed to store a database of songs that were playable on the jukebox. These song items included the name of the song, duration of the clip, and whether or not the song was currently visible on the jukebox’s list of selections.&lt;/p&gt;

&lt;p&gt;The final table was the selections table. If you think of an actual jukebox, a selection is the union of a payment and a song. I am choosing to spend my coin in order to add this particular selection to the jukebox. So, in general, a selection would include a reference to the associated song and coin. That’s easy enough but it really gets complicated when you begin to establish how to connect these selections to an ever evolving queue of selections. More on that later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Title Strips
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzvly8cg832rka0b1nedx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzvly8cg832rka0b1nedx.jpg" alt="Title Strips"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back on the website, I needed to visualize this list of songs in a thematic but responsive manner. I sorta knew from the beginning that I didn’t want users to be able to touch the jukebox. In my mind, the jukebox should always exist at a distance (it’s too mysterious) and we’d use an external panel to interact with it. This led to the two panel layout we landed on, which also solved some responsive UI problems. For the list of selections, I was inspired by how jukeboxes displayed songs and their associated codes. This motif is called a &lt;a href="https://www.mikesarcade.com/arcade/titlestrips.html" rel="noopener noreferrer"&gt;title strip&lt;/a&gt; and it typically references the A and B side of a record in the jukebox. Through a bit of careful logic, I was able to take an evolving list of songs from my database and display them as a list of title strips and their associated letter and number code. First, we needed a list of letters and numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Letters
const letters = ['A','B','C','D']

// Numbers
const numbers = ['1','2','3','4']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we could establish an array of potential selector codes by combining the two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Selector codes
const selectorCodes = computed&amp;lt;string[]&amp;gt;(() =&amp;gt; {
  // Initialize codes
  const codes: string[] = []

  // Loop through letters
  letters.forEach(letter =&amp;gt; {
    // Loop through numbers
    numbers.forEach(number =&amp;gt; {
      // Push code
      codes.push(`${letter}${number}`)

    })

  })

  // Return codes
  return codes

})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provided us with an array of codes such as A1, A2, A3, A4, B1, etc. We could then map our jukebox songs with a potential jukebox code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Selector songs
const selectorSongs = computed&amp;lt;CosmicTitle[]&amp;gt;(() =&amp;gt; {
  // Return empty if no songs
  if (!songs.value) return []

  // Map songs with associated selector code
  return songs.value?.map((song, i) =&amp;gt; {
    return {
      songArtist: song.songArtist,
      songId: song.songId,
      songCode: selectorCodes.value[i],
      songTitle: song.songTitle
    }
  })

})

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And, finally, pair off these songs as title strips which could be visualized by our app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Selector titles
const selectorTitles = computed(() =&amp;gt; {
  // Return empty if no selector songs
  if (!selectorSongs.value) return []

  // Titles
  const titles = []

  // Loop through selector songs by 2
  for (let i = 0; i &amp;lt; selectorSongs.value.length; i += 2) {
    // Initialize pair of titles
    const pair: (CosmicTitle | null)[] = [selectorSongs.value[i]]

    // If second song exists add it, otherwise add null
    if (i + 1 &amp;lt; selectorSongs.value.length) {
      // Push song
      pair.push(selectorSongs.value[i + 1])

    } else {
      // Push null
      pair.push(null)

    }

    // Push pair
    titles.push(pair)

  }

  // Return titles
  return titles

})

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote a custom Vue component to visualize these but they are mostly just a little custom HTML element which displays the song title and associated code. When a user punches in their code to the selector, I use a little helper method to then determine which song was chosen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Find song by code
function findSongByCode(code: string) {
  // Find first song that matches the code
  return selectorSongs.value.find(song =&amp;gt; song.songCode === code)

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s now time to make a cosmic selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a Selection
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1102257464" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;As we determined previously, a selection is a union between a coin and a song. However, in the case of our app, we also need to determine the timing of each selection’s playback. This is because while our app will playback all selections for all users currently sitting on the site, it doesn’t do so with a real-time stream. Instead, I use a technique I developed for &lt;a href="https://www.listeningparty.com" rel="noopener noreferrer"&gt;Listening Party&lt;/a&gt;, which utilizes time to determine which song should be playing when and at what position. But first, we get the coin from the database and make sure it has any remaining plays. Then, we make sure the song the user chose also exists. If both of those things are valid, we can create our new selection.&lt;/p&gt;

&lt;p&gt;To create a new selection, we must first determine if there are any existing selections in the queue and we’re particularly interested in the last selection of the current queue because this new selection would begin when it is finished. We can query for this last selection using the DynamoDB API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Query selections command
const querySelectionsCommand = new QueryCommand({
  TableName: `cosmic-selector-selections`,
  IndexName: "SelectionsEndsAtIndex",
  KeyConditionExpression: "selectorId = :selectorId AND endsAt &amp;gt; :dateValue",
  ExpressionAttributeValues: marshall({
    ":selectorId": "1",
    ":dateValue": Date.now()
  }),
  Limit: 1,
  ScanIndexForward: false
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if an existing queue of selections doesn’t exist, we can simply determine that the new selection should start now and end at the duration of the song.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Selection starts now
selection.startsAt = Date.now()

// Selection ends now plus the song duration
selection.endsAt = Date.now() + song.songDuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, if a queue of selections does exist, our new selection should instead begin at the end of the last selection of the queue and once again end at the duration of the song&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Selection starts at the end of last selection
selection.startsAt = lastSelection.endsAt

// Selection ends at the end of last selection plus the song duration
selection.endsAt = lastSelection.endsAt + song.songDuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now store our new selection in the selections database and alert all users of this new selection using a &lt;a href="https://pusher.com/" rel="noopener noreferrer"&gt;Pusher&lt;/a&gt; integration. We’ll talk about playback in a bit but first there’s one more thing to do. We need to decrement the plays of our coin using an &lt;code&gt;UpdateItemCommand&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Update coin command
const updateCoinCommand = new UpdateItemCommand({
  TableName: `cosmic-selector-coins`,
  Key: marshall({
    coinId: coin.coinId
  }),
  UpdateExpression: "SET updatedAt = :updatedAt, coinPlays = coinPlays - :decrement",
  ExpressionAttributeValues: marshall({
    ":decrement": 1,
    ":updatedAt": Date.now()
   }),
  ReturnValues: "ALL_NEW"
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And, that’s it for new selections. &lt;/p&gt;

&lt;p&gt;Oh, wait, there’s one very important thing. Say there are 50 people at a bar and all of them want to play the jukebox. How would this manifest itself? They would line up and each would get a chance to make a selection. Now, I didn’t want users to have to queue up to make selections, but I wanted to make sure these selections were created one at a time so the queue wasn’t corrupted. We can accomplish this with Lambda using &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html" rel="noopener noreferrer"&gt;reserved concurrency&lt;/a&gt;. This makes sure our create selection function can be called once and must resolve before it is called again. Basically, this is a cheap way of doing a queue and making sure each new selection is handled one at a time. Ok, let’s playback some audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playing Audio
&lt;/h2&gt;

&lt;p&gt;The final piece of this experience involves playing back the audio itself. When users initially get to the site, we can ask the database for the queue of current selections. If a selection exists on the queue, we can determine which selection should currently be playing based on those &lt;code&gt;startsAt&lt;/code&gt; and &lt;code&gt;endsAt&lt;/code&gt; properties we established earlier. Then, depending on the current time, we can determine at what position of the song the jukebox should be playing from. For example, if a 60 second song was played 30 seconds ago, I should be listening from the 30 second mark. It’s a little smoke and mirrors but works nicely.&lt;/p&gt;

&lt;p&gt;What about selections being added to the queue in real-time? Our website is notified of these additions thanks to an integration of &lt;a href="https://pusher.com/" rel="noopener noreferrer"&gt;Pusher&lt;/a&gt; from the server side to the client side. As these selections come through, they are added to the end of the local queue and are played as selections complete. There is a chance the database queue and local queue will be slightly out of sync but as they say, it’s close enough for rock and roll. An added bonus of the Pusher integration is that we can add a real-time ticker of current listeners.&lt;/p&gt;

&lt;p&gt;For actual audio playback, we’re using short lived S3 urls which get signed when they are needed. These MP3 urls are then played back using my audio library of choice: &lt;a href="https://howlerjs.com/" rel="noopener noreferrer"&gt;Howler.js&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue
&lt;/h2&gt;

&lt;p&gt;This project is an amalgamation of everything I try to do as a creative developer. It bridges the gap between the real and digital world, engages the fanbase, expands the band’s lore, and challenges what is possible on the digital web. It is fiercely simple in concept but requires complex thinking to achieve. A challenge for sure but also with a realistic approach. I learned a lot and through all the ups and downs, I’m happy with the outcome. Thanks again to Mercury Records and Loyal T Management for collaborating with me on this project. Thanks to Ben and the rest of Lord Huron for entrusting me with another chapter of their story. Special thanks to some of the best fans in music who not only participated in this campaign but also celebrate these types of projects. The Cosmic Selector Vol 1. is out now.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>blender</category>
      <category>music</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Minting Cosmic Coins for Lord Huron's Cosmic Selector</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Wed, 16 Jul 2025 21:00:59 +0000</pubDate>
      <link>https://dev.to/leemartin/minting-cosmic-coins-for-lord-hurons-cosmic-selector-25gd</link>
      <guid>https://dev.to/leemartin/minting-cosmic-coins-for-lord-hurons-cosmic-selector-25gd</guid>
      <description>&lt;p&gt;Having worked with Lord Huron three times now on the &lt;a href="https://leemartin.dev/follow-the-emerald-star-d5394b69d7da" rel="noopener noreferrer"&gt;Follow The Emerald Star&lt;/a&gt; geofenced listening campaign, the &lt;a href="https://leemartin.dev/may-you-live-until-you-die-6f45b37ca048" rel="noopener noreferrer"&gt;Long Lost Seance&lt;/a&gt; teaser, and the &lt;a href="https://leemartin.dev/building-an-inter-dimensional-video-player-for-lord-huron-5061d53a8df2" rel="noopener noreferrer"&gt;Your Other Life&lt;/a&gt; player, I couldn’t help but notice a teaser video for their new album, The Cosmic Selector Vol. 1, go up at the beginning of the year. This video featured an otherworldly jukebox named the Cosmic Selector and, by habit, I immediately registered the domain &lt;a href="http://www.cosmicselector.com" rel="noopener noreferrer"&gt;cosmicselector.com&lt;/a&gt;. Now, I wasn’t sure if they were going to hire me again (and I wasn’t trying to hold the domain for ransom) but I had a feeling this might be the name of the album and the domain could prove to be a valuable asset for an upcoming marketing campaign. I would give it up, if asked.&lt;/p&gt;

&lt;p&gt;Flash forward to April 1, of all days, and a call was scheduled to discuss the “Cosmic Selector.” I quickly dropped the secret: I had already registered cosmicselector.com and we had a good laugh about it. Naturally, we discussed the jukebox (more on the later) and there was an existing thought to potentially make some physical coins but the purpose of said coins wasn’t clear. The answer was to not look too far. If the website was going to be the “jukebox,” the coins should be the currency to play it. So, we would set out to produce unique physical coins which could be used to play selections from the new album via a digital jukebox.&lt;/p&gt;

&lt;p&gt;Now, I usually try to do one thing I haven’t done before on each project (this is how I self-educate) but this experience required many new “firsts” for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Producing a physical asset&lt;/li&gt;
&lt;li&gt;Using NFC technology so a mobile device could interact with the coin&lt;/li&gt;
&lt;li&gt;Minting coins to a database and writing unique coin URLs to NFCs&lt;/li&gt;
&lt;li&gt;Figuring out simple coin packaging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s just the coins. The jukebox would also have a series of new problems I would need to traverse. For the sake of this dev blog, we’ll stick to the coins for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  3D Printed Coins
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjq96hju4fv7qtxl71q6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjq96hju4fv7qtxl71q6.jpg" alt="Cosmic Coin" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we knew the direction of the campaign, we jumped on a quick follow up call about potential physical coin solutions. This included super pricey metal NFC challenge coins but… our goal was to find an affordable solution that would allow us to get these into as many fans’ hands as possible. I recalled that my local barista, Josh, had recently purchased a 3D printer he was raving about and while I’m not a fan of plastic, I wondered if it would be a viable solution for our NFC coins. So, I left that call with some homework. I would order a 3D printer and look into plastic as a potential affordable solution. We also set a deadline. We would distribute these coins at the end of May at the back to back Red Rock shows, which has turned into a fan pilgrimage, and the accompanying merch bazaar.&lt;/p&gt;

&lt;h3&gt;
  
  
  48 Hours of Prototyping
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F077j11tmicymjqfv5r3r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F077j11tmicymjqfv5r3r.jpg" alt="Prototype" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I knew our time was limited to get this done so as soon as my 3D printer showed up I got to work learning how it functioned and attempting to prototype a coin. I went with a &lt;a href="https://us.store.bambulab.com/products/a1-mini?srsltid=AfmBOoovEKYTe3qM4PdudsWK14MF1c2vnQbTiXWR8VKvxb43vC4SykRs" rel="noopener noreferrer"&gt;Bambu Studio A1 mini&lt;/a&gt; because it was affordable and I read it was user friendly. I began by sizing my &lt;a href="https://www.blender.org/download/releases/4-3/" rel="noopener noreferrer"&gt;Blender&lt;/a&gt; workspace so I was modeling in millimeters and then I made sure the printer was printing at the right size. An easy experiment was to create a quarter shaped disc in Blender, print it on the Bambu, and then compare it to a real quarter. All good. &lt;/p&gt;

&lt;p&gt;I think a good prototype shows off what is possible visually so I aimed to create a design which had a simple arcade token motif on one side and a natural scene relief on the other. That’s when I learned about nozzles.&lt;/p&gt;

&lt;p&gt;A 3D printer kinda works like frosting a cake. You have nozzles of different diameters which can create designs of various levels of detail by extruding plastic. The higher the detail, the longer and more expensive the print. We found ourselves in a rather complicated position because our coin design needed to be both detailed and small. The prototype I created was a bit larger and modeled with a 0.4mm nozzle but I knew we would probably end up using 0.2mm. In addition to getting the details of a two-sided design right, we also needed a spot for the NFC sticker which would live within. I experimented with all sorts of constructions and learned a lot about the pros and cons of different printing strategies. Naturally, with every decision I made, I had to also consider how a 3rd party printer vendor might handle the print. It’s a lot to unpack over one weekend.&lt;/p&gt;

&lt;p&gt;One thing I like about plastic is that it comes in a great range of colors (kinda like vinyl variations) and I started thinking about how coins could be different colors and carry stories from different dimensions. Should a coin look like the emerald star? Maybe a coin might have a bit of moss on it because it was buried in the swamp? Maybe it should be void of color? Maybe we sent them to fans unpainted and encouraged them to design their own coins. My fiancé, &lt;a href="http://instagram.com/anneblenker" rel="noopener noreferrer"&gt;Anne Blenker&lt;/a&gt;, hand-painted one of the prototypes to simulate this idea.&lt;/p&gt;

&lt;p&gt;Finally, I did a bit of thinking about simple packaging which would secure the coin and also provide a bit of instructions on how it worked. I went with a simple coin envelope with The Cosmic Selector Vol. 1 logo printed on the front.&lt;/p&gt;

&lt;p&gt;Needless to say, it was a lot of thinking for 48 hours and all of this was presented in the most verbose client email ever. Seriously. We all jumped on another call, and rightfully so, set our sights on avoiding plastic for a higher quality material. So, I turned off my 3D printer and got back to work on the jukebox. Until…&lt;/p&gt;

&lt;h3&gt;
  
  
  Printing Returns
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbhse6207n26ukoi8qe2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbhse6207n26ukoi8qe2.jpg" alt="Colors" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two weeks before Red Rocks, we realized any alternate solutions we were planning would not arrive on time and 3D printing was officially back on. Panic. I stopped working on the jukebox and jumped right back into the coins.&lt;/p&gt;

&lt;p&gt;Since my initial prototyping session, Tony Wilson had come up with a lovely design for the coin but it was in 2D. So, I quickly got to turning it into a 3D relief design. Tony’s design was a lot smaller at 40mm x 40mm so I had to consider which elements could stay and which had to go based on 0.2mm nozzle detail. I also finalized the coin construction to use a bowl and lid method. The bottom of the design and edge side would be printed like a bowl which the NFC could sit within. The top of the design would be designed as a lid which would be fit into the bowl and glued on top. This should help speed up the assembly process. I also placed an order for a Cosmic Selector logo stamp at &lt;a href="http://StampMaker.com" rel="noopener noreferrer"&gt;StampMaker.com&lt;/a&gt; as well as an order for simple &lt;a href="https://www.amazon.com/dp/B0CGZQLYVX?ref=ppx_yo2ov_dt_b_fed_asin_title" rel="noopener noreferrer"&gt;coin envelopes&lt;/a&gt; from Amazon.&lt;/p&gt;

&lt;p&gt;It then came down to getting the coins printed. We found a partner in Scott from &lt;a href="https://www.instagram.com/standitupcomics/?hl=en" rel="noopener noreferrer"&gt;Stand It Up&lt;/a&gt;. He said the best way to print the quantity we were aiming for in the time we had remaining was to use resin. So, that’s what we did. Scott’s team printed it quickly and the coins were set to arrive the Saturday before the Wednesday show via Fedex for assembly. However, my weekend Fedex guy is notorious for not attempting to get in my building and, sure enough, the delivery was missed. Of course, Monday was Memorial Day, which means the coins would not arrive until Tuesday, the day before the first Red Rocks show. I began to realize I may in fact be hand delivering these coins to Denver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minting Coins and Writing NFCs
&lt;/h3&gt;

&lt;p&gt;While waiting for those coins to arrive, I developed a &lt;a href="https://netmaker.substack.com/p/creating-a-more-personalized-nfc?r=43mpt" rel="noopener noreferrer"&gt;workflow&lt;/a&gt; for minting coins and writing the unique coin URL to an NFC using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_NFC_API" rel="noopener noreferrer"&gt;Web NFC API&lt;/a&gt; and an Android phone. Unlike most NFC campaigns which use a single URL, our NFCs required a different unique URL for every coin because we wanted to track the type of coin it was and whether or not it was “spent” to play a selection on the jukebox. Using the Web NFC API, I could check to see if a NFC already had a Cosmic Coin URL associated with it and if not, mint a new coin in our coin database and write the new Cosmic Coin URL to the waiting NFC. Check out this separate &lt;a href="https://netmaker.substack.com/p/creating-a-more-personalized-nfc?r=43mpt" rel="noopener noreferrer"&gt;dev blog&lt;/a&gt; for more on that workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spray Paint
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2aufvn7a8lg81q58m44.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2aufvn7a8lg81q58m44.jpg" alt="Spray Paint" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Tuesday, the coins finally arrived and we quickly realized the gold resin was much too transparent. (Something we could have remedied with a bit more time.) That’s when Anne made the call to spray paint them gold, which is a common solution in 3D printing. For the Red Rocks coins, we just grabbed a can of gold spray paint from Ace Hardware but in the future we would use &lt;a href="https://www.amazon.com/Montana-Acrylic-Professional-Spray-Paint/dp/B004O78OCQ" rel="noopener noreferrer"&gt;Montana Goldchrome&lt;/a&gt; for a nice metallic shine. So, there we were, spray painting these coins in our apartment parking lot 24 hours before they would be handed to fans in Denver. A lot of neighbors asked what we were up to but they just sorta figured we were creating next year’s Mardi Gras throws really early.&lt;/p&gt;

&lt;h3&gt;
  
  
  To The Mountains
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl32rum2izahnhf2j3ddc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl32rum2izahnhf2j3ddc.jpg" alt="Fan Photo" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With no time left to ship these coins, Anne and I woke up at 3am in New Orleans to catch a 5:40am flight to Denver. That flight got us to Denver with enough time to deliver these to the Tubb’s Bazaar near downtown Denver. We made it to the bazaar and were stamping the final coin envelopes an hour or two before the doors opened. Management had the genius idea to hide these throughout the bazaar and merch offerings. Soon, word started to trickle out that fans were finding coins in their hoodies, at the bar, and behind plants. Then, someone realized the coin could be scanned. That’s when something interesting happened. I had added a statement telling fans not to share their coin url and they were such rule followers that they also didn’t share the cosmicselector.com domain. Honestly, I was pretty proud of how secretive they were and figured it was only a matter of time before the domain leaked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coin Security
&lt;/h3&gt;

&lt;p&gt;As soon as the coins were in fans’ hands, my brain started to worry about the security of the coin URLs. Even though the fans were being very secretive about their unique coin URLs, I was worried about someone else screwing it up: Google. Google Search is VERY good about indexing somewhat hidden URLs and I had this concern that all of the unique coin URLs would show up on Google and bad actors could play other fans’ coins. So, I quickly turned off search engine indexing in robots.txt for the entire website and that seemed to do the trick.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Night at Red Rocks
&lt;/h3&gt;

&lt;p&gt;Feeling like we did everything we could, we got ready for a night at Red Rocks and enjoyed some concessions on the terrace. We had the loveliest night hearing &lt;a href="https://en.wikipedia.org/wiki/Strange_Trails" rel="noopener noreferrer"&gt;Strange Trails&lt;/a&gt; from front to back. Then, the band performed some songs from the new album. At the end of one of the songs, Ben reached into his pocket and threw some coins out to the fans. It was surreal.&lt;/p&gt;

&lt;p&gt;I got to see Ben and management after the show and we celebrated the fact that we got it done. Henry (from management) told me he dropped all the coins in a puddle at some point, adding to the chaos of the coins’ journey to fans’ hands. I then turned my attention to the jukebox. &lt;/p&gt;

&lt;p&gt;Oh yeh, we have to build a jukebox…&lt;/p&gt;

</description>
      <category>nfc</category>
      <category>3dprinting</category>
      <category>music</category>
      <category>marketing</category>
    </item>
    <item>
      <title>How I Designed a Red Dead Redemption 2 Inspired Map in Mapbox Studio</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Thu, 06 Feb 2025 22:40:21 +0000</pubDate>
      <link>https://dev.to/leemartin/how-i-designed-a-red-dead-redemption-2-inspired-map-in-mapbox-studio-4gkh</link>
      <guid>https://dev.to/leemartin/how-i-designed-a-red-dead-redemption-2-inspired-map-in-mapbox-studio-4gkh</guid>
      <description>&lt;p&gt;This is a project I did with Mapbox back in 2018 while we were all playing Red Dead Redemption 2 for the first time. This rather lengthy how-to blog was always hidden away on mapbox.com but the link has been dead for years. So, I've gone ahead and reposted it here for archival purposes along with a working link to the map. Note: Mapbox Studio has evolved since I wrote this so some things may have changed technically but the approach stays the same. I hope you enjoy. - Lee Martin&lt;/p&gt;




&lt;p&gt;Like millions of others, I have been enjoying Rockstar Game’s latest installment in the &lt;a href="https://en.wikipedia.org/wiki/Red_Dead_Redemption_2" rel="noopener noreferrer"&gt;Red Dead Redemption&lt;/a&gt; series: Red Dead Redemption 2. Apart from the deep &lt;a href="https://www.nytimes.com/2018/11/23/opinion/sunday/red-dead-redemption-2-fallout-76-video-games.html" rel="noopener noreferrer"&gt;narrative&lt;/a&gt;, incredible visuals, and deadeye gameplay, I was struck by all the energy put into the user interface and in particular the game map. Rockstar’s fictional United States spans five states and many major cities including one inspired by my home: New Orleans (or in the game, Saint Denis.) Being an avid fan of Mapbox and an amateur at their &lt;a href="http://mapbox.com/studio" rel="noopener noreferrer"&gt;Studio&lt;/a&gt; product, I thought it would be fun to redesign the actual world map in the style of the game map. Herein lies a very in depth tutorial on my design journey.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.mapbox.com/styles/v1/mapbox/cjpn02blt2cw32sqrt9r8e5c3.html?fresh=true&amp;amp;title=true&amp;amp;access_token=pk.eyJ1IjoibGVlbWFydGluIiwiYSI6ImNseGtrZG9weDAyODgybG9vbHo5MTg2OXIifQ.nhi8_IBV3Crw-JCQ-gLubw#16.01/29.961597/-90.057014" rel="noopener noreferrer"&gt;Explore the map&lt;/a&gt; and read on to find out how it was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;By far the hardest part of building this map, is simply that you have to take a break from this incredible game. However, it helps to have it running in the background so you can explore the map in game or from the companion iPad &lt;a href="https://itunes.apple.com/us/app/rdr2-companion/id1293430833?mt=8" rel="noopener noreferrer"&gt;app&lt;/a&gt;. It was helpful for me to use this app to take screenshots and really understand how the map fidelity changes as the user zooms in and out. Another piece of advice is to try not to explore too much while you’re building the map and rather focus on each task. Maps, by nature, are fun to explore and I found myself using up a lot of my time simply typing in random locations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Colors
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms3hniylgypayksgzw3i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms3hniylgypayksgzw3i.jpg" alt="Colors" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the first things I did was recreate the game map’s color palette in my favorite design tool, &lt;a href="https://www.figma.com/" rel="noopener noreferrer"&gt;Figma&lt;/a&gt;. To do this, I simply used the eyedropper tool to sample some colors from a series of game screenshots. If you don’t count things like red routes and yellow mission circles, there are really only five colors on the permanent map. Take note of these because we’ll be using them a lot later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Land #DEC29B — The base of the map is a canvas color&lt;/li&gt;
&lt;li&gt;Ink #40423D — The next prominent color is a dark black ink color&lt;/li&gt;
&lt;li&gt;Water #9E9985 — Water is somewhere between ink and canvas with a hint of blue/green&lt;/li&gt;
&lt;li&gt;Contours #C8B28D — The contour lines and fills look like stains on the canvas&lt;/li&gt;
&lt;li&gt;Pencil #716454 — The viewpoints on the map are printed in a burnt tone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With your color palette built, it’s time to dig into typography.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typography
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fueh828y8yt0k3q6i3ick.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fueh828y8yt0k3q6i3ick.jpg" alt="Typography" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The game map uses several different typefaces to label places of interest from massive slab serifs to handwritten cursive. Rather than spending a single dime on paid fonts, I headed over to &lt;a href="https://fonts.google.com/" rel="noopener noreferrer"&gt;Google Fonts&lt;/a&gt; and quickly found decent alternatives to each type. Here’s the breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State Names — &lt;a href="https://fonts.google.com/specimen/Merriweather" rel="noopener noreferrer"&gt;Merriweather Black&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;City Names — &lt;a href="https://fonts.google.com/specimen/Raleway" rel="noopener noreferrer"&gt;Raleway Black&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Major Bodies of Water — &lt;a href="https://fonts.google.com/specimen/Noto+Serif" rel="noopener noreferrer"&gt;Noto Serif&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Minor Bodies of Water and Rivers — &lt;a href="https://fonts.google.com/specimen/Crimson+Text" rel="noopener noreferrer"&gt;Crimson Text&lt;/a&gt; Bold Italic&lt;/li&gt;
&lt;li&gt;Station Names — &lt;a href="https://fonts.google.com/specimen/Raleway" rel="noopener noreferrer"&gt;Raleway Black&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Natural Features — &lt;a href="https://fonts.google.com/specimen/Chau+Philomene+One" rel="noopener noreferrer"&gt;Chau Philomene One&lt;/a&gt; Italic&lt;/li&gt;
&lt;li&gt;Viewpoints — &lt;a href="https://fonts.google.com/specimen/Homemade+Apple" rel="noopener noreferrer"&gt;Homemade Apple&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Forts — &lt;a href="https://fonts.google.com/specimen/Lato" rel="noopener noreferrer"&gt;Lato&lt;/a&gt; Black&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go ahead and grab those typefaces or any better alternative you find from Google Fonts. We’ll load them into Mapbox Studio soon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Iconography
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6o3o3ihbzg5b95rgwg9x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6o3o3ihbzg5b95rgwg9x.jpg" alt="Iconography" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I turned to the Mapbox Maki Icon &lt;a href="https://www.mapbox.com/maki-icons/editor/" rel="noopener noreferrer"&gt;editor&lt;/a&gt; in order to recreate the game map’s icon set by simply setting their background to black, foreground color to white, and making them round. I then referenced the OpenStreetMap tags Mapbox makes available to build out a list of alternate categories. The final list of RDR2 places and their equivalent Mapbox place type looks something like this.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bait Shop — Aquarium&lt;/li&gt;
&lt;li&gt;Barber — Hairdresser&lt;/li&gt;
&lt;li&gt;Butcher — Butcher&lt;/li&gt;
&lt;li&gt;Doctor — Doctor, Hospital&lt;/li&gt;
&lt;li&gt;General Store — General&lt;/li&gt;
&lt;li&gt;Hotel&lt;/li&gt;
&lt;li&gt;Photo Studio&lt;/li&gt;
&lt;li&gt;Post Office&lt;/li&gt;
&lt;li&gt;Saloon — Bar&lt;/li&gt;
&lt;li&gt;Show — Cinema, Theater&lt;/li&gt;
&lt;li&gt;Stable — Bicycle Rental&lt;/li&gt;
&lt;li&gt;Stagecoach — Bus Station&lt;/li&gt;
&lt;li&gt;Tailor — Clothes&lt;/li&gt;
&lt;li&gt;Trapper — Fabric&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you’re happy with your icon set, you can download them from the editor as one zip.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale
&lt;/h3&gt;

&lt;p&gt;It’s helpful to wrap your head around the difference in scale between the world map and the game map as Mapbox will allow you to style every one of 22! different zoom scales. By comparing the geographical sizing of an area of similar size, I estimated that the Red Dead Redemption Map starts around 11 and goes up to 18 or so. So, if you zoom out to 11, your map features should have similar sizing. Conversely, if you zoom into 18, features should be of similar sizing to a completely zoomed in RDR2 map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapbox Studio
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32nuz9dljygehd9cib9c.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32nuz9dljygehd9cib9c.jpeg" alt="Mapbox Studio" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Start Blank
&lt;/h3&gt;

&lt;p&gt;I like building up my map from a blank canvas rather than using an existing template so I don’t get overwhelmed with layers and rather try to make the exact decisions necessary to recreate the style. In order to create a new blank map, head to &lt;a href="https://www.mapbox.com/studio/" rel="noopener noreferrer"&gt;Studio&lt;/a&gt; and click the “Pick a template of upload a style” button. This will bring up a new dialogue which includes a “Start Blank” button you can click to create a new blank map.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interface
&lt;/h3&gt;

&lt;p&gt;I think the Mapbox Studio interface is pretty intuitive and certainly easier to use than the item selector in RDR2. The left panel will include all of the graphical layers you’ll create similar to a layer panel in Figma or Photoshop. The open area on the right will display your map as it is designed. The top right corner includes another set of useful features and indicators including the map’s current zoom level and position. In addition, it includes a search box that allows you to quickly jump around to different locations around the world thanks to Mapbox’s geocoding technology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upload Assets
&lt;/h3&gt;

&lt;p&gt;Click on the “Fonts” button in the top right corner (or press t on your keyboard) to bring up your account fonts. From here you can click the “Upload new font” button in order to upload each of the fonts you downloaded from Google Fonts into your account. Next click on the “Images” button (or press i) to bring up your images manager and you can then upload your Maki icon set.&lt;/p&gt;

&lt;p&gt;Finally, let’s click the background layer in the left panel and click the trash icon (or press ⌘ + Delete) to delete the layer so we really have a blank map to start with. It’s now time to design our map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Land and Water
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Land
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygftyyn69mv1bipp5n1h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygftyyn69mv1bipp5n1h.jpg" alt="Land" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every feature on our map begins as a new layer in Mapbox Studio. Let’s recreate our first layer (the background layer) by clicking the “Add layer” button or simply pressing n on your keyboard. When a new layer is created, Mapbox immediately opens that layers data source selection screen. We will be doing amazing things in this tab later but for now select “Background layer” as the source and then click “Create background layer.” Mapbox will then jump to the style tab of the layer panel which will allow us to adjust the aesthetic of our layer. In the case of our land layer, let’s simply change the hex color to be the same as our land palette color. Finally, we can hover over the name background on this panel and click to edit it. Let’s call it “Land.”&lt;/p&gt;

&lt;p&gt;Good job creating your first layer. Yes it’s bare but let’s pretend it is a completely unexplored map at the moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Water
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frp1j1gvyrfqi162sm23e.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frp1j1gvyrfqi162sm23e.jpg" alt="Water" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Fill
&lt;/h4&gt;

&lt;p&gt;As someone who actually grew up down the bayou, I love how RDR2 handles water and makes it another crucial part of your exploration. Let’s add some water to our map by creating another new layer. Again, this will redirect you to our new layer’s data source tab. Instead of choosing background layer again, open “Mapbox Streets v7” in the active sources and click the “water” source. Make sure your map is completely zoomed out (you can use your mousewheel) and you should see green everywhere there is water in our world. Mapbox provides this sort of blueprint view of all of their data to help you understand what features will be visible depending on your source and filtering selections. We’ll take a closer look at this later. For now, you can click “Style” at the top of the layer panel to jump to the styling tab. From here, we can change the color of our water to reflect what we decided on earlier. I think that you’ll agree that this one simple addition almost immediately allows our app to begin to reflect some of that RDR2 map aesthetic. Now you may think that the next logical step is to simply style the 1px stroke provided by this same map layer but we’ll want to do something a bit more sophisticated. Namely, we’re going to style our water outlines to resize themselves depending on the current zoom level. For this, we’ll need to create another layer for the water outlines. Before that, let’s go ahead and make the 1px stroke transparent by setting the color’s alpha property to zero. Finally, let’s rename this layer “Water Fill.”&lt;/p&gt;

&lt;h4&gt;
  
  
  Line
&lt;/h4&gt;

&lt;p&gt;Click on “Add layer” and again select “water” from the “Mapbox Streets v7” source. In the left column of the data tab, you’ll see a list of additional features including Type, Filter, and Zoom extent. Click Type and change the type to Line. You will then see our blueprint of visible features turn into a line around the water. Yes, this is awesome. Let’s jump over to the styling tab. First, let’s change the color of our line to be the ink color from our palette. Now a 1px ink colored line is fine but wouldn’t it be nice if the width of our line grew in size as we zoomed into our map like it does on the RDR2 map? Let’s do that. In the same style tab, click “Width” in the left column. From here you’ll see the default width of 1px and a couple of additional options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Style across zoom range&lt;/li&gt;
&lt;li&gt;Style across data range&lt;/li&gt;
&lt;li&gt;Style with data conditions&lt;/li&gt;
&lt;li&gt;Use a formula&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’re beginning to realize just how powerful this tool is right? Let’s click “Style across zoom range.” Mapbox Studio allows you to style pretty much anything based on the map’s current zoom level. In the case of our water lines, we would like them to grow in size as our map’s zoom level increases. A definition of style at zoom level is called a “stop” and by default Mapbox provides a stop for the highest and lowest levels (0 and 22.) As we discussed earlier in the “Scale” section, we’re going to be focused on zoom levels that more closely represent that of the RDR2 map. Now what I did to achieve this is not scientific at all, I simply zoomed in and out of the game map to decide when water lines should appear and the general sizing of the line depending on the zoom level. Let’s adjust the first stop’s zoom level from 0 to 15. You can leave the line width at 1px. Let’s then click the 2nd stop. You can leave the zoom level at 22 but we’ll want to adjust the line width to be 36px. Zooming in and out near a coast line will show that this looks pretty good but we can do better. Right above the two stop’s you just adjusted is another feature called “Rate of change.” Let’s change our rate of change from linear to exponential and set the base to 1.3. You just created your first dynamically sized line. Nice one! One more thing. By default our water lines are joined by sharp angled corners. Wouldn’t it be nice if these could be more rounded like the RDR2 map? Well click “Join” in the left column and then choose the “Round” line join option. That should smooth things out a bit. Let’s rename this layer “Water Line.”&lt;/p&gt;

&lt;p&gt;Since we now have two layers for our water, let’s group them in a folder in the layers panel. To do this, simply click the Water Fill layer and then shift-click to also select the Water Line layer. From here you can click the little folder icon or simple press g on your keyboard to group your water layers. Finally, if you highlight the new Group at the top of the stack, you should see a pencil you can click to rename the group to “Water.”&lt;/p&gt;

&lt;p&gt;If you made it this far, be proud of yourself. You’ve already learned one of the most powerful features of Mapbox Studio and we’ll make good use of zoom level based styling in the sections to come. This is also a good time to mention that it helped me a lot to focus on one small area while designing this map rather than jumping all over the world and getting lost in exploration. A good choice is a nice city park like beautiful Stanley Park in Vancouver. Simply search “Stanley Park” in the top right corner to jump to that location or try one that has a nice mix of features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contours
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Line
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36bw4l3gmmn0qzchklbr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36bw4l3gmmn0qzchklbr.jpg" alt="Contour Line" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another key feature of the RDR2 map is its use of contour lines to give you an idea of what sort of topography you can expect from different regions of the map. Can Mapbox recreate this feature? Of course it can! Let’s add another new layer and select “Mapbox Terrain V2” in unused sources to select the “contour” data source. Then change the Type to Line just like you did with the Water Line layer. The blueprint view should now show green outlines that follow the contours of the earth. This really is a beautiful way to represent the slopes and valleys of our planet.&lt;/p&gt;

&lt;p&gt;Let’s jump to “Style” for this layer to adjust the color and width to our contour lines. First, simply adjust the hex color by using the appropriate swatch from our palette. Then, we’ll want to again style the line width according to the zoom level. Our first stop will be at the zoom level 13 with a line width of 1px. Our second stop will be at the zoom level of 22 with a line width of 36px. We’ll again adjust the line to use exponential sizing and in this instance change the base to 1.4. Finally, let’s rename this layer to “Contour Line.”&lt;/p&gt;

&lt;h4&gt;
  
  
  Fill
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadowfe3waxasxm3c88cy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadowfe3waxasxm3c88cy.jpg" alt="Contour Fill" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition to contour lines, the game map includes an indication of steeper changes by using simple hill shading. We can mimic this effect in Mapbox Studio also. Add another layer and again select “Mapbox Terrain V2” from the sources section. Instead of choosing “contour” again, we’ll instead select “hillshade.” Now hillshade will provide polygons that include features of several different levels. I decided to filter this layer to only include highlights at the highest levels. In order to filter the data, we’ll click “Filter” in the left column within our new layer’s panel. From here we’ll click “Add filter” and choose “class” from the available fields. Studio will then ask us which class we’d like to show. Click “Empty” in the class selection dialogue and then click “highlight” in the list of filters. Next, let’s add another filter to only show highlights of a certain level. Click “Add filter” again and choose “level.” From here, you can click the dropdown which says “Is any of” to change the operator to “is greater than or equal to.” You can then click “Empty” again and select the filter of 90. Well done. You just created your first layer filter.&lt;/p&gt;

&lt;p&gt;Styling for this layer couldn’t be any easier, simply change the color to the same one you used for your contour lines. This will allow the hills to blend into the contour lines the same way it does on the RDR2 map. Let’s again rename this layer “Contour Fill” and then group it along with “Contour Line” to create a new group called, you guessed it, “Contour.”&lt;/p&gt;

&lt;p&gt;Now you might notice a bit of clashing between the water outlines and contour lines. This is because the water lines should appear above the contour lines to look best. In addition to organizing our layers, the layer panel also provides a built in hierarchy that simply stacks layers on top of each other from the bottom most layer to the top most layer. So, in order to make the Water group appear above the Contour group, we simply click and drag the Water group on top. Let’s do this and we’ll be ready to move onto more lines!&lt;/p&gt;

&lt;h2&gt;
  
  
  Paths, Roads, and Borders
&lt;/h2&gt;

&lt;p&gt;From what I can tell, the RDR2 map consists of a few different line types: major roads, minor roads, paths (or trails), state borders, and railway lines. Let’s first look at these roads, paths, and borders as we can build off of our zoom level styling work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Major Roads
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgzrgy9udq26gsx9025c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgzrgy9udq26gsx9025c.jpg" alt="Major Roads" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thinking graphically, a highway line is about 3 times the width of the water line we declared earlier and also the same ink color. It helps to use these simple measurements as we continue to pile more layers onto our map. Let’s again add a new layer and choose “road” from the “Mapbox Streets v7” data source. Mapbox will immediately highlight every single road in the world but let’s filter this down to motorways only. Click “Filter,” “Add filter,” and choose “class” as the filter field. Then click “Empty” and select “motorway” as the filter. If you’re still looking at Stanley Park, you will not see a visible line because there is no Interstate running through the park. :D Simply zoom out to find the nearest motorway to the north. Once you do, let’s jump over to styling. Again, we’ll make the line our map’s ink color. We’ll then create two stops. The first at the zoom level of 13 with a line width of 1px and the second at zoom level 22 with a line width of 72px. Again, we’ll set an exponential rate of change with a base of 1.4. Finally, let’s call this layer “Major Roads.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Minor Roads
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvf5965echfmpe4zj24p.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvf5965echfmpe4zj24p.jpg" alt="Minor Roads" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Minor roads are about twice as wide as our water lines but again use the same ink color. We’ll add the same “road” data source as the “Major Roads” layer but this time add more filtering to bring in additional road types. Click “Filter,” “Add filter,” and choose “class” once again. Then click “Empty” once again and choose the “motorway_link” filter. Then instead of leaving this dialogue, we’ll click “Add new filter value” and select “primary.” Repeat this step until “secondary,” “street,” “tertiary,” and “trunk” are added to the list of filters. This will make a nice set of minor roads visible on our map. Let’s get this layer styled also. First, change the color to Ink again. Then your width should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 13, line width: 1px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 48px&lt;/li&gt;
&lt;li&gt;Exponential Change, base: 1.4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s make sure the join for our minor and major roads are round like our “Water Line” and then rename this layer to “Minor Roads.” Now let’s learn about dashed paths while adding trails to our map.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paths
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajhi1b5oc3u362pxgttq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajhi1b5oc3u362pxgttq.jpg" alt="Paths" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The RDR2 map includes dashed lines that represent walking paths and trails. We can add this in a similar fashion to previous lines with one addition to styling. You know the drill: “Add layer,” select “Mapbox Streets v7,” choose “road,” and then click “Filter.” From here, we’ll filter by the class “path” this time. Jump back over to styling and turn the color Ink once again. Nice one. Round out that join and then set up your line width to the following zoom styling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 15, line width: 1px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 24px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now in the same left column you find color and width, you’ll see a function called “Dash array” which allows you to declare sizes for dashes (and gaps between those dashes.) Click this to begin adjusting the line’s dash array. Click “Add dash with gap” and set the dash to 3 and the gap to 4. Pretty cool, right? If you zoom in, you’ll notice our path dashes are very square even though they are round in the game. Well, we can adjust this also. 💪 Similar to joins, Studio will allow us to adjust the caps of lines which by default are squared off. Click “Cap” in the left column and choose “round” as your cap type. Let then rename this layer “Paths.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Borders
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzrrseabjrk3wyubbbkfs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzrrseabjrk3wyubbbkfs.jpg" alt="Borders" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thickest line on the game map represents the borders between each major area of the map. I like to think that RDR2 wanted these to represent state borders so let’s style that data source in Studio as well. In addition to begin thicker than the lines we’ve already added, the border lines include a rather interesting dash array which we can achieve by evolving on the “Path” work we just did. First add a new layer and choose the “admin” data source from “Mapbox Streets v7.” You may need to zoom out to see the lines that become visible as this will only major borders. When you do that, you might notice that in addition to lines between states, we’ll also see lines representing maritime borders. To have our border lines end abruptly when they run into the water like on the game map, let’s filter maritime borders off. Click “Filter,” “Add filter,” and “maritime” as your filter field. Then click “Empty” and set the filter to 0 to hide the maritime lines. Nice work, now let’s style it.&lt;/p&gt;

&lt;p&gt;First things first, you guessed it: turn that line Ink colored. Let’s then set up our border’s width to be styled across a zoom range like before. Here’s the parameters that work nicely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 6, line width: 1px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 144px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Very nice. Now just round off both the “Join” and “Cap.” We can now add a dash array which is similar to the game’s map. If you look at the game map, you’ll see that the line is solid and then there is a gap, dash, gap, dash, gap of equal spacing. Let’s setup our dash array exactly that way. Click “Dash Array” and set the first dash to 10 and the gap to 1. Then click the handy “Add dash” button and set it as 1. Repeat this for another gap, dash, and gap. Setting them all to 1. That’s it! Rename this layer to “Border” and pat yourself on the back for unlocking a rare achievement: Dash Arrays.&lt;/p&gt;

&lt;p&gt;Since our roads and borders are very similar, let’s group all of these in a group called “Lines.” Congratulations, this is really starting to feel like the game map. While I’m tempted to jump straight to the Railways work with you since it is similar to roads and borders, let’s quickly add some buildings first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buildings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj5lui0jbnnkydogpkmgy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj5lui0jbnnkydogpkmgy.jpg" alt="Buildings" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The buildings on the game map couldn’t be simpler. They have a stroke that’s identical in width to our path lines (and also the same ink color.) In addition they have a fill which is the same as the contour color. Similar to water, we’ll want to break buildings into two layers (fill and lines) so we have better control over the stroke width. Let’s start by adding the fill since it should appear under the stroke.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fill
&lt;/h3&gt;

&lt;p&gt;Add a new layer and select the data “building” from the “Mapbox Streets v7” source. Jump over to Style and change the color to be the same as our contour color. Rename this layer to “Building Fill” and you’re done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Line
&lt;/h3&gt;

&lt;p&gt;Go ahead duplicate the “Building Fill” layer by select it and clicking the duplicate button or pressing d on your keyboard. Click the new layer and rename it “Building Line” then click “Select data” to bring up the data source selection for this layer. Our data source is still right because we’re still styling a building but we’ll want to change the Type from “Fill” to “Line” so we’re styling the lines instead. Once you’ve changed that, jump back to the Style tab. First, change the color of the line to Ink. Then, set up the same zoom range styling as “Path” for the width:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 15, line width: 1px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 24px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure that join is rounded and take pride in all the buildings around the world you just built. Finally, group both building layers in a new group called “Buildings.” Now let’s add our rail system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Railways
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn68pymv56ew2okqjye24.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn68pymv56ew2okqjye24.jpg" alt="Railways" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Railways are a great way to get around in the game (and real life.) I still think about taking the Rocky Mountaineer from Vancouver to Whistler a few years back. It was incredible and I spent the whole ride hanging off a viewing platform enjoying views of Howe Sound. The railways on the game map consist of three elements. First there is the overall line which is the same width as a major road. Next comes a series of dots along the path which are the same color as our Land. Finally, each station has a donut shaped ring around it. Let’s create each of these in studio. Search the following coordinates to position the map near the station I stepped out of in Whistler: -122.994,50.096.&lt;/p&gt;

&lt;h3&gt;
  
  
  Line
&lt;/h3&gt;

&lt;p&gt;Add a new layer and select “road” from “Mapbox Streets v7” as the data source. Then filter this data source by class of “major_rail” to only show major railway lines. The style of this line is similar to that of a major roadway so let’s just copy what we have there. Turn the line an Ink color and create the following zoom styling for width:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 13, line width: 1px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 72px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, set the “Join” to round and rename the layer “Railway Line.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Dots
&lt;/h3&gt;

&lt;p&gt;Duplicate the “Railway Line” and rename it “Railway Dot” then click “Select data” to navigate to the layer’s data source selector. Instead of a “Line” “Type,” let’s change to “Circle.” Mapbox will warn you that this change will drop any previous styling for this layer which is okay because this is a new layer. Click “Okay.” If you’re still positioned around the Whistler station, you should see a series of green dots following the path of the railway line. Let’s style these. Head over to styling and change the color to be the same as the land. This is our first “Circle” layer but it works the same ways as fills or lines. We’re going to simply use zoom styling to adjust the size of our circle radius so it fits nicely within our railway line. Setup the following zoom styling for your “Radius:”&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 15, line width: 1px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 28px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nice work. You just conquered circles, no problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stations
&lt;/h3&gt;

&lt;p&gt;Add a new layer and select “rail_station_label” from “Mapbox Streets v7” as the data source. You should see a single green dot light up for the Whistler station. Let’s style it. First, we’ll give it a similar styling to the “Railway Dot” layer and then we’ll add a zoom styling for it’s stroke width. Let’s start by adjusting the color to be our land color and adding the same “Radius” zoom styling as the railway dots. You can quickly add an existing layer’s styling by choosing “Apply existing value,” filtering by the name of the desired layer, and selecting the style. Once you’ve done that, you can change the “Stroke Color” to be our Ink color. Finally, let’s add some zoom styling for the “Stroke Width” property so it creates a responsive donut around our circle. These values should work nicely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 15, line width: 1px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 64px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rename this layer “Railway Station” and group all of your railway layers into a new group called “Railway.” Well done. Your world is now connected with an RDR2 inspired railway. You’re a regular Leviticus Cornwall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typography
&lt;/h2&gt;

&lt;p&gt;As covered earlier in the “Prepare” section, the RDR2 map uses over 8 different typefaces to label everything from cities and rivers to natural features and famous forts. Luckily, Mapbox Studio has data sources that cover all of these subjects. In order to add these labels to our map, we must simple choose the correct data source, set our layer type to “Symbol,” “Filter” accordingly, and then style. Let’s start with major city names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cities
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkz3mu88j15ho4t2o6u2s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkz3mu88j15ho4t2o6u2s.jpg" alt="Cities" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The game map has a few major cities which are labeled including the jewel of the south Saint Denis. You do know I live in the city it was inspired by… New Orleans? Yes, I do. Naturally that makes me love this game even more. Let’s label it. Add a new layer, select “place_label” from “Mapbox Streets v7” as your data source, and then change your layer “Type” to “Symbol.” This should light up all sorts of cities on your map. Let’s filter down by major cities only. Click “Filter” and add a filter for type, adding values for both “town” and “city.” Let’s another filter for “localrank” and set the value to 1. This will greatly decrease the label density, leaving only major cities. Once you’ve done this jump over to styling and don’t panic if you don’t see a label. “Text field” should be preselected and we’ll need to tell Studio that we wish to use the “name” property from our data sources as the text on the label. In order to do this, click the little icon under “Insert a data field” and choose name. You should then see the names show up. Let’s style them.&lt;/p&gt;

&lt;p&gt;First, set the color to Ink like our previous labels and then select “Raleway Black” as your typeface. Click the “Transform” function and choose Uppercase as the transform. Then set your letter spacing to 0.2em, giving it a bit more spacing like in the game. In general, this works pretty well but we’ll want to add some zoom styling for the text size to make it perform a bit more like the game map. Click “Size” and add the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 8, line width: 8px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width:144px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That should do nicely. Rename your layer “City Name” and let’s move on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Viewpoints
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ubmgrdmaw6dk9r93gdv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ubmgrdmaw6dk9r93gdv.jpg" alt="Viewpoints" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By far my favorite typeface on the map, may not actually be a typeface at all. It seems like RDR2 had someone hand write some of the more interesting viewpoints and locations on the map. Luckily Google Font had a typeface called &lt;a href="https://fonts.google.com/specimen/Homemade+Apple" rel="noopener noreferrer"&gt;Homemade Apple&lt;/a&gt; which comes very close to emulating the effect. Search “Ferguson Point” using the “Search places” box and we’ll work on adding a label for it. Create a new layer and choose “poi_label” from “Mapbox Streets v7” as your data source. Let’s make this layer the “Type” of “Symbol” as well. The poi data source of Mapbox is incredibly rich with data and we’ll be putting this to good use in the Iconography section later. For now, let’s “Filter” the data by the type of “viewpoint.” This should highlight Ferguson Point in Stanley Park. Let’s style it.&lt;/p&gt;

&lt;p&gt;Again, let’s make sure the “Text field” is using the data source’s name field. Instead of Ink, we’ll change the color of this label to the Pencil color we defined earlier. Let’s then change the typeface to “Homemade Apple.” Very nice. Now, I noticed on the map that a lot of the labels tend to be on two lines so let’s reduce the “Max width” to 5em in order to simulate this. Now we simply need to add some zoom styling once again for the text size. Here’s what works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 8, line width: 8px&lt;/li&gt;
&lt;li&gt;Zoom 22, line width: 72px&lt;/li&gt;
&lt;li&gt;Exponential change, base: 1.2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s probably my favorite layer of the map. Let’s name it “Viewpoints” and work on some mountain peaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Natural Features
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnttyz3a1f2nc3eoldt3n.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnttyz3a1f2nc3eoldt3n.jpg" alt="Natural Features" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The game map labels a bunch of natural features like Tempest Rim, Window Rock, and the Three Sisters in an all caps italic typeface. We can add a similar set of labels to our map by styling the mountain peak labels data source from Mapbox Studio. Add a new layer and choose “mountain_peak_label” from “Mapbox Street v7,” making sure the layer “Type” is set to “Symbol” once again. This layer by default will label anything from a peak to a hill so let’s add a “Filter” of “elevation_ft” and set it to “greater than” “10000.”&lt;/p&gt;

&lt;p&gt;Search “Pikes Peak” to reposition the map near that particular peak. We’ll style this one in a similar manner, setting the “Text field” to “name,” changing the color to Ink, and transforming the text to uppercase. We’ll once again change the word spacing to 5em in order to force the name to fall one 2 or more lines. (Pike’s Peak is small enough to stay on one line.) Finally, we can add some zoom styling for the text size. Since it is close to the same size as viewpoints, we can reuse the styling from that label. Click “Size” and choose “Apply existing value” and then select the “Viewpoints” text sizing from the list. Nice work. Rename this layer “Natural Features” and we’ll move onto some Forts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forts
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8fjbnbdp6d9z0fy6t7ck.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8fjbnbdp6d9z0fy6t7ck.jpg" alt="Forts" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Forts are a common place for adventure in the RDR series and their prominence in the story is made apparent by their permanent labeling on the game map. Let’s make sure to label the forts (and castles) on our map as well. Create a new layer and choose “poi_label” from “Mapbox Streets v7,” making sure the layer “Type” is set to “Symbol.” We’ll then “Filter” the data by type of “fort” and “castle.” Search for “Fort Independence” and jump over to styling. Set the “Text field” to “name,” change the color to Ink, transform the type to uppercase, set the max width to 5em, and adjust the letter spacing to 0.1em. You can then change the typeface to “Lato Black.” Once again, you can use the same zoom styling for size as “Natural Features” and “Viewpoints” by simply clicking “Size” and choosing “Apply existing value.” Rename this layer “Forts” and we’ll add some train stations next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stations
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh440lbavmxix6a80hbqd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh440lbavmxix6a80hbqd.jpg" alt="Stations" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition to adding a circle for each railway station, the game map chooses to label some of the stations that appear outside of town. For the sake of our map, let’s go ahead and add similar labels to the major rail stations on our map. Add a new layer and choose “rail_station_label” from “Mapbox Streets v7,” making sure the layer “Type” is set to “Symbol.” Then create a “Filter” to make sure the “network” is “rail.” “Whistler” is once again a good location to jump to because it has a station right on the line. Let’s search for that location and then jump to styling. You know the drill: set the “Text field” to name, color to Ink, transform to uppercase, and max width to 5em. We’re also going to set the “Letter spacing” to be 0.2em and the font to be “Raleway Black” like the city names. We can once again reuse the same size styling from “Natural Features.” Nice.&lt;/p&gt;

&lt;p&gt;Now the name of the station should appear right on top the line. However, on the game map, the label for Bacchus Station and Benedict Point station is actually positioned off to the side. We can create a similar layout by adjusting the symbol position. Click the “Position” tab to jump to this function. First, let’s set the “Text anchor” to be “Left” so that the label is anchored, you guessed it, to the left. Now there is probably still a bit of overlap between “Whistler” and the donut station, we’ll need to offset the text slightly to the right in order to remedy this. This will work best as a simple zoom styling. We’ll keep the rate of change to linear this time and setup the following stops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom 12, x: 0em, y: 0em&lt;/li&gt;
&lt;li&gt;Zoom 22, x: 2em, y; 0em&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nice, the label should now be positioned slightly to the right. Rename this layer “Stations” and we’ll start taking a look at water labels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Major Water Body
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhgxhd1yd2r8nha18ff7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhgxhd1yd2r8nha18ff7.jpg" alt="Major Water Body" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The largest body of water on the game map is Flat Iron Lake, followed by the San Luis and Lannahechee rivers. These are very simply styled as large labels. Water is rather tricky to style simply because it comes in all shapes and sizes. However, Mapbox will actually allow us to filter by the area of water to distinguish between large and small water sources. Let’s give that a whirl. “Mille Lacs Lake” is a hefty lake in Minnesota so let’s search for that one to position our map. Zoom out to make sure the whole lake is in view. Then create a new layer and choose “water_label” from “Mapbox Streets v7” as our data source. Once again, set the layer “Type” to “Symbol.” If you click on the Mille Lacs Lake feature on the blueprint map, you’ll see that the lake has an area of 1082656512. Let’s use that number as a starting point to distinguish large bodies of water. Simple add a “Filter” for “area” that is “greater than or equal to” 1000000000. With that filter setup, let’s get into styling.&lt;/p&gt;

&lt;p&gt;Once again, set your “Text field” to use the data’s name, adjust the color to Ink, and transform the text to uppercase. We’ll also want to set the letter spacing to 1em and adjust the “Line height” property to be 2em. You can then change the font to “Noto Serif Bold Italic.” You can then apply the same styling for “Size” as the “City Name” layer. Go ahead and rename that layer “Major Water Body” and we’ll work on minor water bodies next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minor Water Body
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yppym6q6b4szw2icvt9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yppym6q6b4szw2icvt9.jpg" alt="Minor Water Body" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For all smaller water bodies (but not too small) we’re going to apply a different label treatment. This is found on the game map in the form of ponds, pools, lagoons, and smaller lakes. Let’s start by duplicating the “Major Water Body” label you just created and renaming it “Minor Water Body.” Then jump back to your data source by clicking “Select Data.” From here we’ll adjust the “Filter” to areas of water “less than” 1000000000 and also “greater than” 10000. (I noticed a couple of fountains gaining this distinction when not adding this threshold.) You may adjust as needed. Now search the map for a smaller lake you’re familiar with and open up the style tab.&lt;/p&gt;

&lt;p&gt;Since the style is still that of a “Major Water Body,” let’s adjust it. First, change the font to “Crimson Text Bold Italic.” Reset the text transform back to “None” so that it is no longer in all caps. Bring the letter spacing down to 0.1em and the line height down to 0.9em, making the text much more snug. You should also change the max width to 5em. Finally, let’s apply the same “Size” styling as our Forts and Stations. It’s now time to look at rivers!&lt;/p&gt;

&lt;h3&gt;
  
  
  Rivers
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftlgvwgt99n37pwqze7kd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftlgvwgt99n37pwqze7kd.jpg" alt="Rivers" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;RDR2 will have you fording plenty of rivers from the Kamassa to the Dakota. These are set in the same type and sizing as minor water bodies. What’s unique about their labeling is how it follows the shape of the actual river. Mapbox Studio, once again, has a distinction for this type of label: Waterway Label. Let’s add this layer to label our map’s rivers as well. Create a new layer and choose “waterway_label” from “Mapbox Streets v7” as your data source and again set the layer type to “Symbol.” Let’s go ahead and then “Filter” the data by the “class” of “river” so we’re not styling any canals or streams in this manner. You can then jump over to styling.&lt;/p&gt;

&lt;p&gt;This should now be second nature to you but let’s it anyway: adjust the “Text field” property so it uses the data’s name field, set the color to Ink, change the font to “Crimson Text Bold Italic,” and the letter spacing to 0.1em. You can then give it the same “Size” styling as the “Minor Water Bodies.” Sweet, now let’s make it follow the curvature of the river. Click the “Placement” tab and choose “Line center” as the “Placement” instead of circle. Your labels should now follow the line of the river. Good work! Let’s rename this layer “Rivers.”&lt;/p&gt;

&lt;p&gt;Before we move on, let’s group all of our typography layers in a new group called “Typography.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Icons
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwgmyns103i5wnprxc5y.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwgmyns103i5wnprxc5y.jpg" alt="Icons" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The game map uses many icons and markers to help you navigate. For the sake of our map, we’ll be focused on emulating the permanent place icons you can find in each town such as Saloons and Butchers. Adding an Icon to your Mapbox map is similar to adding a text label. It starts with picking the appropriate data source, making sure the layer is of type “Symbol,” and filtering down to the appropriate category. Make sure you have your icon images we created earlier uploaded to Studio and let’s quickly add them to our map.&lt;/p&gt;

&lt;h3&gt;
  
  
  First Icon
&lt;/h3&gt;

&lt;p&gt;We’ll start by creating an icon for Post Offices. It’s here you can mail in your legendary fish, read letters from love ones, and get hot tips about possible stagecoach robberies. Start by adding a new layer to your map and choosing “poi_label” from the “Mapbox Streets v7” data source. Make sure your layer “Type” is set to “Symbol.” Then add a “Filter” of “type” and filter it by the value of “Post Office.” Make sure your map is positioned near a Post Office and let’s jump to Style once again.&lt;/p&gt;

&lt;p&gt;Woah, wait. Before you start adding “name” as your “Text field…” Don’t. You don’t need to anymore. Instead, click on the “Icon” tab as that’s where we’ll be spending our time. From here, simply clicking the mail icon to establish it as your symbol’s icon. I would then set the size to 2. That’s it! Rename the layer “Post Offices” and let’s create the others.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Icons
&lt;/h3&gt;

&lt;p&gt;For each other icon, you simply need to start by duplicating the “Post Office” and then changing the data source filter to reflect your new category. Finally, make sure to change the icon to reflect the new category and rename the layer accordingly. Reference the category list from preparation section to make this easier. Once you get all of these icons added to your map, group all of the layers in a new group called Iconography.&lt;/p&gt;

&lt;h2&gt;
  
  
  Texture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe97gphco26i4v0f1tvto.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe97gphco26i4v0f1tvto.jpg" alt="Texture" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our map, thus far, has been composed of solid colors but a closer look at the game map reveals a subtle texture. We can add this easily to our map by creating a new background layer, giving it a pattern, and moving it to the top of our layer stack. Mapbox Studio expects an SVG vector texture so I designed a very simple grain texture in Illustrator and followed Mapbox’s &lt;a href="https://www.mapbox.com/help/studio-troubleshooting-svg/" rel="noopener noreferrer"&gt;instructions&lt;/a&gt; on putting it on the right format. Go ahead and download the texture I created &lt;a href="https://docs.google.com/document/d/1ZwIKZ3dgW7o164GWEMV27T7IakwQUkJPKJo16iQe3pY/edit#" rel="noopener noreferrer"&gt;here&lt;/a&gt; and upload it to your map images. Once you’ve done that, add a new layer and instead of adding a data source, choose “Background layer” and click “Create background layer.” This will cause your map to black out but we can fix this by simply adjusting the “Pattern.” Click it and choose your texture from the images. Then adjust the layer opacity down to 0.01. Nice work!&lt;/p&gt;

&lt;h2&gt;
  
  
  Game Over?
&lt;/h2&gt;

&lt;p&gt;Not quite. If you made it this far, congratulate yourself. You now have an incredible map which you can use to start blurring the lines between your game life and the real world. There are plenty of elements and features to the game map that I’ve left out which you should try to tackle. Here’s a few additional achievements you can unlock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drawing an illustration of a fish, uploading it as a map icon, and adding it to your favorite lake as a legendary fish location&lt;/li&gt;
&lt;li&gt;Adding yourself as a main quest icon. (Homemade Apple works great for this)&lt;/li&gt;
&lt;li&gt;Importing your newly created map into &lt;a href="https://www.mapbox.com/mapbox-gl-js/api/" rel="noopener noreferrer"&gt;Mapbox GL JS&lt;/a&gt; to recreate the game’s &lt;a href="https://leemartin.dev/recreating-the-red-dead-redemption-2-compass-using-mapbox-4bc670b141cd" rel="noopener noreferrer"&gt;compass&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Using the &lt;a href="https://www.mapbox.com/api-documentation/#directions" rel="noopener noreferrer"&gt;Mapbox Directions API&lt;/a&gt; to build red line routing&lt;/li&gt;
&lt;li&gt;Importing a geojson of your state or county’s boundaries and styling it to make yourself Wanted in that area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I just want to thank Rockstar Games for the inspiration. You really are building worlds there. Finally, thanks to Mapbox for supporting this adventure and building the tools that make it possible.&lt;/p&gt;

&lt;p&gt;Thank you for coming along on this cattle drive.&lt;/p&gt;

</description>
      <category>mapbox</category>
      <category>gamedev</category>
      <category>design</category>
      <category>map</category>
    </item>
    <item>
      <title>Building an AI Powered Camera for David Bowie</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Sat, 01 Feb 2025 16:41:23 +0000</pubDate>
      <link>https://dev.to/leemartin/building-an-ai-powered-camera-for-david-bowie-239d</link>
      <guid>https://dev.to/leemartin/building-an-ai-powered-camera-for-david-bowie-239d</guid>
      <description>&lt;p&gt;Last year, when Universal Music Group announced they were taking their music off TikTok, I immediately started thinking about ways you could recreate some of the TikTok user experience on the web.&lt;/p&gt;

&lt;p&gt;I began with the feed. Pretty simple to emulate the snappy vertical scrolling the app is known for. Then, I set my sights on the camera. Naturally, I’ve built many web based camera concepts over the years but TikTok is known for its AI filters. So, I wanted to try and develop a web based, AI powered camera. The outcome of that (unofficial) experiment is called &lt;a href="https://heroes.camera/" rel="noopener noreferrer"&gt;Heroes Camera&lt;/a&gt; and it uses AI to face swap you and David Bowie so you can become a parody of the “Heroes” album cover. Honestly, my focus was less on AI and more on user experience. I wanted the app to feel simple, familiar, and magic. Become a hero, today:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.heroes.camera" rel="noopener noreferrer"&gt;www.heroes.camera&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I originally put this app up, it garnered a few 100 usages and a chuckle from friends and followers. Then, I noticed a swift uptick of uses… 1000, 10000, 50000…? What was happening? I dived into the analytics only to discover that the app went viral in Japan. As it turns out, David Bowie had an incredible &lt;a href="https://sabukaru.online/articles/david-bowies-love-for-japan" rel="noopener noreferrer"&gt;relationship&lt;/a&gt; with Japan and Japanese style. A lot of this was thanks to his Japanese costume designer Kansai Yamamoto. Apparently the application really resonated with Japanese users and it ended up being covered on &lt;a href="https://news.yahoo.co.jp/expert/articles/822ced2f364670edc19c73e5bfdd30edcd18d9b4" rel="noopener noreferrer"&gt;Yahoo Japan&lt;/a&gt;. Since then, I visited Japan for 3 months and can say I also have a deep affection for this country and its people. I can’t wait to go back. To all of you from Japan who used the app: ありがとうございます!&lt;/p&gt;

&lt;p&gt;Read on to learn how this app came together in a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/909700391" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This simple app is built on Nuxt and uses a face swap model from &lt;a href="https://replicate.com/" rel="noopener noreferrer"&gt;Replicate&lt;/a&gt; in order to swap David Bowie’s face with your own. All we need to do is capture a new photo of the user's face or allow them to submit an existing photo, then we’ll pass that photo alongside the Heroes cover to swap faces. Like most AI stuff, it is fearfully simple. Let’s start by getting a photo from the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capture Photo or Image Upload
&lt;/h3&gt;

&lt;p&gt;I recently discussed in a dev blog for The Black Keys how I integrate MediaDevices to access a user’s camera and capture a photo of them (or their surroundings.) Check out that &lt;a href="https://www.leemartin.com/tattoo-shop" rel="noopener noreferrer"&gt;dev blog&lt;/a&gt; for more info. In order to obtain an existing image from a user’s photo library or computer, we just need a file picker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;”file”&lt;/span&gt; &lt;span class="na"&gt;accept=&lt;/span&gt;&lt;span class="s"&gt;”image/*”&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;”fileSelected”&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a bit of Javascript. I actually read the image and place it onto an input canvas as I’ll be passing a data url to the prediction function later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// File selected&lt;/span&gt;
&lt;span class="c1"&gt;// —-------&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fileSelected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// File&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;// Create url&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// New image&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// On load&lt;/span&gt;
  &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Prepare image&lt;/span&gt;
    &lt;span class="nf"&gt;prepareImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Image src&lt;/span&gt;
  &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Prepare image&lt;/span&gt;
&lt;span class="c1"&gt;// —-------&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;prepareImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create canvas&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Resize&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;
  &lt;span class="c1"&gt;// Context&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw image&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Update input&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that our image has been drawn on a canvas, we can send it through to Replicate so it may predict the face swap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Replicate to Swap Faces
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxkgm4gkzj9a5cll8pab.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxkgm4gkzj9a5cll8pab.jpg" alt="Heroes" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s first set up a Nuxt &lt;a href="https://nuxt.com/docs/guide/directory-structure/server" rel="noopener noreferrer"&gt;server&lt;/a&gt; route which will run the face swap model on Replicate based on a target image (the Heroes cover) and a source image (the user’s photo.) We’ll integrate the official Replicate Node.js &lt;a href="https://classic.yarnpkg.com/en/package/replicate" rel="noopener noreferrer"&gt;client&lt;/a&gt; in order to do this. This code is pretty straightforward but it is worth noting that I store my replicate config details using the Nuxt composable &lt;a href="https://nuxt.com/docs/api/composables/use-runtime-config" rel="noopener noreferrer"&gt;useRuntimeConfig&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Replicate&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;replicate&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Body&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Config&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Client&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;replicate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Replicate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replicateApiToken&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Model&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replicateModel&lt;/span&gt;

  &lt;span class="c1"&gt;// Run model&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;target_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;heroes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jpg&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;source_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source_image&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Return&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, in order to call this from the client, we merely need to post the input photo as a Base64 to this new API endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Predict&lt;/span&gt;
&lt;span class="c1"&gt;// —-------&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Source image as base64&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;jpeg&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Predict&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;predict&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;source_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceImage&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

 &lt;span class="c1"&gt;// Image&lt;/span&gt;
 &lt;span class="c1"&gt;// data.image&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now decide how we want to display the &lt;code&gt;data.image&lt;/code&gt; result back to our user. You can simply throw it up in an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag or generate a reveal video on the fly like I’ve done using &lt;a href="https://pixijs.com/" rel="noopener noreferrer"&gt;Pixi.JS&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder" rel="noopener noreferrer"&gt;MediaRecorder&lt;/a&gt;. Perhaps a topic for another dev blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The web can literally do anything these days and it is a free sandbox to explore and challenge the artist/fan relationship. When it comes to relying on social platforms like TikTok, I give all artists the same advice:&lt;/p&gt;

&lt;p&gt;Use everyone.&lt;br&gt;
Trust no one.&lt;/p&gt;

&lt;p&gt;Social platforms come and go and typically leave a lot of carnage in their wake. It’s important to grab attention where you can but do not be surprised if that community you’re building disappears overnight. Focus as much attention on channels you own, like email newsletter, sms, past purchasers of your merch store, etc. Only then, will we be kings and queens.&lt;/p&gt;

</description>
      <category>music</category>
      <category>marketing</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Tattooing The Black Keys Fans Using MediaDevices and PixiJS</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Thu, 30 Jan 2025 15:07:08 +0000</pubDate>
      <link>https://dev.to/leemartin/tattooing-the-black-keys-fans-using-mediadevices-and-pixijs-hhh</link>
      <guid>https://dev.to/leemartin/tattooing-the-black-keys-fans-using-mediadevices-and-pixijs-hhh</guid>
      <description>&lt;p&gt;One of the classic ways a fan can declare their loyalty for a band is by getting a tattoo of the band’s logo inked somewhere on their body. Whether that’s the Foo Fighters “FF,” the Slipknot tribal “S,” or yes, the Black Eyed Peas “?.” In fact, I have a sneaking suspicion that some of you reading this blog have some music related ink on you.&lt;/p&gt;

&lt;p&gt;The aesthetic of The Black Keys new release, No Rain No Flowers, is based around the world of American traditional tattoos. I’m talking roses, hearts, scrolls, and sailor jerry inspired typography. As an interactive extra to the album and tour announcement, we’ve developed a new web app that allows fans to virtually tattoo themselves to show their allegiance and then encourage them to join the band's fan club, The Lonely Boys &amp;amp; Girls Club. I researched so many ways to accomplish this (AR, skin segmentation, marker tracking, etc) but landed on something simple and functional: 2D sticker tech. Using either your front or back camera, you can position, rotate, and scale the tattoo on your body and snap a photo of the result. We use a bit of blending and opacity to allow it to bleed into your skin. It’s certainly easier to give someone else a tattoo than give yourself one but I think that’s true to form. &lt;/p&gt;

&lt;p&gt;Are you ready to be inked? Join the club today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tattoo.theblackkeys.com" rel="noopener noreferrer"&gt;tattoo.theblackkeys.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read on to find out how this activation was developed using PixiJS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1051687730" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;For this application, we gain access to the user’s camera using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices" rel="noopener noreferrer"&gt;MediaDevices&lt;/a&gt; and then place it onto a &lt;a href="https://pixijs.com/" rel="noopener noreferrer"&gt;PixiJS&lt;/a&gt; canvas as a &lt;a href="https://pixijs.com/8.x/examples/sprite/video" rel="noopener noreferrer"&gt;video sprite&lt;/a&gt;. Then, we load the tattoo as an additional sprite and give it a bit of opacity and blending to bleed it into the user’s skin. Simple controls are added to allow the user to rotate, scale, and position the tattoo for the perfect inking. &lt;/p&gt;

&lt;h3&gt;
  
  
  Accessing Camera
&lt;/h3&gt;

&lt;p&gt;I’ve written about accessing a user’s camera using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" rel="noopener noreferrer"&gt;getUserMedia&lt;/a&gt; method of MediaDevices many times before. Not much has changed with this API implementation over time but I have personally started using a custom Typescript written Vue composable to integrate this technology into the experiences I'm building. I’ve made this app’s implementation available as a gist &lt;a href="https://gist.github.com/leemartin/6c7f8c6d563af8db3a76749938d35592" rel="noopener noreferrer"&gt;here&lt;/a&gt;. As long as I have a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tag with the id “cameraVideo” somewhere in my dom, all I need to do is call the &lt;code&gt;start()&lt;/code&gt; method to gain access to the user’s camera. I wrap this method in a promise and fire a resolve only when the &lt;code&gt;onloadedmetadata&lt;/code&gt; event is complete, signaling the camera stream is ready for action. I’ve also gone ahead and caught the primary errors and have provided real sentences I can display for my users. For this particular app, I’ve added the &lt;code&gt;flip()&lt;/code&gt; method to flip the user’s camera from back to front. I also track which way the camera is facing using &lt;a href="https://nuxt.com/docs/api/composables/use-state" rel="noopener noreferrer"&gt;useState&lt;/a&gt; so my app can react appropriately (usually involves mirroring the camera image horizontally when it is facing the user.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Pixi Setup
&lt;/h3&gt;

&lt;p&gt;One tip from the start is that since we’re going to be capturing the Pixi canvas as an image later, we’ll need to preserve the drawing buffer when initializing the Pixi app. You can do this by adding the appropriate setting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pixi app&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Init&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;pixiCanvas&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;preserveDrawingBuffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since our camera stream is being displayed in a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element on our page, we can easily add it as a sprite on Pixi by using the &lt;code&gt;from()&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sprite from video&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoSprite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes you’re dealing with a canvas size that is different from the camera stream size. To handle this I use a little utility library called &lt;a href="https://www.npmjs.com/package/intrinsic-scale" rel="noopener noreferrer"&gt;intrinsic-scale&lt;/a&gt; to calculate the dimensions and position which would resize the camera in a way that “covers” the canvas. It’s extremely helpful when I’m building these camera apps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Calculate covering&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resizeToFit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoHeight&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Size&lt;/span&gt;
&lt;span class="nx"&gt;videoSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;
&lt;span class="nx"&gt;videoSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;

&lt;span class="c1"&gt;// Position&lt;/span&gt;
&lt;span class="nx"&gt;videoSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;
&lt;span class="nx"&gt;videoSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the tattoo, we just use the Assets load method of PixiJS to load the image texture and then create a new tattoo sprite using it. In order to blend the tattoo into the user’s skin, we can lower the opacity slightly and also apply a multiply blend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Load tattoo texture&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tattooTexture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="nx"&gt;tattoo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jpg&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Create tattoo sprite&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tattooSprite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Sprite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tattooTexture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Apply multiply blend&lt;/span&gt;
&lt;span class="nx"&gt;tattooSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blendMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="nx"&gt;multiply&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;

&lt;span class="c1"&gt;// Adjust opacity&lt;/span&gt;
&lt;span class="nx"&gt;tattooSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user is given control to reposition, rotate, and scale the sprite. PixiJS has a great example regarding dragging &lt;a href="https://pixijs.com/8.x/examples/events/dragging" rel="noopener noreferrer"&gt;here&lt;/a&gt;. For rotation and scaling, I decided to wire up a &lt;a href="https://www.w3schools.com/howto/howto_js_rangeslider.asp" rel="noopener noreferrer"&gt;HTML range slider&lt;/a&gt; using Vue reactivity. When the user changes the value via the slider, we can adjust the property on the tattoo sprite. Here’s the HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;”range”&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;”0”&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;”2”&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;”scale”&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the Javascript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reactive&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Watch scale&lt;/span&gt;
&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;tattooSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Originally, I used &lt;a href="https://hammerjs.github.io/" rel="noopener noreferrer"&gt;HammerJS&lt;/a&gt; to allow the user to use their fingers to do this but I found it rather difficult to use two fingers and try to capture a photo of myself. It’s a bit easier to simply hold my phone firmly and use the sliders to adjust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inking Tattoo
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F42dqnxwismfs6tpn9miw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F42dqnxwismfs6tpn9miw.jpg" alt="Tattoo example" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order to ink the user aka take the photo, we simply need to use the &lt;code&gt;toDataURL()&lt;/code&gt; and/or &lt;code&gt;toBlob()&lt;/code&gt; method of HTML canvas. I use toDataURL to capture a preview url which I can display back to the user in an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag on the outro page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Canvas to data url&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pixiCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;jpeg&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;”url”&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blob is used to provide the user with either a &lt;a href="https://gist.github.com/leemartin/4734e5d350ae198ee3e6098f16577b84" rel="noopener noreferrer"&gt;download&lt;/a&gt; or we can use it alongside the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API" rel="noopener noreferrer"&gt;Web Share API&lt;/a&gt; to prompt the user to share the image via one of their installed apps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Canvas to blob&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pixiCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;jpeg&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// File from blob&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;tattoo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jpg&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Web share&lt;/span&gt;
&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Side story here. I always wanted to get my father’s American traditional style tattoo of a rose and scroll adorned with the timeless phrase “Mom.” Or as he calls it, “The worst decision of my life.” 😅 However, my older sister, Maria,  beat me to it! However, I still plan on joining the The Loving Sons &amp;amp; Daughters Club soon.&lt;/p&gt;

&lt;p&gt;Special thanks to Amber Nagle, Nina Schollnick, and David Adcock for dragging me to the tattoo shop with them. Be on the lookout for new tattoo design options at The Lonely Boys &amp;amp; Girls Tattoo Shop soon.&lt;/p&gt;

</description>
      <category>music</category>
      <category>marketing</category>
      <category>socialmedia</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Finding Stars and Affirmations in the Sky with Three.js for Ayra Starr</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Mon, 01 Apr 2024 15:00:00 +0000</pubDate>
      <link>https://dev.to/leemartin/finding-stars-and-affirmations-in-the-sky-with-threejs-for-ayra-starr-2e4g</link>
      <guid>https://dev.to/leemartin/finding-stars-and-affirmations-in-the-sky-with-threejs-for-ayra-starr-2e4g</guid>
      <description>&lt;p&gt;The Afrobeats revolution was well underway at &lt;a href="https://mavinrecords.com/"&gt;Mavin Records&lt;/a&gt; long before Universal Music Group &lt;a href="https://www.forbes.com/sites/imeekpo/2024/02/27/umg-buys-major-stake-in-mavin-records-set-to-boost-afrobeats-scene/?sh=299ee14b2a65"&gt;purchased&lt;/a&gt; a majority stake in the label, thanks to the meteoric rise of artists like &lt;a href="https://en.wikipedia.org/wiki/Rema_(musician)"&gt;Rema&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Ayra_Starr"&gt;Ayra Starr&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Fans are encouraged to visit &lt;a href="https://affirmations.ayrastarr.com"&gt;affirmations.ayrastarr.com&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;Here's how we used &lt;a href="https://threejs.org"&gt;Three.js&lt;/a&gt; to place and find affirmations in the sky.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/929072102" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://blog.bitsrc.io/using-three-js-to-hear-the-dawn-with-jack-white-bde73334b95a"&gt;Twilight Receiver&lt;/a&gt; to the Pop Smoke &lt;a href="https://leemartin.dev/building-an-interactive-tracklist-reveal-for-pop-smoke-ba0d64e2b784"&gt;Tracklist Reveal&lt;/a&gt;, it's a simple but powerful mechanic. Each time I've developed one of these, I dreamed of a little wayfinder component (inspired by &lt;a href="https://apps.apple.com/us/app/sky-guide/id576588894"&gt;Sky Guide&lt;/a&gt;) 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 &lt;a href="https://www.leemartin.com"&gt;dev blogs&lt;/a&gt; on web AR projects for a more indepth look at how I build these apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Placing Star in Sky
&lt;/h3&gt;

&lt;p&gt;The star in our sky is simply a Three.js &lt;a href="https://threejs.org/docs/index.html?q=sprite#api/en/objects/Sprite"&gt;Sprite&lt;/a&gt; with a star image texture. We place it in a random place in the sky using &lt;a href="https://threejs.org/docs/index.html?q=vector#api/en/math/Vector3.setFromSphericalCoords"&gt;setFromSphericalCoords&lt;/a&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Texture&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;texture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextureLoader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/images/star.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Color space&lt;/span&gt;
&lt;span class="nx"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colorSpace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SRGBColorSpace&lt;/span&gt;

&lt;span class="c1"&gt;// Material&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SpriteMaterial&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;depthTest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Star&lt;/span&gt;
&lt;span class="nx"&gt;star&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sprite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Spherical position&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setFromSphericalCoords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MathUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degToRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MathUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degToRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Copy position&lt;/span&gt;
&lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Add to scene&lt;/span&gt;
&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Finding Star in Sky
&lt;/h3&gt;

&lt;p&gt;In order to allow users to use their device as a controller to adjust the position of the camera and &lt;em&gt;find&lt;/em&gt; stars, I use the depreciated &lt;a href="https://gist.github.com/leemartin/6e0a5ce4a5d239eb3c84a4efbee43a9a"&gt;DeviceOrientationControls&lt;/a&gt; by &lt;a href="https://www.npmjs.com/package/patch-package"&gt;patching&lt;/a&gt; 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 &lt;a href="https://gist.github.com/leemartin/6f9bab074f36e74a1a6034f14697bb2c"&gt;composable&lt;/a&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Device Orientation Controls&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeviceOrientationControls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just make sure to update the controls in your render step using &lt;code&gt;controls.update()&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Targeting Star
&lt;/h3&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Raycaster&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raycaster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Raycaster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Pointer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pointer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, back in our render step. We can update the raycaster with the latest camera position and use the &lt;a href=""&gt;intersectObject&lt;/a&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Update ray&lt;/span&gt;
&lt;span class="nx"&gt;raycaster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFromCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Check for intersections&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intersects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raycaster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intersectObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// If intersects&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intersects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Targeting star&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Not targeting star&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's look at the new piece of UX: the wayfinder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Star Wayfinder
&lt;/h3&gt;

&lt;p&gt;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 &lt;a href="https://medium.com/nerd-for-tech/local-space-vs-world-space-in-unity-6a9944470478"&gt;local space&lt;/a&gt;. We can then calculate the directional angle in degrees using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2"&gt;atan2&lt;/a&gt; and adjust it by -90°. We then have a degree angle we can use with CSS to rotate the wayfinder element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Clone star position&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Convert from world space to camera's local space&lt;/span&gt;
&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;worldToLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Calculate angle in degrees&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;angleDeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;

&lt;span class="c1"&gt;// Adjust by 90 degrees&lt;/span&gt;
&lt;span class="nx"&gt;angleDeg&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;

&lt;span class="c1"&gt;// Rotate compass&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#wayfinder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`rotate(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;angleDeg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;deg)`&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be on the lookout for an evolution of this component in future projects.&lt;/p&gt;

</description>
      <category>three</category>
      <category>vue</category>
      <category>nuxt</category>
      <category>music</category>
    </item>
    <item>
      <title>Using Vue Computed Properties to Count Down to New Armin van Burren</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Sun, 24 Mar 2024 17:07:11 +0000</pubDate>
      <link>https://dev.to/leemartin/using-vue-computed-properties-to-count-down-to-new-armin-van-burren-130n</link>
      <guid>https://dev.to/leemartin/using-vue-computed-properties-to-count-down-to-new-armin-van-burren-130n</guid>
      <description>&lt;p&gt;I’ve always thought of the countdown clock as a mainstay digital marketing tactic and I’ve developed a lot of them in my career. Maybe 50? A slow ticking clock builds anticipation but also helps establish “when” something will happen. And, isn’t that what we’re trying to do? Increase hype and awareness through thematic means and reward fans for their attention. &lt;/p&gt;

&lt;p&gt;A few weeks back Janice Renée Wendel and Lucas Wijkhuizen from &lt;a href="https://www.armadamusic.com"&gt;Armada Music&lt;/a&gt; contacted me about an upcoming &lt;a href="https://en.wikipedia.org/wiki/Armin_van_Buuren"&gt;Armin van Buuren&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Gryffin"&gt;Gryffin&lt;/a&gt; track “What Took You So Long.” The &lt;a href="https://www.instagram.com/jorisvanmeegen/"&gt;Joris van Meegen&lt;/a&gt; designed single visual includes a sort of ancient timepiece on top of a geometric vector design. While the first thing that came to mind was a rather complicated “sundial” concept I’ve been shopping around since the Daniel Ceasar &lt;a href="https://www.leemartin.com/mate-in-one"&gt;campaign&lt;/a&gt;, the limited time we had led us to a simpler, countdown “clock” concept. An obvious start would be animating the hand/needle/gnomon on the timepiece so it makes one revolution from the start of our campaign to the track releasing. However, once you have a start and end time, you also gain a “percentage” of the time elapsed. What if we also included a preview clip of the song and the amount of audio you get to listen to is connected to how much of the countdown has elapsed? Using the outer edge of the geometric design, we established a series of time “segments” which unlock over time and in turn unlock more of the audio preview. It’s a simple tactic which will hopefully increase anticipation.&lt;/p&gt;

&lt;p&gt;In an effort to generate some pre-saves, this activation is hidden behind a &lt;a href="https://www.feature.fm"&gt;Feature.fm&lt;/a&gt; presave, which provides a post pre-save redirect url. So, once a user successfully presaves the track via their DSP of choice, Feature redirects them to our clock. Presave “What Took You So Long” today to enjoy an &lt;a href="https://www.whattookyousolong.net"&gt;evolving preview&lt;/a&gt; of the upcoming release and read on to learn how it came together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/926852800" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A Vue &lt;a href="https://vuejs.org/guide/essentials/computed"&gt;computed property&lt;/a&gt; is defined as a data property which depends on another property. It is a surprisingly simple and powerful expression which allows you to compute all sorts of useful properties based on the values of another. In the case of this app, "time" drives the computation of all of the properties that make our clock tick. Let's start from the beginning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Countdown
&lt;/h3&gt;

&lt;p&gt;Every countdown clock has a current (now) time and the time we are counting down to. Our clock also requires a start time so we can determine the percentage time passed since the start of our campaign. We can define these easily using the &lt;a href="https://vuejs.org/guide/essentials/reactivity-fundamentals.html#ref"&gt;ref&lt;/a&gt; function and use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC"&gt;Date.UTC&lt;/a&gt; to make sure we're counting down to the same time regardless of the user's timezone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Start&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="c1"&gt;// Now&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// End&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's declare the most obvious computed property: &lt;code&gt;finished&lt;/code&gt;. This will help us determine if the countdown has completed. All we need to do is check to see if the current time is greater than or equal to the countdown ending time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Finished&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If now greater than end&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to tick the clock forward, we simply need to start an interval which ticks every second (or sooner, depending on your use case.) Within this interval, the now value is updated and if the countdown is finished, we can stop ticking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Start tick&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startTick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Set tick interval&lt;/span&gt;
  &lt;span class="nx"&gt;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Update now&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// If finished&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Stop tick&lt;/span&gt;
      &lt;span class="nf"&gt;stopTick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Stop tick&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopTick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Clear interval&lt;/span&gt;
  &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's compute the properties which drive our clock visual.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clock
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5wzyjjj7jop6kf7gnfa.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5wzyjjj7jop6kf7gnfa.jpg" alt="What Took You So Long Artwork" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As I mentioned in the intro, the clock image has a series of 32 segments near the outer circular edge. I thought it would be nice to fill these up visually as time passed. In order to do that, we'll need to compute some properties. First, let's computed the time span of the entire countdown. This is the overall time from the start of activation to its completion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Time span&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// End minus start&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll also need the time elapsed, which is how much time has &lt;em&gt;elapsed&lt;/em&gt; since the countdown begun.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Time elapsed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeElapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Now minus start&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will also be helpful to know the percentage of time elapsed for visual purposes. Note how this property is deriving from other computed properties. This is where computed properties get super powerful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Time percent&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timePercent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Elapsed time divided by timespan&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;timeElapsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;timeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 32 time segments on the clock image so let's determine how much time each of these segments take. In order to do that, we simply divide the entire span of time by the amount of segments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Time segment&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// One segment of timespan&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;timeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we know how much time makes up a time segment, we can determine which segment we're on by getting the remainder of the elapsed time and the end time. Then, dividing that value by the time segment value. Finally, we'll use floor to round down to the largest integer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Current segment&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Current unlocked segment&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;timeElapsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;timeSegment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can visualize the &lt;em&gt;filling&lt;/em&gt; of segments many ways, depending on your user case. In the end, I created a little &lt;a href="https://gist.github.com/leemartin/d314b14f1817fd8075f49b98f613db08"&gt;component&lt;/a&gt; which uses a CSS conic gradient to create a sort of donut chart. You could also do this using canvas, especially if you're keen to create a dynamic image of the countdown for sharing.&lt;/p&gt;

&lt;p&gt;The last bit of the clock visual is rotating the clock hand. We can do this by simply multiplying the percentage of time elapsed by 360 (degrees.) Don't worry about &lt;code&gt;seek&lt;/code&gt;. We'll get into that next when talking about the audio player.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Clock rotation&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clockRotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If seek is 0&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Time angle&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;timePercent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Seeked angle&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;seekPercent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, all we need to do is use CSS to rotate the clock hand image. Shout out to Vue for &lt;a href="https://vuejs.org/guide/essentials/class-and-style.html"&gt;style bindings&lt;/a&gt; also.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hand"&lt;/span&gt; &lt;span class="na"&gt;:style=&lt;/span&gt;&lt;span class="s"&gt;"{ transform: `rotate(${clockRotation}deg)` }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have a ticking clock. Let's bring some of this countdown logic into a dynamic audio player.&lt;/p&gt;

&lt;h3&gt;
  
  
  Player
&lt;/h3&gt;

&lt;p&gt;In addition to the clock, it was our ambition to add an audio player which allowed users to play a preview of the upcoming song and the amount of preview they could listen to was connected to the amount of time elapsed on the countdown. So, as the countdown ticked closer to release, they were able to hear more of the track. &lt;/p&gt;

&lt;p&gt;First, let's load the track with our audio library of choice: &lt;a href="https://howlerjs.com"&gt;Howler.js&lt;/a&gt;. Keeping track of the track's &lt;code&gt;duration&lt;/code&gt; is important. We'll get into the &lt;code&gt;startListening&lt;/code&gt; and &lt;code&gt;stopListening&lt;/code&gt; methods soon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;sound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Howl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/what-took-you-so-long.mp3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;html5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Update duration&lt;/span&gt;
    &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onplay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Start listening&lt;/span&gt;
    &lt;span class="nf"&gt;startListening&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stop listening&lt;/span&gt;
    &lt;span class="nf"&gt;stopListening&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onpause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stop listening&lt;/span&gt;
    &lt;span class="nf"&gt;stopListening&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onstop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stop listening&lt;/span&gt;
    &lt;span class="nf"&gt;stopListening&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have the track's duration, we can computed the audio segment equivalent by diving the duration by the total amount of segments (32.) We'll use this to visualize the preview playback on the clock also.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Audio segment&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// One segment of audio&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the track plays, a &lt;code&gt;seek&lt;/code&gt; property is updated to represent the current position in time. (We'll set this in a different method.) We can use seek and duration to determine which &lt;em&gt;segment&lt;/em&gt; the playback is on. Again, this is the audio player equivalent to the clock segment visual.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Seek segment&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seekSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Current seeked segment&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;audioSegment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another computed property we'll need is determining how much of the audio track is unlocked for listening. This is determined simply by multiplying the duration of the track by the percentage of time elapsed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Audio unlocked&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioUnlocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Time percent times duration&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;timePercent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all these properties in place, we'll need a method that is constantly updating the &lt;code&gt;seek&lt;/code&gt; time when a track is playing and checks to see if the current seek is greater than the unlocked time. If so, we'll stop the track. Let's just call it &lt;code&gt;listen()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Listen&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Request animation frame&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Update&lt;/span&gt;
  &lt;span class="nx"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// If seek is greater than unlocked&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioUnlocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stop sound&lt;/span&gt;
    &lt;span class="nx"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And back in the Howler.js setup, we start and stop &lt;em&gt;listening&lt;/em&gt; depending on if the track is playing. We'll use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame"&gt;requestAnimationFrame&lt;/a&gt; to update things as fast as the browser wishes for us to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Start listening&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startListening&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Request animation frame&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Stop listening&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopListening&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Cancel animation frame&lt;/span&gt;
  &lt;span class="nf"&gt;cancelAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when a user plays the track, the listener will start and as soon as the seek meets the unlocked value, the track will stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Thanks again to Janice Renée Wendel, Lucas Wijkhuizen and their team at Armada Music for this opportunity. Their attention to detail is unrivaled and they entered this project with a great sense of excitement to do something special for Armin’s fans. I’ll gladly make time for them.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>music</category>
      <category>howler</category>
      <category>nuxt</category>
    </item>
    <item>
      <title>Wishing Upon A Star with Web AR for Disney’s Wish</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Sat, 25 Nov 2023 13:51:00 +0000</pubDate>
      <link>https://dev.to/leemartin/wishing-upon-a-star-with-web-ar-for-disneys-wish-3ck6</link>
      <guid>https://dev.to/leemartin/wishing-upon-a-star-with-web-ar-for-disneys-wish-3ck6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Reposted from my dev blog: &lt;a href="https://leemartin.com/wish-star-finder" rel="noopener noreferrer"&gt;leemartin.com/wish-star-finder&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’ve had the opportunity to work with Disney a couple of times in the past few years on a number of Instagram filters but I’ve never been hired to build a web app for the company. So, imagine my delight when Dani Ratliff from Disney Music contacted me about their new film &lt;a href="https://movies.disney.com/wish" rel="noopener noreferrer"&gt;Wish&lt;/a&gt; and was wondering if I’d be interested in taking the celestial learnings I’ve applied to other client projects for &lt;a href="https://blog.bitsrc.io/using-three-js-to-hear-the-dawn-with-jack-white-bde73334b95a" rel="noopener noreferrer"&gt;Jack White&lt;/a&gt;, &lt;a href="https://leemartin.dev/launching-earthling-messages-into-orbit-for-eddie-vedder-f00d42f9d481" rel="noopener noreferrer"&gt;Eddie Vedder&lt;/a&gt;, &lt;a href="https://www.instagram.com/p/CfG7fjErHER/" rel="noopener noreferrer"&gt;Shinedown&lt;/a&gt;, etc and attempt to build something unique for Wish on… the web. Finally, &lt;em&gt;my&lt;/em&gt; wish came true.&lt;/p&gt;

&lt;p&gt;Wish is the culmination of 100 years of Disney, a company that has been a creative inspiration to me since I was a child. I’ve always admired Disney’s ability to weave R&amp;amp;D, engineering, and artistry to tell stories in new ways. From them, I learned that technology should fade into the background and allow the magic of the interaction to shine through in simple and accessible ways. So how does one take these learnings and apply it to marketing a film like Wish?&lt;/p&gt;

&lt;p&gt;In the film, our main character Asha makes a wish on a star. A scene we’ve seen countless times in Disney films. However, in this instance, the star answers and appears to her as a celestial being aptly called, “Star.” &lt;/p&gt;

&lt;p&gt;What if we developed an AR experience in the browser which plots a field of wishing stars in the user’s sky. Then, when the user points their device at one of these stars, it unlocks a piece of content from the upcoming film. Users then have the option to wish on that star and watch as the star listens to their request. Once the user finishes wishing, the wishing star bursts in response and reveals the user, themselves, only to be shortly joined by the character “Star.” This entire magical experience is recorded and then offered back to the user as a shareable video.&lt;/p&gt;

&lt;p&gt;Make a wish on a star today at &lt;a href="https://wish.disneymusic.co" rel="noopener noreferrer"&gt;wish.disneymusic.co&lt;/a&gt; and read on to learn how it all came together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wireframes
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/885654824" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Once a scope is agreed upon and a proposal is approved, I provide my clients with a series of designless wireframes which illustrate the entire user journey. This allows everyone to understand how the application will function without getting sidetracked by the distraction of visual design. Here’s a look at some of those wireframes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fwireframe.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fwireframe.jpg" alt="Wish Star Finder Wireframes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once these wireframes are approved, I will begin developing a prototype version of the application which also does not include design. This allows me to get buy-in on the UX before we delve into the opinionated world of design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions
&lt;/h2&gt;

&lt;p&gt;Since our application is a web AR app, we’ll need the user to agree to a few permissions in order to gain access to the technology required to &lt;em&gt;augment their reality&lt;/em&gt;. Access to their camera is required so the application may stream an image of their reality. We also require access to their device’s motion and orientation so that they may use their device like a controller and point it at stars in the sky. Finally, we’ll need access to their microphone to receive and record their spoken wish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Camera &amp;amp; Microphone
&lt;/h3&gt;

&lt;p&gt;We use &lt;a href="https://webrtc.org/" rel="noopener noreferrer"&gt;WebRTC&lt;/a&gt; to gain access to a user’s camera and microphone using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" rel="noopener noreferrer"&gt;getUserMedia&lt;/a&gt; method. Typically, I would gain access to both of these from the same call. However, our experience requires the camera to flip from facing the environment to facing the user and I noticed that the small period of time the flip occurred (and microphone wasn’t available) contributed to a bit of audio lagging in the final recorded video. This was one of the nastier bugs I faced in development. So, we’ll just access each of these on their own media streams so that the camera can flip independently from the microphone. &lt;/p&gt;

&lt;p&gt;I decided to write simple composables for both the camera and the microphone for maximum reusability. Here’s what the microphone looks like.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useMicrophone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// State&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;microphoneStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;microphoneStream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Start microphone&lt;/span&gt;
  &lt;span class="c1"&gt;// ----------&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Promise&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Get microphone&lt;/span&gt;
        &lt;span class="nx"&gt;microphoneStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;// Resolve&lt;/span&gt;
        &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Microphone error&lt;/span&gt;
        &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NotFoundError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please enable the microphone on your device.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NotAllowedError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please allow access to your microphone.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NotReadableError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please close all other tabs which are using your microphone.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This experience requires access to your microphone.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Stop microphone&lt;/span&gt;
  &lt;span class="c1"&gt;// ----------&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If stream exists&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;microphoneStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Stop all streams&lt;/span&gt;
      &lt;span class="nx"&gt;microphoneStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

      &lt;span class="c1"&gt;// Set stream to null&lt;/span&gt;
      &lt;span class="nx"&gt;microphoneStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;    

  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Return&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;stop&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can then import the start and stop methods from this composable on any required page or component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMicrophone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use a state variable to keep track of the microphone stream because it is used in a few places throughout the app.&lt;/p&gt;

&lt;p&gt;The camera composable works in a similar way with a few differences. First, the view state is passed to the camera &lt;code&gt;start&lt;/code&gt; method so the camera is facing the appropriate direction. Next, I manually update a video tag with the camera stream and wait for its metadata to load since we’ll be rendering the visual from this video tag in our AR scene.&lt;/p&gt;

&lt;h3&gt;
  
  
  Motion
&lt;/h3&gt;

&lt;p&gt;The API for gaining access to a user’s device motion and orientation is a bit different from WebRTC but not too complicated once you get the hang of it. To keep my code concise, I created a composable for this permission also. So, even though the permission APIs between media devices and device motion are different, at least they can seem similar in my code. The trick to accessing motion is making sure &lt;code&gt;DeviceMotionEvent&lt;/code&gt; and &lt;code&gt;DeviceOrientationEvent&lt;/code&gt; exist on the &lt;code&gt;window&lt;/code&gt; and then using the &lt;code&gt;requestPermission&lt;/code&gt; method to gain access to it. If everything was successful, the method will respond with &lt;code&gt;granted&lt;/code&gt;. Here’s what my composable looks like.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useOrientation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Start orientation&lt;/span&gt;
  &lt;span class="c1"&gt;// ----------&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Promise&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// If request permission exists&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DeviceMotionEvent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DeviceOrientationEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestPermission&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Request permission&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DeviceOrientationEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermission&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Log&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orientation permission: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="c1"&gt;// If granted&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Resolve&lt;/span&gt;
            &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

          &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Revoke&lt;/span&gt;
            &lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

          &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Log&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orientation error: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="c1"&gt;// Revoke&lt;/span&gt;
          &lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This experience requires access to your device's motion.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="p"&gt;})&lt;/span&gt;

      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Log&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orientation does not exist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Resolve&lt;/span&gt;
        &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Return&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;start&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Access Granted
&lt;/h3&gt;

&lt;p&gt;I keep track of the status of all of these permission requests on the same screen and compute whether or not they are successful. Once the user has enabled their camera, microphone, and motion, they may continue the experience.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grantedAccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// All access granted&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cameraStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;microphoneStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;orientationEnabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s create a sky of wishing stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wishing Stars
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fstars.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fstars.jpg" alt="Star Types"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our application augments a Three.js powered scene of wishing stars on top of a user’s sky. The user may then point their device at one of these stars to reveal the content associated with that particular star. Contentful is used by the client to manage these stars and a new one is added every Wednesday at the wishing time of 11:11. Let’s talk a little bit about plotting these stars and targeting them with the user’s device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Stars
&lt;/h3&gt;

&lt;p&gt;As I mentioned, Contentful is used by the client to manage the wishing stars in the sky and their associated content. Content can be an image, video, or audio. Once the content is added to Contentful, we can use their Publishing API to fetch them for plotting. To do this, I first set up a Contentful plugin in Nuxt and set up the associated environment variables in Netlify.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Imports&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;contentful&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contentful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Define plugin&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nuxtApp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use config&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="kr"&gt;public&lt;/span&gt;

  &lt;span class="c1"&gt;// Provide contentful client as helper&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;contentful&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentful&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentfulSpaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentfulAccessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentfulHost&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can then use this client to fetch all the star entries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$contentful&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have our wishing stars, let’s plot them in the sky.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plotting Stars
&lt;/h3&gt;

&lt;p&gt;As mentioned, we’re using Three.js to power the AR visual of our web app. I’ll save you the basics of building with Three.js since they have wonderful docs and instead focus on the specifics of our application. A wishing star in our sky is one that the user can interact with. So, even though there are many stars visible on the app, most of these are what I call “idle stars” and they cannot be interacted with. A wishing star is actually a Three.js Group which includes several Sprites (or a plane that always faces the user.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fstar.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fstar.jpg" alt="Wishing Star Layers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The body of the star is the graphic of the iconic 8-point wishing star we’ve seen in many Disney films. &lt;/li&gt;
&lt;li&gt;The glow of the star is the glow of light surrounding the 8-point star. This will animate as the user speaks their wish.&lt;/li&gt;
&lt;li&gt;The flare of the star is the star burst graphic that appears when the wishing star begins to answer a user’s wish.&lt;/li&gt;
&lt;li&gt;The flash is a flat white Sprite that is used to envelop the user’s screen in bright white light after the star bursts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both the idle and wish stars need to be plotted in the upper hemisphere of the user’s reality, aka their sky. In order to do this, we come up with a random spherical positioning 60 degrees and below the sky directly above the user and then position the star there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Random position&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setFromSphericalCoords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MathUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degToRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MathUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degToRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Position star&lt;/span&gt;
&lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these stars are added to another &lt;code&gt;wishStars&lt;/code&gt; group so they can be easily targeted later.&lt;/p&gt;

&lt;p&gt;In order to make the sky itself a bit more interesting, we plot a Sphere in the scene and give it a Disney Wish texture. I use a gradient fade so the texture is more noticeable the higher the user points up in the sky. This helps differentiate the user’s reality with the Wish reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Targeting Stars
&lt;/h3&gt;

&lt;p&gt;With these wishing star groups and their associated sprite layers plotted in our textured sky, we can add DeviceOrientationControls and a Raycaster to determine which star the user is currently pointing at. Check out the Eddie Vedder “Earthling” dev blog for more context on resurrecting the defunct DeviceOrientationControls using patch-package. Once the user’s device orientation is controlling the camera, we can use a Raycaster to determine if they are intersecting with a wish star. First, let’s set up our Raycaster and a 2d Pointer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Raycaster&lt;/span&gt;
&lt;span class="nx"&gt;raycaster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Raycaster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Pointer&lt;/span&gt;
&lt;span class="nx"&gt;pointer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, in our render loop, we can update the raycaster and check for any intersections with wish stars.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Update ray&lt;/span&gt;
&lt;span class="nx"&gt;raycaster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFromCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Check for intersections&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nf"&gt;intersectObjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wishStars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This combination of DeviceOrientationControls and Raycaster works really well. However, I didn’t want the associated content to unlock immediately but rather when the user paused over the wishing star for a moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unlocking Stars
&lt;/h3&gt;

&lt;p&gt;When a user intersects with a wishing star, I use Greensock to tween a progress variable from 0.0 to 1.0 over 3 seconds. If the user stops pointing at that wishing star before the tween completes, the tween is killed and the wishing star content is not revealed. However, if the animation completes, the star content will reveal itself as a &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element (more on that soon.) Here’s a look at that Greensock tween.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;starUnlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;starProgress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="nx"&gt;linear&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Star visible&lt;/span&gt;
    &lt;span class="nx"&gt;starVisible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tween can be killed with the &lt;code&gt;kill&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;starUnlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order for the user to know this is happening, we need a clear visual of the unlock progressing. To achieve this, I designed a custom SVG scope in the Wish aesthetic which receives a &lt;code&gt;starProgress&lt;/code&gt; prop and animates a glowing white circle progress. Here’s a CodePen of that component.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/leemartin/embed/GRzrLdv?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Revealing Content
&lt;/h3&gt;

&lt;p&gt;When the user successfully unlocks a wishing star’s content, it is revealed in a Dialog element. I love this standardized method of bringing up a modal dialog box that must be interacted with in some manner in order for it to be closed. It can also be nicely customized in CSS and I did my best through the use of pseudo elements to give it a Disney Wish flare.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/leemartin/embed/JjxbqoJ?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;We can show a dialog by calling the &lt;code&gt;showModal&lt;/code&gt; method and hide it by using the &lt;code&gt;close&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Show modal&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;showModal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Close modal&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the case of our modal, users have the ability to close it and return to exploring the sky or close it and begin wishing on the currently targeted star. Before we talk about wishing, let’s discuss our setup for recording the wishing experience as a video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wish Recorder
&lt;/h2&gt;

&lt;p&gt;Before we allow the user to begin their wish, we need to set up the infrastructure that will record it. Rather than directly record the Three.js renderer canvas, I set up a separate recording canvas with the story-friendly dimensions of 1080px by 1920px. We can then overlay additional images and videos which are not visible in the user experience but should be present on the final shareable video. All of this visual content is then recorded with MediaRecorder but we can’t forget to manage and record the audio of the experience also. Namely, the audio coming from the microphone and “Star” character video.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recording Canvas
&lt;/h3&gt;

&lt;p&gt;The recording canvas itself is simply a blank 1080 x 1920 canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Recording canvas&lt;/span&gt;
&lt;span class="nx"&gt;recordingCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Size&lt;/span&gt;
&lt;span class="nx"&gt;recordingCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;
&lt;span class="nx"&gt;recordingCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;

&lt;span class="c1"&gt;// Recording context&lt;/span&gt;
&lt;span class="nx"&gt;recordingContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;recordingCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can scale, position, and draw the Three.js renderer canvas onto this recording canvas by calculating a cover size using the &lt;a href="https://www.npmjs.com/package/intrinsic-scale" rel="noopener noreferrer"&gt;intrinsic-scale&lt;/a&gt; helper library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get cover&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordingCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recordingCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threeCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threeCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Draw three&lt;/span&gt;
&lt;span class="nx"&gt;recordingContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threeCanvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threeCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threeCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to drawing the Three.js canvas, we can draw image and video layers to add a bit of a story and more visual elements to the final recorded video. &lt;/p&gt;

&lt;p&gt;For example, I could draw the Wish logo and a little quote from the film over the Three.js scene. What about the moment the user’s screen goes white? Instead of that simply being a blank white screen in the final video, we’ve made it a short video clip which shows the “Star” character zipping past the white screen before appearing to the user. To manage this, I use a &lt;code&gt;shot&lt;/code&gt; variable to determine which additional layer to draw when rendering the recording canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;wish&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Draw overlay image&lt;/span&gt;
  &lt;span class="nx"&gt;recordingContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;overlayImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;flyby&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Draw video&lt;/span&gt;
  &lt;span class="nx"&gt;recordingContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;flybyVideo&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Draw end card&lt;/span&gt;
  &lt;span class="nx"&gt;recordingContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then adjust the current “shot” as the user progresses through the interaction. Now that we’re rendering the canvas we’d like to record, let’s set up our MediaRecorder to record it and all of the audio happening in our experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  MediaRecorder
&lt;/h3&gt;

&lt;p&gt;The MediaRecorder interface is a fascinating piece of standard browser technology that allows us to record a group of visual and audio streams as new video or audio files. In the case of our app, we’re interested in recording the “recording canvas” we’re rendering with all of our visual content. In addition, we’re interested in recording the user’s microphone when they are wishing and the “Star” characters sound effects when it is on screen. &lt;/p&gt;

&lt;h4&gt;
  
  
  Video
&lt;/h4&gt;

&lt;p&gt;To begin with, capturing a visual stream and associated video track of the recording canvas couldn’t be simpler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get canvas stream&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvasStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;recordingCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captureStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Get video tracks&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;videoTrack&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvasStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVideoTracks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Audio
&lt;/h4&gt;

&lt;p&gt;As for the audio… Here's what I settled on after a lot of engineering work. The audio in our experience is coming from three different places. First, from the microphone stream when the user is wishing. Second, from the “Star” flyby video which is rendered onto the recording canvas secretly when the user’s screen is white. And lastly, from the “Star” appearance video which is part of the Three.js scene featuring the user’s video. One of these is an existing media stream and the other two are existing media elements (video tags on in the HTML.) In order to manage this variety of sources and their associated volume, we can use the Web Audio API. &lt;/p&gt;

&lt;p&gt;First, we’ll set up a new media stream destination to receive all of these sources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create audio context&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Create destination&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamDestination&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our microphone, we’ll create a new stream source and connect it to a gain node so we may dynamically control its volume when recording. We then connect the gain node to our destination stream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Microphone source&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;microphoneSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;microphoneStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Microphone volume&lt;/span&gt;
&lt;span class="nx"&gt;microphoneVolume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Adjust volume&lt;/span&gt;
&lt;span class="nx"&gt;microphoneVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="c1"&gt;// Connect to volume&lt;/span&gt;
&lt;span class="nx"&gt;microphoneSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;microphoneVolume&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Connect to destination&lt;/span&gt;
&lt;span class="nx"&gt;microphoneVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our “Star” videos, we’ll create a pair of media element sources and also connect them to individual gain nodes so we can adjust their volume as needed. Unlike the microphone, we’ll want the user to hear these videos through the app so we’ll connect their gain nodes to the primary audio context destination in addition to the destination stream. Here’s an example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Star video source&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;starSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaElementSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;starVideo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Star volume&lt;/span&gt;
&lt;span class="nx"&gt;starVolume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Adjust volume&lt;/span&gt;
&lt;span class="nx"&gt;starVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="c1"&gt;// Connect to volume&lt;/span&gt;
&lt;span class="nx"&gt;starSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;starVolume&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Connect to destination&lt;/span&gt;
&lt;span class="nx"&gt;starVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Connect star volume to context destination&lt;/span&gt;
&lt;span class="nx"&gt;starVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all of these sources now connected to the destination stream, we can access the stream and get its associated audio track.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get audio stream&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;

&lt;span class="c1"&gt;// Get audio track&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;audioTrack&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAudioTracks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then combine this audio track with the recording canvas video track to create our final video stream which is what will be used to finally initialize our MediaRecorder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create video stream&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MediaStream&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;videoTrack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioTrack&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;// Recorder&lt;/span&gt;
&lt;span class="nx"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this is already starting to feel like a MediaRecorder dev blog, I’m going to stop short of how to use MediaRecorder and instead direct you to the excellent documentation on the subject. The most important bits are storing video chunks as they are available and creating the video blob once recording is complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On data available&lt;/span&gt;
&lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ondataavailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If size&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Push new chunks&lt;/span&gt;
    &lt;span class="nx"&gt;recorderChunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// On stop&lt;/span&gt;
&lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onstop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create blob&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recorderChunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Store blob&lt;/span&gt;
  &lt;span class="nx"&gt;videoBlob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Analyzer
&lt;/h3&gt;

&lt;p&gt;In addition to using Web Audio to wrangle our audio sources for recording, we’ll use it to set up an Analyzer node to receive volume levels from a user’s microphone as they speak. Then, we’ll use these levels to adjust the glow of the wishing star so it seems like the star is glowing in response. Here’s how you set up a very simple analyser and connect it to the microphone source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create analyser&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAnalyser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Set fft size&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fftSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;

&lt;span class="c1"&gt;// Connect microphone to analyser&lt;/span&gt;
&lt;span class="nx"&gt;microphoneSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Initialize analyser data&lt;/span&gt;
&lt;span class="nx"&gt;analyserData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fftSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll discuss how to use this analyser next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wishing On A Star
&lt;/h2&gt;

&lt;p&gt;With the recorder setup and ready to receive wishes, it is time to let the user make their wish. As the user speaks into their microphone, our Web Audio analyzer will adjust the glow of the wishing star so it seems to be listening. Once the user has finished wishing or the wishing time elapses, the wishing star will react by bursting and flashing the screen white. This is very similar to what Asha faces in the film when she makes her own wish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listening Star
&lt;/h3&gt;

&lt;p&gt;Within our renderer loop, we’ll analyze the microphone to get an average volume level when the user is wishing. Then we’ll use this level to adjust the wishing star glow. First, we’ll use the &lt;code&gt;getFloatTimeDomainData&lt;/code&gt; to get the current waveform array of magnitude data. Then we’ll determine an average of this data and adjust it with Math.min and Math.max until it is giving a pronounced value between 0.5 and 1.0. Finally, we’ll adjust the opacity of the glow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get time domain data&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFloatTimeDomainData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyserData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Sum&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="c1"&gt;// Loop through data&lt;/span&gt;
&lt;span class="nx"&gt;analyserData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Get average&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;analyserData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;

&lt;span class="c1"&gt;// Get level&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Adjust glow opacity&lt;/span&gt;
&lt;span class="nx"&gt;glow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the wishing star will glow as the user speaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Star Reaction
&lt;/h3&gt;

&lt;p&gt;Once the user finishes their wish, the wishing star will burst until the entire screen goes white. This effect is achieved by using a Greensock timeline to tween the scale and opacity of the flare and flash sprites within the wishing star group. Here’s that setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Timeline&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Fade in flare&lt;/span&gt;
&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;power4.in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Scale flare&lt;/span&gt;
&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;power4.in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Fade in flash&lt;/span&gt;
&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the flare fades in, it is also scaled to 200 x and y. Then, the flash appears and the screen goes white.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wishing Time
&lt;/h3&gt;

&lt;p&gt;In an attempt to keep the final recorded video on the shorter side, users are given a maximum of 15 seconds to make their wish. The solution here is somewhat similar to the Greensock tween powered star progress scope we used to unlock wishing stars. However, instead of using an elaborate SVG scope, I’m just using a simple progress bar in the header of the experience. Here’s the Greensock tween.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;wishTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wishProgress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="nx"&gt;linear&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stop wishing&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users also have the ability to click a “Finish Wish” button if they finish wishing before the 15 seconds are up. At which point, the &lt;code&gt;wishTimeout&lt;/code&gt; tween would be killed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Star Answers
&lt;/h2&gt;

&lt;p&gt;With the screen white and our wisher waiting patiently, we use this opportunity to flip their device camera so it is now facing them. In addition, we no longer record their microphone audio and instead focus on recording the “Star” character audio. When the user finally sees themselves, they are not alone. They are accompanied by “Star” who has heard and answered their wish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Camera Flip
&lt;/h3&gt;

&lt;p&gt;Since we put together that nice composable for accessing the user’s camera, in order to flip it, we simply need to stop the camera, adjust the view state, and start the camera again. Again, we do this independently from the microphone to help fight audio lag on the final recorded video.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stop camera&lt;/span&gt;
&lt;span class="nf"&gt;stopCamera&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// View user&lt;/span&gt;
&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;

&lt;span class="c1"&gt;// Start camera&lt;/span&gt;
&lt;span class="nf"&gt;startCamera&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s adjust the audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audio Flip
&lt;/h3&gt;

&lt;p&gt;While the user is wishing, we’re interested in recording their microphone audio but when they’re done, we’re interested in recording the “Star” character SFX. Remember those Web Audio gain nodes we set up earlier? Here’s where they come into play. By adjusting the gains, we can mute the microphone audio and unmute the “Star” audio for recording.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mute microphone&lt;/span&gt;
&lt;span class="nx"&gt;microphoneVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="c1"&gt;// Unmute flyby&lt;/span&gt;
&lt;span class="nx"&gt;flybyVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="c1"&gt;// Unmute star&lt;/span&gt;
&lt;span class="nx"&gt;starVolume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I love to see Web Audio and MediaRecorder work together in this manner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Star Video Sprite
&lt;/h3&gt;

&lt;p&gt;The final magical element of our experience is when the “Star” character appears to the user to sprinkle some stardust on them. We’re all stars after all. The star video is also a Three.js Sprite,  which is resized to cover the full dimensions of the Three.js scene. Transparency is achieved by creating a star video source which has both color and alpha channels and using both separately as maps for the sprite material. Let’s start with initializing the video textures.&lt;/p&gt;

&lt;h4&gt;
  
  
  Video Textures
&lt;/h4&gt;

&lt;p&gt;Here’s an example video showing the stacking of both color and alpha channels. Luckily, Disney provided excellent video toolkit assets which had these channels available to me. What we want to do is use the top for our full color texture map and the bottom for our alpha map. That way, areas which are black on the alpha map texture will be transparent. This involves creating new VideoTextures from the &lt;code&gt;startVideo&lt;/code&gt; video tag and using offset and repeat to isolate each specific channel/area.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Star texture&lt;/span&gt;
&lt;span class="nx"&gt;starTexture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VideoTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;starVideo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Wrap&lt;/span&gt;
&lt;span class="nx"&gt;starTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RepeatWrapping&lt;/span&gt;
&lt;span class="nx"&gt;starTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RepeatWrapping&lt;/span&gt;

&lt;span class="c1"&gt;// Offset&lt;/span&gt;
&lt;span class="nx"&gt;starTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Repeat&lt;/span&gt;
&lt;span class="nx"&gt;starTexture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Star alpha&lt;/span&gt;
&lt;span class="nx"&gt;starAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VideoTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;starVideo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Wrap&lt;/span&gt;
&lt;span class="nx"&gt;starAlpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RepeatWrapping&lt;/span&gt;
&lt;span class="nx"&gt;starAlpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RepeatWrapping&lt;/span&gt;

&lt;span class="c1"&gt;// Offset&lt;/span&gt;
&lt;span class="nx"&gt;starAlpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Repeat&lt;/span&gt;
&lt;span class="nx"&gt;starAlpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then use these when initializing our sprite material.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SpriteMaterial&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;alphaMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;starAlpha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;depthTest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;starTexture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DoubleSide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Resizing Sprite
&lt;/h4&gt;

&lt;p&gt;In order to have the video sprite fill the screen, we’ll calculate the vertical field of view and then the screen height based on how far the camera is from our sprite. It’s two lines of fancy math that effectively resizes the sprite so it fills the entire vertical space of the screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vertical field of view&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;vFOV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MathUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degToRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fov&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Height&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vFOV&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;starVideo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Scale&lt;/span&gt;
&lt;span class="nx"&gt;starVideo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Playing Sprite
&lt;/h4&gt;

&lt;p&gt;With our star video sprite resized to fit the view and properly textured, we simply need to play the &lt;code&gt;starVideo&lt;/code&gt; tag to have the “Star” character appear in front of our user. I’ll also listen for the “ended” event so I know when to conclude the recording experience.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Play star video&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;starVideo&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Wait until ended&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;starVideo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ended&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Stop recording&lt;/span&gt;

&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sharing
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/885655724" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Once the video is created with MediaRecorder, it can be shared directly to any social app using the Web Share API. This involves creating a file out of the video blob and passing it to the Web Share API. I like using this mime library to determine the precise extension and type from the video blob.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Extension&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoBlob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Type&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// File name&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`wish.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="c1"&gt;// File&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;videoBlob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Share file&lt;/span&gt;
&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fbook.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.leemartin.com%2Fimages%2Fprojects%2Fwish-star-finder%2Fbook.jpg" alt="The Art of Wish"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While I was provided a ton of Disney Wish toolkit assets, there wasn’t a whole lot of web friendly graphic design associated with this project. For example, Disney Wish does not have a custom built website, just a standardized page on the Disney movies domain. So, in order to create a responsive design system for this project, I had to look further. I found inspiration by exploring the full range of merchandise associated with the film, from activity books to backpacks and gel pens to UNO cards. I even purchased The Art of Wish off Amazon. This allowed me to get a birds eye view of how a bunch of different stakeholders were handling design and translate some of their choices into a responsive design system of colors, typography, and buttons. Again, I try not to go overboard here. The design should feel familiar but also work well on a simple web UI. One highlight is simply the primary button which uses before and after pseudo elements to create ornamental edges in the style of the architecture seen in Rosas. Here’s a CodePen of that button construction.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/leemartin/embed/ExrNJoV?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Thanks so much to Dani Ratliff, Weston Lyon, Natalia Castillo, and their entire team Disney Music for this opportunity. Special thanks to the privacy, legal, and technology teams at Disney who helped me pull off my first web app for the company. I truly hope we can build more magical things together.&lt;/p&gt;

</description>
      <category>webar</category>
      <category>threejs</category>
      <category>vue</category>
      <category>wish</category>
    </item>
    <item>
      <title>Vue Sci-Fi Scanner Transition</title>
      <dc:creator>Lee Martin</dc:creator>
      <pubDate>Mon, 18 May 2020 15:28:16 +0000</pubDate>
      <link>https://dev.to/leemartin/vue-sci-fi-scanner-transition-45hj</link>
      <guid>https://dev.to/leemartin/vue-sci-fi-scanner-transition-45hj</guid>
      <description>&lt;p&gt;I recently had the opportunity to launch &lt;a href="https://space.airkhruang.com"&gt;Shelter In Space&lt;/a&gt; for the band Khruangbin which allows users to generate a Spotify or Apple Music playlist (curated by them) for a home activity of their choosing. The design of this app was inspired by the guide animations from the 1980's BBC television adaption of The Hitchhiker's Guide to the Galaxy. One of the key components I tried to recreate was the vertical scanning effect which transitioned between content. Click the image in the Codepen below to see the final solution and read on to better understand how it was developed.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/leemartin/embed/oNjQNqm?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;First, the structure. &lt;code&gt;.scanner&lt;/code&gt; holds two divs which both have background images. One is a pair of touching hands and another is Buddah. Both of these are absolutely positioned on top of each other. There is a single Vue property of state which allows toggling between the two via a click event on the &lt;code&gt;.scanner&lt;/code&gt; div. &lt;/p&gt;

&lt;p&gt;Let's first talk about the clipping transition which clips one image out of frame while showing the other. Vue has great &lt;a href="https://vuejs.org/v2/guide/transitions.html"&gt;documentation&lt;/a&gt; on ways to apply enter/leave transitions which an item is removed or added from the DOM. In our case, we'll be using the transition classes which Vue applies automatically. The &lt;code&gt;clip-path&lt;/code&gt; CSS property allows you to &lt;em&gt;clip&lt;/em&gt; an element in all sorts of shapes and sizes. We're only interested in an inset rectangular shape. &lt;/p&gt;

&lt;p&gt;For example, if you wanted to clip 50% of a div from the top.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clip-path: inset(50% 0 0 0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What about 75% from the bottom?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clip-path: inset(0 0 75% 0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;clip-path&lt;/code&gt; property is also animatable. With this knowledge, we can setup our transition classes accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Clip 100% from bottom before shown */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-enter&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Transition to no clipping from bottom */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-enter-to&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Start from no clipping */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-leave&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Transition to 100% clipping from top */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-leave-to&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Set time and easing */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-enter-active&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.scan-leave-action&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clip-path&lt;/span&gt; &lt;span class="m"&gt;2s&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to add &lt;code&gt;key&lt;/code&gt; attributes to your content and clicking should show the &lt;code&gt;clip-path&lt;/code&gt; transition. Now, let's discuss the scanner line itself. In the source material, the scanner lines sits over both the incoming and outgoing content at the point of transition and turns content beneath it white without affecting the black background. CSS has a property called &lt;code&gt;backdrop-filter&lt;/code&gt; which allows you to apply CSS filters such as blur or brightness to elements &lt;em&gt;beneath&lt;/em&gt; the styled div. While most folks might use this to create blurred overlays, let's use it to &lt;strong&gt;brighten&lt;/strong&gt; the illustrations. Rather than create a new div for the line, we'll add a &lt;code&gt;::before&lt;/code&gt; pseudo element to each image div and position it absolutely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.scanner&lt;/span&gt; &lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;grayscale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;grayscale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: I'm using the transform here to make sure the line isn't present before or after transition. There's probably a smarter way to do this but it works.&lt;/p&gt;

&lt;p&gt;With the pseudo element in place, we can expand on our transition classes to simply move the position of the line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Start at the top */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-enter&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.scan-leave&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Transition to the bottom */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-enter-to&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.scan-leave-to&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Set timing and ease */&lt;/span&gt;
&lt;span class="nc"&gt;.scan-enter-active&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.scan-leave-active&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;top&lt;/span&gt; &lt;span class="m"&gt;2s&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's about it. As a Vue  beginner, I was very happy with this result and I think it adds a lot of magic to our &lt;a href="https://space.airkhruang.com"&gt;project&lt;/a&gt; without adding much complexity.&lt;/p&gt;

</description>
      <category>scanner</category>
      <category>space</category>
      <category>vue</category>
      <category>transition</category>
    </item>
  </channel>
</rss>
