Last active
February 22, 2021 12:26
-
-
Save s-zeid/30fe9a2b42c833dce7512228fa80ccdf to your computer and use it in GitHub Desktop.
Set a favicon from an emoji defined in an HTML attribute. Uses the Canvas API. — https://on.bnay.me/emoji-favicon/
This file contains 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
"use strict"; | |
//export default | |
class EmojiFavicon { | |
static DEFAULT_ATTRIBUTE_NAME() { return "data-emoji-favicon"; } | |
static set(emoji, shadowColor, size) { | |
shadowColor = (typeof shadowColor === "string") ? shadowColor : null; | |
size = (typeof size === "number" && size > 0) ? size : 64; | |
if (emoji && emoji.length) { | |
const newLink = document.createElement("link"); | |
newLink.rel = "icon"; | |
newLink.href = this.renderEmoji(emoji, shadowColor, size); | |
for (const oldLink of document.querySelectorAll("link[rel='icon']")) { | |
oldLink.remove(); | |
} | |
document.head.appendChild(newLink); | |
} | |
} | |
static setFromAttribute(element, attributeName, size, forceShadowColor) { | |
attributeName = (typeof attributeName === "string" && attributeName) | |
? attributeName : this.DEFAULT_ATTRIBUTE_NAME(); | |
size = (typeof size === "number" && size > 0) ? size : 64; | |
forceShadowColor = (typeof forceShadowColor === "string") ? forceShadowColor : null; | |
if (!element.hasAttribute(attributeName)) { | |
element.setAttribute(attributeName, ""); | |
} | |
const args = this.parseAttributeValue(element.getAttribute(attributeName), forceShadowColor); | |
this.set.apply(this, args.concat([size])); | |
const emojiFaviconObserver = new MutationObserver(mutations => { | |
for (const mutation of mutations) { | |
if (mutation.attributeName === attributeName) { | |
const args = this.parseAttributeValue(element.getAttribute(attributeName), forceShadowColor); | |
this.set.apply(this, args.concat([size])); | |
} | |
} | |
}); | |
emojiFaviconObserver.observe(element, { attributes: true }); | |
return emojiFaviconObserver; | |
} | |
static parseAttributeValue(attributeValue, forceShadowColor) { | |
attributeValue = (typeof attributeValue === "string") ? attributeValue.trim() : ""; | |
forceShadowColor = (typeof forceShadowColor === "string") ? forceShadowColor.trim() : null; | |
const valueParts = attributeValue.split(" "); | |
const emoji = valueParts.splice(0, 1)[0].trim(); | |
const shadowColor = (forceShadowColor || valueParts.splice(0, 1)[0] || "").trim(); | |
return [emoji, shadowColor]; | |
} | |
static renderEmoji(emoji, shadowColor, size) { | |
emoji = (typeof emoji === "string") ? emoji.trim() : ""; | |
shadowColor = (typeof shadowColor === "string") ? shadowColor.trim() : null; | |
size = (typeof size === "number" && size > 0) ? size : 64; | |
const EMOJI_VARIATION_SELECTOR = "\uFE0F"; | |
const canvas = document.createElement("canvas"); | |
canvas.width = canvas.height = size; | |
const ctx = canvas.getContext("2d"); | |
ctx.font = `${size * 0.875}px sans-serif`; | |
ctx.textAlign = "center"; | |
ctx.textBaseline = "bottom"; | |
if (shadowColor) { | |
ctx.shadowColor = shadowColor; | |
ctx.shadowBlur = 1 * size / 16; | |
} | |
for (let i = 0; i < (shadowColor ? 2 : 1); i++) { | |
ctx.fillText(emoji + EMOJI_VARIATION_SELECTOR, size / 2, size); | |
} | |
return canvas.toDataURL("image/png"); | |
} | |
} | |
(function(currentScript) { | |
if (!document.querySelector("head link[rel='icon']")) { | |
const attributeName = EmojiFavicon.DEFAULT_ATTRIBUTE_NAME(); | |
for (const element of [currentScript, document.documentElement]) { | |
if (element && element.hasAttribute(attributeName)) { | |
EmojiFavicon.setFromAttribute(element, attributeName); | |
} | |
} | |
} | |
})(document.currentScript); |
This file contains 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" /> | |
<title>Emoji Favicon</title> | |
<script src="emoji-favicon.js" data-emoji-favicon="☃ #0cde"></script> | |
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1" /> | |
<style> | |
html { font-family: sans-serif; | |
background: var(--background, #fff); color: var(--color, #000); } | |
body { margin: 0 auto; padding: 4rem 2rem; max-width: 800px; box-sizing: border-box; } | |
a { color: var(--accent, #00b); } | |
a:not(:hover):not(:focus) { text-decoration: none; } | |
a:focus { outline: none; } | |
h1 { margin-top: 0; } | |
h1, h2 { font-weight: normal; } | |
pre, code { font-family: monospace; font-size: 1.25em; } | |
footer { margin-top: 4rem; } | |
</style> | |
<style> | |
:root { | |
--accent: #a02; | |
} | |
#emoji-codepoints, #shadow-value { | |
font-family: monospace; font-size: 1.25em; | |
} | |
#shadow-color { | |
display: inline-block; box-sizing: border-box; | |
width: 0.875em; height: 0.875em; transform: translateY(0.09375em); | |
border: 1.5px solid currentColor; border-radius: 100%; | |
background: var(--value, inherit); | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1>Emoji Favicon</h1> | |
</header> | |
<main> | |
<p> | |
This page's favicon should be the emoji "<slot id="emoji-char">[ _ ]</slot>" | |
(<slot id="emoji-codepoints">[ _ ]</slot>). | |
</p> | |
<p id="shadow-yes"> | |
It should have a shadow with the color "<slot id="shadow-value">[ _ ]</slot>" | |
<span id="shadow-color"></span>. | |
</p> | |
<p id="shadow-no"> | |
It should not have a shadow. | |
</p> | |
<p> | |
Try changing the <code>data-emoji-favicon</code> attribute on the | |
first <code><script></code> element. | |
</p> | |
</main> | |
<footer> | |
<p> | |
<a href="https://gist.github.com/30fe9a2b42c833dce7512228fa80ccdf">Source code</a> | |
(<a href="https://gist.github.com/e42f61b2cbe83a15765c18107352083f">X11 License</a>) | |
</p> | |
</footer> | |
<script> | |
function update(attributeValue) { | |
const EMOJI_VARIATION_SELECTOR = "\uFE0F"; | |
const [emoji, shadowColor] = EmojiFavicon.parseAttributeValue(attributeValue); | |
function toCodePoints(emoji) { | |
const result = []; | |
let i = 0; | |
while (i < emoji.length) { | |
const codePoint = emoji.codePointAt(i); | |
i += String.fromCodePoint(codePoint).length; | |
result.push("U+" + codePoint.toString(16).toUpperCase()); | |
} | |
return result.join(" "); | |
} | |
document.querySelector("#emoji-char").textContent = emoji + EMOJI_VARIATION_SELECTOR; | |
document.querySelector("#emoji-codepoints").textContent = toCodePoints(emoji); | |
document.querySelector("#shadow-value").textContent = shadowColor; | |
document.querySelector("#shadow-color").style.setProperty("--value", shadowColor); | |
document.querySelector("#shadow-yes").hidden = !shadowColor; | |
document.querySelector("#shadow-no").hidden = !!shadowColor; | |
} | |
function main() { | |
const attributeName = EmojiFavicon.DEFAULT_ATTRIBUTE_NAME().replace(/[^a-z-]/i, ""); | |
const element = document.querySelector(`script[${attributeName}]`); | |
window.element = element; | |
update(element.getAttribute(attributeName)); | |
const emojiFaviconObserver = new MutationObserver(mutations => { | |
for (const mutation of mutations) { | |
if (mutation.attributeName === attributeName) { | |
update(element.getAttribute(attributeName)); | |
} | |
} | |
}); | |
emojiFaviconObserver.observe(element, { attributes: true }); | |
} | |
main(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment