Last active
August 22, 2023 12:30
-
-
Save bellbind/b9d1df2b906b5cc1d0cbd072d1e216e2 to your computer and use it in GitHub Desktop.
[SVG][JavaScript] SVG Analog Clock
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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"> 8/ 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
</head> | |
<body style="display: flex; background-color: #3c4044; color: #dddddd;"> | |
<div style="text-align: center;"> | |
<h1><img src="./clock.svg" /></h1> | |
<img src="./clock.svg" /> | |
<p>NOTE: script and web font are disabled</p> | |
</div> | |
<hr /> | |
<div style="text-align: center;"> | |
<h1><object data="./clock.svg"></object></h1> | |
<object data="./clock.svg"></object> | |
<p>NOTE: cannot omit a closing tag </object></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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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"] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
demo: