#app
View Compiled
/* PrismJS 1.23.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=css+css-extras&plugins=line-numbers+inline-color+toolbar+copy-to-clipboard */
/**
 * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
 * Based on https://github.com/chriskempson/tomorrow-theme
 * @author Rose Pritchard
 */

code[class*="language-"],
pre[class*="language-"] {
  color: #ccc;
  background: none;
  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
  font-size: 1em;
  text-align: left;
  white-space: pre;
  word-spacing: normal;
  word-break: normal;
  word-wrap: normal;
  line-height: 1.5;

  -moz-tab-size: 4;
  -o-tab-size: 4;
  tab-size: 4;

  -webkit-hyphens: none;
  -moz-hyphens: none;
  -ms-hyphens: none;
  hyphens: none;

}

/* Code blocks */
pre[class*="language-"] {
  padding: 2rem;
  margin: 0;
  overflow: auto;
  outline: transparent;
}

:not(pre) > code[class*="language-"],
pre[class*="language-"] {
  background: #2d2d2d;
}

/* Inline code */
:not(pre) > code[class*="language-"] {
  padding: .1em;
  border-radius: .3em;
  white-space: normal;
}

.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
  color: #999;
}

.token.punctuation {
  color: #ccc;
}

.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
  color: #e2777a;
}

.token.function-name {
  color: #6196cc;
}

.token.boolean,
.token.number,
.token.function {
  color: #f08d49;
}

.token.property,
.token.class-name,
.token.constant,
.token.symbol {
  color: #f8c555;
}

.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
  color: #cc99cd;
}

.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
  color: #7ec699;
}

.token.operator,
.token.entity,
.token.url {
  color: #67cdcc;
}

.token.important,
.token.bold {
  font-weight: bold;
}
.token.italic {
  font-style: italic;
}

.token.entity {
  cursor: help;
}

.token.inserted {
  color: green;
}

pre[class*="language-"].line-numbers {
  position: relative;
  padding-left: 3.8em;
  counter-reset: linenumber;
}

pre[class*="language-"].line-numbers > code {
  position: relative;
  white-space: inherit;
}

.line-numbers .line-numbers-rows {
  position: absolute;
  pointer-events: none;
  top: 0;
  font-size: 100%;
  left: -3.8em;
  width: 3em; /* works for line-numbers below 1000 lines */
  letter-spacing: -1px;
  border-right: 1px solid #999;

  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;

}

  .line-numbers-rows > span {
    display: block;
    counter-increment: linenumber;
  }

    .line-numbers-rows > span:before {
      content: counter(linenumber);
      color: #999;
      display: block;
      padding-right: 0.8em;
      text-align: right;
    }

span.inline-color-wrapper {
  /*
   * The background image is the following SVG inline in base 64:
   *
   * <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2">
   *     <path fill="gray" d="M0 0h2v2H0z"/>
   *     <path fill="white" d="M0 0h1v1H0zM1 1h1v1H1z"/>
   * </svg>
   *
   * SVG-inlining explained:
   * https://stackoverflow.com/a/21626701/7595472
   */
  background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyIDIiPjxwYXRoIGZpbGw9ImdyYXkiIGQ9Ik0wIDBoMnYySDB6Ii8+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0wIDBoMXYxSDB6TTEgMWgxdjFIMXoiLz48L3N2Zz4=");
  /* This is to prevent visual glitches where one pixel from the repeating pattern could be seen. */
  background-position: center;
  background-size: 110%;

  display: inline-block;
  height: 1.333ch;
  width: 1.333ch;
  margin: 0 .333ch;
  box-sizing: border-box;
  border: 1px solid white;
  outline: 1px solid rgba(0,0,0,.5);
  overflow: hidden;
}

span.inline-color {
  display: block;
  /* To prevent visual glitches again */
  height: 120%;
  width: 120%;
}

div.code-toolbar {
  position: relative;
}

div.code-toolbar > .toolbar {
  position: absolute;
  top: .3em;
  right: .2em;
  opacity: 1;
}

div.code-toolbar:hover > .toolbar {
  opacity: 1;
}

/* Separate line b/c rules are thrown out if selector is invalid.
   IE11 and old Edge versions don't support :focus-within. */
div.code-toolbar:focus-within > .toolbar {
  opacity: 1;
}

div.code-toolbar > .toolbar .toolbar-item {
  display: inline-block;
}

div.code-toolbar > .toolbar a {
  cursor: pointer;
}

div.code-toolbar > .toolbar button {
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  line-height: normal;
  overflow: visible;
  padding: 0;
  -webkit-user-select: none; /* for button */
  -moz-user-select: none;
  -ms-user-select: none;
}

div.code-toolbar > .toolbar a,
div.code-toolbar > .toolbar button {
  color: #bbb;
  font-size: 1rem;
  padding: 0.5rem;
  font-family: sans-serif;
  background: hsl(0, 0%, 25%);
  border-radius: .5em;
  outline: transparent;
  cursor: pointer;
}

div.code-toolbar > .toolbar a:hover,
div.code-toolbar > .toolbar a:focus,
div.code-toolbar > .toolbar button:hover,
div.code-toolbar > .toolbar button:focus,
div.code-toolbar > .toolbar span:hover,
div.code-toolbar > .toolbar span:focus {
  background: hsl(0, 0%, 40%);
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  background: hsl(210, 30%, 60%);
  overflow-x: hidden;
  overflow-y: scroll;
}

pre {
  border-radius: 6px;
}

.watch {
  height: 300px;
  align-self: center;
  justify-self: center;
}

.container {
  display: grid;
  grid-template-columns: auto auto;
  grid-template-rows: auto auto;
}

.slider-thumb {
  background: transparent;
  position: relative;

  &:after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    background: hsl(210, 80%, 70%);
    height: 24px;
    width: 24px;
    border: '2px solid hsl(%s, 80%, 50%)' % var(--hue, 210)
  }

  &:active {
    border: '2px solid hsl(%s, 80%, 50%)' % var(--hue, 210);
  }
}

.slider-track {
  height: 10px;
  width: 100%;
  min-width: 100px;
  background: hsl(0, 0%, 90%);
  border: 1px solid hsl(0, 0%, 70%);
}

.watch {
  grid-columm: 2;
}

label {
  font-weight: bold;
  font-family: sans-serif;
  color: hsl(0, 0, 10%);
}

.code-block {
  grid-row: 1 / -1;
}

.controls {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: 1fr 1fr;
  align-items: center;
  padding: 1rem;
  grid-gap: 1rem 2rem;
}
View Compiled
import gsap from 'https://cdn.skypack.dev/gsap'
import React, {
  useEffect,
  useRef,
  useState,
} from 'https://cdn.skypack.dev/react'
import T from 'https://cdn.skypack.dev/prop-types'
import { Range } from 'https://cdn.skypack.dev/react-range'
import { render } from 'https://cdn.skypack.dev/react-dom'
import Prism from 'https://cdn.skypack.dev/prismjs'

const ROOT_NODE = document.querySelector('#app')
const SPEED = 12
const getCode = (timeWindow, duration) => `/**
 * LOOP is a timeline moving the hands
 * It has repeat: -1 set
 * Use "fromTo" to animate a time window
 * This is "Meta GSAP" 😎
 */
gsap.fromTo(LOOP, {
  totalTime: ${timeWindow[0]},
}, {
  totalTime: ${timeWindow[1]},
  duration: ${duration[0]},
  ease: 'none',
  repeat: -1,
})
`

const getCodeMarkup = (timeWindow, duration) => {
  const code = getCode(timeWindow, duration)
  return Prism.highlight(code, Prism.languages.javascript, 'javascript')
}

const Watch = ({ timeWindow, duration }) => {
  const minuteRef = useRef(null)
  const hourRef = useRef(null)
  const rawRef = useRef(null)
  const loopRef = useRef(null)
  // Sets up the timeline.
  useEffect(() => {
    rawRef.current = gsap
      .timeline({
        repeat: -1,
        paused: true,
      })
      .to(
        minuteRef.current,
        {
          rotate: 360,
          repeat: 12,
          duration: SPEED / 13,
          transformOrigin: '50% 100%',
          ease: 'none',
          immediateRender: false,
        },
        0
      )
      .to(
        hourRef.current,
        {
          rotate: 360,
          immediateRender: false,
          duration: SPEED,
          transformOrigin: '50% 100%',
          ease: 'none',
        },
        0
      )
  }, [])
  useEffect(() => {
    loopRef.current = gsap.fromTo(
      rawRef.current,
      {
        totalTime: timeWindow[0],
      },
      {
        duration: duration[0],
        totalTime: timeWindow[1],
        ease: 'none',
        repeat: -1,
      }
    )
  }, [duration, timeWindow])
  // useEffect(() => {})
  return (
    <svg
      className="watch"
      viewBox="0 0 574 707"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      xmlnsXlink="http://www.w3.org/1999/xlink">
      <rect
        x={526}
        y={328}
        width={41}
        height={53}
        rx={9}
        fill="hsl(35, 90%, 65%)"
      />
      <rect x={168} width={238} height={707} rx={12} fill="hsl(0, 0%, 85%)" />
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M168 608.916V169.084C203.384 149.897 243.92 139 287 139s83.616 10.897 119 30.084v439.832C370.616 628.103 330.08 639 287 639s-83.616-10.897-119-30.084z"
        fill="hsla(0, 0%, 45%, 0.65)"
      />
      <circle
        cx={287}
        cy={354}
        r={249}
        fill="hsl(0, 0%, 74%)"
        stroke="#fff"
        strokeWidth={2}
      />
      <circle
        cx={287}
        cy={354}
        r={207.5}
        fill="hsl(0, 0%, 42%)"
        stroke="#4F4F4F"
        strokeWidth={5}
      />
      <circle
        cx={287}
        cy={354}
        r={207.5}
        fill="hsl(0, 0%, 42%)"
        stroke="#4F4F4F"
        strokeWidth={5}
      />
      <path
        d="M285 176h4v15h-4v-15zM374.268 198.847l3.464 2-7.5 12.991-3.464-2 7.5-12.991zM440.153 263.268l2 3.464-12.991 7.5-2-3.464 12.991-7.5zM465 352v4h-15v-4h15zM442.153 441.268l-2 3.464-12.991-7.5 2-3.464 12.991 7.5zM377.732 507.153l-3.464 2-7.5-12.991 3.464-2 7.5 12.991zM289 532h-4v-15h4v15zM199.732 509.153l-3.464-2 7.5-12.991 3.464 2-7.5 12.991zM133.847 444.732l-2-3.464 12.991-7.5 2 3.464-12.991 7.5zM109 356v-4h15v4h-15zM131.847 266.732l2-3.464 12.991 7.5-2 3.464-12.991-7.5zM196.268 200.847l3.464-2 7.5 12.991-3.464 2-7.5-12.991z"
        fill="#000"
      />
      <path fill="url(#prefix__pattern0)" d="M267 234h40v40h-40z" />
      <rect
        ref={minuteRef}
        x={283}
        y={245}
        width={8}
        height={109}
        rx={4}
        fill="hsl(0, 0%, 0%)"
      />
      <rect
        ref={hourRef}
        x={281}
        y={263}
        width={12}
        height={91}
        rx={6}
        fill="hsl(0, 0%, 0%)"
      />
      <circle cx={287} cy={354} r={12} fill="#000" />
      <defs>
        <pattern
          id="prefix__pattern0"
          patternContentUnits="objectBoundingBox"
          width={1}
          height={1}>
          <use xlinkHref="#prefix__image0" transform="scale(.00333)" />
        </pattern>
        <image
          id="prefix__image0"
          width={300}
          height={300}
          xlinkHref="https://assets.codepen.io/605876/watch-branding-bear.png"
        />
      </defs>
    </svg>
  )
}
Watch.propTypes = {
  timeWindow: T.array,
  duration: T.array,
}

const App = () => {
  const [timeWindow, setTimeWindow] = useState([0, 12])
  const [duration, setDuration] = useState([12])
  const jsRef = useRef(getCodeMarkup(timeWindow, duration))
  return (
    <div className="container">
      <div className="code-block">
        <pre>
          <code
            className="language-javascript"
            dangerouslySetInnerHTML={{ __html: jsRef.current }}
          />
        </pre>
      </div>
      <Watch timeWindow={timeWindow} duration={duration} />
      <div className="controls">
        <label>Time Window</label>
        <Range
          step={1}
          min={0}
          max={36}
          values={timeWindow}
          onChange={values => {
            jsRef.current = getCodeMarkup(values, duration)
            setTimeWindow(values)
          }}
          renderTrack={({ props, children }) => (
            <div
              {...props}
              className="slider-track"
              style={{
                ...props.style,
              }}>
              {children}
            </div>
          )}
          renderThumb={({ props }) => (
            <div
              {...props}
              className="slider-thumb"
              style={{
                ...props.style,
                borderRadius: '50%',
                outline: 'transparent',
                height: '44px',
                width: '44px',
              }}
            />
          )}
        />
        <label>Duration</label>
        <Range
          step={1}
          min={1}
          max={36}
          values={duration}
          onChange={values => {
            jsRef.current = getCodeMarkup(timeWindow, values)
            setDuration(values)
          }}
          renderTrack={({ props, children }) => (
            <div
              {...props}
              className="slider-track"
              style={{
                ...props.style,
              }}>
              {children}
            </div>
          )}
          renderThumb={({ props }) => (
            <div
              {...props}
              className="slider-thumb"
              style={{
                ...props.style,
                borderRadius: '50%',
                outline: 'transparent',
                height: '44px',
                width: '44px',
              }}
            />
          )}
        />
      </div>
    </div>
  )
}

render(<App />, ROOT_NODE)
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.