<div class="column">
<h1>The Standard-agnostic Clock</h1>
<div class="intro">
<p>
Ever wondered why standards are important?
</p>
<p>Play around with the parameters below and try to tell what time it is. 😉
</p>
</div>
<div id="app">
<div class="controls">
<div class="control">
<h3>Spinning direction:</h3>
<div>
<label class="option-row">
<input type="radio"v-model="params.direction" :value="1"/>
<span class="label">
Clockwise
</span>
</label>
</div>
<div>
<label class="option-row">
<input type="radio"/ v-model="params.direction" :value="-1">
<span class="label">
Counterclockwise
</span>
</label>
</div>
</div>
<div class="control">
<h3>Rotation:</h3>
<label>
<p>Where should the 12 be?</p>
<div class="rotation-offset-selector">
<input v-for="n in 12" type="radio" v-model="params.rotationOffset" :value="12 - n" :style="nTransform(n, 50)">
</div>
</label>
</div>
</div>
<div class="clock-container">
<div class="clock">
<div v-for="n in 12" class="hour" :style="hourTransform(n)">
{{n}}
</div>
<div class="handle hours-handle" :style="hoursHandleTransform"></div>
<div class="handle minutes-handle" :style="minutesHandleTransform"></div>
<div class="handle seconds-handle" :style="secondsHandleTransform"></div>
</div>
</div>
</div>
<footer>
<p>Inspired by the Nonstandard Clock described by Donald Norman in <i>The Design of Everyday Things</i>.</p>
</footer>
</div>
$radius: 100px;
$gray-1: hsl(0, 0%, 88%);
$gray-2: hsl(0, 0%, 92%);
$gray-3: hsl(0, 0%, 96%);
$border-radius: .5rem;
$text-color: hsl(24, 0%, 20%);
$outer-border-color: hsl(24, 0%, 74%);
$inner-border-color: hsl(0, 0%, 82%);
$outer-border: 1px solid $outer-border-color;
$inner-border: 1px solid $inner-border-color;
html,
body {
height: 100%;
}
body {
margin: 0;
background-color: $gray-1;
font-family: "Work Sans", sans-serif;
display: flex;
flex-direction: column;
color: $text-color;
}
.column {
max-width: 620px;
margin-left: auto;
margin-right: auto;
}
h1,
.intro,
footer {
padding-left: 1rem;
padding-right: 1rem;
}
h1 {
font-family: "Raleway", sans-serif;
font-weight: 700;
}
.intro p {
margin: 0;
}
.intro {
margin-bottom: .5rem;
}
#app {
display: flex;
flex-direction: row;
margin: 1rem;
}
.controls {
border-right: $inner-border;
border-top: $outer-border;
border-left: $outer-border;
border-bottom: $outer-border;
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
background-color: hsla(0, 0%, 100%, 0.6);
height: 100%;
h3 {
font-weight: 500;
font-size: 1rem;
margin-bottom: 0.5rem;
}
.control {
display: flex;
flex-direction: column;
padding: 1rem;
h3 {
margin-top: 0;
}
}
.control:not(:first-child) {
border-top: $inner-border;
}
.option-row {
display: flex;
align-items: center;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
label {
color: hsl(0, 0%, 45%);
}
label input[type="radio"] {
margin: 0;
}
label input[type="radio"] + .label {
margin-left: 0.5rem;
}
label p {
margin-top: 0;
}
}
.rotation-offset-selector {
$radius: 50px;
$input-side: 12px;
width: $radius*2;
height: $radius*2;
position: relative;
margin: $input-side*2 auto $input-side;
input {
position: absolute;
top: $radius - $input-side/2;
left: $radius - $input-side/2;
display: flex;
align-items: center;
justify-content: space-around;
}
}
.clock-container {
flex-grow: 1;
display: flex;
align-items: center;
background-color: $gray-2;
border-top: $outer-border;
border-right: $outer-border;
border-bottom: $outer-border;
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
.clock {
$padding: $radius / 6;
background-color: white;
position: relative;
width: $radius * 2;
height: $radius * 2;
margin: auto;
border: 1px solid hsl(0, 0, 70%);
border-radius: 50%;
padding: $padding;
box-shadow: 3px 2px 10px 4px rgba(0, 0, 0, 0.15);
color: hsl(0, 0, 40%);
.hour,
.handle {
position: absolute;
top: $radius + $padding;
left: $radius + $padding;
}
.hour {
width: 0;
height: 0;
display: flex;
align-items: center;
justify-content: space-around;
}
.handle {
background-color: black;
transform-origin: left center;
transition: transform 0.3s;
}
.hours-handle {
width: $radius * 1/2;
height: 3px;
margin-top: -1.5px;
}
.minutes-handle {
width: $radius * 3/4;
height: 2px;
margin-top: -1px;
}
.seconds-handle {
width: $radius * 5/6;
height: 1px;
margin-top: -0.5px;
opacity: 0.35;
}
&:after {
$r: 3px;
display: block;
content: '';
background-color: black;
width: $r*2;
height: $r*2;
left: $radius + $padding - $r;
top: $radius + $padding - $r;
position: absolute;
border-radius: 50%;
}
}
View Compiled
new Vue({
el: '#app',
data() {
return {
currentTime: this.getCurrentTime(),
initialTime: this.getCurrentTime(),
interval: null,
params: {
direction: Math.random() < .5 ? -1 : 1,
rotationOffset: Math.floor(Math.random()*12)
}
}
},
mounted() {
this.interval = setInterval(() => {
this.currentTime = this.getCurrentTime();
}, 1000);
},
beforeDestroy() {
clearInterval(this.interval);
},
computed: {
hoursHandleTransform() {
const hour = this.currentTime.hours;
const angle = this.hoursAngle(hour);
return this.handleTransform(angle);
},
minutesHandleTransform() {
const angle = this.minutesAngle(this.currentTime.minutes);
return this.handleTransform(angle);
},
secondsHandleTransform() {
const angle = this.secondsAngle(this.currentTime.seconds);
return this.handleTransform(angle);
}
},
methods: {
angleTransform(angle, radius = 100) {
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
return {
transform: `translate(${x}px, ${y}px)`
}
},
getCurrentTime() {
if(!this.initialTime) {
return this.getInitialTime();
}
const timestamp = new Date().getTime();
const deltaSeconds = (timestamp - this.initialTime.timestamp)/1000;
const currentMinuteFloorTimestamp = timestamp - timestamp%60000;
const initialMinuteFloorTimestamp = this.initialTime.timestamp - this.initialTime.timestamp%60000;
const deltaMinutes = (currentMinuteFloorTimestamp - initialMinuteFloorTimestamp)/60000;
return {
hours: this.initialTime.hours + this.initialTime.minutes/60 + deltaSeconds/3600,
minutes: this.initialTime.minutes + deltaMinutes,
seconds: this.initialTime.seconds + deltaSeconds
}
},
getInitialTime() {
const now = new Date();
return {
hours: now.getHours(),
minutes: Math.floor(now.getMinutes()),
seconds: now.getSeconds(),
timestamp: now.getTime(),
}
},
handleTransform(angle) {
return {
transform: `rotate(${angle}rad)`
}
},
hoursAngle(hour) {
return 2*Math.PI*(hour*this.params.direction + this.params.rotationOffset)/12;
},
hourTransform(hour, radius = 100) {
const angle = this.hoursAngle(hour);
return this.angleTransform(angle, radius);
},
minutesAngle(minute) {
return 2*Math.PI*(this.params.direction*minute/60 + this.params.rotationOffset/12);
},
nTransform(n, radius = 50) {
const angle = - 2*Math.PI*n/12;
return this.angleTransform(angle, radius);
},
secondsAngle(second) {
return 2*Math.PI*(this.params.direction*second/60 + this.params.rotationOffset/12);
}
}
})