Play Among the Stars

 
Overview

This was a project I threw together in like 2 hours for a job interview challenge a few years ago. Looking back, I’m slapping my younger self for writing some silly code, but I’m still proud of the final product and creative process surrounding the prompt: Bring a real-life object to life with HTML, CSS and JavaScript in two hours.

NOTE: It looks like crap on mobile devices. #todo

The original photo I took (left); my coded rendering (right).

Concept

I live in Los Angeles, and my office is located in the heart of the Hollywood Walk of Fame.

I pass by dozens of these iconic stars every day, but my favorite by far goes to one of my musical idols: Frank Sinatra (who basically ran this city back in the day... and is the only celebrity I know of with not one, but TWO stars to his name. A true legend indeed).

Every time I see this star on my way to work, I can't help but to hear one of his many tunes in my head, so I thought it would be a fun project to express my perspective of this object by bringing it to life and recreating it in the form of good ol' HTML, CSS and JavaScript.


check out the final product

☟☟☟

 🔈 sound on! 🔈


HTML
<!DOCTYPE data-preserve-html-node="true" html>


<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<meta http-equiv="X-UA-Compatible" content="ie=edge">

<title>Play Among the Stars</title>

<link rel="shortcut icon" type="image/png" href="./images/favicon.png">

<link href="https://fonts.googleapis.com/css?family=Abel" rel="stylesheet">

<link rel="stylesheet" href="./css/main.css">

<link rel="stylesheet" href="./css/record.css">

<link rel="stylesheet" href="./css/animations.css">

<div class="container flex-center">
    <div class="outer-star"></div>
    <div class="star inner-star flex-center">
      <div class="sinatra flex-center">Frank Sinatra</div>
      <div class="record-container flex-center">
        <div class="record-border circle flex-center">
          <div class="record-vinyl circle flex-center">
            <div class="record-center-border circle flex-center">
              <div class="record-center circle flex-center"></div>
            </div>
          </div>
        </div>
      </div>
      <div class="record-arm"></div>
    </div>
    <div class="instructions"><p>Click the vinyl</p></div>
    <div class="controls flex-center">
      <button class="play-pause">||</button>
      <button class="replay"></button>
    </div>
  </div>

<audio src="./audio/needleDrop.mp3"></audio>

<audio src="./audio/flyMeToTheMoon.mp3"></audio>

<script src="main.js"></script>
CSS
/*----- MAIN -----*/

body {
  padding: 80px;
  background-color: #222527;
  height: 80vh;
  overflow: hidden;
}

.container {
  margin: 0 auto;
  position: relative;
  width: 60vw;
  height: 60vw;
  transition: all .2s ease-in-out;
}

.outer-star {
  position: absolute;
  width: 90%;
  height: 90%;
  color: white;
  background-color: #ffecb2;
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
  -webkit-clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

.inner-star {
  position: absolute;
  flex-direction: column;
  width: 86%;
  height: 86%;
  color: white;
  background-color: #cf9d96;
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
  -webkit-clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

.sinatra {
  font-family: 'Abel', sans-serif;
  color: white;
  font-size: 3.9vw;
  text-transform: uppercase;
  position: relative;
  top: 2vh;
  z-index: 1;
}

.instructions {
  display: '';
  font-family: 'Abel', sans-serif;
  color: white;
  font-size: 1.9vw;
  text-transform: uppercase;
  position: absolute;
  bottom: 10vh;
  transition: opacity 3s;
  -moz-transition: opacity 3s;
  -webkit-transition: opacity 3s;
}

.controls {
  opacity: 0;
  position: absolute;
  bottom: 7vh;
  right: -14vw;
  z-index: 4;
  height: 7vw;
  padding: 0 1vw;
}

.controls > button {
  font-size: 6vw;
  color: #00000024;
  background-color: inherit;
  border: none;
  transition: all .2s ease-in-out;
  -moz-transition: all .2s ease-in-out;
  -webkit-transition: all .2s ease-in-out;
}

.controls > button:hover {
  cursor: pointer;
  transform: scale(1.2)
}

button:focus {
  outline: none;
}

/* Custom utility class */
.flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}


/*----- ANIMATIONS -----*/

@keyframes spinning{
  from{
    transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -webkit-transform: rotate(360deg);
  }
}

@-webkit-keyframes spinning {
  from {
    transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -webkit-transform: rotate(360deg);
  }
}

.spinning {
  animation: spinning 4s linear infinite;
  -moz-animation: spinning 4s linear infinite;
  -webkit-animation: spinning 4s linear infinite;
  transition: all 1s ease-in-out;
  -moz-transition: all 1s ease-in-out;
  -webkit-transition: all 1s ease-in-out;
}
    
.move-arm {
  transform: rotate(-90deg);
  transition: all 2.5s;
  -moz-transition: all 2.5s;
  -webkit-transition: all 2.5s;
  transition-timing-function: cubic-bezier(0.38,-0.24, 0.7, 1.1);
}

.fade-in {
  opacity: 1;
  transition: opacity .5s ease-in-out;
  -moz-transition: opacity .5s ease-in-out;
  -webkit-transition: opacity .5s ease-in-out;
}

.fade-out {
  opacity: 0;
  transition: opacity .5s ease-in-out;
  -moz-transition: opacity .5s ease-in-out;
  -webkit-transition: opacity .5s ease-in-out;
}

.record-border {
  transition: all .2s ease-in-out;
  -moz-transition: all .2s ease-in-out;
  -webkit-transition: all .2s ease-in-out;
}

.record-border:hover { 
  box-shadow: 0px 2px 44px #222528;
  cursor: pointer;
  transform: scale(1.1);
  transition: all .2s ease-in-out;
  -moz-transition: all .2s ease-in-out;
  -webkit-transition: all .2s ease-in-out;
} 


/*----- RECORD -----*/

.record-container {
  position: relative;
  top: 5%;
  z-index: 2;
  width: 9vw;
  height: 9vw;
}

.record-border {
  width: 100%;
  height: 100%;
  background-color: #9b8a6c;
  border: .4vw solid #ffffe3;
  box-sizing: border-box;
}

.record-vinyl {
  width: 90%;
  height: 90%;
  background-color: #ffffe3;
  background: linear-gradient(-90deg, #fdfdfd 0%, #e8e877 50%, #fdfdfd);
}

.record-center-border {
  width: 32%;
  height: 32%;
  background-color: #52502a;
}

.record-center {
  width: 90%;
  height: 90%;
  background-color: #52502a;
  border: 0.9vw solid #ffffe3;
  box-sizing: border-box;
}

.record-arm {
  width: 6.8%;
  height: 1.6%;
  background-color: #ffffe3;
  border-style: solid;
  border-color: #9b8a6c;
  border-width: 0.2vw 0;
  border-radius: 0 3vh 3vh 0;
  z-index: 1000;
  transform-origin: 100%;
  transform: rotate(-125deg);
  position: relative;
  top: -11.5%;
  box-sizing: border-box;
  clip-path: polygon(0 15%, 100% 0%, 100% 100%, 0 90%);
  -webkit-clip-path: polygon(0 15%, 100% 0%, 100% 100%, 0 90%);
}

/* Custom utility class */
.circle {
  border-radius: 50%;
}
JavaScript
/*--------- Global Variables ---------*/
const vinyl = document.querySelector('.record-vinyl');
const recordBorder = document.querySelector('.record-border');
const instructions = document.querySelector('.instructions');
const song = document.querySelector('audio:nth-child(3)');
const playPause = document.querySelector('.play-pause');

let isPlaying = false;
const ANIMATION_TIMING = {
  first: 2800,
  second: 6000,
  third: 9000,
};

/*--------- Functions ---------*/
function handlePlay() {
  // Initial Animations
  document.querySelector('.record-arm').classList.add('move-arm');
  recordBorder.style.pointerEvents = 'none';
  instructions.classList.add('fade-out');
  // Play sequence
  document.querySelector('audio:nth-child(2)').play();
  setTimeout(function spinVinyl() {
    vinyl.classList.add('spinning');
  }, ANIMATION_TIMING.first);
  setTimeout(function playSong() {
    song.play();
    instructions.innerHTML = "Enjoy.";
    instructions.classList.remove('fade-out');
    instructions.classList.add('fade-in');
  }, ANIMATION_TIMING.second);
  setTimeout(function addControls() {
    instructions.classList.add('fade-out');
    document.querySelector('.controls').classList.add('fade-in');
  }, ANIMATION_TIMING.third);
};

function togglePlay() {
  vinyl.classList.toggle('spinning');
  isPlaying = !isPlaying,
  isPlaying ? (
    song.pause(),
    playPause.innerHTML = "▶︎"
  ) : (
    song.play(),
    playPause.innerHTML = "||"
  )
};

song.onended = function() {
  vinyl.classList.remove('spinning');
};

/*--------- Event Listeners ---------*/
recordBorder.addEventListener('click', handlePlay, {once: true});
playPause.addEventListener('click', togglePlay);
document.querySelector('.replay').addEventListener('click', function() {
  location.reload();
});
Next Steps

Next Steps

I only had three hours to build this (which is cruel, because I was having a blast working on this and just want to keep going by adding more features and perfecting the rendering), so needeless to say, there are some definite improvements that can be made in the form of: more features, bug fixes, performance and responsiveness.

Here's what I would do next:

  • Add more tracks
    • I originally wanted to be able to click on a certain letter of his name which would respectively play a tune that starts with that letter (i.e. 'S' would trigger 'Summerwind')
    • Add two more controls that would play the next or previous tune
    • It would be really cool to animate a new vinyl going on the player for a new track, or the arm moving to a different point like a real record player
  • Make it look better on mobile devices
    • While I did my best to make this app responsive by using viewport units and percentages wherever possible in the CSS, it isn't quite perfect on super small screens
  • Refine the styles to perfectly match the original photo
    • Add the coarse, grainy sparkle texture to the ground and the terrazo/brass texture to the star
      • I initially gave this a shot, but it took away from my alotted time, so I through that feature in the icebox and went with a flat look instead
    • Add the lines which frame the star
      • CSS Grid would be perfect for this
    • Fix the spacing of the vinyl record (the original has some wider spacing in between the vinyl itself and the border surrounding it)
    • Add the little circle that represents the record player's needle
      • It probably just needs another tiny <div class="circle"> attached to the arm, but I ran out of time.
    • Fix the colors of the vinyl record
      • I started with a flat color (as is in the photo), but needed to add a gradient to show it spinning.
      • There is a subtle, grainy shadow on the border of the vinyl which I would love to recreate but ran out of time
  • Fix the bugs
    • A couple bugs in here are driving me crazy:
      • In Chrome, the record player's arm's border gets all wacky during an animation event
      • I think maybe the position: absolute of the star itself or some other funky bug is causing the body to not display at 100vh, even when it is set to that value.
      • The circles get oddly skewed as the animation begins. I have a feeling this is due to me setting the width/height dynamically depending on the viewport size, so in retrospect it probably would have been better to go with SVG's instead of pure CSS.
  • Simplify the CSS
    • While I feel I was as concise as possible while styling this, especially with utility classes for the many divs using .flex-center {display: flex; align-content: center; justify-content: center} and the .circle: {border-radius: 50%}, there are definitely ways I could use pseudo-classes and better nesting in the HTML to dry up / simplify the CSS
  • Simplify the JavaScript
    • I stuck with the methods I use the most for events and interactivity, but there are surely other more elegant ES6 features I could utilize to simplify the functions, which would in turn boost the performance
    • I chose to go with the setTimeout route for the animation/play sequence, but it would probably be cleaner and easier to alter timing in the future with specific event listeners based on DOM Events like transitionend or play or ended, etc.
  • Add more interactivity
    • I would love to show the lyrics of the track beneath the star while the audio plays.
    • Maybe add a button that displays a modal pop up with Frank's bio.
    • Create some more natural animations
      • It would be really cool to link the animations to the soundwaves of the audio track itself.
  • Expand the app to show more stars
    • I definitely had to reel myself in when I started thinking of what this could be. How cool would it be to basically recreate the entire Walk of Fame, so a user could click a "Forward" or "Back" button, which brings them to the actual next star on the street, then click play and if the star belongs to a:
      • Musician like Frank, animate the record player but play a track of the respective musician
      • Film or TV star, have the projector play a famous clip of theirs
      • Radio star, animate the microphone as a well-known voice-over sample plays
  • Create an augmented reality app
    • Pretty out there, I know, but how awesome would it be to be able to hold your phone over a star, and it animates the star right in front of your eyes? A guy can dream, can't he?!