Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active August 22, 2023 12:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bellbind/b9d1df2b906b5cc1d0cbd072d1e216e2 to your computer and use it in GitHub Desktop.
Save bellbind/b9d1df2b906b5cc1d0cbd072d1e216e2 to your computer and use it in GitHub Desktop.
[SVG][JavaScript] SVG Analog Clock
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no" />
<link rel="icon" href="././clock.svg" />
<link rel="manifest" href="./manifest.json" />
<title>SVG Clock</title>
<style>
html {height: 100%;}
body {
margin: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #334444;
}
object {
width: 100vmin;
height: 100vmin;
}
</style>
</head>
<body>
<object data="./clock.svg" />
</body>
</html>
Display the source blob
Display the rendered blob
Raw
<svg xmlns="http://www.w3.org/2000/svg" width="320px" height="320px" viewBox="-160 -160 320 320">
<style><![CDATA[
@import url('https://fonts.googleapis.com/css2?family=Niconne&family=Noto+Sans+Mono:wght@500&family=Sancreek&display=swap');
]]></style>
<g fill="currentcolor">
<circle cx="0" cy="0" r="150" stroke="currentcolor" stroke-width="10" fill="#eae1cf"/>
<circle cx="0" cy="0" r="5" />
<rect x="-5" y="-150" width="10" height="15" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(30)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(60)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(90)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(120)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(150)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(180)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(210)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(240)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(270)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(300)" />
<rect x="-5" y="-150" width="10" height="10" transform="rotate(330)" />
<circle cx="0" cy="-142" r="2" transform="rotate(6)" />
<circle cx="0" cy="-142" r="2" transform="rotate(12)" />
<circle cx="0" cy="-142" r="2" transform="rotate(18)" />
<circle cx="0" cy="-142" r="2" transform="rotate(24)" />
<circle cx="0" cy="-142" r="2" transform="rotate(36)" />
<circle cx="0" cy="-142" r="2" transform="rotate(42)" />
<circle cx="0" cy="-142" r="2" transform="rotate(48)" />
<circle cx="0" cy="-142" r="2" transform="rotate(54)" />
<circle cx="0" cy="-142" r="2" transform="rotate(66)" />
<circle cx="0" cy="-142" r="2" transform="rotate(72)" />
<circle cx="0" cy="-142" r="2" transform="rotate(78)" />
<circle cx="0" cy="-142" r="2" transform="rotate(84)" />
<circle cx="0" cy="-142" r="2" transform="rotate(96)" />
<circle cx="0" cy="-142" r="2" transform="rotate(102)" />
<circle cx="0" cy="-142" r="2" transform="rotate(108)" />
<circle cx="0" cy="-142" r="2" transform="rotate(114)" />
<circle cx="0" cy="-142" r="2" transform="rotate(126)" />
<circle cx="0" cy="-142" r="2" transform="rotate(132)" />
<circle cx="0" cy="-142" r="2" transform="rotate(138)" />
<circle cx="0" cy="-142" r="2" transform="rotate(144)" />
<circle cx="0" cy="-142" r="2" transform="rotate(156)" />
<circle cx="0" cy="-142" r="2" transform="rotate(162)" />
<circle cx="0" cy="-142" r="2" transform="rotate(168)" />
<circle cx="0" cy="-142" r="2" transform="rotate(174)" />
<circle cx="0" cy="-142" r="2" transform="rotate(186)" />
<circle cx="0" cy="-142" r="2" transform="rotate(192)" />
<circle cx="0" cy="-142" r="2" transform="rotate(198)" />
<circle cx="0" cy="-142" r="2" transform="rotate(204)" />
<circle cx="0" cy="-142" r="2" transform="rotate(216)" />
<circle cx="0" cy="-142" r="2" transform="rotate(222)" />
<circle cx="0" cy="-142" r="2" transform="rotate(228)" />
<circle cx="0" cy="-142" r="2" transform="rotate(234)" />
<circle cx="0" cy="-142" r="2" transform="rotate(246)" />
<circle cx="0" cy="-142" r="2" transform="rotate(252)" />
<circle cx="0" cy="-142" r="2" transform="rotate(258)" />
<circle cx="0" cy="-142" r="2" transform="rotate(264)" />
<circle cx="0" cy="-142" r="2" transform="rotate(276)" />
<circle cx="0" cy="-142" r="2" transform="rotate(282)" />
<circle cx="0" cy="-142" r="2" transform="rotate(288)" />
<circle cx="0" cy="-142" r="2" transform="rotate(294)" />
<circle cx="0" cy="-142" r="2" transform="rotate(306)" />
<circle cx="0" cy="-142" r="2" transform="rotate(312)" />
<circle cx="0" cy="-142" r="2" transform="rotate(318)" />
<circle cx="0" cy="-142" r="2" transform="rotate(324)" />
<circle cx="0" cy="-142" r="2" transform="rotate(336)" />
<circle cx="0" cy="-142" r="2" transform="rotate(342)" />
<circle cx="0" cy="-142" r="2" transform="rotate(348)" />
<circle cx="0" cy="-142" r="2" transform="rotate(354)" />
</g>
<filter id="inset-shadow">
<!-- inset shadow by http://jsfiddle.net/kkPM4/
Drawing shapes should have dummy fill with some "fill-opacity" between 0.1 and 0.2
-->
<feComponentTransfer in="SourceAlpha" result="inside">
<!-- inset alpha is 1: drawing with inside -->
<feFuncA type="discrete" tableValues="0 1 1 1 1 1 1 1 1 1"/>
</feComponentTransfer>
<feComponentTransfer in="SourceGraphic" result="drawing">
<!-- inset alpha is 0: drawing only -->
<feFuncA type="discrete" tableValues="0 0 1 1 1 1 1 1 1 1"/>
</feComponentTransfer>
<!-- black colored drawing -->
<feColorMatrix type="matrix" in="drawing" result="black-drawing" values="0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 1 0" />
<!--shadow around target -->
<feGaussianBlur in="black-drawing" result="shadow1" stdDeviation="2" />
<feGaussianBlur in="black-drawing" result="shadow2" stdDeviation="4" />
<feGaussianBlur in="black-drawing" result="shadow3" stdDeviation="8" />
<feMerge result="shadow">
<feMergeNode in="shadow1" mode="normal"/>
<feMergeNode in="shadow2" mode="normal"/>
<feMergeNode in="shadow3" mode="normal"/>
</feMerge>
<!-- drop outside shadow -->
<feComposite operator="in" in="inside" in2="shadow" result="inset-shadow"/>
<!-- put target over inset-shadow -->
<feComposite operator="over" in="drawing" in2="inset-shadow"/>
</filter>
<g id="digital" display="block">
<rect x="-60" y="30" width="120" height="50" rx="5" ry="5" fill="#778899" />
<rect x="-60" y="30" width="120" height="50" rx="5" ry="5" stroke="black" stroke-width="1.5" fill-opacity="0.125" filter="url(#inset-shadow)"/>
<rect x="-60" y="30" width="120" height="50" rx="5" ry="5" stroke="#250d00" stroke-width="2" fill="transparent" />
<g fill="#332211" font-size="20" font-family="'Noto Sans Mono',monospace" text-anchor="middle">
<text dy="0.35em" id="date-text" x="0" y="45">&#8199;8/&#8199;5 Fri</text>
<text dy="0.35em" id="time-text" x="0" y="65">00:00:00</text>
</g>
</g>
<g font-size="30" font-family="'Sancreek',cursive" text-anchor="middle">
<text dy="0.35em" transform="translate(0 -120)">12</text>
<text dy="0.35em" transform="rotate(30),translate(0,-120),rotate(-30)">1</text>
<text dy="0.35em" transform="rotate(60),translate(0,-120),rotate(-60)">2</text>
<text dy="0.35em" transform="rotate(90),translate(0,-120),rotate(-90)">3</text>
<text dy="0.35em" transform="rotate(120),translate(0,-120),rotate(-120)">4</text>
<text dy="0.35em" transform="rotate(150),translate(0,-120),rotate(-150)">5</text>
<text dy="0.35em" transform="rotate(180),translate(0,-120),rotate(-180)">6</text>
<text dy="0.35em" transform="rotate(210),translate(0,-120),rotate(-210)">7</text>
<text dy="0.35em" transform="rotate(240),translate(0,-120),rotate(-240)">8</text>
<text dy="0.35em" transform="rotate(270),translate(0,-120),rotate(-270)">9</text>
<text dy="0.35em" transform="rotate(300),translate(0,-120),rotate(-300)">10</text>
<text dy="0.35em" transform="rotate(330),translate(0,-120),rotate(-330)">11</text>
</g>
<path id="logo-path" fill="none" stroke="none" d="M -80 0 A 80 80 180 0 1 80 0" />
<text font-size="15" font-family="'Niconne',cursive,serif" text-anchor="middle">
<textPath dy="0.35" href="#logo-path" startOffset="50%">SVG Clock</textPath>
</text>
<g fill="currentcolor">
<filter id="hshadow">
<feDropShadow dx="1" dy="1" stdDeviation="1" />
</filter>
<g filter="url(#hshadow)">
<rect id="hour" x="-5" y="-100" width="10" height="105" rx="5" ry="5" />
</g>
<filter id="mshadow">
<feDropShadow dx="2" dy="2" stdDeviation="2" />
</filter>
<g filter="url(#mshadow)">
<rect id="minute" x="-2" y="-130" width="4" height="132" rx="2" stroke="#212121" ry="2"/>
</g>
<filter id="sshadow">
<feDropShadow dx="3" dy="3" stdDeviation="3" />
</filter>
<g filter="url(#sshadow)">
<rect id="second" x="-1" y="-140" width="2" height="160" fill="#6c272d" rx="1" ry="1" />
<circle r="3" fill="#6c272d" />
</g>
</g>
<script><![CDATA[
const svg = document.documentElement;
const dateText = document.getElementById("date-text");
const timeText = document.getElementById("time-text");
const formatDate = date => {
const m = date.getMonth() + 1;
const d = date.getDate();
const w = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()];
return `${m > 9 ? m : `\u{2007}${m}`}/${d > 9 ? d : `\u{2007}${d}`} ${w}`;
};
const formatTime = date => {
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
};
const hour = svg.getElementById("hour");
const minute = svg.getElementById("minute");
const second = svg.getElementById("second");
const htrans = svg.createSVGTransform();
const mtrans = svg.createSVGTransform();
const strans = svg.createSVGTransform();
hour.transform.baseVal.appendItem(htrans);
minute.transform.baseVal.appendItem(mtrans);
second.transform.baseVal.appendItem(strans);
const setDateTime = date => {
const s = date.getSeconds();
const m = date.getMinutes() * 60 + s;
const h = (date.getHours() % 12) * 3600 + m;
htrans.setRotate(h / 120, 0, 0);
mtrans.setRotate(m / 10, 0, 0);
strans.setRotate(s * 6, 0, 0);
dateText.textContent = formatDate(date);
timeText.textContent = formatTime(date);
};
let run = true;
// tick sound
let sec = 0;
let ac = null;
const toggleAudio = () => {
if (ac) {
ac.close();
ac = null;
} else {
ac = new AudioContext();
}
};
const tick = () => {
if (!ac) return;
const ws = ac.createWaveShaper();
ws.connect(ac.destination);
const g = ac.createGain();
g.connect(ws);
g.gain.setValueAtTime(20, ac.currentTime);
g.gain.exponentialRampToValueAtTime(0.00001, ac.currentTime + 0.01);
g.gain.linearRampToValueAtTime(0.00001, ac.currentTime + 0.199);
g.gain.exponentialRampToValueAtTime(10, ac.currentTime + 0.2);
g.gain.exponentialRampToValueAtTime(0.00001, ac.currentTime + 0.21);
g.gain.linearRampToValueAtTime(0, ac.currentTime + 0.5);
const p = ac.createBiquadFilter();
p.connect(g);
p.type = "bandpass";
p.frequency.value = 440 * 2 ** (3);
p.Q.value = 110 * 2 ** (4);
const o = ac.createOscillator();
o.connect(p);
o.type = "sine";
o.frequency.value = (440 * 2 ** (10/12)) * 2 **(2);
o.start(ac.currentTime);
o.stop(ac.currentTime + 0.5);
o.addEventListener("ended", () => {ws.disconnect();}, {once: true});
};
document.addEventListener("click", ev => toggleAudio());
const loop = () => {
if (!run) return;
const now = new Date();
setDateTime(now);
const s = now.getSeconds();
if (s !== sec) {
sec = s;
tick();
}
requestAnimationFrame(loop);
};
loop();
// exported API via objectElement.contentWindow.clock
const controls = {
setDate(date) {
run = false;
setDateTime(date);
},
restart() {
run = true;
loop();
},
stop() {
run = false;
},
showDigital() {
document.getElementById("digital").setAttribute("display", "block");
},
hideDigital() {
document.getElementById("digital").setAttribute("display", "none");
},
get audioEnabled() {return ac !== null;},
toggleAudio,
};
globalThis.clock = controls;
]]></script>
</svg>
<!doctype html>
<html>
<head>
</head>
<body style="display: flex; background-color: #3c4044; color: #dddddd;">
<div style="text-align: center;">
<h1>&lt;img src="./clock.svg" /&gt;</h1>
<img src="./clock.svg" />
<p>NOTE: script and web font are disabled</p>
</div>
<hr />
<div style="text-align: center;">
<h1>&lt;object data="./clock.svg"&gt;&lt;/object&gt;</h1>
<object data="./clock.svg"></object>
<p>NOTE: cannot omit a closing tag &lt;/object&gt;</p>
</div>
<hr />
<div style="text-align: center;">
<h1>objectElement.contentWindow from HTML</h1>
<object data="./clock.svg"></object>
<div>
<button onclick="this.parentNode.parentNode.querySelector('object').contentWindow.clock.showDigital()">show digital panel</button>
<button onclick="this.parentNode.parentNode.querySelector('object').contentWindow.clock.hideDigital()">hide digital panel</button>
</div>
<p>NOTE: cannot access contentWindow when file: protocol</p>
</div>
</body>
</html>
{
"short_name": "SVG Clock",
"name": "SVG Analog Clock",
"icons": [
{
"src": "./clock.svg",
"type": "image/svg+xml",
"sizes": "320x320"
}
],
"start_url": "./clock.html",
"theme_color": "#334444",
"background_color": "#334444",
"display": "standalone",
"display_override": ["fullscreen", "minimal-ui", "window-controls-overlay", "browser"]
}