Progressive Web Application

Neil HaddleyJuly 24, 2021

Building a Progressive Web Application

Mobilepwaprogressive-web-appservice-workermobile

I created a GitHub project/repository.

I started a project

I started a project

I named the project "clock"

I named the project "clock"

I clicked the "Create repository" button

I clicked the "Create repository" button

I clicked the "Code" dropdown. I selected the "Open with GitHub Desktop" button.

I clicked the "Code" dropdown. I selected the "Open with GitHub Desktop" button.

I opened the project using GitHub Desktop

I opened the project using GitHub Desktop

The repository was cloned to my laptop.

The repository was cloned to my laptop.

I added an index.html page

I added an index.html page

I added the Clock.jpg image

I added the Clock.jpg image

The clock code is available here:

Create an Azure Static Web App

I navigated to the Azure Portal.

I used the filter to locate "Static Web Apps"

I used the filter to locate "Static Web Apps"

I created a Static Web App

I created a Static Web App

I created the Static Web App in a new Resource Group

I created the Static Web App in a new Resource Group

I selected the "Free" hosting plan. I clicked the "Sign in with GitHub" button.

I selected the "Free" hosting plan. I clicked the "Sign in with GitHub" button.

I allowed Azure to access GitHub

I allowed Azure to access GitHub

I selected the clock repository I created earlier

I selected the clock repository I created earlier

I specified the build details.

I specified the build details.

I clicked the "Create" button

I clicked the "Create" button

A GitHub Action published content from GitHub to Azure

A GitHub Action published content from GitHub to Azure

The clock application files have been moved

The web page ran and displayed the clock

The web page ran and displayed the clock

Adding the Progressive Web Application manifest and service worker

To convert the web page to a Progressive Web Application, I added a manifest and a service worker.

The manifest file describes the Progressive Web Application

The manifest file describes the Progressive Web Application

I added a manifest file reference to the index.html web page

I added a manifest file reference to the index.html web page

I added code to the index.html page to register the service worker.

I added code to the index.html page to register the service worker.

PWA asset generator

I used the pwa-asset-generator to generate a set of application images and icons from the Clock.jpg file.

BASH
1% npx pwa-asset-generator Clock.jpg icons
pwa-asset-generator

pwa-asset-generator

I copied the `<link rel=apple...` tags from the terminal to the head section of the index.html page

I copied the `<link rel=apple...` tags from the terminal to the head section of the index.html page

I copied the details of the generated icons to the manifest.json file

I copied the details of the generated icons to the manifest.json file

Service Worker

To have the app run offline, I defined install and fetch event handlers. The install handler copies offline.html to the browser cache. The fetch handler serves the cached file when no network connection is available.

https://developers.google.com/web/fundamentals/primers/service-workers

The service worker was registered

The service worker was registered

The offline.html file was copied to Cache Storage

The offline.html file was copied to Cache Storage

iOS

I installed the Progressive Web Application on an Apple mobile phone.

I used "Add to Home Screen" to add the application to the iPhone Home Screen

I used "Add to Home Screen" to add the application to the iPhone Home Screen

The Clock application on the iPhone Home Screen

The Clock application on the iPhone Home Screen

The Clock application ran on iPhone without an Internet connection

The Clock application ran on iPhone without an Internet connection

Android

I installed the Progressive Web Application on an Android mobile phone.

I selected the Install app menu item

I selected the Install app menu item

I confirmed the installation

I confirmed the installation

The application was added to the Android Home Screen

The application was added to the Android Home Screen

The running application

The running application

MacBook

I installed the Progressive Web Application on a MacBook.

The Clock app can be "installed" onto a MacBook

The Clock app can be "installed" onto a MacBook

I confirmed the installation

I confirmed the installation

The Clock Application running on the MacBook

The Clock Application running on the MacBook

Windows 10

I installed the Progressive Web Application on a Windows 10 laptop.

The Clock app can be "installed" onto a Windows 10 laptop

The Clock app can be "installed" onto a Windows 10 laptop

I confirmed the installation

I confirmed the installation

Launching the Clock application from the start menu

Launching the Clock application from the start menu

The Clock Application running on the Windows 10 laptop

The Clock Application running on the Windows 10 laptop

Lighthouse

I ran the Lighthouse report in Google Chrome DevTools to evaluate the Progressive Web Application.

I reviewed the Lighthouse report

I reviewed the Lighthouse report

sw.js

JAVASCRIPT
1const CACHE_NAME = 'clock-cache-v1';
2const urlsToCache = [
3  '/offline.html',
4];
5const OFFLINE_URL = "offline.html";
6
7
8self.addEventListener('install', event => {
9  event.waitUntil(
10    caches.open(CACHE_NAME)
11      .then(function (cache) {
12        console.log('Opened cache');
13        return cache.addAll(urlsToCache);
14      })
15  );
16});
17
18
19this.addEventListener('fetch', event => {
20  if (event.request.method === 'GET' &&
21    event.request.headers.get('accept').includes('text/html')) {
22    event.respondWith(
23      fetch(event.request.url).catch(error => {
24        return caches.match(OFFLINE_URL);
25      })
26    );
27  }
28  else {
29    event.respondWith(fetch(event.request));
30  }
31});

offline.html

HTML
1<!DOCTYPE html>
2<html lang="en-US">
3
4<head>
5
6    <meta charset="UTF-8">
7
8    <title>Clock</title>
9
10    <meta name="viewport" content="width=device-width, initial-scale=1">
11
12    <meta name="apple-mobile-web-app-capable" content="yes">
13
14    <meta name="description" content="Clock app">
15    <style>
16        #clockContainer {
17            position: relative;
18            margin: auto;
19            height: 40vw;
20            width: 40vw;
21
22            background: url("data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAEsASwBAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APf6KKKKKKKKKKKCQBk1jah4r0TTCVuL+IyD/lnGd7fkK5q8+J9omRZafNL6NKwQfkMmsS5+JGtS58lLW3HbCFj+ZP8ASs9vFXiW8J239yc9oYwP/QRTd/im653atJn3koGneJjz5Gpn/gTf40otPE8XIj1RfoX/AMacNT8T2nW51NMf3wxH6irEPjnxBbnD3iye0sS/0wa17X4lXiYF3p8Mg9YnKH9c1vWXxA0a5ws5mtWP/PRMr+YzXR2l9aX0fmWtzFMvrG4NWKKKKKKKKKKKKKKKKKKKKKKKq32o2em25nvbmOCMd3OM/T1/CuI1b4mRIWj0m1Mh6edPwv4KOT+OK5GfVPEHiWUxmW5uQT/qoRhB+A4/OtGx8AajOA11NDar/dHzt+Q4/WugtPAekwYM7T3Lf7TbR+Q/xrZttE0u0x5Gn2yEd/LBP5mtBE2jCLgf7IxT/LkP8LUvkSf3DR5Ug/gNG2QdmFQS2sE4KzQRSA9nQH+dZlz4V0W5yTYrGT3iJT+XFYl34AiOWs710PZZl3D8x/hWFc+G9b0l/OSFzt5822YnH5c1d03x1q9iQlyy3cY4Il4cf8CH9c12uk+MtK1QrGZTbTn/AJZzcZPs3Q10Oc0UUUUUUUUUUUUUUUUUVHPPFbQvNPIkcSDLO5wAPrXAa98SFQtBosYc9PtMi8f8BXv9T+Vclbadrfii6NwTLOScG4mbCr9D/QV12meBLC1CvfMbuX+791B+HU/jXUwW6RRiK3iVEHRI1wB+Aq2lo5+8QtTLaRjrlvrUqxovRQPwp2cDPQV5P8Q/jXYeFbuLT9FS21S9BJuP3h8uEemV6tnt2xzXdab4khl8D2viXUdltE9gt5OFyQgKbiB3NeSn49a3qUt3NoXg6S5sbVd8sjO7sif3n2jC/rXffDn4lWXxAs7nZatZ31rt863Z942noytgZGQe3FdsQO4zTDEh/hx9KjaAfwn86jMbr2/EVnahomnamD9qtkZ+0i/K4/EVyGp+B7mANJp8n2hP+eb4D/4H9KpaX4l1fQJfs7FniQ4a3nz8v07j+Veh6J4o0/W1CRv5VzjmCQ8/h6/hW1RRRRRRRRRRRRRRRWL4g8T2Hh+DM7eZcMMxwIfmb3PoPevLNR1jWPFt+sJDSZOY7aL7q+//ANc102i+Bre2Cz6oVuJevkj7i/X+9/KuxihyqxxIAqjACjAA/pVyO0UcyHJ9B0qwqhRhQAPaloorP13TTrOgahpgm8k3dtJB5mM7NykZx3xmvmb4s/DvSfAGkaHHYyTXFzcvN9ouJjy+0LgBRwo5Pvz1r37wdZWuo/DHQrS9t47i2l0yBZIpV3Kw2DgjvXnHjfx14f8AA8F74X8EaZb/ANq3ZMdwbWP5IWI24wPvPzjA4Hfnitj4KfD2+8Jafd6pq6GG+v1VVtieYYxz83+0T27YHevVzSUd6SmtGrdRUTRFeRyKztS0iy1WLZdwhiB8rjhl+hrhNX8LXukMbi3LT26nIkQYZPqB/MVr+H/HMkRS21YmSPotwBll/wB71Hv1rv4pY54llidXjcZVlOQRT6KKKKKKKKKKKK47xZ42i0ffZWBWW/6M3VYfr6n2/OuC0rRNR8TXr3Ekj+WWzNcyc5PoPU+3avSdL0iz0i2EFpFjP33PLOfc/wBK2IrQnDScD0ryvx38Yrrwd41ttEXRtlnE8clzPKctLE3UxgdMc8nuCMCvW7a5hvLWK5t5FlgmQSRupyGUjII/CpaKKKK8t+MvgLWvHMGjpo4tibRpTJ50uz7wXGODnoa6MaRr2n/CuDR9MaFNch02O1RzJhEkChSwbHYZI46gV4lpvwY+I2j6gL/T7mxt7tcgTJd/MM9cEr1969S+HOh+P9K1a8l8Xat9stHg2wp9p8za+4c4wMcZr0c9KSg0lFeS/ET4xN4P8WWmkafZw3wiXdfozEMC2NqKR0bHJ4PUCvULSU3thb3MlvJbSSxq7QyY3xkjO1scZHSnMpXrXKa74Qiu91zpyrFcdWi6K/09D+lc/ouv33hy7aCRHaANiW3fgqfUeh/nXp+n6hbanaJc2sgeNvzB9COxq1RRRRRRRRRRXDeNPGf9n79M0yQG7IxLMv8Ayy9h/tfy+tcn4a8Ly6zILu73pZZznPzSnvg+nqa9MtrZIYkt7eJUjQYVFGABWlDAsXPVvWpq88+Kvw3Pj3TrN7KSGDU7WQBJZchTEx+ZTgZ46j6H1rpPBfht/CXha00V9Qlvvs4IEsihcZOdoHZR2yTW/RRRRRSGikpO1JQaSivH9O+Cr23xM/4SHUNTGpaeJGu9sy4lafOVDgcFQecjHQDFev0h5FRsmOR0rE13w/b6zDu4julHyS46+zeo/lXFWF/qHhfVWVkKkHEsLH5XH+ehr1DTdSttVskurZ9yNwQeqn0PvVyiiiiiiiiuO8beLf7HhNhZOPt8i/Mw/wCWKnv9T2/OuL8L+Gn1mf7XdhhZI3JJ5lb0z6epr0+CAYWKJAqqMAAYCitGKJYlwOvc+tPpaKKKKKKKKQ0UlJSUGkoopKSio2XnIrJ1zRIdZtdpwlwg/dS+nsfauI0vUr3wxqzq6MADtnhJ+8PUe/oa9StLuG+tY7m3cPFIMqRU9FFFFFFYnifxBF4f0ppzhriTKwRn+JvU+w6mvLNF0q68TaxJJcSOY93mXMx6nPYe5/QV6rbWyRRR21vGEjQBUVegFakUSxJgde59akpKWiiiiiiiikNFJRTaDSUUUlIaKTvTCO9YPiTQV1W286FQLyIfKf74/un+lc54V15tIvfstyxFpK2GDf8ALNumf8a9NByMiiiiiio5547aCSeZwkcalmY9AB1NeLaxqN34t8RAwqTvbyreM/wr7/zP/wBavSNH0uHSNOjtIBnHLvjl2PU1vQQ+UmT949amopKWiiiiiiiikopKQ0lBpKKKSkNFJSU0iuL8X6JsY6nbr8rHE6jsezf41qeC9bN3bf2dcNmaFcxkn7yen1H8q6yiiiivPfiRrxRE0W3flwJLgg9v4V/Hr+VReBtF+zWh1Sdf3s4xED/Cnr+P8vrXdWkWT5hHA6VcoooooooooooopKQ0UhpKKSikNFJRTaDSUyWJJonilUMjgqynuDXm13BceHNdBiJ3RMHiY/xL7/qDXp1hexahYw3cJ+SRc49D3H4VZooqtqN9FpunXF7OcRwoXPv7fj0rxjT7efxR4lJnJJmcyzsP4V7j+QFetQxD5Io1CqAAAOgArzT4vfEHxJ4L1PSo9GtGjslBkmuJod0M5PAiz2wOTyDyPSug+HPxU0/x8HtBaTWmpwx+ZLFgvGVyBlX+p6HB+tegUUUUUUUUUUUhopKKSkpKKKSikpKSs3XtdsPDejXGranI8dpAAXZELnk4AAHqTivItK+Otxrfj3T7C20kx6NPJ5LLtMlw2eA5xwADyQM8Z5Ne3d6wfFml/btLM8a5mtssMdSvcf1/Cs3wNqnlzyabI3yyfvIv94dR+I5/Cu7oorz74matsgttJjbmQ+dLj+6OFH55P4UngTS/sulNfOuJbo/L7IOn5nJ/Ku3tI8KXPU8CnXdnbX9rJa3lvFcW8o2vFKgZWHoQaxvDHgrQvB7Xx0Wz+z/bJBJIC5bGBwozyFGSce5roaKKKKKKKKKSikopDSUUlFFJRSUhpKrahYW2qadc2F5GJba5iaKVD3UjBrG8K+CNA8HWvlaRZKkrDElzJ800n1b09hgV0NGMjBGR3Fea6jbyaD4gJh48txLCfVeoH8xXp9pcx3lpFcxHKSoGH41NQTgV4hqs8niXxdJ5ZJFxOIovZBwD+QzXq9vAkMUVvEuERQij2HArUUBVAHQU6iloooooooopKKKSikpKSiikopDRSUlFJRQK5jxpY+ZZw3qj5om2N/unp+v86s+B77ztNls2OWgfK/7rc/zzXU1jeK9QOm+Gb64U4cx+Wn+83yj+dedeALHztZlumHy20fy/7zcD9M16farmQt6CrlFLRRmjNLRRRSUZopKKKTNJQaSiikopKKbRSUUUneodQtBe6fcWzf8ALRCo+vb9cVxHhK6Np4gjjY4EwMTD36j9RXpNcF8T7zZp9jZA/wCtlMjD2UYH6tSeA7TyNAacj5riUt+A4H9a7S3XEefU1MDS0UZpaKKKKKKKTNFFJSUUlFFJRSGikNJRSUUUlPB4Fecauh0zxJMycbJhMn0PzV6dG6yRrIpyrAEfQ15R8SLrzfEiQ54gt1GPckn/AArtdDtvsmhWMGMFYFz9SMn+dbqDCKPQU6lzS0UUZozRmijNGaKKSikopKKKKSikopKSikopDRTk5zXFeNINupwTY/1kWD9Qf/r11nh+c3GgWTk5IjCn8OP6V5T4qY3njS9XrmdYh+AC16qqBSqDoMLV6loopc0tFFFFFFJRmkoozSUUUUlFIaKQ0lBpKKKSilj61zXjaLNpaS/3ZGX8xn+lXvB0wOghGP3JWA/n/WvNn/0nx0xPO/Uf/Z69ZjGZR9aj13WLfw/oV9q92GMFpC0rKvVsdAPcnA/GvEdM8ffFPxdY6lrmhW+nQ6dZMd0HlqScDcVUtyxAwT09q774V/Ef/hPdLuEvII4NUsyvnLFnZIrdHUHpyCCP8a9BoopaM0ZozRmkooornfGfjPTPBOinUNQLOztsgt48b5n9B6Adz2/KvDrv48+L7i5MlnY6ZbQZ+WJomkOPdiwz+AFehfDz4wW3i2+XSNVtUsNVcfutjExT45IXPKt7HOcda9QopKKSikopKSiikNeS+LPGvjm48bjw14U0j7MgYoL28tjtlIGWYMw2hBg+pP44qj4O+JvidfiH/wAIf4rgtZZ2laDzbdApjkAJH3eGU49MjNe0R/erF8YJu0QH+7Mp/nWLoF4bexkTcR+9J/QVymm/N42hz3vj/wChGvWov9Yv1qr4m0SPxJ4Z1HRpZDEt5A0fmAZ2HqD+BArw3wzo3xM8GadrGgWNhp7WE5eSS+knVkg+TDOMNn7o6Fc8dKr/ALOMch8VaxIM+UtiFb0yZBj+Rr6PpaM0UUUUUUUUlfNvx5u57j4hWdnKT9nt7FGiXtl2Ysf0A/CvI7i5m+0MA7KFOAAcYq/b3k9rNY6hAxS6hlSSNh13KwINfbqMWjViMEgEj0zS0UlFFJSUUlFFJXP65f22q2N5oOm+IbS11i4ieOHZcAyRMOSQqndkAGvA9F+0/DP4yx2eqJBq088scZu23GQCbH7xcnhvm5znvzzmvp1RiTHpxWR4sH/Egk/66J/OuQsDiFuv3u30FY1oPJ8bxg/w35H/AI+a9Zj/ANYv1qS9s4NQsbiyuk8y3uI2ikTONysMEZrxf/hRWsac99baH4yltdNvhsnieJtzpz8rbThup546mvRPAngPTfAekSWdk7z3E7B7i5kADSEdBgdFHOB7murooopaSiiiiiivL/jB8PLrxbZ2+raQofVbFCnkk48+LOdoP94HJHrk+1fOF5bSWt00Go6fPBdIcNHLGyNn3Br0b4afDPU/EWt2mratZSWmi2rrKBMhU3JByFVTztzjJ6Y4FfTFFJRRTaKKSiikory3xX8Hhqnij/hJPD2sto2pM/mviMlTJ3dSCCpPccg8+tN8M/B5rDxSviTxJrj6zqCSCVAUIXeOjMScnHGBwOBXqiffFZPi040Fh6yoK5bTIGkt3IB+/j9BWFqY+yeN7hugS+3/AIbgf616uOJB9atUZpaKKKKKKKKKKKKKayKxBZVYjoWAOKXrRSZoopKSiikoopKKSkp8X3/wrC8ZPt0qFO7TD9Aag8LWXn6U7kdZj/IVx3jmE2/i26cDHmKko/75A/mK9ItZhPaQTDpJGr/mM1oA5GaWijNLRRRRRRRRSUZooopKKKSkopKKKKSikopKki6k1y/jSXmzh/3nP6CtjwrF5Xh+Anq5Z/1/+tXI/Eq126hY3YHEkbRk+6nI/nW54UuftPhqzOctGpiP/ATj+WK6KM5QU7NLRRRRmjNGaM0ZozRRRRSZoopM0lFFJRRRSUUlFIaKmiHy59a4XxXP5utugOREip+PU/zrudPt/s2nW0GMFI1B+uOa574gWX2nw4Z1GWtpFk/A8H+f6Vh+ALvMF5ZE8qwlUex4P8hXcwngipaKKM0UtFFFFFJmjNFFFJmikoopKKKKSikNFJRSVZ4RMk4CjJNed2ynVvESEjImn3n/AHc5/kK9KqvfWqX1hPayfdmjZD+IryTw5cPpPiaJJvly5t5Qe2Tj+YFepodrCp80ZpaKKKKKKKKKKKTNFJRRmkoooopKKQ0UUlFLGNzj25qn4iu/smizkHDy/ul/Hr+maw/Bln5l5PdkfLEuxfqf/rD9a7WivKfHWmmx8QG5QYjuh5gI7OOG/ofxrtdE1AanpFvdZ+crtk9mHB/x/GtZTlRS0UuaM0ZozRRmjNGaM0UlFFGaKSlpKKKSikoopKOlIanhXC5PeuN8X3vnX6Win5IFy3+8f/rYrpfD1j9h0aFGGJJP3j/U/wD1sVq0Vz3jLSf7U0GQxrme3/ex+px1H4j+QrkPA2qCC9k0+RvkuPmjz/fHb8R/KvQUODj1ryb4qfFvUPCWovoelacY7wxrJ9tuQCm1hwY179xk9weKZ8F18cSXWoal4gFw+mX6iRZL1yJGkHAZFPO0jjsOBivYaKKKKKKKKKKKSiiiiikzRRSUUUlYHjO21u98I6ja+HjEupTRGNDI+zCnhtp7NjIGeOa8I8I+P/G/g3XLfwzqVhc3waRYUsLrIlXJwPLc9vTOV+lfS0al2AIwe/OcU+9u47CyluX+7Gucep7CuD0i0fWNcUzfMCxlmPtnP6nAr0eiiivI/E2lvoPiAvb5SJ2863Yfw88j8D+mK77SNSj1XTYrtMBmGHX+6w6ilvNC0nU9RtNRvdOt7i7swRBLKgYx564zx2/DtWl3oopc0UUZozRmkoooooooopM0UUlFJRRSVXmsbS4ube5mtoZZ7YloJXQFoiRg7T1HBrQiTauT1Nch4t1Pzp1sIm+SI7pD6t2H4Vs+F9N+w6b50i4muMMc9QvYf1/Gt2iiisXxRoo1vSXiQD7RF88JPr6fj0rgPC2sHSNSMFwSlvMdsgb+BugP9DXpSnHNSZooooooooooooooopM0UUmaKKSiikzSVJEm45PQVX1nU10uwaXgyt8sS+rev0FcjoGmtqupmSbLRRnfKx/iPp+Jr0LpRRRRRXnvjnw/5Mp1a2T925xcKB91v7349/erPhDXvtcA065f9/EP3TE/fUdvqP5V1gNOpKKKWiiiiiiikzRRRSZooopM0UUlJTkUu2B+NTSyRWtu0sjBI0GWY9hXAX93ca9qy+WhO47IY/Qe/wDM13Wl6dHplilvHyRy7f3m7mrtFFFFFMliSeJ4pUDxuCrKehB7V5Vr+iT+HNUWWBnEDNvt5R1UjsfcfqK7Lw/rses2mGwl1GP3qev+0Pb+VbWaM0UZpaKKSjNGaKKKKTNFFFJmiikopKVVLnAqyqrGh5AA5JNcR4h1s6jN9mtyfsqHqP8Alo3r9PSt7w3on9nw/abhf9KkHQ/wL6fX1rfoooooooqrqFhb6nZSWtym6Nx+IPYj3ry2/sL/AML6urK5BU5hmA4cf56iu60PXINZtsjCXCD95Fnp7j1Fa1FFZXiDxFpfhfS/7R1e5Fva+Yse7aWJZjwABye5+gNXLHULPVLOO8sLqG6tpBlJYXDKfxFWaKKKKTNGaKKKTNFFJWFa+MdAv/Eb6BaanDPqKRtI0cR3KADyN3QsOuB2zW5RSqhc4H51ZVVjQ8gAckmuN8QeIPtha0tGxb9Hcf8ALT2+n86veG/D5jK394nz9Yo2HT/aPv6V1VFFFFFFFFFVNS0221Wza2uk3IeQR1U+oPrXmOpaVqHhjUkkV2ABzDcJ0b2Pv6iuv0HxJBqqrBNtivAPudn91/wrepK+e/jd/wAJHr3jPTdEi025Sw3LFZNt+S4mf7zZHHHTB5ABPevafCXhu28JeGbPRrXDCFcyyAf62Q8s34np7AVt5ooooorG1Lxb4c0e6+y6lrun2lx3ilnUMPqO341pWt5bX1slzZ3EVxBIMpLE4dWHsRxU2aKKSjNJXzR438Kap4G+KFlf+GraV1u5vtNhHChbD5+eLA7c9P7rV9I2k0txZQTT27200kau8DkExMRkqSOODxVqOItyeBUsksVrA0krrHGgyWY8CuJ1vxDJqRNvbbktc4/2pPr7e1afh/w35ZS8vk+frHEe3uff2rqqKKKKKKKKKKKhu7SC+tnt7mJZInGCrV5zr3hW50hzc2pea0ByGH3o/r/jVzRfGDIFt9TJZeizgZI/3h3+tdlFJHPEskTq8bDKspyDSsgOOAcHIz2NFFFFJmjNc/451mbQPA+s6pbNtuILY+U391yQqn8Cc/hXzN4WufBkej6tc+LIb6/1K5YpALfJeIYy0xJIGdxHXPQ8c17d8GrXw7BoV4/hzWL+8ikkUz214qo0D4PIVem4d8kHHtXpdJRmkoopVjLsCF5HQ+lTpCq8nk1T1PWbTS0/etvlI+WJfvH6+grjLu/v9eu1j2s3P7uGPovv/wDXNdRonhuLT9txc7ZbrqP7qfT1PvW/RRRRRRRRRRRRRQRkYNcnrfguC7LXGnFYJjyYz9xvp6H9K5SC71Xw5dmIh4WzloZBlW9//riut0vxZY322O4P2Wc8Yc/IT7H/ABrf4YA9QehpCnpTSCOoooorB8a6LL4h8F6vpMGPPubYiIE4BcEMo/EgD8a8H+GPjDw/4J07xDpviWyljvJjjY1vuMgCkGJs/d5Pfjmtz9njS71brWdWMTx2EkSwIT0kcNu49do7+9e80lFFPWN27Y+tSrAo68mmXd7a2EXmXMyRL2B6n6DvXKan4tlmDR2CmJP+erfeP0Haqem6Be6rJ50haOJjkzSclvoO9drp2l2umQ7LePBP3nPLN9TV2iiiiiiiiiiiiiiiiq17YWuoQGG6hSVPRhyPoe1cbqngaWMtJpsvmL/zykOGH0PQ/jWLb6jq+gzeTulix/yxmXKn6A/0ro7HxrbyALewNC39+P5l/LqP1robXULO9XNtcxS+ytz+XWrBQdxSGMdjTfLPYik8pvb86oXnh/S9RmE19pVjdSjo89ujt+ZGaux2whiWKKNI41GFRAFCj2A6U/ym9qUQ+rU8RKOuTTvlRd3CgdSeKzLvxFplnkG481x/DF8369K56+8X3cwK2sa26f3j8zf4CqVpo+p6xL5xVyG6zTE4/wAT+FdVpvheysSsk3+kTDu4+UfQf41u0UUUUUUUUUUUUUUUUUUUVDc2lveRGK5hSVD/AAuua5y+8D2UxLWkslux/hPzr+vP61z114S1ezbdHEJwOjQtz+RwagTV9a0xtjXFxHj+Cdcj/wAerSg8aXq/663gl91ypq/F43tz/rbKVf8AccH/AAq0njHTG+8twv1jB/kakHizSf8AnpL/AN+jQfFulAcPMfpEahfxlYL9yC4c/QD+tVZfGp6Q2I+ryf4Cs+fxXqc3CPHDn/nmnP65qutrrGrNkpdTg93JC/rxWraeDLl8G7uEiX+6g3H8+ldBY+HtOsCGSASSD+OX5j/gK1aKKKKKKKKKKKKKKKKKKKKKKKKKa8aSLtdFZfRhkVnT+H9JuCS9jCCe6Db/ACqhP4O0oqWQTp7LJn+eawbzQLW3LbJJjg45I/wrP/s+LP33/Mf4VJDpsMjgF5PwI/wrdsvC1hPgvJcH6MB/StWLwrpEXJt2kP8AtuTWjBp9nbY8i1hjI7qgz+dWaKKKKKKKKKKKKKK//9k=");
23
24            background-size: 100%;
25        }
26
27        #hour,
28        #minute,
29        #second {
30            position: absolute;
31            background: black;
32            border-radius: 10px;
33            transform-origin: bottom;
34        }
35
36        #hour {
37            width: 1.8%;
38            height: 25%;
39            top: 25%;
40            left: 48.85%;
41            opacity: 0.8;
42        }
43
44        #minute {
45            width: 1.6%;
46            height: 30%;
47            top: 19%;
48            left: 48.9%;
49            opacity: 0.8;
50        }
51
52        #second {
53            width: 1%;
54            height: 40%;
55            top: 9%;
56            left: 49.25%;
57            opacity: 0.8;
58        }
59    </style>
60
61    <script>
62
63        setInterval(() => {
64            d = new Date();
65            hr = d.getHours();
66            min = d.getMinutes();
67            sec = d.getSeconds();
68            hr_rotation = 30 * hr + min / 2;
69            min_rotation = 6 * min;
70            sec_rotation = 6 * sec;
71
72            hour.style.transform = `rotate(${hr_rotation}deg)`;
73            minute.style.transform = `rotate(${min_rotation}deg)`;
74            second.style.transform = `rotate(${sec_rotation}deg)`;
75
76            hour.style.visibility = "visible";
77            minute.style.visibility = "visible";
78            second.style.visibility = "visible";
79        }, 1000);
80
81    </script>
82</head>
83
84<body>
85
86    <div id="clockContainer">
87        <div id="hour"></div>
88        <div id="minute"></div>
89        <div id="second"></div>
90    </div>
91
92</body>
93
94</html>