<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);
    }
  }
})

External CSS

  1. https://fonts.googleapis.com/css?family=Raleway:700|Work+Sans&amp;display=swap

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js