Building SVG Clock using ReactJs

Building SVG Clock using ReactJs

3 years ago, May 5, 2021
Reading time: 11 mins

DEMO: https://df2y4.csb.app/

GitHub: https://github.com/dejavu1987/react-svg-clock

This blog post is an exercise from my previous blog post Introduction to Polar Coordinates, plotting on radial diagrams. If you have not read the post and have no idea how radial coordinates work. Please go to the post and read and come back. If you are confident, lets go build a Clock.

We will start with a simple React component Clock. It has 2 states, hour, min and sec, which will be set to current time every second.

import { useState } from "react";
import "./styles.css";

import useInterval from "./useInterval";

export default function Clock() {
  const [hour, setHour] = useState<number>(0);
  const [min, setMin] = useState<number>(0);
  const [sec, setSec] = useState<number>(0);

  useInterval(() => {
    const now = new Date();

    const m = now.getMinutes();
    const h = now.getHours();
    const s = now.getSeconds();
    
    setHour(h > 11 ? h - 12 : h);
    setMin(m);
    setSec(s);
    
  }, 1000);


  return (
    <div className="clock">
      <h1>React SVG Clock</h1>
      <h2>
        Polar coordinates fundamentals
      </h2>
      <h3>
        Hour: {hour}, Minutes: {min}, Seconds: {sec}
      </h3>
    </div>
  );
}

useTimeout() is a custom React hook that will call the callback function every given delay.

import useInterval from "./useInterval";
 
 
+const getAngleFromRatio = (ratio: number): number => {
+  return ratio * 2 * Math.PI;
+};
+
 export default function Clock() {
   const [hour, setHour] = useState<number>(0);
   const [min, setMin] = useState<number>(0);
   const [sec, setSec] = useState<number>(0);
 
+  const [hourAngle, setHourAngle] = useState<number>(0);
+  const [minAngle, setMinAngle] = useState<number>(0);
+  const [secAngle, setSecAngle] = useState<number>(0);
+
 
   useInterval(() => {
     const now = new Date();
@@ -20,6 +28,10 @@ export default function Clock() {
     setHour(h > 11 ? h - 12 : h);
     setMin(m);
     setSec(s);
+
+    setHourAngle(getAngleFromRatio(h / 12));
+    setMinAngle(getAngleFromRatio(m / 60));
+    setSecAngle(getAngleFromRatio(s / 60));
     
   }, 1000);
 
@@ -33,6 +45,9 @@ export default function Clock() {
       <h3>
         Hour: {hour}, Minutes: {min}, Seconds: {sec}
       </h3>
+      {hourAngle}
+      {minAngle}
+      {secAngle}
     </div>
   );
 }
{hourAngle}
       {minAngle}
       {secAngle}
+
+      <h4>Test 0deg</h4>
+      {getAngleFromRatio(0 / 12)} === {0}
+      <h4>Test 360deg</h4>
+      {getAngleFromRatio(12 / 12)} === {2 * Math.PI}
+      <h4>Test 180deg</h4>
+      {getAngleFromRatio(6 / 12)} === {Math.PI}
+      <h4>Test 90deg</h4>
+      {getAngleFromRatio(3 / 12)} === {Math.PI / 2}
     </div>
 import useInterval from "./useInterval";
 
+type coord = {
+  x: number;
+  y: number;
+};
+
+const BASE_DIMENSION = 400;
+const RADIUS = BASE_DIMENSION / 2;
 
 const getAngleFromRatio = (ratio: number): number => {
   return ratio * 2 * Math.PI;
 };
 
+const polarToCartesian = (r: number, angle: number): coord => {
+  return {
+    x: r * Math.cos(angle) + RADIUS,
+    y: -r * Math.sin(angle) + RADIUS
+  };
+};
+
...

<h3>
         Hour: {hour}, Minutes: {min}, Seconds: {sec}
       </h3>
-      {hourAngle}
-      {minAngle}
-      {secAngle}
-
-      <h4>Test 0deg</h4>
-      {getAngleFromRatio(0 / 12)} === {0}
-      <h4>Test 360deg</h4>
-      {getAngleFromRatio(12 / 12)} === {2 * Math.PI}
-      <h4>Test 180deg</h4>
-      {getAngleFromRatio(6 / 12)} === {Math.PI}
-      <h4>Test 90deg</h4>
-      {getAngleFromRatio(3 / 12)} === {Math.PI / 2}
+      <svg width={BASE_DIMENSION} height={BASE_DIMENSION}>
+        <circle
+          r={RADIUS}
+          cx={RADIUS}
+          cy={RADIUS}
+          fill="#333"
+        ></circle>
+        <circle
+          r={(RADIUS) * 0.7}
+          cx={RADIUS}
+          cy={RADIUS}
+          fill="rgba(0,0,0,0.3)"
+          stroke="white"
+        ></circle>
+        <circle
+          r={10}
+          fill="white"
+          cx={RADIUS}
+          cy={RADIUS}
+        ></circle>
+      </svg>
     </div>
   );
 }
        ></circle>
+        {new Array(12).fill("").map((d, i) => {
+          const numPos = polarToCartesian(
+            RADIUS - 25,
+            getAngleFromRatio(i / 12)
+          );
+          return (
+            <>
+              <text
+                x={numPos.x}
+                y={numPos.y}
+                fill="white"
+                textAnchor="middle"
+                dy=".5rem"
+                fontSize="2rem"
+              >
+                {i}
+              </text>
+              <text
+                x={numPos.x}
+                y={numPos.y}
+                fill="white"
+                textAnchor="middle"
+                dx="-1rem"
+                fontSize=".5rem"
+              >
+              {i*5}
+              </text>
+            </>
+          );
+        })}
       </svg>

 const BASE_DIMENSION = 400;
 const RADIUS = BASE_DIMENSION / 2;
+const minR = RADIUS * 0.74;
+const secR = RADIUS * 0.7;
+const hourR = RADIUS * 0.4;
 
 const getAngleFromRatio = (ratio: number): number => {
   return ratio * 2 * Math.PI;
@@ -31,6 +34,10 @@ export default function Clock() {
   const [minAngle, setMinAngle] = useState<number>(0);
   const [secAngle, setSecAngle] = useState<number>(0);
 
+  const [hourPt, setHourPt] = useState<coord>();
+  const [minPt, setMinPt] = useState<coord>();
+  const [secPt, setSecPt] = useState<coord>();
+
   useInterval(() => {
     const now = new Date();
 
@@ -46,6 +53,10 @@ export default function Clock() {
     setMinAngle(getAngleFromRatio(m / 60));
     setSecAngle(getAngleFromRatio(s / 60));
 
+    setHourPt(polarToCartesian(hourR, hourAngle));
+    setMinPt(polarToCartesian(minR, minAngle));
+    setSecPt(polarToCartesian(secR, secAngle));
+
   }, 1000);
 
 
@@ -108,6 +119,31 @@ export default function Clock() {
             </>
           );
         })}
+        {hourPt && (
+          <line
+            x1={BASE_DIMENSION / 2}
+            y1={BASE_DIMENSION / 2}
+            stroke="white"
+            x2={hourPt.x}
+            y2={hourPt.y}
+            stroke-width={4}
+            markerEnd="url(#head)"
+          ></line>
+        )}
+        {minPt && (
+          <line
+            x1={BASE_DIMENSION / 2}
+            y1={BASE_DIMENSION / 2}
+            stroke="#aaa"
+            x2={minPt.x}
+            y2={minPt.y}
+            stroke-width={2}
+            markerEnd="url(#head)"
+          ></line>
+        )}
+        {secPt && (
+          <circle r={9} fill="white" cx={secPt.x} cy={secPt.y}></circle>
+        )}
       </svg>
 const getAngleFromRatio = (ratio: number): number => {
-  return ratio * 2 * Math.PI;
+  return Math.PI/2 + ratio * 2 * Math.PI;
 };
const getAngleFromRatio = (ratio: number): number => {
-  return Math.PI/2 + ratio * 2 * Math.PI;
+  return 2 * Math.PI - ratio * 2 * Math.PI + Math.PI/2;
 };

With that + some cosmetic fixes you will end up with following

SVG Clock built in ReactJs
Previous
Implementing cocktail recommendation engine using neo4j
Next
Introduction to Polar Coordinates, plotting on radial diagrams
© 2024 Anil Maharjan