Skip to content

Instantly share code, notes, and snippets.

@s-zeid
Last active February 22, 2021 12:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save s-zeid/30fe9a2b42c833dce7512228fa80ccdf to your computer and use it in GitHub Desktop.
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/
"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);
<!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>&lt;script&gt;</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