├── .gitignore ├── LICENSE.md ├── README.md ├── dist ├── emoji-favicon-toolkit.js ├── emoji-favicon-toolkit.min.js ├── index.html └── test-img-element.html ├── package.json ├── src └── emoji-favicon-toolkit.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2017 [OFTN Inc.][1] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | [1]: https://oftn.org 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Emoji Favicon Toolkit 2 | 3 | This code only works in browsers that support ServiceWorker favicons, such as Chrome 72+. 4 | 5 | ## Usage 6 | 7 | 1. Download [/dist/emoji-favicon-toolkit.js](https://raw.githubusercontent.com/eligrey/emoji-favicon-toolkit/master/dist/emoji-favicon-toolkit.js) 8 | 2. Include ` 9 | 12 | 13 | 14 | 15 |

Emoji Favicon Toolkit Demo

16 |

Lorem ipsum dolor sit amet consectetuer adipscing elit… 17 | Test loading 18 | /favicon.ico with 19 | <img> element. 20 |

21 | 22 | 23 | -------------------------------------------------------------------------------- /dist/test-img-element.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Emoji Favicon Toolkit Demo 7 | 8 | 9 | 10 |

Emoji Favicon Toolkit Demo

11 |

Lorem ipsum dolor sit amet consectetuer adipscing elit…

12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoji-favicon-toolkit", 3 | "version": "0.1.0", 4 | "description": "Set your favicon to emoji using canvas & cache as /favicon.ico with service workers", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "tsc", 8 | "minify": "uglifyjs --compress --mangle --output ./dist/emoji-favicon-toolkit.min.js -- ./dist/emoji-favicon-toolkit.js", 9 | "build": "npm run compile && npm run minify", 10 | "start": "cd dist && python -m SimpleHTTPServer 8080", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/eligrey/emoji-favicon-toolkit.git" 16 | }, 17 | "keywords": [ 18 | "emoji", 19 | "browser", 20 | "dom", 21 | "canvas", 22 | "favicon", 23 | "toolkit", 24 | "service" 25 | ], 26 | "author": "OFTN Inc.", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/eligrey/emoji-favicon-toolkit/issues" 30 | }, 31 | "homepage": "https://github.com/eligrey/emoji-favicon-toolkit#readme", 32 | "devDependencies": { 33 | "typescript": "^2.6.2", 34 | "uglify-js": "^3.2.2" 35 | } 36 | } -------------------------------------------------------------------------------- /src/emoji-favicon-toolkit.ts: -------------------------------------------------------------------------------- 1 | // https://w3c.github.io/ServiceWorker/#extendableevent-interface 2 | interface ExtendableEvent extends Event { 3 | waitUntil(f: Promise): void; 4 | } 5 | 6 | // https://w3c.github.io/ServiceWorker/#fetchevent-interface 7 | interface FetchEvent extends ExtendableEvent { 8 | readonly request: Request; 9 | readonly preloadResponse: Promise; 10 | readonly clientId: string; 11 | readonly reservedClientId: string; 12 | readonly targetClientId: string; 13 | 14 | respondWith(r: Promise): void; 15 | } 16 | 17 | const set_emoji_favicon = (() => { 18 | 19 | const is_worker = !self.document; 20 | const mime_image = 'image/png'; 21 | 22 | if (is_worker) { 23 | // Assume we are running in a service worker. 24 | 25 | // Constants 26 | const cache_name = 'favicon-cache'; 27 | const favicon_uri = '/favicon.ico'; 28 | 29 | self.addEventListener('message', (event: MessageEvent) => { 30 | const array_buffer = event.data as ArrayBuffer; 31 | const faviconRequest = new Request(favicon_uri); 32 | const faviconResponse = new Response(array_buffer, { 33 | headers: { 34 | "Content-Type": mime_image, 35 | "Content-Length": String(array_buffer.byteLength) 36 | } 37 | }); 38 | caches.open(cache_name).then((cache: Cache) => { 39 | cache.put(faviconRequest, faviconResponse); 40 | }); 41 | }); 42 | 43 | self.addEventListener('fetch', (event: FetchEvent) => { 44 | event.respondWith(caches.open(cache_name).then((cache: Cache) => { 45 | return cache.match(event.request).then((response: Response) => { 46 | if (response) { 47 | // Return the favicon stored in the cache 48 | return response; 49 | } else { 50 | // Perform an actual request to the server 51 | return fetch(event.request); 52 | } 53 | }); 54 | })); 55 | }); 56 | 57 | } else { 58 | // Assume we are running in the browser. 59 | 60 | // Window load promise 61 | const window_load = new Promise((resolve: (value?: any) => void): void => { 62 | window.addEventListener('load', resolve); 63 | }); 64 | 65 | // Constants 66 | const ns = 'http://www.w3.org/1999/xhtml'; 67 | const mime_text_regex = /^\s*(?:text\/plain)\s*(?:$|;)/i; 68 | const size = 256; // Anything larger will causes problems in Google Chrome 69 | const pixelgrid = 16; 70 | const self_uri = document.currentScript.getAttribute('src'); 71 | const service_worker_container = navigator.serviceWorker; 72 | 73 | // Elements 74 | const canvas = document.createElementNS(ns, 'canvas') as HTMLCanvasElement; 75 | const link = document.createElementNS(ns, 'link') as HTMLLinkElement; 76 | const context = canvas.getContext('2d'); 77 | 78 | // Canvas setup 79 | canvas.width = canvas.height = size; 80 | context.font = `normal normal normal ${size}px/${size}px sans-serif`; 81 | context.textAlign = 'center'; 82 | context.textBaseline = 'middle'; 83 | 84 | // Link setup 85 | link.rel = 'icon'; 86 | link.type = mime_image; 87 | link.setAttribute('sizes', `${size}x${size}`); 88 | 89 | // Scan document for statically-defined favicons 90 | const lastlink = [].slice.call(document.getElementsByTagNameNS(ns, 'link'), 0).filter((link: HTMLLinkElement) => { 91 | return link.rel.toLowerCase() === 'icon' && mime_text_regex.test(link.type); 92 | }).pop(); 93 | 94 | if (lastlink) { 95 | const xhr = new XMLHttpRequest; 96 | const uri = lastlink.href.trim().replace(/^data:(;base64)?,/, "data:text/plain;charset=utf-8$1,"); 97 | xhr.open('GET', uri); 98 | xhr.addEventListener('load', () => { 99 | if (xhr.readyState === xhr.DONE && xhr.status === 200) { 100 | const emoji = xhr.responseText; 101 | set_emoji_favicon(emoji, false); 102 | } 103 | }) 104 | xhr.send(); 105 | } 106 | 107 | function set_emoji_favicon(emoji: any, cacheWithServiceWorker: any): void { 108 | // Normalize arguments 109 | const char = String(emoji) || ''; 110 | const cache = Boolean(cacheWithServiceWorker); 111 | 112 | // Calculate sizing 113 | const metric = context.measureText(char); 114 | const iconsize = metric.width; 115 | const center = (size + size / pixelgrid) / 2; 116 | 117 | const scale = Math.min(size / iconsize, 1); 118 | const center_scaled = center / scale; 119 | 120 | // Draw emoji 121 | context.clearRect(0, 0, size, size); 122 | context.save(); 123 | context.scale(scale, scale); 124 | context.fillText(char, center_scaled, center_scaled); 125 | context.restore(); 126 | 127 | // Update favicon element 128 | link.href = canvas.toDataURL(mime_image); 129 | document.getElementsByTagName('head')[0].appendChild(link); 130 | 131 | // Add favicon to cache 132 | if (cache && service_worker_container) { 133 | canvas.toBlob((blob: Blob): void => { 134 | const reader = new FileReader(); 135 | reader.addEventListener('loadend', () => { 136 | const array_buffer = reader.result; 137 | // https://developers.google.com/web/fundamentals/primers/service-workers/registration 138 | window_load.then(() => { 139 | service_worker_container.register(self_uri, { scope: '/' }); 140 | service_worker_container.ready.then((registration: ServiceWorkerRegistration) => { 141 | // https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast 142 | registration.active.postMessage(array_buffer, [array_buffer]); 143 | }) 144 | }); 145 | }); 146 | reader.readAsArrayBuffer(blob); 147 | }, mime_image); 148 | } 149 | } 150 | 151 | return set_emoji_favicon; 152 | } 153 | })(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "None", 6 | "target": "es5", 7 | "lib": [ 8 | "dom", 9 | "es2015.promise", 10 | "es5", 11 | "scripthost" 12 | ] 13 | } 14 | } --------------------------------------------------------------------------------