├── .gitignore ├── README.md ├── app.js ├── index.html ├── package.json └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Analogue World Clocks 2 | 3 | YouTube Video: https://youtu.be/tYKlLQmg6OM 4 | 5 | Building out an analogue clock includes a surprising amount of detail. In this video, we’ll build out our own analogue clock by creating a custom SVG, styling it with plain CSS, and animating it with vanilla JavaScript. Included in this project: 6 | - Creating a custom SVG with circle, text, and path elements 7 | - Using requestAnimationFrame to animate the clock performantly 8 | - Converting the project from a functional style to an object oriented stying 9 | - Adding world clocks from different timezones with date-fns and date-fns-tz 10 | 11 | --------------------------------------- 12 | 13 | 🔗 Key Links 🔗 14 | - Live Demo: https://codinginpublic.dev/projects/analogue-world-clocks/ 15 | - GitHub: https://github.com/coding-in-public/analogue-world-clocks 16 | 17 | --------------------------------------- 18 | 19 | 🔗 Additional Links 🔗 20 | - HappyHues (color palette): https://www.happyhues.co/ 21 | - NodeJS: https://nodejs.org/en/ 22 | - ParcelJS: https://parceljs.org/ 23 | - NPM package for timezones: https://www.npmjs.com/package/date-fns-tz 24 | - List of timezones: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 25 | 26 | --------------------------------------- 27 | 28 | 📹 Related/Mentioned Videos 📹 29 | - How to Animation SVG Strokes: https://youtu.be/-Na_WRk3k74 30 | 31 | --------------------------------------- 32 | 33 | 🌐 Connect With Me 🌐 34 | - Website: https://www.codinginpublic.dev 35 | - Blog: https://www.chrispennington.blog 36 | - Twitter: https://twitter.com/cpenned 37 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import { utcToZonedTime } from 'date-fns-tz' 2 | 3 | class Clock { 4 | constructor(el){ 5 | this.clockEl = el; 6 | this.UI = {}; 7 | this.initializeClock(); 8 | } 9 | 10 | updateClock = () => { 11 | // GETTING TIME 12 | const date = new Date(); 13 | const now = utcToZonedTime(date, this.clockEl.dataset.locale); 14 | // const date = now.getDate(); 15 | const seconds = (now.getSeconds() + now.getMilliseconds() / 1000) / 60 * 360; 16 | const minutes = (now.getMinutes() + now.getSeconds() / 60) / 60 * 360; 17 | const hours = (now.getHours() + now.getMinutes() / 60) / 12 * 360; 18 | // UI Update 19 | this.UI.date.textContent = now.getDate(); 20 | this.UI.am_pm.textContent = now.getHours() > 12 ? 'PM' : 'AM'; 21 | this.UI.second.style.transform = `rotate(${seconds}deg)`; 22 | this.UI.minute.style.transform = `rotate(${minutes}deg)`; 23 | this.UI.hour.style.transform = `rotate(${hours}deg)`; 24 | requestAnimationFrame(this.updateClock) 25 | } 26 | 27 | initializeClock() { 28 | this.clockEl.innerHTML = ` 29 | 30 | 31 | 23 32 | am 33 | 34 | 35 | 36 | 37 | ` 38 | this.UI.date = this.clockEl.querySelector('.date'); 39 | this.UI.am_pm = this.clockEl.querySelector('.am-pm'); 40 | this.UI.second = this.clockEl.querySelector('.hand--second'); 41 | this.UI.minute = this.clockEl.querySelector('.hand--minute'); 42 | this.UI.hour = this.clockEl.querySelector('.hand--hour'); 43 | requestAnimationFrame(this.updateClock) 44 | } 45 | } 46 | 47 | const clocks = document.querySelectorAll('.clock'); 48 | clocks.forEach(el => new Clock(el)) 49 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | World Clock 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

Algiers

18 |
19 |
20 |
21 |
22 |

Denver

23 |
24 |
25 |
26 |
27 |

Pohnpei

28 |
29 |
30 |
31 |
32 |

Cocos

33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "world-clock", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "parcel": "^2.2.1" 15 | }, 16 | "dependencies": { 17 | "date-fns-tz": "^1.2.2" 18 | } 19 | } -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | *, *::after, *::before { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | :root { 8 | --bkg: #232946; 9 | --dark: #121629; 10 | --purple: #b8c1ec; 11 | --pink: #eebbc3; 12 | --white:#fffffe; 13 | } 14 | 15 | body { 16 | background-color: var(--bkg); 17 | display: grid; 18 | place-items: center; 19 | min-height: 100vh; 20 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 21 | font-size: 2rem; 22 | } 23 | 24 | .container { 25 | display: flex; 26 | flex-wrap: wrap; 27 | justify-content: center; 28 | gap: 4rem; 29 | margin: 1rem; 30 | } 31 | 32 | .clock--wrapper { 33 | display: grid; 34 | place-items: center; 35 | gap: 2rem; 36 | color: var(--white); 37 | } 38 | 39 | .clockface { 40 | filter: drop-shadow(2px 10px 10px var(--dark)); 41 | } 42 | 43 | .ring--seconds { 44 | fill: var(--dark); 45 | stroke: var(--pink); 46 | stroke-width: 5; 47 | stroke-dasharray:.1 .9; stroke-dashoffset: .05; 48 | } 49 | 50 | .ring--hours { 51 | fill: transparent; 52 | stroke: var(--purple); 53 | stroke-width: 10; 54 | stroke-dasharray:.05 .95; stroke-dashoffset: .025; 55 | } 56 | 57 | .ring--center { 58 | fill: var(--pink); 59 | stroke: var(--white); 60 | stroke-width: 2.5; 61 | } 62 | 63 | .hand { 64 | stroke: var(--pink); 65 | stroke-linecap: round; 66 | } 67 | 68 | .hand--hour { 69 | transform: rotate(45deg); 70 | stroke-width: 5; 71 | stroke: var(--white); 72 | } 73 | 74 | .hand--minute { 75 | stroke: var(--purple); 76 | transform: rotate(93deg); 77 | stroke-width: 5; 78 | } 79 | 80 | .date { 81 | fill: var(--white); 82 | font-size: 1.5rem; 83 | } 84 | 85 | .am-pm { 86 | fill: var(--pink); 87 | font-size: .8rem; 88 | text-transform: uppercase; 89 | } --------------------------------------------------------------------------------