mediamusicstuff

Bringing album art back to life

So, after asking Spotify to treat album art properly for over a decade I ended up giving up and fixing it myself (and let them know).

I now have a nice 12″ x 12″ screen embedded in my wall that, when I’m using Spotify, gets the album art and puts it on the screen. And when it’s not in use, it defaults back to a clock 🙂

It’s rather lovely, I must say, and creates exactly the effect I was wanting it to.

And, in case you are wondering, the album cover above is from my album, Binary Dust (more on that at https://binarydust.org).

Here’s how

What kit? Not a lot…

1 x very old 4:3 screen (this one only had a VGA input) which I picked up for free. It’s nicely matt, and not too bright. I suspect a more modern monitor may actually be a bit too bright and shiny.
1 x Raspberry Pi Model B
1 x HDMI to VGA cable
1 x bit of old ply cut as a square-crop frame and painted with blackboard paint and a couple of strips of single-sided sticky foam tape stuck on the verticals (not essential, but helps mask the edges of the screen from light bleed)
10 x patience because I’m well out of practice on code

Cutting the square was ok (easiest if you have a multi-tool electric saw). Patience was needed for the code…

Setting up your Pi to be in kiosk mode

Make sure your Pi is up-to-date and installed with the Chromium browser. You’ll also need to install an extra so that you can refresh the browser from the command line, so add xdotool with

sudo apt-get install xdotool

and, we’ll need a version of ping that can wake up devices so that arp can find them, called fping

sudo apt-get install fping

Then we need to set it so that it boots directly into kiosk mode.

Edit the file /etc/xdg/openbox/autostart as follows

# Disable any form of screen saver / screen blanking / power management
 xset s off
 xset s noblank
 xset -dpms

# # Allow quitting the X server with CTRL-ATL-Backspace
 setxkbmap -option terminate:ctrl_alt_bksp

# # get the IP of the amp
/home/pi/screen/scan.sh

# # Start Chromium in kiosk mode
 sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' ~/.config/chromium/'Local State'
 sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]\+"/"exit_type":"Normal"/' ~/.config/chromiu
m/Default/Preferences
  chromium-browser --disable-infobars --kiosk /home/pi/screen/album-cover.html 

Note the path at the end to where your HTML file is, and the call to scan, which gets the IP address of the amp based on its unique MAC (hardware) address on boot and drops it into a file called hifi.ip. Note that to get the MAC address of the amp (00:06:78:14:0d:fa), I ran arp -a from my Mac laptop and looked through for the IPs to find the one that matched the one displayed on the amp itself.

scan.sh

#!/bin/bash

cd /home/pi/screen
hifi=`cat hifi.ip` # get the last IP used
up=`ping -c 1 $hifi  &> /dev/null && echo yes || echo no` # check if it's alive

if test "$up" = "no" ; then
# sometimes arp only recognises pre-existing things on the network if they've done something first. This hack uses fping to wake up everything on the subnet so that arp can find it 
fping -gaq 192.168.1.0/24 &> /dev/null 

# we expect arp does actually find the new IP, filtered out using the hifi's MAC address, and drop it into a file
arp -a | grep 00:06:78:14:0d:fa | awk '{print $2}' | sed -e 's/(//g' | sed -e 's/)//g' > /home/pi/screen/hifi.ip
fi

Now some fun. If you are using an old monitor, you need to force it to be VGA. This tripped me up for a while as I could boot the pi without the HDMI cable plugged in, then plug the cable in and it’d work, but if I booted it with the cable in from the start, it wouldn’t. Also fun fact: you can’t plug the HDMI output of your Mac into VGA (eg. using an adapted cable) <grumble grumble DRM>. Mac blocks it.

So, in /boot/config.txt you need to set

hdmi_group=1:
hdmi_mode=1

Or, depending on your monitor, fiddle around with it. It also lets you rotate the screen (which I’ve used in other dashboard-like applications when mounting monitors on their sides).

You’ll also want to get rid of the cursor, so edit your  ~/.bash_profile  to

[[ -z $DISPLAY && $XDG_VTNR -eq 1 ]] && startx -- -nocursor

Getting the album image

I was expecting to have to go get the album art via the Spotify API, and slightly dreading that as my coding skills are now terrible, and so I was hoping to just ugly-hack it in shell scripts (and not have to open sockets and auth and APIs and such)… and … I lucked out as my amplifier (Marantz sr7007) has Spotify and its own (albeit truly dreadful) web interface.

This meant I could find a rendered image when the amp is on, at

http://[IP ADDRESS OF MY AMP]/NetAudio/art.asp-jpg

(the IP sometimes varies, see above).

So, hurra! Let’s get to the hack that works for me.

We need to get the image, but not have the browser screen refresh/flash every 2 seconds as that’ll be more than distracting. So, here’s a shell script that gets the image, tests if the image has changed from the last time it checked (using the md5sum of the current live image vs this latest one), and if it has, refreshes the screen:

test-refresh.sh

#!/bin/sh
cd /home/pi/screen 
hifi=`cat hifi.ip` # gets the IP address of the amp
albumart="http://$hifi/NetAudio/art.asp-jpg"
curl $albumart > nowtmp.jpg # gets the image, saves locally
mv nowtmp.jpg now.jpg # do as a two-step in case download latency leads to partial-download comparisons

x=`md5sum now.jpg  | awk '{print $1}'` # gets a unique number for this image
y=`md5sum live.jpg | awk '{print $1}'` # gets a unique number for live image
if test "$x"  != "$y"; then # if they're not the same
  cp now.jpg live.jpg. # update the live image
  ./refresh.sh # and refresh the browser
fi

Remember the autostart has already loaded the web page, so refresh just needs to effectively hit reload if the image has changed. Here’s a script to do that:

refresh.sh

#!/bin/sh 
export DISPLAY=:0
export XAUTHORITY=/home/pi/.Xauthority
xdotool getactivewindow
xdotool key F5

Yup, it does the equivalent of hit F5 on the keyboard.

So, now to the web page, album-cover.html which puts the image in the right place.

<head> 
    <link href="album-cover.css" rel="stylesheet" type="text/css" />  
</head>
<body style="background-color: black;">
<div id='outerdiv'>
<iframe src="live.jpg" id='inneriframe' scrolling=no></iframe>
</div>
</body>

And the CSS is… interesting. Because [reasons that I’ve stopped trying to work out] …something about the 4:3 ratio, the HDMI settings, shoddy CSS coding, the way that chromium works on a Pi, who knows, I had to hack the CSS on the Pi to be different to my testing on my Mac (which worked fine without tweaking/stretching/etc). Much faffing around later… this:

album-cover.css

@charset "utf-8";  
#outerdiv
{
width:600px;
height:600px;
overflow:hidden;
position:relative;
border-width:0px;
border-style:none;
padding: 0px;
text-align: center;
align:center;
margin-left: auto;
margin-right: auto;
margin-top: auto;
margin-bottom: auto;
background-color: black;
object-fit: contain;
}

#inneriframe
{
position:absolute;
top:0px;
left:32px; // manual tweak to the edge of the wooden frame
width:768px;
height:768px;
border-width:0px;
align:center;
margin-left: auto;
margin-right: auto;
margin-top: auto;
margin-bottom: auto;
// now we unsquish the image because the Pi squished it somehow 
object-fit: contain;-webkit-transform: scale(.90, .81); 
-webkit-transform-origin: 0 0;
}

And so, now we’ve got the image, and managed to put it on the screen, and -just about- got it to fill a square with a square image.

Now I wanted to test if the amp was on/off and replace the image with a clock if Spotify wasn’t on. Nice blunt instrument here:

up-amp.sh

#!/bin/bash
cd /home/pi/screen/
hifi=`cat hifi.ip`
up=`ping -c 1 $hifi &> /dev/null && echo yes || echo no` # is it up?
if test "$up" = "yes" ; then # if it's up 
 if test `cat status.flag` = "no" ; then  # update the flag and the cover
	 echo "yes" > status.flag
  	 cp album-cover-live-safe.html album-cover.html
         ./refresh.sh
 fi	 
else # if it's not up
 if test `cat status.flag` = "yes" ; then # update flag and switch to clock	 
	 echo "no" > status.flag
 	 cp clock-analogue.html album-cover.html
         ./refresh.sh
 fi
fi
sleep 5  

The code for the clock is at the bottom of this post – there are loads of examples out there.

Now, finally, we need to get the Pi to keep checking what’s happening on the amp, and since the Pi isn’t doing anything else, and I’m lazy, we’ll get a script to test/get the image from the amp every 2 seconds. But because cron doesn’t do less than 1-minute resolution, I wrote a script that can be run every minute that then runs the checking command every 5 seconds for a minute.

albumcron.sh 

#!/bin/sh
i=0
while [ $i -lt 12 ]; do # run 12 times at five-second intervals in one minute
 /home/pi/screen/test-refresh.sh & # test for a new image
 sleep 5 # wait two seconds
 i=$(( i + 1 )) 
done

And so, finally, we can ask cron to do something! Run

> crontab -e

And enter these two lines

* * * * * /home/pi/screen/albumcron.sh > /dev/null 2>&1 # two-sec art checks
* * * * * /home/pi/screen/up-amp.sh > /dev/null 2>&1 # one minute amp checks

This checks if the amp is on once a minute and, if so, refreshes everything. The Pi also tries to get a new image every two seconds while it’s on and, no, I’ve not written* a thing to make it not do that when the amp is off because, unfortunately for it, it now all just works—in spite of shonky code and inefficient processes—and I have music to listen to and, frankly, no one cares (apart from my ego). Good enough.

*update: turns out I did as it was simple: just add if test `cat status.flag` = “yes” ; then around the albumcron.sh while statement.

The clock thing

I tried a few different ones, but liked the way this looks on the wall and it has a nice ‘glitchy’ second hand that makes it feel analogue.

Source: https://codepen.io/rodnylobos/pen/OJmZOx and my edited one below to faff move it into the centre, size is to as big as it’ll go, and set the background colour to match the blackboard paint hue.

<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>Clock Snap</title>
<script src="http://s.codepen.io/assets/libs/modernizr.js" type="text/javascript"></script>
<style type="text/css">
html, body, p {
background: #444;
background-color: #444;
margin: 5px;
height: 100%; overflow: hidden
}
svg{
display: block;
margin:  -150px 0px 0px -120px;
background-color: #444;
}
</style>
</head>
<body>
<svg version="1.1" id="clock" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0"
width="900px" height="900px" viewBox="0 0 500 600" 
xml:space="preserve">
<circle id="face" fill="#F4F3ED" cx="243.869" cy="250.796" r="130.8"/>
<path id="rim" fill="#383838" d="M243.869,101.184c-82.629,0-149.612,66.984-149.612,149.612c0,82.629,66.983,149.612,149.612,149.612
S393.48,333.425,393.48,250.796S326.498,101.184,243.869,101.184z M243.869,386.455c-74.922,0-135.659-60.736-135.659-135.659
c0-74.922,60.737-135.659,135.659-135.659c74.922,0,135.658,60.737,135.658,135.659
C379.527,325.719,318.791,386.455,243.869,386.455z"/>
<g id="inner">
<g opacity="0.2">
<path fill="#C4C4C4" d="M243.869,114.648c-75.748,0-137.154,61.406-137.154,137.153c0,75.749,61.406,137.154,137.154,137.154
c75.748,0,137.153-61.405,137.153-137.154C381.022,176.054,319.617,114.648,243.869,114.648z M243.869,382.56
c-72.216,0-130.758-58.543-130.758-130.758s58.542-130.758,130.758-130.758c72.216,0,130.758,58.543,130.758,130.758
S316.085,382.56,243.869,382.56z"/>
</g>
<g>
<path fill="#282828" d="M243.869,113.637c-75.748,0-137.154,61.406-137.154,137.153c0,75.749,61.406,137.154,137.154,137.154
c75.748,0,137.153-61.405,137.153-137.154C381.022,175.043,319.617,113.637,243.869,113.637z M243.869,381.548
c-72.216,0-130.758-58.542-130.758-130.757c0-72.216,58.542-130.758,130.758-130.758c72.216,0,130.758,58.543,130.758,130.758
C374.627,323.005,316.085,381.548,243.869,381.548z"/>
</g>
</g>
<g id="markings">
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="243.5" y1="139" x2="243.5" y2="133"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="231.817" y1="139.651" x2="231.19" y2="133.684"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="220.266" y1="141.52" x2="219.018" y2="135.65"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="208.973" y1="144.585" x2="207.119" y2="138.879"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="198.063" y1="148.814" x2="195.623" y2="143.333"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="187.655" y1="154.161" x2="184.655" y2="148.965"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="177.862" y1="160.566" x2="174.335" y2="155.712"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="168.792" y1="167.96" x2="164.778" y2="163.501"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="160.545" y1="176.262" x2="156.087" y2="172.246"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="153.211" y1="185.379" x2="148.358" y2="181.852"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="146.871" y1="195.214" x2="141.675" y2="192.213"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="141.593" y1="205.658" x2="136.112" y2="203.216"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="137.436" y1="216.596" x2="131.729" y2="214.741"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="134.445" y1="227.909" x2="128.576" y2="226.66"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="132.653" y1="239.472" x2="126.685" y2="238.843"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="132.079" y1="251.16" x2="126.079" y2="251.158"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="132.73" y1="262.843" x2="126.762" y2="263.468"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="134.598" y1="274.395" x2="128.729" y2="275.64"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="137.664" y1="285.688" x2="131.958" y2="287.539"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="141.893" y1="296.598" x2="136.412" y2="299.035"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="147.24" y1="307.006" x2="142.043" y2="310.004"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="153.645" y1="316.799" x2="148.791" y2="320.323"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="161.04" y1="325.868" x2="156.58" y2="329.881"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="169.341" y1="334.115" x2="165.325" y2="338.572"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="178.459" y1="341.449" x2="174.931" y2="346.302"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="188.294" y1="347.789" x2="185.292" y2="352.984"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="198.738" y1="353.066" x2="196.295" y2="358.548"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="209.676" y1="357.223" x2="207.82" y2="362.93"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="220.989" y1="360.214" x2="219.739" y2="366.084"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="232.552" y1="362.006" x2="231.922" y2="367.975"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="244.239" y1="362.58" x2="244.237" y2="368.582"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="255.921" y1="361.93" x2="256.547" y2="367.898"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="267.472" y1="360.062" x2="268.719" y2="365.932"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="278.765" y1="356.996" x2="280.619" y2="362.703"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="289.675" y1="352.767" x2="292.116" y2="358.248"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="300.083" y1="347.42" x2="303.083" y2="352.616"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="309.876" y1="341.015" x2="313.403" y2="345.869"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="318.946" y1="333.621" x2="322.96" y2="338.08"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="327.193" y1="325.319" x2="331.651" y2="329.334"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="334.527" y1="316.201" x2="339.38" y2="319.728"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="340.868" y1="306.367" x2="346.063" y2="309.367"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="346.146" y1="295.924" x2="351.626" y2="298.364"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="350.303" y1="284.986" x2="356.008" y2="286.84"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="353.294" y1="273.673" x2="359.162" y2="274.92"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="355.087" y1="262.11" x2="361.052" y2="262.737"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="356" y1="250.5" x2="362" y2="250.5"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="355.355" y1="238.781" x2="361.323" y2="238.153"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="353.489" y1="227.193" x2="359.359" y2="225.945"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="350.422" y1="215.864" x2="356.129" y2="214.01"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="346.188" y1="204.918" x2="351.669" y2="202.477"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="340.833" y1="194.474" x2="346.029" y2="191.474"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="334.415" y1="184.647" x2="339.269" y2="181.12"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="327.004" y1="175.545" x2="331.463" y2="171.529"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="318.684" y1="167.268" x2="322.699" y2="162.807"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="309.543" y1="159.905" x2="313.07" y2="155.049"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="299.684" y1="153.538" x2="302.683" y2="148.34"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="289.212" y1="148.237" x2="291.652" y2="142.753"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="278.245" y1="144.059" x2="280.097" y2="138.351"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="266.9" y1="141.05" x2="268.145" y2="135.179"/>
<line fill="none" stroke="#3F3F3F" stroke-miterlimit="10" x1="255.302" y1="139.244" x2="255.927" y2="133.275"/>
<polygon fill="#3F3F3F" points="247.391,133 243.5,141.05 239.609,133 	"/>
<polygon fill="#3F3F3F" points="188.022,147.021 188.677,155.938 181.283,150.912 	"/>
<polygon fill="#3F3F3F" points="143.617,188.848 148.643,196.243 139.726,195.588 	"/>
<polygon fill="#3F3F3F" points="126.074,247.273 134.125,251.165 126.076,255.056 	"/>
<polygon fill="#3F3F3F" points="140.095,306.644 149.013,305.988 143.988,313.382 	"/>
<polygon fill="#3F3F3F" points="181.922,351.049 189.318,346.022 188.663,354.938 	"/>
<polygon fill="#3F3F3F" points="240.349,368.591 244.24,360.54 248.13,368.589 	"/>
<polygon fill="#3F3F3F" points="299.718,354.569 299.062,345.652 306.457,350.677 	"/>
<polygon fill="#3F3F3F" points="344.123,312.742 339.096,305.348 348.012,306.002 	"/>
<polygon fill="#3F3F3F" points="362,254.316 353.951,250.426 362,246.534 	"/>
<polygon fill="#3F3F3F" points="347.934,194.779 339.018,195.435 344.042,188.04 	"/>
<polygon fill="#3F3F3F" points="305.984,150.252 298.59,155.277 299.244,146.361 	"/>
<rect x="282" y="152.98" fill="none" width="17.366" height="27.947"/>
<text transform="matrix(1 0 0 1 282 174.4307)" fill="#303030" font-family="'Futura-Medium'" font-size="26">1</text>
<rect x="320.699" y="188.474" fill="none" width="17.202" height="26.267"/>
<text transform="matrix(1 0 0 1 320.6987 209.9229)" fill="#303030" font-family="'Futura-Medium'" font-size="26">2</text>
<rect x="335.04" y="238.872" fill="none" width="21.03" height="24.585"/>
<text transform="matrix(1 0 0 1 335.0396 260.3213)" fill="#303030" font-family="'Futura-Medium'" font-size="26">3</text>
<rect x="319.699" y="290.242" fill="none" width="17.202" height="23.557"/>
<text transform="matrix(1 0 0 1 319.6987 311.6914)" fill="#303030" font-family="'Futura-Medium'" font-size="26">4</text>
<rect x="284.5" y="323.319" fill="none" width="19.212" height="22.511"/>
<text transform="matrix(1 0 0 1 284.5 344.7695)" fill="#303030" font-family="'Futura-Medium'" font-size="26">5</text>
<rect x="235.552" y="336.08" fill="none" width="19.938" height="24.15"/>
<text transform="matrix(1 0 0 1 235.5522 357.5293)" fill="#303030" font-family="'Futura-Medium'" font-size="26">6</text>
<rect x="189.373" y="322.319" fill="none" width="19.673" height="25.003"/>
<text transform="matrix(1 0 0 1 189.3726 343.7695)" fill="#303030" font-family="'Futura-Medium'" font-size="26">7</text>
<rect x="151.066" y="287.539" fill="none" width="17.726" height="25.203"/>
<text transform="matrix(1 0 0 1 151.0664 308.9883)" fill="#303030" font-family="'Futura-Medium'" font-size="26">8</text>
<rect x="136.392" y="241.25" fill="none" width="20.696" height="22.348"/>
<text transform="matrix(1 0 0 1 136.3916 262.6992)" fill="#303030" font-family="'Futura-Medium'" font-size="26">9</text>
<rect x="149.066" y="191.474" fill="none" width="36.554" height="27.122"/>
<text transform="matrix(1 0 0 1 149.0664 212.9229)" fill="#303030" font-family="'Futura-Medium'" font-size="26">10</text>
<rect x="184.967" y="158.518" fill="none" width="36.021" height="27.13"/>
<text transform="matrix(1 0 0 1 184.9673 179.9668)" fill="#303030" font-family="'Futura-Medium'" font-size="26">11</text>
<rect x="225.723" y="144.514" fill="none" width="37.029" height="29.25"/>
<text transform="matrix(1 0 0 1 225.7227 165.9639)" fill="#303030" font-family="'Futura-Medium'" font-size="26">12</text>
</g>
<path id="hours" fill="#3A3A3A" d="M242.515,270.21c-0.44,0-0.856-0.355-0.926-0.79l-3.156-19.811c-0.069-0.435-0.103-1.149-0.074-1.588
l4.038-62.009c0.03-0.439,0.414-0.798,0.854-0.798h0.5c0.44,0,0.823,0.359,0.852,0.798l4.042,62.205
c0.028,0.439-0.015,1.152-0.097,1.584l-3.712,19.623c-0.082,0.433-0.508,0.786-0.948,0.786H242.515z"/>
<path id="minutes" fill="#3A3A3A" d="M247.862,249.75l-2.866,24.244c-0.099,1.198-0.498,2.18-1.497,2.179c-0.999,0-1.397-0.98-1.498-2.179
l-2.861-24.508c-0.099-1.199,3.479-93.985,3.479-93.985c0.036-1.201-0.117-2.183,0.881-2.183c0.999,0,0.847,0.982,0.882,2.183
L247.862,249.75z"/>
<g id="seconds">
<line fill="none" stroke="#BF4116" stroke-miterlimit="10" x1="243.5" y1="143" x2="243.5" y2="266"/>
<circle fill="none" stroke="#BF4116" stroke-miterlimit="10" cx="243.5" cy="271" r="5"/>
<circle fill="#BF4116" cx="243.5" cy="251" r="3.917"/>
</g>
</svg>
<script src='http://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.2.0/snap.svg-min.js'></script>
</body>
</html>
<script type="text/javascript">
var s = Snap(document.getElementById("clock"));
var seconds = s.select("#seconds"),
minutes = s.select("#minutes"),
hours   = s.select("#hours"),
rim     = s.select("#rim"), 
face    = {
elem: s.select("#face"),
cx: s.select("#face").getBBox().cx,
cy: s.select("#face").getBBox().cy,
},
angle   = 0,
easing = function(a) {
return a==!!a?a:Math.pow(4,-10*a)*Math.sin((a-.075)*2*Math.PI/.3)+1;
};
var sshadow = seconds.clone(),
mshadow = minutes.clone(),
hshadow = hours.clone(),
rshadow = rim.clone(),
shadows = [sshadow, mshadow, hshadow];
//Insert shadows before their respective opaque pals
seconds.before(sshadow);
minutes.before(mshadow);
hours.before(hshadow);
rim.before(rshadow);
//Create a filter to make a blurry black version of a thing
var filter = Snap.filter.blur(0.1) + Snap.filter.brightness(0);
//Add the filter, shift and opacity to each of the shadows
shadows.forEach(function(el){
el.attr({
transform: "translate(0, 2)",
opacity: 0.2,
filter: s.filter(filter)
});
})
rshadow.attr({
transform: "translate(0, 8) ",
opacity: 0.5,
filter: s.filter(Snap.filter.blur(0, 8)+Snap.filter.brightness(0)),
})
function update() {
var time = new Date();
setHours(time);
setMinutes(time);
setSeconds(time);
}
function setHours(t) {
var hour = t.getHours();
hour %= 12;
hour += Math.floor(t.getMinutes()/10)/6;
var angle = hour*360/12;
hours.animate(
{transform: "rotate("+angle+" 244 251)"},
100,
mina.linear,
function(){
if (angle === 360){
hours.attr({transform: "rotate("+0+" "+face.cx+" "+face.cy+")"});
hshadow.attr({transform: "translate(0, 2) rotate("+0+" "+face.cx+" "+face.cy+2+")"});
}
}
);
hshadow.animate(
{transform: "translate(0, 2) rotate("+angle+" "+face.cx+" "+face.cy+2+")"},
100,
mina.linear
);
}
function setMinutes(t) {
var minute = t.getMinutes();
minute %= 60;
minute += Math.floor(t.getSeconds()/10)/6;
var angle = minute*360/60;
minutes.animate(
{transform: "rotate("+angle+" "+face.cx+" "+face.cy+")"},
100,
mina.linear,
function(){
if (angle === 360){
minutes.attr({transform: "rotate("+0+" "+face.cx+" "+face.cy+")"});
mshadow.attr({transform: "translate(0, 2) rotate("+0+" "+face.cx+" "+face.cy+2+")"});
}
}
);
mshadow.animate(
{transform: "translate(0, 2) rotate("+angle+" "+face.cx+" "+face.cy+2+")"},
100,
mina.linear
);
}
function setSeconds(t) {
t = t.getSeconds();
t %= 60;
var angle = t*360/60;
//if ticking over to 0 seconds, animate angle to 360 and then switch angle to 0
if (angle === 0) angle = 360;
seconds.animate(
{transform: "rotate("+angle+" "+face.cx+" "+face.cy+")"},
600,
easing,
function(){
if (angle === 360){
seconds.attr({transform: "rotate("+0+" "+face.cx+" "+face.cy+")"});
sshadow.attr({transform: "translate(0, 2) rotate("+0+" "+face.cx+" "+face.cy+2+")"});
}
}
);
sshadow.animate(
{transform: "translate(0, 2) rotate("+angle+" "+face.cx+" "+face.cy+2+")"},
600,
easing
);
}
setInterval(update, 1000);	
</script>

One thought on “Bringing album art back to life

Comments are closed.