added monochrome version of Noto Emoji (#2324)

This commit is contained in:
ArnoldSmith86 2024-10-03 21:44:34 +02:00 committed by GitHub
parent c006a7d2bc
commit e503775698
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 88 additions and 18 deletions

Binary file not shown.

View File

@ -309,6 +309,19 @@
font-family: "Material Icons Two Tone";
}
@font-face {
font-family: "Noto Emoji";
font-style: normal;
font-weight: 300 700;
src: url("i/fonts/NotoEmoji-VariableFont_wght.ttf") format("truetype");
font-display: block;
}
.emoji-monochrome {
font-family: "Noto Emoji";
font-weight: 400;
}
@font-face {
font-family: "VTT-Symbols";
src: url("i/fonts/VTT-Symbols.ttf?version=6") format("truetype");
@ -461,9 +474,15 @@
word-break: break-all;
}
#symbolPickerOverlay .emoji:hover, #symbolPickerOverlay .emojiFlag, #symbolPickerOverlay.fewResults .emoji {
#symbolPickerOverlay .emoji-monochrome {
font-size: calc(0.85 * var(--symbolSize));
vertical-align: top;
}
#symbolPickerOverlay .emoji-color:hover, #symbolPickerOverlay .emojiFlag, #symbolPickerOverlay.fewResults .emoji-color {
background: var(--url) center no-repeat;
color: transparent;
top: -3px;
}
#symbolPickerOverlay .gameicons {
@ -488,11 +507,12 @@
#symbolPickerOverlay .hidden,
#symbolPickerOverlay.hideImages h2.imageCategory,
#symbolPickerOverlay.hideImages .emoji,
#symbolPickerOverlay.hideImages .emoji-color,
#symbolPickerOverlay.hideImages .gameicons,
#symbolPickerOverlay.hideFonts h2.fontCategory,
#symbolPickerOverlay.hideFonts .symbols,
#symbolPickerOverlay.hideFonts .material-icons {
#symbolPickerOverlay.hideFonts .material-icons,
#symbolPickerOverlay.hideFonts .emoji-monochrome {
display: none;
}

View File

@ -985,7 +985,7 @@ button.visualProgress {
width: 1em;
}
.richtextSymbol.symbols {
top: 0.1em;
top: 0.15em;
}
.richtextSymbol.material-icons {
top: 0.2em;
@ -993,6 +993,10 @@ button.visualProgress {
.richtextSymbol.gameicons, img.emoji {
vertical-align: bottom;
}
.richtextSymbol.emoji-monochrome {
font-size: 1em !important;
width: 1.2em !important;
}
.symbols {
font-family: 'VTT-Symbols';

View File

@ -108,6 +108,8 @@ export function regexEscape(string) {
}
export function setText(node, text) {
if(node.classList.contains('emoji-monochrome'))
text = toNotoMonochrome(text);
for(const child of node.childNodes) {
if(child.nodeType == Node.TEXT_NODE) {
child.nodeValue = text;
@ -366,7 +368,29 @@ export function formField(field, dom, id) {
function emojis2images(dom) {
const regex = /\ud83c\udff4(\udb40[\udc61-\udc7a])+\udb40\udc7f|(\ud83c[\udde6-\uddff]){2}|([\#\*0-9]\ufe0f?\u20e3)|(\u00a9|\u00ae|[\u203c\u2049\u20e3\u2122\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23e9-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u261d\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u265f\u2660\u2663\u2665\u2666\u2668\u267b\u267e\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26ce\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299]|\ud83c[\udc04\udccf\udd70\udd71\udd7e\udd7f\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude02\ude1a\ude2f\ude32-\ude3a\ude50\ude51\udf00-\udf21\udf24-\udf93\udf96\udf97\udf99-\udf9b\udf9e-\udff0\udff3-\udff5\udff7-\udfff]|\ud83d[\udc00-\udcfd\udcff-\udd3d\udd49-\udd4e\udd50-\udd67\udd6f\udd70\udd73-\udd7a\udd87\udd8a-\udd8d\udd90\udd95\udd96\udda4\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa-\ude4f\ude80-\udec5\udecb-\uded2\uded5-\uded7\udedc-\udee5\udee9\udeeb\udeec\udef0\udef3-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0c-\udd3a\udd3c-\udd45\udd47-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec5\udece-\udedb\udee0-\udee8\udef0-\udef8])((\ud83c[\udffb-\udfff])?(\ud83e[\uddb0-\uddb3])?(\ufe0f?\u200d([\u2000-\u3300]|[\ud83c-\ud83e][\ud000-\udfff])\ufe0f?)?)*/g;
dom.innerHTML = dom.innerHTML.replace(regex, m=>`<img class="emoji" src="i/noto-emoji/emoji_u${emojiToFilename(m)}.svg" alt="${m}">`);
function replaceEmojisInNode(node) {
// Skip nodes with the class "emoji-monochrome"
if (node.classList && node.classList.contains('emoji-monochrome')) {
return;
}
// Process text nodes only
node.childNodes.forEach(child => {
if (child.nodeType === Node.TEXT_NODE) {
const replacedContent = child.textContent.replace(regex, m =>
`<img class="emoji" src="i/noto-emoji/emoji_u${emojiToFilename(m)}.svg" alt="${m}">`
);
const span = document.createElement('span');
span.innerHTML = replacedContent;
child.replaceWith(span);
} else if (child.nodeType === Node.ELEMENT_NODE) {
replaceEmojisInNode(child); // Recurse into child elements
}
});
}
replaceEmojisInNode(dom);
}
function images2emojis(dom) {
@ -459,6 +483,21 @@ function emojiToFilename(emoji) {
return [...emoji].map(char => char.codePointAt(0).toString(16).padStart(4, '0')).join('_').replace(/_fe0f/g, '');
}
function toNotoMonochrome(emoji) {
return emoji
.replace(/[\u{1f3fb}-\u{1f3ff}]/ug, '') // remove skin tone modifiers (they are not supported by Noto Emoji)
.replace(/\u200d[\u2640\u2642]/, '') // remove gender modifiers (they are not supported by Noto Emoji)
.replace(/\ufe0f/g, '') // remove variation selectors (they tell Firefox to use color emoji)
.replace(/\u{1faf1}\u200d\u{1faf2}/ug, '🤝') // join variants of emojis that have
.replace(/.*\u200d\u{1f91d}\u200d.*/ug, '👭') // two people interacting because
.replace(/.*\u200d\u2764\u200d\u{1f48b}\u200d.*/ug, '💏') // they have a special notation for
.replace(/.*\u200d\u2764\u200d.*/ug, '💑'); // skin and gender variants
}
function skipForNotoMonochrome(emoji) {
return emoji.match(/^[\u{1f3c3}-\u{1f3cc}]\u{fe0f}?\u{200d}[\u{2640}\u{2642}]\u{fe0f}(\u{200d}\u{27a1}\u{fe0f})?|\u{1f468}|\u{1f468}\u{200d}[\u{1f33e}\u{1f373}\u{1f37c}\u{1f393}\u{1f3a4}\u{1f3a8}\u{1f3eb}\u{1f3ed}\u{1f4bb}\u{1f4bc}\u{1f527}\u{1f52c}\u{1f680}\u{1f692}\u{1f9af}\u{1f9b1}\u{1f9b2}\u{1f9bc}\u{1f9bd}]|\u{1f468}\u{200d}[\u{1f9af}\u{1f9bc}\u{1f9bd}]\u{200d}\u{27a1}\u{fe0f}|\u{1f468}\u{200d}[\u{2695}\u{2696}\u{2708}]\u{fe0f}|\u{1f468}\u{200d}\u{2764}\u{fe0f}\u{200d}(\u{1f468}|\u{1f48b}\u{200d}\u{1f468})|\u{1f469}\u{200d}[\u{1f33e}\u{1f373}\u{1f393}\u{1f3a4}\u{1f3a8}\u{1f3eb}\u{1f3ed}\u{1f4bb}\u{1f4bc}\u{1f527}\u{1f52c}\u{1f680}\u{1f692}\u{1f9af}-\u{1f9b3}\u{1f9bc}\u{1f9bd}]|\u{1f469}\u{200d}[\u{1f9af}\u{1f9bc}\u{1f9bd}]\u{200d}\u{27a1}\u{fe0f}|\u{1f469}\u{200d}[\u{2695}\u{2696}\u{2708}]\u{fe0f}|\u{1f469}\u{200d}\u{2764}\u{fe0f}\u{200d}(\u{1f48b}\u{200d})?[\u{1f468}\u{1f469}]|\u{1f46b}|\u{1f46c}|\u{200d}[\u{2640}\u{2642}]|\u{1f478}|\u{1f57a}|\u{1f934}|\u{1f936}|\u{1f9d1}\u{200d}(\u{1f37c}|\u{1f384}|\u{1f91d}\u{200d}\u{1f9d1})|\u{1fac3}|\u{1fac4}$/u);
}
let symbolData = null;
export async function loadSymbolPicker() {
if(symbolData === null) {
@ -466,18 +505,30 @@ export async function loadSymbolPicker() {
symbolData = await (await fetch('i/fonts/symbols.json')).json();
let list = '';
for(const [ category, symbols ] of Object.entries(symbolData)) {
list += `<h2 class="${category.match(/Material|VTT/)?'fontCategory':'imageCategory'}">${category}</h2>`;
if(category == 'Emoji - Flags')
continue;
list += `<h2 class="${category.match(/Material|VTT|Emoji/)?'fontCategory':'imageCategory'}">${category}</h2>`;
for(const [ symbol, keywords ] of Object.entries(symbols)) {
if(symbol.includes('/')) {
const gameIconsIndex = keywords.shift();
// increase resource limits in /etc/ImageMagick-6/policy.xml to 8GiB and then: montage -background none assets/game-icons.net/*/*.svg -geometry 48x48+0+0 -tile 60x assets/game-icons.net/overview.png
list += `<i class="gameicons" title="game-icons.net: ${symbol}" data-type="game-icons" data-symbol="${symbol}" data-keywords="${symbol.split('/')[1]},${keywords.join().toLowerCase()}" style="--x:${gameIconsIndex%60};--y:${Math.floor(gameIconsIndex/60)};--url:url('i/game-icons.net/${symbol}.svg')"></i>`;
} else {
let className = 'emoji';
let className = 'emoji-monochrome';
if(symbol[0] == '[')
className = 'symbols';
else if(symbol.match(/^[a-z0-9_]+$/))
className = 'material-icons';
if(className != 'emoji-monochrome' || !skipForNotoMonochrome(symbol))
list += `<i class="${className}" title="${className}: ${symbol}" data-type="${className}" data-symbol="${symbol}" data-keywords="${symbol},${keywords.join().toLowerCase()}" style="--url:url('i/noto-emoji/emoji_u${emojiToFilename(symbol)}.svg')">${toNotoMonochrome(symbol)}</i>`;
}
}
}
for(const [ category, symbols ] of Object.entries(symbolData)) {
if(category.match(/Emoji/)) {
list += `<h2 class="imageCategory">${category}</h2>`;
for(const [ symbol, keywords ] of Object.entries(symbols)) {
let className = 'emoji-color';
if(category == 'Emoji - Flags')
className += ' emojiFlag';
list += `<i class="${className}" title="${className}: ${symbol}" data-type="${className}" data-symbol="${symbol}" data-keywords="${symbol},${keywords.join().toLowerCase()}" style="--url:url('i/noto-emoji/emoji_u${emojiToFilename(symbol)}.svg')">${symbol}</i>`;
@ -522,9 +573,9 @@ export async function pickSymbol(type='all', bigPreviews=true, closeOverlay=true
icon.onclick = function(e) {
if(closeOverlay)
showOverlay(null);
const isImage = ['emoji','game-icons'].indexOf(icon.dataset.type) != -1;
const isImage = ['emoji-color','game-icons'].indexOf(icon.dataset.type) != -1;
let url = null;
if(icon.dataset.type == 'emoji')
if(icon.dataset.type == 'emoji-color')
url = `/i/noto-emoji/emoji_u${emojiToFilename(icon.dataset.symbol)}.svg`;
if(icon.dataset.type == 'game-icons')
url = `/i/game-icons.net/${icon.dataset.symbol}.svg`;
@ -612,15 +663,10 @@ export function addRichtextControls(dom) {
if(icon.classList.contains('gameicons')) {
document.execCommand('inserthtml', false, `<i class="richtextSymbol gameicons"><img src="i/game-icons.net/${icon.dataset.symbol}.svg"></i>`);
} else {
let className = 'emoji';
if(icon.innerText[0] == '[')
className = 'symbols';
else if(icon.innerText.match(/^[a-z0-9_]+$/))
className = 'material-icons';
if(className == 'emoji')
if(icon.classList.contains('emoji-color'))
document.execCommand('inserthtml', false, icon.innerText);
else
document.execCommand('inserthtml', false, `<i class="richtextSymbol ${className}">${icon.innerText}</i>`);
document.execCommand('inserthtml', false, `<i class="richtextSymbol ${icon.className}">${icon.innerText}</i>`);
}
for(const insertedSymbol of $a('.richtextSymbol'))
insertedSymbol.contentEditable = false; // adding the property above causes Chrome to insert two icons

View File

@ -300,7 +300,7 @@ const jeCommands = [
{
id: 'je_symbolPickerText',
name: 'pick an asset from the symbol picker',
context: '^button ↦ text$',
context: '^(button|basic) ↦ text$',
call: async function() {
const a = await pickSymbol('fonts');
if(a) {
@ -311,7 +311,7 @@ const jeCommands = [
}
},
show: function() {
return [ 'symbols', 'material-icons' ].indexOf(jeStateNow.classes) != -1;
return [ 'symbols', 'material-icons', 'emoji-monochrome' ].indexOf(jeStateNow.classes) != -1;
}
},
{