├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── docs ├── CNAME ├── favicon.ico ├── index.html ├── kitchen │ ├── data.js │ ├── emojidata.js │ ├── icon.png │ ├── index.html │ ├── script.js │ └── style.css ├── util │ └── paste.html └── wallpaper │ ├── index.html │ ├── runes.js │ ├── script.js │ └── style.css ├── figma └── kitchen │ ├── .gitignore │ ├── Cover.png │ ├── Icon.png │ ├── README.md │ ├── code.ts │ ├── manifest-figma.json │ ├── manifest.json │ ├── package.json │ └── tsconfig.json ├── netlify.toml ├── netlify └── edge-functions │ ├── cache.js │ └── metadata.js ├── package.json └── tools ├── fetchemoji.py └── fetchemoji.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | # Local Netlify folder 5 | .netlify 6 | node_modules 7 | package-lock.json 8 | docs/kitchen/emoji-en-US.js 9 | tools/fetchemoji.json 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "netlify dev", 6 | "type": "node", 7 | "request": "launch", 8 | "skipFiles": ["/**"], 9 | "outFiles": ["${workspaceFolder}/.netlify/functions-serve/**/*.js"], 10 | "program": "${workspaceFolder}/node_modules/.bin/netlify", 11 | "args": ["dev"], 12 | "console": "integratedTerminal", 13 | "env": { "BROWSER": "none" }, 14 | "serverReadyAction": { 15 | "pattern": "Server now ready on (https?://[\w:.-]+)", 16 | "uriFormat": "%s", 17 | "action": "debugWithChrome" 18 | } 19 | }, 20 | { 21 | "name": "netlify functions:serve", 22 | "type": "node", 23 | "request": "launch", 24 | "skipFiles": ["/**"], 25 | "outFiles": ["${workspaceFolder}/.netlify/functions-serve/**/*.js"], 26 | "program": "${workspaceFolder}/node_modules/.bin/netlify", 27 | "args": ["functions:serve"], 28 | "console": "integratedTerminal" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.enablePaths": [ 4 | "netlify/edge-functions" 5 | ], 6 | "deno.unstable": true, 7 | "deno.importMap": ".netlify/edge-functions-import-map.json", 8 | "deno.path": "/Users/jitkoff/Library/Preferences/netlify/deno-cli/deno" 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nicholas Jitkoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emoji-supply 2 | 3 | Source for http://emoji.supply/wallpaper and http://emoji.supply/kitchen 4 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | git.emoji.supply -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alcor/emoji-supply/c84b4dcc98af4acc6a77504050ff9d76c1bccea8/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | emoji.supply 8 | 9 | 48 | 49 | 50 |
🖼
51 | Emoji Wallpaper 52 |
53 | 54 |
🎨
55 | Emoji Kitchen Browser 56 |
57 | 58 | 64 | 65 | -------------------------------------------------------------------------------- /docs/kitchen/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alcor/emoji-supply/c84b4dcc98af4acc6a77504050ff9d76c1bccea8/docs/kitchen/icon.png -------------------------------------------------------------------------------- /docs/kitchen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Emoji Kitchen Browser 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 31 | 32 | 33 | 39 |
40 |
41 |

42 | 43 | 44 | 45 |

46 |

47 | This page lets you browse the thousands of delightful combinations of 48 | Emoji Kitchen, 49 | available in Gboard for Android. 50 |

51 |

52 | All credit goes to the Emoji Kitchen team for the care they put into emoji, standards, and imaginary creatures. 53 |

54 |

Source code: on GitHub, by @alcor

55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 |
66 | 67 |
68 | 69 |
70 |
Copied
71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /docs/kitchen/script.js: -------------------------------------------------------------------------------- 1 | const el = (selector, ...args) => { 2 | var attrs = (args[0] && typeof args[0] === 'object' && !Array.isArray(args[0]) && !(args[0] instanceof HTMLElement)) ? args.shift() : {}; 3 | 4 | let classes = selector.split("."); 5 | if (classes.length > 0) selector = classes.shift(); 6 | if (classes.length) attrs.className = classes.join(" ") 7 | 8 | let id = selector.split("#"); 9 | if (id.length > 0) selector = id.shift(); 10 | if (id.length) attrs.id = id[0]; 11 | 12 | var node = document.createElement(selector.length > 0 ? selector : "div"); 13 | for (let prop in attrs) { 14 | if (attrs.hasOwnProperty(prop) && attrs[prop] != undefined) { 15 | if (prop.indexOf("data-") == 0) { 16 | let dataProp = prop.substring(5).replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); 17 | node.dataset[dataProp] = attrs[prop]; 18 | } else { 19 | node[prop] = attrs[prop]; 20 | } 21 | } 22 | } 23 | 24 | const append = (child) => { 25 | if (Array.isArray(child)) return child.forEach(append); 26 | if (typeof child == "string") child = document.createTextNode(child); 27 | if (child) node.appendChild(child); 28 | }; 29 | args.forEach(append); 30 | 31 | return node; 32 | }; 33 | window.el = el; 34 | 35 | const codePointToText = (codePoint) => { 36 | let cps = codePoint.split("-").map(hex => parseInt(hex, 16)); 37 | let emoji = String.fromCodePoint(...cps); 38 | return emoji; 39 | } 40 | 41 | function convertBase(value, from_base, to_base) { 42 | value = value.toString(); 43 | var range = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/'.split(''); 44 | var from_range = range.slice(0, from_base); 45 | var to_range = range.slice(0, to_base); 46 | 47 | var dec_value = value.split('').reverse().reduce(function (carry, digit, index) { 48 | if (from_range.indexOf(digit) === -1) throw new Error('Invalid digit `'+digit+'` for base '+from_base+'.'); 49 | return carry += from_range.indexOf(digit) * (Math.pow(from_base, index)); 50 | }, 0); 51 | 52 | var new_value = ''; 53 | while (dec_value > 0) { 54 | new_value = to_range[dec_value % to_base] + new_value; 55 | dec_value = (dec_value - (dec_value % to_base)) / to_base; 56 | } 57 | return new_value || '0'; 58 | } 59 | 60 | 61 | const emojiUrl = (codePoint) => { 62 | let cp = codePoint.split("-").filter(x => x !== "fe0f").map(s => s.padStart(4, "0")).join("_"); 63 | return `https://raw.githubusercontent.com/googlefonts/noto-emoji/main/png/128/emoji_u${cp}.png`; 64 | return `https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u${cp}.svg` 65 | } 66 | 67 | const mixmojiUrl = (r, c) => { 68 | let padZeros = r < 20220500; // Revisions before 0522 had preceding zeros 69 | c[0] = c[0].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u"); 70 | c[1] = c[1].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u"); 71 | return `https://www.gstatic.com/android/keyboard/emojikitchen/${r}/u${c[0]}/u${c[0]}_u${c[1]}.png` 72 | } 73 | 74 | const copyToClipboard = async (e) => { 75 | try { 76 | let blob = e.target.blob; 77 | 78 | if (!blob) { 79 | const imgURL = e.target.src; 80 | // .replace("https://www.gstatic.com/android/keyboard/emojikitchen", "https://emoji.supply/emojikitchen"); 81 | const data = await fetch(imgURL) 82 | blob = await data.blob(); 83 | } 84 | 85 | await navigator.clipboard.write([ 86 | new ClipboardItem({ 87 | [blob.type]: blob 88 | }) 89 | ]); 90 | 91 | document.body.classList.add("copied"); 92 | setTimeout(function () { document.body.classList.remove("copied"); }, 2000); 93 | 94 | console.log('Copied image to clipboard'); 95 | } catch (err) { 96 | console.error(err.name, err.message); 97 | } 98 | } 99 | 100 | const focusEmoji = (e) => { 101 | selectEmoji(undefined, e.target.targetId); 102 | selectMixmoji(undefined, pc.name.split("_")); 103 | } 104 | const swapEmoji = (e) => { 105 | selectEmoji(undefined, p2.targetId); 106 | selectMixmoji(undefined, pc.name.split("_")); 107 | 108 | window.scrollTo(0, 0); 109 | window.visualViewport.scale = 1.0; 110 | 111 | } 112 | 113 | 114 | 115 | 116 | const scrollElement = (e) => { 117 | e.target.onscroll = undefined; 118 | setTimeout(() => { e.target.onscroll = scrollElement; }, 2000); 119 | document.documentElement.className = e.target.id.replace("#", ""); 120 | } 121 | 122 | const setFavicon = (url) => { 123 | document.getElementById("favicon").href = url; 124 | } 125 | 126 | let isFigmaNative = navigator.userAgent.includes("Figma"); 127 | let isIframe = window.self !== window.top; 128 | 129 | 130 | const clickResult = (e) => { 131 | if (isIframe) { 132 | parent.postMessage({ pluginMessage: {clickedImage: e.target.src}, pluginId:'*'}, '*') 133 | } else { 134 | copyToClipboard(e); 135 | } 136 | } 137 | 138 | let offset = {} 139 | 140 | const dragStart = (e) => { 141 | offset.x = e.offsetX; 142 | offset.y = e.offsetY; 143 | } 144 | 145 | const dragEnd = (e) => { 146 | 147 | // Don't proceed if the item was dropped inside the plugin window. 148 | // if (e.view.length === 0) return; 149 | console.log("View", e.view.length, e.view) 150 | 151 | window.parent.postMessage( 152 | { 153 | pluginId: '*', 154 | pluginDrop: { 155 | clientX: e.clientX, 156 | clientY: e.clientY, 157 | items: [{ type: 'text/uri-list', data: e.target.src }], 158 | dropMetadata: { 159 | fromBrowser: true, 160 | itemSize: { 161 | width: e.target.clientWidth, 162 | height: e.target.clientHeight 163 | }, 164 | windowSize : { 165 | width: window.outerWidth, 166 | height: window.outerHeight 167 | }, 168 | dropPosition: { 169 | x: e.clientX, 170 | y: e.clientY 171 | }, 172 | offset: offset, 173 | }, 174 | } 175 | }, 176 | '*' 177 | ); 178 | } 179 | 180 | let p1 = document.getElementById("p1") 181 | let p2 = document.getElementById("p2") 182 | let pc = document.getElementById("pc"); 183 | let plus = document.getElementById("plus"); 184 | 185 | let emojiContainer = document.getElementById("emoji-container"); 186 | let mixmojiContainer = document.getElementById("mixmoji-container"); 187 | plus.onclick = swapEmoji; 188 | // p1.onclick = focusEmoji; 189 | // p2.onclick = focusEmoji; 190 | 191 | p1.onclick = clickResult; 192 | p2.onclick = clickResult; 193 | pc.onclick = clickResult; 194 | 195 | 196 | plus.addEventListener('dblclick', function(event) { 197 | event.preventDefault(); 198 | }, { passive: false }); 199 | 200 | document.addEventListener('dblclick', function(event) { 201 | event.preventDefault(); 202 | }, { passive: false }); 203 | 204 | let search = document.getElementById("search") 205 | 206 | window.addEventListener('load', function() { 207 | search.focus(); 208 | }); 209 | 210 | search.oninput = (e) => { 211 | let query = e.target.value; 212 | 213 | if (query.length < 3) { 214 | [...document.querySelectorAll(".emoji")].forEach(el => { 215 | el.classList.remove("dimmed") 216 | el.classList.remove("promoted") 217 | }); 218 | } else { 219 | let words = query.split(" "); 220 | let word1 = words.shift(); 221 | let word2 = words.shift(); 222 | let firstMatch; 223 | [...document.querySelectorAll(".emoji")].forEach(el => { 224 | let index = window.points.indexOf(el.id); 225 | let matchRE = new RegExp(`[, ]${word1}`, "i"); 226 | let visible = window.point_names[index].match(matchRE); //includes("," + query); 227 | if (visible) console.log(el.id, window.point_names[index]) 228 | el.classList.toggle("dimmed", !visible); 229 | el.classList.toggle("promoted", visible) 230 | if (!firstMatch && visible) firstMatch = el; 231 | }) 232 | 233 | if (firstMatch) focusFirst(); 234 | document.documentElement.className = "mixmoji-container"; 235 | } 236 | 237 | }; 238 | 239 | let lastFirst = undefined; 240 | const focusFirst = () => { 241 | for (const element of document.querySelectorAll(".emoji")) { 242 | if (!element.classList.contains("dimmed")) { 243 | if (lastFirst != element) clickedEmoji({target:element}); 244 | lastFirst = element; 245 | document.documentElement.className = "mixmoji-container"; 246 | break; 247 | } 248 | } 249 | } 250 | 251 | document.addEventListener('keydown', function(e) { 252 | if (e.key === 'Enter' || e.keyCode === 13) { 253 | search.value = ""; 254 | console.log('Enter key was pressed'); 255 | focusFirst(); 256 | [...document.querySelectorAll(".emoji")].forEach(el => { 257 | el.classList.remove("dimmed") 258 | el.classList.remove("promoted") 259 | }); 260 | } else if (e.key === 'Escape' || e.keyCode === 27) { 261 | search.value = ""; 262 | 263 | [...document.querySelectorAll(".emoji")].forEach(el => { 264 | el.classList.remove("dimmed") 265 | el.classList.remove("promoted") 266 | }); 267 | document.documentElement.className = ""; 268 | } 269 | search.focus(); 270 | 271 | 272 | }); 273 | 274 | if (isIframe && !isFigmaNative) { 275 | // document.addEventListener('dragstart', dragStart); 276 | document.addEventListener('dragend', dragEnd); 277 | } 278 | 279 | const updateBlobForElement = async (element) => { 280 | const data = await fetch(element.src) 281 | const blob = await data.blob(); 282 | element.blob = blob; 283 | } 284 | 285 | let selectedMixmoji = undefined; 286 | const selectMixmoji = (e, parents) => { 287 | if (e) document.documentElement.className = "mixmoji-container"; 288 | let img = e?.target || document.getElementById(parents.join("_")) || document.getElementById(parents.reverse().join("_")); 289 | 290 | 291 | if (!img) return; 292 | 293 | selectedMixmoji?.classList.remove("selected") 294 | selectedMixmoji = img; 295 | selectedMixmoji.classList.add("selected"); 296 | 297 | parents = img?.c ? Array.from(img?.c) : undefined; 298 | 299 | let comboString = parents.map(codePointToText).join(" + "); 300 | console.log("Selecting Mix", img.id, comboString); 301 | 302 | pc.src = img.src; 303 | setFavicon(pc.src); 304 | updateBlobForElement(pc); 305 | 306 | document.getElementById("md-title").content = comboString; 307 | 308 | document.getElementById("preview-container").classList.add("mix") 309 | pc.name = parents.join("_"); 310 | document.title = "= " + comboString; 311 | 312 | 313 | gtag('event', 'view_item', { 'event_label': comboString, 'event_category': 'mixmoji', 'non_interaction': !e }); 314 | 315 | let p2id = (parents[0] == emoji1.id) ? parents.pop() : parents.shift(); 316 | let p1id = parents.pop(); 317 | 318 | emoji1?.classList.remove("selected"); 319 | emoji2?.classList.remove("secondary"); 320 | emoji1 = document.getElementById(p1id); 321 | emoji2 = document.getElementById(p2id); 322 | emoji1?.classList.add("selected"); 323 | emoji2?.classList.add("secondary"); 324 | 325 | p1.src = emoji1.src.replace("128", "512"); 326 | p1.targetId = p1id; 327 | p2.src = emoji2.src.replace("128", "512"); 328 | p2.targetId = p2id; 329 | p2.parentElement.classList.add("active"); 330 | updateBlobForElement(p1); 331 | updateBlobForElement(p2); 332 | 333 | let url = "/kitchen/?" + img.c.map(cc => codePointToText(cc)).join("+"); 334 | url += "=" + parseInt(img.date, 16).toString(36); 335 | window.history.replaceState({}, "", url); 336 | } 337 | 338 | let emoji1 = undefined; 339 | let emoji2 = undefined; 340 | let pinnedEmoji = undefined; 341 | 342 | let recents = localStorage.getItem("recents") ? JSON.parse(localStorage.getItem("recents")) : []; 343 | let favorites = localStorage.getItem("favorites") ? JSON.parse(localStorage.getItem("favorites")) : []; 344 | 345 | const clickedEmoji = (e) => { 346 | if (e) document.documentElement.className = "emoji-container"; 347 | let target = e.target.closest("div"); 348 | 349 | gtag('event', 'view_item', { 'event_label': codePointToText(target.id), 'event_category': 'emoji', 'non_interaction': true }); 350 | 351 | if (target == pinnedEmoji) { 352 | console.log("unpin", pinnedEmoji.title); 353 | pinnedEmoji.classList.remove("pinned"); 354 | pinnedEmoji = undefined; 355 | } else if (e.detail == 2) { 356 | console.log("pinning", target.title) 357 | pinnedEmoji?.classList.remove("pinned"); 358 | pinnedEmoji = target; 359 | pinnedEmoji.classList.add("pinned"); 360 | return; 361 | } 362 | selectEmoji(undefined, target.id); 363 | 364 | if (pinnedEmoji) { 365 | selectMixmoji(undefined, [pinnedEmoji.id, target.id]); 366 | } 367 | } 368 | 369 | const imageLoaded = (e) => { e.target.classList.add("loaded") } 370 | 371 | const selectEmoji = (e, id) => { 372 | let target = e?.target ?? document.getElementById(id); 373 | id = target.id; 374 | console.log("Selecting Base", id, codePointToText(id)); 375 | document.getElementById("preview-container").classList.remove("mix") 376 | 377 | window.history.replaceState({}, "", "/kitchen/?" + codePointToText(id)); 378 | 379 | emoji1?.classList.remove("selected"); 380 | emoji2?.classList.remove("secondary"); 381 | 382 | if (pinnedEmoji) { 383 | emoji2 = target; 384 | emoji1 = pinnedEmoji; 385 | } else { 386 | emoji2 = emoji1; 387 | emoji1 = target; 388 | } 389 | 390 | emoji1?.classList.add("selected"); 391 | emoji2?.classList.add("secondary"); 392 | 393 | recents = recents.filter(i => i !== id) 394 | recents.unshift(id); 395 | recents.splice(36); 396 | localStorage.setItem("recents", JSON.stringify(recents)); 397 | 398 | document.title = " = " + codePointToText(id); 399 | 400 | setFavicon(target.src); 401 | p1.src = emoji1.src.replace("128", "512"); 402 | p2.src = emoji2?.src.replace("128", "512"); 403 | updateBlobForElement(p1); 404 | updateBlobForElement(p2); 405 | pc.src = ""; 406 | 407 | emojiContainer.onscroll = scrollElement; 408 | mixmojiContainer.onscroll = scrollElement; 409 | 410 | let index = window.points.indexOf(target.id); 411 | let b64 = convertBase(index, 10, 64); 412 | console.log("index", index, b64); 413 | const re = new RegExp("^.*\\." + b64 + "\\..*$", "gm"); 414 | 415 | 416 | let parent = document.getElementById("mixmoji-container"); 417 | parent.classList.remove("hidden"); 418 | parent.scrollTo(0, 0); 419 | parent.childNodes.forEach(child => { parent.removeChild(child) }); 420 | 421 | const array = [...window.pairs.matchAll(re)]; 422 | let validPairs = [] 423 | let div = el("div#mixmoji-content", { className: array.length < 20 ? "sparse content" : "content" }, 424 | array.map(match => { 425 | 426 | let string = match.pop(); 427 | let [d, c1, c2] = string.split("."); 428 | c1 = window.points[convertBase(c1, 64, 10)]; 429 | c2 = window.points[convertBase(c2, 64, 10)]; 430 | d = window.revisions[convertBase(d, 64, 10)]; 431 | 432 | let className = ["mixmoji"]; 433 | className.push("c-" + c1); 434 | className.push("c-" + c2); 435 | let altParent = c1 == id ? c2 : c1; 436 | let index = recents.indexOf(altParent); 437 | 438 | if (index == 0 && c1 == c2) { 439 | index = -1; 440 | } 441 | validPairs.push(altParent) 442 | 443 | let url = mixmojiUrl(d, [c1, c2]); 444 | 445 | if (index > 0 || c1 == c2) { 446 | className.push("featured"); 447 | } 448 | 449 | return el("img", { 450 | id: [c1, c2].join("_"), 451 | date: d, 452 | className: className.join(" "), c: [c1, c2], onclick: selectMixmoji, 453 | style: "transition: all 0.3s " + Math.random() / 8 + "s ease-out;" + (index < 0 ? "" : "order:" + (-10 + index)), 454 | onload: imageLoaded, 455 | src: url, 456 | draggable: true, 457 | loading: "lazy" 458 | }, codePointToText(c1), codePointToText(c2)) 459 | }) 460 | ) 461 | if (1) { 462 | [...document.querySelectorAll(".emoji")].forEach(el => { 463 | el.classList.toggle("dud", !validPairs.includes(el.id)); 464 | }) 465 | } 466 | parent.appendChild(div); 467 | if (1) { 468 | selectMixmoji(undefined, [emoji1?.id, emoji2?.id]); 469 | } 470 | } 471 | 472 | let div = el("div#emoji-content.content", {}, 473 | window.points.map((point, index) => { 474 | let dud = window.counts[index] < 31; 475 | let url = emojiUrl(point); 476 | let text = codePointToText(point); 477 | let className = ["emoji"]; 478 | if (dud) className.push("dud"); 479 | if (favorites.includes("point")) className.push("favorite"); 480 | return el("div", { id: point, title: text, src: url, className: className.join(" ") }, el("span", text), 481 | el("img", { onclick: clickedEmoji, onload: imageLoaded, src: url, draggable: true, loading: "lazy" }) 482 | ); 483 | }) 484 | ) 485 | emojiContainer.appendChild(div); 486 | 487 | let query = decodeURIComponent(location.search.substring(1)); 488 | if (query.includes("&")) query = ""; 489 | if (query.length) { 490 | let date = undefined; 491 | if (query.indexOf("=")) { 492 | [query, date] = query.split("="); 493 | date = parseInt(date, 36); 494 | } 495 | let components = query.split("+"); 496 | 497 | components = components.map(c => Array.from(decodeURIComponent(c)).map(a => a.codePointAt(0).toString(16)).join("-")); 498 | 499 | if (components.length > 0) { 500 | document.documentElement.className = "mixmoji-container"; 501 | 502 | selectEmoji(undefined, components[0]) 503 | if (components.length > 1) { 504 | selectMixmoji(undefined, components); 505 | } 506 | } 507 | } 508 | document.body.addEventListener('touchmove', function (e) { e.preventDefault(); }); 509 | 510 | if (!navigator.share) document.getElementById("share").style.display = "none" 511 | document.getElementById("copy").style.display = "none" 512 | 513 | about = () => { 514 | document.documentElement.classList.add('showAbout'); 515 | document.documentElement.classList.remove('showMenu') 516 | } 517 | 518 | share = () => { 519 | document.documentElement.classList.remove('showMenu'); 520 | navigator.share({ 521 | title: document.title.replace("=", "").trim(), 522 | url: location.href 523 | }) 524 | .catch(console.error); 525 | } 526 | 527 | copy = () => { 528 | document.documentElement.classList.remove('showMenu') 529 | 530 | var text = pc.src || location.href; 531 | var dummy = document.createElement("input"); 532 | document.body.appendChild(dummy); 533 | dummy.value = text; 534 | dummy.select(); 535 | document.execCommand("copy"); 536 | document.body.removeChild(dummy); 537 | 538 | document.body.classList.add("copied"); 539 | setTimeout(function () { document.body.classList.remove("copied"); }, 2000); 540 | } 541 | 542 | if (/Mobi|Android/i.test(navigator.userAgent)) { 543 | document.body.classList.add("mobile"); 544 | } -------------------------------------------------------------------------------- /docs/kitchen/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100vh; 3 | height: -webkit-fill-available; 4 | } 5 | body { 6 | font-size: 20px; 7 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 8 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 9 | display: flex; 10 | position: relative; 11 | 12 | flex-direction: column; 13 | height: 100vh; 14 | height: var(--app-height); 15 | max-height: 100vh; 16 | margin: 0; 17 | overflow: hidden; 18 | background-color: var(--background-color); 19 | color: var(--text-color); 20 | justify-content: flex-end; 21 | /* width: 100%; */ 22 | } 23 | svg path { 24 | fill: var(--text-color); 25 | } 26 | 27 | :root { 28 | --background-color: hsl(180deg 5% 100%); 29 | --background-color-2: hsl(180deg 5% 95%); 30 | --background-color-3: hsl(180deg 5% 90%); 31 | --text-color: hsl(180deg 5% 30%); 32 | --border-color: hsla(0, 0%, 0%, 0.2); 33 | --hover-color: rgba(0, 0, 0, 0.05); 34 | } 35 | 36 | ::-webkit-scrollbar { 37 | display: none; 38 | scrollbar-width: none; 39 | } 40 | .container { 41 | display: flex; 42 | flex: 1 1 50%; 43 | overflow-y: scroll; 44 | transition: all 200ms ease-out; 45 | box-sizing: border-box; 46 | overflow-x: hidden; 47 | } 48 | .content { 49 | display: grid; 50 | grid-template-columns: repeat(auto-fill, 1em); 51 | grid-template-rows: repeat(auto-fill, 1em); 52 | max-height: 100vh; 53 | max-width: 100vmin; 54 | margin: 0 auto; 55 | flex-wrap: wrap; 56 | flex-direction: row; 57 | justify-content: center; 58 | align-items: center; 59 | width: 100vw; 60 | height: 100%; 61 | } 62 | #emoji-content { 63 | padding-top: 1vmin; 64 | font-size: calc(min(10vmin, 48px)); 65 | } 66 | #mixmoji-content { 67 | font-size: calc(min(20vmin, 96px)); 68 | padding-top: 1vmin; 69 | } 70 | #emoji-container { 71 | flex-direction: column; 72 | } 73 | html.emoji-container #emoji-container, 74 | html.mixmoji-container #mixmoji-container { 75 | flex: 1 1 80%; 76 | } 77 | html.allEmoji-container #emoji-container { 78 | flex: 1 1 100%; 79 | } 80 | html.allEmoji-container #mixmoji-container { 81 | flex: 1 1 0vh; 82 | padding-top: 0; 83 | } 84 | #mixmoji-container { 85 | border-top: 1px solid var(--border-color); 86 | background-color: var(--background-color-2); 87 | } 88 | html.allEmoji-container #preview-container { 89 | opacity: 0; 90 | margin-top: -20vh; 91 | } 92 | /* #preview:not(.mix) .icon{ 93 | display:none; 94 | } 95 | */ 96 | 97 | .search-container { 98 | display: flex; 99 | justify-content: center; 100 | /* position:fixed; */ 101 | /* transform:scale(0.0); */ 102 | } 103 | 104 | #search { 105 | /* border-radius:20px; */ 106 | letter-spacing: 2px;; 107 | width: 100vh; 108 | max-width: 100vw; 109 | margin-top: 8px; 110 | padding: 0 4px; 111 | border: 1px solid #eee; 112 | text-align: center; 113 | text-transform: uppercase; 114 | line-height: 32px; 115 | font-size: 20px; 116 | font-weight:300; 117 | border: 0; 118 | } 119 | 120 | #toast { 121 | position:absolute; 122 | bottom: calc(max(72px, 15vmin) + 2em); 123 | right:10px; 124 | z-index:1000; 125 | color: var(--background-color); 126 | background-color: var(--text-color); 127 | padding:0.25em 0.5em; 128 | margin:auto; 129 | text-align:center; 130 | border-radius:4px; 131 | transition:transform 300ms, opacity 300ms; 132 | opacity:1.0; 133 | pointer-events: none; 134 | } 135 | 136 | body:not(.copied) #toast { 137 | transform: translateY(-4px); 138 | opacity:0.0; 139 | } 140 | body.mobile .search-container { 141 | display: none; 142 | } 143 | 144 | #search:focus { 145 | outline: none; 146 | border-color: var(--text-color); 147 | border: none; 148 | } 149 | 150 | .hidden { 151 | display: none !important; 152 | } 153 | .promoted { 154 | order: -100; 155 | opacity:100% !important; 156 | } 157 | .dimmed { 158 | display:none; 159 | } 160 | #emoji-container { 161 | background-color: var(--background-color); 162 | } 163 | @media screen and (max-width: 600px) { 164 | #emoji-container { 165 | padding-top: 0; 166 | } 167 | } 168 | #emoji-content, 169 | #mixmoji-content { 170 | } 171 | .emoji { 172 | border-radius: 8px; 173 | } 174 | .emoji { 175 | box-sizing: border-box; 176 | } 177 | .emoji span { 178 | user-select: none; 179 | } 180 | .emoji.selected { 181 | background-color: var(--background-color); 182 | background-color: var(--text-color); 183 | position: sticky; 184 | bottom: 8px; 185 | top: 8px; 186 | opacity: 1; 187 | z-index: 100; 188 | } 189 | .emoji.secondary { 190 | background-color: var(--background-color); 191 | background-color: rgba(128, 128, 128, 1); 192 | position: sticky; 193 | bottom: 8px; 194 | top: 8px; 195 | opacity: 1; 196 | z-index: 100; 197 | } 198 | .emoji img { 199 | cursor: pointer; 200 | padding: 0.05em; 201 | width: 1em; 202 | height: 1em; 203 | display: block; 204 | box-sizing: border-box; 205 | touch-action: manipulation; 206 | opacity: 0; 207 | transition: opacity 500ms; 208 | } 209 | 210 | .emoji img.loaded { 211 | opacity: 1; 212 | } 213 | 214 | .emoji.dud { 215 | opacity: 0.667; 216 | } 217 | .emoji.dud:hover { 218 | opacity: 1; 219 | } 220 | img.c-2615 { 221 | order: 1; 222 | } 223 | .emoji.pinned:before { 224 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' fill='none' viewBox='0 0 13 13'%3e%3cpath fill='black' d='M4.054 11.119h4.734c.642 0 .955-.318.955-1.015v-3.65c0-.627-.259-.95-.79-1.005V4.196c0-1.875-1.23-2.78-2.532-2.78-1.303 0-2.532.905-2.532 2.78v1.278c-.487.075-.79.393-.79.98v3.65c0 .697.313 1.015.955 1.015Zm.636-7.027c0-1.248.8-1.91 1.73-1.91.93 0 1.732.662 1.732 1.91v1.353l-3.462.004V4.092Z'/%3e%3c/svg%3e"); 225 | content: ""; 226 | background-position: center; 227 | background-size: cover; 228 | position: absolute; 229 | top: -5px; 230 | right: -7px; 231 | font-size: 0.2em; 232 | color: var(--text-color); 233 | opacity: 1; 234 | background-color: var(--background-color); 235 | width: 2.5em; 236 | height: 2.5em; 237 | border-radius: 100%; 238 | line-height: 1.5em; 239 | text-align: center; 240 | border: 2px solid black; 241 | box-sizing: border-box; 242 | } 243 | .emoji.favorite:before { 244 | content: ""; 245 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' fill='none' viewBox='0 0 13 13'%3e%3cpath fill='white' d='M4.054 11.119h4.734c.642 0 .955-.318.955-1.015v-3.65c0-.627-.259-.95-.79-1.005V4.196c0-1.875-1.23-2.78-2.532-2.78-1.303 0-2.532.905-2.532 2.78v1.278c-.487.075-.79.393-.79.98v3.65c0 .697.313 1.015.955 1.015Zm.636-7.027c0-1.248.8-1.91 1.73-1.91.93 0 1.732.662 1.732 1.91v1.353l-3.462.004V4.092Z'/%3e%3c/svg%3e"); 246 | background-position: center; 247 | background-size: cover; 248 | position: absolute; 249 | top: -5px; 250 | right: -7px; 251 | font-size: 0.2em; 252 | color: var(--text-color); 253 | opacity: 1; 254 | background-color: var(--background-color); 255 | width: 2.5em; 256 | height: 2.5em; 257 | border-radius: 100%; 258 | line-height: 1.5em; 259 | text-align: center; 260 | border: 2px solid black; 261 | box-sizing: border-box; 262 | } 263 | #emoji-container .emoji.pinned { 264 | transform: scale(1); 265 | box-shadow: 0 0 0 2px var(--text-color); 266 | background-color: var(--background-color); 267 | position: sticky; 268 | bottom: 14px; 269 | } 270 | #emoji-content > div { 271 | color: transparent; 272 | height: 1em; 273 | width: 1em; 274 | user-select: none; 275 | transition: background-color 500ms, opacity 500ms; 276 | } 277 | #emoji-content > div > span { 278 | color: transparent; 279 | position: absolute; 280 | pointer-events: none; 281 | } 282 | .emoji:hover { 283 | background-color: var(--text-color); 284 | } 285 | .mixmoji { 286 | width: 1em; 287 | opacity: 0; 288 | transition: all 1s ease; 289 | height: 1em; 290 | padding: 0.05em; 291 | box-sizing: border-box; 292 | border: 2px solid transparent; 293 | border-radius: 0.2em; 294 | touch-action: manipulation; 295 | box-sizing: border-box; 296 | transform: scale(0.5); 297 | } 298 | .mixmoji.selected { 299 | background-color: var(--background-color); 300 | background-color: var(--text-color); 301 | position: sticky; 302 | bottom: 8px; 303 | top: 8px; 304 | opacity: 1; 305 | transform: scale(1); 306 | z-index: 100; 307 | border-color: var(--background-color); 308 | } 309 | .mixmoji.loaded { 310 | opacity: 1; 311 | transform: scale(1); 312 | } 313 | 314 | #mixmoji-container .sparse { 315 | margin: auto; 316 | flex-wrap: wrap; 317 | gap: 0 0.3em; 318 | /* height: auto; */ 319 | } 320 | #mixmoji-content:before { 321 | width: auto; 322 | height: 2px; 323 | background-color: var(--text-color); 324 | position: fixed; 325 | } 326 | #preview-container { 327 | /* top: 0; */ 328 | left: 0; 329 | right: 0; 330 | display: flex; 331 | justify-content: center; 332 | padding: 1vmin; 333 | align-items: center; 334 | height: 1.5em; 335 | box-shadow: 0 2px 9px rgba(0, 0, 0, 0.2); 336 | -webkit-backdrop-filter: blur(10px); 337 | backdrop-filter: blur(10px); 338 | border-top: 1px solid var(--border-color); 339 | z-index: 100; 340 | flex: 0 0 auto; 341 | font-size: calc(max(72px, 15vmin)); 342 | background-color: var(--background-color-3); 343 | user-select: none; 344 | padding-right: 0.25em; 345 | /* -webkit-touch-callout: none; */ 346 | -webkit-user-select: none; 347 | box-sizing: border-box; 348 | bottom: 0; 349 | } 350 | .preview img { 351 | transition: all 0.5s; 352 | touch-action: manipulation; 353 | } 354 | img[src="undefined"], 355 | img[src=""] { 356 | display: none; 357 | } 358 | div.preview { 359 | width: 1em; 360 | height: 1em; 361 | overflow: hidden; 362 | display: flex; 363 | flex-direction: column; 364 | flex-wrap: wrap; 365 | } 366 | .preview img { 367 | width: 100%; 368 | height: 100%; 369 | cursor: default; 370 | } 371 | .preview:after { 372 | content: ""; 373 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' fill='none' viewBox='0 0 48 48'%3e%3cpath fill='black' fill-opacity='.5' d='M22.648 31.926c1.368 0 1.993-.938 1.993-2.188v-.664c.039-2.578.957-3.652 4.082-5.8 3.359-2.266 5.488-4.883 5.488-8.653 0-5.86-4.766-9.219-10.703-9.219-4.414 0-8.281 2.09-9.942 5.86-.41.918-.586 1.816-.586 2.558 0 1.114.645 1.895 1.836 1.895.996 0 1.66-.586 1.954-1.543.996-3.711 3.457-5.117 6.601-5.117 3.809 0 6.797 2.148 6.797 5.547 0 2.793-1.738 4.355-4.238 6.113-3.067 2.129-5.313 4.414-5.313 7.851v1.23c0 1.25.684 2.13 2.031 2.13Zm0 10.683a2.776 2.776 0 0 0 2.793-2.773 2.764 2.764 0 0 0-2.793-2.773 2.772 2.772 0 0 0-2.773 2.773 2.784 2.784 0 0 0 2.773 2.773Z'/%3e%3c/svg%3e"); 374 | width: 1em; 375 | height: 1em; 376 | background-repeat: no-repeat; 377 | background-position: center; 378 | } 379 | img#\31 f60d, 380 | img#\31 f618, 381 | img#\31 f970, 382 | img#\31 f60d, 383 | img#\31 f498, 384 | img#\31 f495, 385 | img#\32 763-fe0f, 386 | img#\31 f31f, 387 | img#\31 f493, 388 | .featured { 389 | order: -1; 390 | } 391 | .showAbout #about { 392 | opacity: 1; 393 | pointer-events: all; 394 | /* transform: translateY(-0.5em); */ 395 | display: flex; 396 | } 397 | #about { 398 | width: 360px; 399 | height: 600px; 400 | display: flex; 401 | font-size:18px; 402 | opacity: 0; 403 | pointer-events: none; 404 | /* transform: translateY(-1em); */ 405 | transition: all 0.2s; 406 | position: absolute; 407 | border: 1px solid rgba(255, 255, 255, 0.2); 408 | background: linear-gradient(45deg, var(--background-color), transparent); 409 | left: 50%; 410 | position: absolute; 411 | margin-left: -160px; 412 | z-index: 1000; 413 | padding: 1em; 414 | justify-content: space-between; 415 | border-top: none; 416 | box-sizing: border-box; 417 | flex-direction: column; 418 | -webkit-backdrop-filter: blur(10px); 419 | backdrop-filter: blur(10px); 420 | top: 2px; 421 | display: none; 422 | border: 1px solid var(--border-color); 423 | } 424 | 425 | #about a { 426 | color: currentColor; 427 | } 428 | 429 | .icon { 430 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='144' height='96' fill='none' viewBox='0 0 144 96'%3e%3cpath fill='black' d='M14.344 42.59h17.89c2.93 0 4.375-1.465 4.375-4.649V24.484c0-2.773-1.113-4.238-3.379-4.55v-4.727c0-7.363-4.824-10.918-9.94-10.918-5.118 0-9.942 3.555-9.942 10.918v4.727c-2.266.312-3.38 1.777-3.38 4.55v13.457c0 3.184 1.446 4.649 4.376 4.649Zm2.148-27.793c0-4.902 3.145-7.5 6.797-7.5 3.652 0 6.797 2.598 6.797 7.5v5.078H16.492v-5.078ZM14.461 39.66c-.899 0-1.348-.41-1.348-1.504V24.29c0-1.094.45-1.484 1.348-1.484h17.676c.898 0 1.328.39 1.328 1.484v13.867c0 1.094-.43 1.504-1.328 1.504H14.46ZM9.625 73.637h12.637v12.636c0 .938.781 1.739 1.738 1.739s1.758-.801 1.758-1.739V73.637h12.617c.938 0 1.738-.781 1.738-1.739 0-.957-.8-1.757-1.738-1.757H25.758V57.523c0-.937-.801-1.738-1.758-1.738s-1.738.8-1.738 1.738v12.618H9.625c-.938 0-1.738.8-1.738 1.757 0 .958.8 1.739 1.738 1.739Zm46.863-37.199c-.664.664-.683 1.796 0 2.46.684.664 1.817.664 2.48 0l12.5-12.5 12.5 12.5c.665.664 1.817.684 2.481 0 .664-.683.664-1.796 0-2.46l-12.5-12.52 12.5-12.5c.664-.664.684-1.797 0-2.461-.683-.684-1.816-.684-2.48 0l-12.5 12.5-12.5-12.5c-.664-.684-1.817-.703-2.48 0-.665.684-.665 1.797 0 2.461l12.5 12.5-12.5 12.52Zm3.657 31.066h23.71c.938 0 1.739-.8 1.739-1.758 0-.957-.801-1.738-1.738-1.738H60.145c-.938 0-1.739.781-1.739 1.738s.801 1.758 1.739 1.758Zm0 12.344h23.71c.938 0 1.739-.782 1.739-1.739 0-.957-.801-1.757-1.738-1.757H60.145c-.938 0-1.739.8-1.739 1.757 0 .957.801 1.739 1.739 1.739Zm58.503-47.922c1.368 0 1.993-.938 1.993-2.188v-.664c.039-2.578.957-3.652 4.082-5.8 3.359-2.266 5.488-4.883 5.488-8.653 0-5.86-4.766-9.219-10.703-9.219-4.414 0-8.281 2.09-9.942 5.86-.41.918-.586 1.816-.586 2.558 0 1.114.645 1.895 1.836 1.895.996 0 1.661-.586 1.954-1.543.996-3.711 3.457-5.117 6.601-5.117 3.809 0 6.797 2.148 6.797 5.547 0 2.793-1.738 4.355-4.238 6.113-3.067 2.129-5.313 4.414-5.313 7.851v1.23c0 1.25.684 2.13 2.031 2.13Zm0 10.683a2.776 2.776 0 0 0 2.793-2.773 2.763 2.763 0 0 0-2.793-2.773 2.771 2.771 0 0 0-2.773 2.773 2.784 2.784 0 0 0 2.773 2.773Zm-.714 19.446h3.339c1.075 0 1.954-.88 1.954-1.934a1.947 1.947 0 0 0-1.954-1.953h-3.339A1.93 1.93 0 0 0 116 60.121c0 1.055.859 1.934 1.934 1.934Zm0 11.816h3.339a1.96 1.96 0 0 0 1.954-1.953 1.942 1.942 0 0 0-1.954-1.934h-3.339c-1.075 0-1.934.86-1.934 1.934 0 1.074.859 1.953 1.934 1.953Zm0 11.797h3.339c1.075 0 1.954-.879 1.954-1.934a1.947 1.947 0 0 0-1.954-1.953h-3.339A1.93 1.93 0 0 0 116 83.734c0 1.055.859 1.934 1.934 1.934Z'/%3e%3c/svg%3e"); 431 | width: 48px; 432 | height: 48px; 433 | margin: 0 -4px; 434 | } 435 | 436 | .icon.lockIcon { 437 | background-position: 0 0; 438 | } 439 | .icon.addIcon { 440 | background-position: 0 -48px; 441 | } 442 | .icon.closeIcon { 443 | background-position: -48px 0; 444 | margin-left: auto; 445 | margin-bottom: auto; 446 | /* width: 32px; */ 447 | /* height: 32px; */ 448 | /* background-size: auto; */ 449 | transform: scale(0.75); 450 | margin-top: 0.5em; 451 | } 452 | .icon.equalsIcon { 453 | background-position: -48px -48px; 454 | } 455 | .icon.questionIcon { 456 | background-position: -96px 0; 457 | } 458 | .icon.menuIcon { 459 | background-position: -96px -48px; 460 | width: 32px; 461 | position: absolute; 462 | right: 16px; 463 | } 464 | .favorite { 465 | } 466 | .allEmojis div#mixmoji-container { 467 | display: none; 468 | } 469 | @media screen and (max-width: 768px) { 470 | #about { 471 | width: 100%; 472 | height: 100%; 473 | position: absolute; 474 | margin-top: 0; 475 | top: 0; 476 | left: 0; 477 | margin-left: 0; 478 | 479 | border: none; 480 | } 481 | } 482 | svg fill { 483 | fill: red; 484 | } 485 | 486 | .menu-item { 487 | padding: 0.0em 1em; 488 | line-height: 40px; 489 | cursor: default; 490 | } 491 | .menu-item:hover { 492 | background-color: var(--hover-color); 493 | } 494 | 495 | html:not(.showMenu) #menu { 496 | display: none; 497 | } 498 | 499 | div#menu { 500 | border: 1px solid rgba(100, 100, 100, 0.9); 501 | background-color: var(--background-color-3); 502 | position: fixed; 503 | bottom: 95px; 504 | right: 0; 505 | z-index: 1000; 506 | backdrop-filter: blur(10px); 507 | font-size: 18px; 508 | user-select: none; 509 | min-width:160px; 510 | padding: 12px 0; 511 | } 512 | 513 | .preview { 514 | transform: scale(1.0); 515 | transition: transform 0.3s ease-in-out; 516 | } 517 | 518 | .preview:hover { 519 | transform: scale(1.1); 520 | 521 | transition: transform 0.0s linear; 522 | } 523 | 524 | 525 | .preview:active { 526 | transform: scale(1.0); 527 | transition: transform 0s; 528 | 529 | } 530 | 531 | 532 | @media (prefers-color-scheme: dark) { 533 | :root { 534 | --background-color: hsl(180deg 5% 10%); 535 | --background-color-2: hsl(180deg 5% 15%); 536 | --background-color-3: hsl(180deg 5% 20%); 537 | --text-color: white; 538 | --border-color: hsla(0, 0%, 84%, 0.5); 539 | --hover-color: rgba(255, 255, 255, 0.05); 540 | } 541 | .icon { 542 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='144' height='96' fill='none' viewBox='0 0 144 96'%3e%3cpath fill='white' d='M14.344 42.59h17.89c2.93 0 4.375-1.465 4.375-4.649V24.484c0-2.773-1.113-4.238-3.379-4.55v-4.727c0-7.363-4.824-10.918-9.94-10.918-5.118 0-9.942 3.555-9.942 10.918v4.727c-2.266.312-3.38 1.777-3.38 4.55v13.457c0 3.184 1.446 4.649 4.376 4.649Zm2.148-27.793c0-4.902 3.145-7.5 6.797-7.5 3.652 0 6.797 2.598 6.797 7.5v5.078H16.492v-5.078ZM14.461 39.66c-.899 0-1.348-.41-1.348-1.504V24.29c0-1.094.45-1.484 1.348-1.484h17.676c.898 0 1.328.39 1.328 1.484v13.867c0 1.094-.43 1.504-1.328 1.504H14.46ZM9.625 73.637h12.637v12.636c0 .938.781 1.739 1.738 1.739s1.758-.801 1.758-1.739V73.637h12.617c.938 0 1.738-.781 1.738-1.739 0-.957-.8-1.757-1.738-1.757H25.758V57.523c0-.937-.801-1.738-1.758-1.738s-1.738.8-1.738 1.738v12.618H9.625c-.938 0-1.738.8-1.738 1.757 0 .958.8 1.739 1.738 1.739Zm46.863-37.199c-.664.664-.683 1.796 0 2.46.684.664 1.817.664 2.48 0l12.5-12.5 12.5 12.5c.665.664 1.817.684 2.481 0 .664-.683.664-1.796 0-2.46l-12.5-12.52 12.5-12.5c.664-.664.684-1.797 0-2.461-.683-.684-1.816-.684-2.48 0l-12.5 12.5-12.5-12.5c-.664-.684-1.817-.703-2.48 0-.665.684-.665 1.797 0 2.461l12.5 12.5-12.5 12.52Zm3.657 31.066h23.71c.938 0 1.739-.8 1.739-1.758 0-.957-.801-1.738-1.738-1.738H60.145c-.938 0-1.739.781-1.739 1.738s.801 1.758 1.739 1.758Zm0 12.344h23.71c.938 0 1.739-.782 1.739-1.739 0-.957-.801-1.757-1.738-1.757H60.145c-.938 0-1.739.8-1.739 1.757 0 .957.801 1.739 1.739 1.739Zm58.503-47.922c1.368 0 1.993-.938 1.993-2.188v-.664c.039-2.578.957-3.652 4.082-5.8 3.359-2.266 5.488-4.883 5.488-8.653 0-5.86-4.766-9.219-10.703-9.219-4.414 0-8.281 2.09-9.942 5.86-.41.918-.586 1.816-.586 2.558 0 1.114.645 1.895 1.836 1.895.996 0 1.661-.586 1.954-1.543.996-3.711 3.457-5.117 6.601-5.117 3.809 0 6.797 2.148 6.797 5.547 0 2.793-1.738 4.355-4.238 6.113-3.067 2.129-5.313 4.414-5.313 7.851v1.23c0 1.25.684 2.13 2.031 2.13Zm0 10.683a2.776 2.776 0 0 0 2.793-2.773 2.763 2.763 0 0 0-2.793-2.773 2.771 2.771 0 0 0-2.773 2.773 2.784 2.784 0 0 0 2.773 2.773Zm-.714 19.446h3.339c1.075 0 1.954-.88 1.954-1.934a1.947 1.947 0 0 0-1.954-1.953h-3.339A1.93 1.93 0 0 0 116 60.121c0 1.055.859 1.934 1.934 1.934Zm0 11.816h3.339a1.96 1.96 0 0 0 1.954-1.953 1.942 1.942 0 0 0-1.954-1.934h-3.339c-1.075 0-1.934.86-1.934 1.934 0 1.074.859 1.953 1.934 1.953Zm0 11.797h3.339c1.075 0 1.954-.879 1.954-1.934a1.947 1.947 0 0 0-1.954-1.953h-3.339A1.93 1.93 0 0 0 116 83.734c0 1.055.859 1.934 1.934 1.934Z'/%3e%3c/svg%3e"); 543 | } 544 | .preview:after { 545 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' fill='none' viewBox='0 0 48 48'%3e%3cpath fill='white' fill-opacity='.5' d='M22.648 31.926c1.368 0 1.993-.938 1.993-2.188v-.664c.039-2.578.957-3.652 4.082-5.8 3.359-2.266 5.488-4.883 5.488-8.653 0-5.86-4.766-9.219-10.703-9.219-4.414 0-8.281 2.09-9.942 5.86-.41.918-.586 1.816-.586 2.558 0 1.114.645 1.895 1.836 1.895.996 0 1.66-.586 1.954-1.543.996-3.711 3.457-5.117 6.601-5.117 3.809 0 6.797 2.148 6.797 5.547 0 2.793-1.738 4.355-4.238 6.113-3.067 2.129-5.313 4.414-5.313 7.851v1.23c0 1.25.684 2.13 2.031 2.13Zm0 10.683a2.776 2.776 0 0 0 2.793-2.773 2.764 2.764 0 0 0-2.793-2.773 2.772 2.772 0 0 0-2.773 2.773 2.784 2.784 0 0 0 2.773 2.773Z'/%3e%3c/svg%3e"); 546 | } 547 | .emoji.pinned:before { 548 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' fill='none' viewBox='0 0 13 13'%3e%3cpath fill='white' d='M4.054 11.119h4.734c.642 0 .955-.318.955-1.015v-3.65c0-.627-.259-.95-.79-1.005V4.196c0-1.875-1.23-2.78-2.532-2.78-1.303 0-2.532.905-2.532 2.78v1.278c-.487.075-.79.393-.79.98v3.65c0 .697.313 1.015.955 1.015Zm.636-7.027c0-1.248.8-1.91 1.73-1.91.93 0 1.732.662 1.732 1.91v1.353l-3.462.004V4.092Z'/%3e%3c/svg%3e"); 549 | } 550 | } 551 | -------------------------------------------------------------------------------- /docs/util/paste.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 123 | 124 |

125 | 126 | 127 |

128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /docs/wallpaper/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Emoji Wallpaper 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 40 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | 52 | 53 |
54 | 59 | 60 | 61 | 62 |      63 | 71 | 72 |
73 | 74 | 75 | 80 | 81 | 82 | 83 | 90 | 91 |
92 | 93 |
94 | 95 | font size: 96 | 97 | 102 |
103 | spacing: , 104 | margin: 105 |
AnimateFullbleed 106 |
107 |

108 |

109 | 110 | COPY LINK 111 | 112 | Download 113 |
114 | 115 |
116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/wallpaper/runes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const HIGH_SURROGATE_START = 0xd800 4 | const HIGH_SURROGATE_END = 0xdbff 5 | 6 | const LOW_SURROGATE_START = 0xdc00 7 | 8 | const REGIONAL_INDICATOR_START = 0x1f1e6 9 | const REGIONAL_INDICATOR_END = 0x1f1ff 10 | 11 | const FITZPATRICK_MODIFIER_START = 0x1f3fb 12 | const FITZPATRICK_MODIFIER_END = 0x1f3ff 13 | 14 | const VARIATION_MODIFIER_START = 0xfe00 15 | const VARIATION_MODIFIER_END = 0xfe0f 16 | 17 | const DIACRITICAL_MARKS_START = 0x20d0 18 | const DIACRITICAL_MARKS_END = 0x20ff 19 | 20 | const ZWJ = 0x200d 21 | 22 | const GRAPHEMS = [ 23 | 0x0308, // ( ◌̈ ) COMBINING DIAERESIS 24 | 0x0937, // ( ष ) DEVANAGARI LETTER SSA 25 | 0x0937, // ( ष ) DEVANAGARI LETTER SSA 26 | 0x093F, // ( ि ) DEVANAGARI VOWEL SIGN I 27 | 0x093F, // ( ि ) DEVANAGARI VOWEL SIGN I 28 | 0x0BA8, // ( ந ) TAMIL LETTER NA 29 | 0x0BBF, // ( ி ) TAMIL VOWEL SIGN I 30 | 0x0BCD, // ( ◌்) TAMIL SIGN VIRAMA 31 | 0x0E31, // ( ◌ั ) THAI CHARACTER MAI HAN-AKAT 32 | 0x0E33, // ( ำ ) THAI CHARACTER SARA AM 33 | 0x0E40, // ( เ ) THAI CHARACTER SARA E 34 | 0x0E49, // ( เ ) THAI CHARACTER MAI THO 35 | 0x1100, // ( ᄀ ) HANGUL CHOSEONG KIYEOK 36 | 0x1161, // ( ᅡ ) HANGUL JUNGSEONG A 37 | 0x11A8 // ( ᆨ ) HANGUL JONGSEONG KIYEOK 38 | ] 39 | 40 | function runes (string) { 41 | if (typeof string !== 'string') { 42 | throw new Error('string cannot be undefined or null') 43 | } 44 | const result = [] 45 | let i = 0 46 | let increment = 0 47 | while (i < string.length) { 48 | increment += nextUnits(i + increment, string) 49 | if (isGraphem(string[i + increment])) { 50 | increment++ 51 | } 52 | if (isVariationSelector(string[i + increment])) { 53 | increment++ 54 | } 55 | if (isDiacriticalMark(string[i + increment])) { 56 | increment++ 57 | } 58 | if (isZeroWidthJoiner(string[i + increment])) { 59 | increment++ 60 | continue 61 | } 62 | result.push(string.substring(i, i + increment)) 63 | i += increment 64 | increment = 0 65 | } 66 | return result 67 | } 68 | 69 | // Decide how many code units make up the current character. 70 | // BMP characters: 1 code unit 71 | // Non-BMP characters (represented by surrogate pairs): 2 code units 72 | // Emoji with skin-tone modifiers: 4 code units (2 code points) 73 | // Country flags: 4 code units (2 code points) 74 | // Variations: 2 code units 75 | function nextUnits (i, string) { 76 | const current = string[i] 77 | // If we don't have a value that is part of a surrogate pair, or we're at 78 | // the end, only take the value at i 79 | if (!isFirstOfSurrogatePair(current) || i === string.length - 1) { 80 | return 1 81 | } 82 | 83 | const currentPair = current + string[i + 1] 84 | let nextPair = string.substring(i + 2, i + 5) 85 | 86 | // Country flags are comprised of two regional indicator symbols, 87 | // each represented by a surrogate pair. 88 | // See http://emojipedia.org/flags/ 89 | // If both pairs are regional indicator symbols, take 4 90 | if (isRegionalIndicator(currentPair) && isRegionalIndicator(nextPair)) { 91 | return 4 92 | } 93 | 94 | // If the next pair make a Fitzpatrick skin tone 95 | // modifier, take 4 96 | // See http://emojipedia.org/modifiers/ 97 | // Technically, only some code points are meant to be 98 | // combined with the skin tone modifiers. This function 99 | // does not check the current pair to see if it is 100 | // one of them. 101 | if (isFitzpatrickModifier(nextPair)) { 102 | return 4 103 | } 104 | return 2 105 | } 106 | 107 | function isFirstOfSurrogatePair (string) { 108 | return string && betweenInclusive(string[0].charCodeAt(0), HIGH_SURROGATE_START, HIGH_SURROGATE_END) 109 | } 110 | 111 | function isRegionalIndicator (string) { 112 | return betweenInclusive(codePointFromSurrogatePair(string), REGIONAL_INDICATOR_START, REGIONAL_INDICATOR_END) 113 | } 114 | 115 | function isFitzpatrickModifier (string) { 116 | return betweenInclusive(codePointFromSurrogatePair(string), FITZPATRICK_MODIFIER_START, FITZPATRICK_MODIFIER_END) 117 | } 118 | 119 | function isVariationSelector (string) { 120 | return typeof string === 'string' && betweenInclusive(string.charCodeAt(0), VARIATION_MODIFIER_START, VARIATION_MODIFIER_END) 121 | } 122 | 123 | function isDiacriticalMark (string) { 124 | return typeof string === 'string' && betweenInclusive(string.charCodeAt(0), DIACRITICAL_MARKS_START, DIACRITICAL_MARKS_END) 125 | } 126 | 127 | function isGraphem (string) { 128 | return typeof string === 'string' && GRAPHEMS.indexOf(string.charCodeAt(0)) !== -1 129 | } 130 | 131 | function isZeroWidthJoiner (string) { 132 | return typeof string === 'string' && string.charCodeAt(0) === ZWJ 133 | } 134 | 135 | function codePointFromSurrogatePair (pair) { 136 | const highOffset = pair.charCodeAt(0) - HIGH_SURROGATE_START 137 | const lowOffset = pair.charCodeAt(1) - LOW_SURROGATE_START 138 | return (highOffset << 10) + lowOffset + 0x10000 139 | } 140 | 141 | function betweenInclusive (value, lower, upper) { 142 | return value >= lower && value <= upper 143 | } 144 | 145 | function substring (string, start, width) { 146 | const chars = runes(string) 147 | if (start === undefined) { 148 | return string 149 | } 150 | if (start >= chars.length) { 151 | return '' 152 | } 153 | const rest = chars.length - start 154 | const stringWidth = width === undefined ? rest : width 155 | let endIndex = start + stringWidth 156 | if (endIndex > (start + rest)) { 157 | endIndex = undefined 158 | } 159 | return chars.slice(start, endIndex).join('') 160 | } 161 | 162 | // TODO: Convert this back to a module 163 | // module.exports = runes 164 | // module.exports.substr = substring 165 | -------------------------------------------------------------------------------- /docs/wallpaper/script.js: -------------------------------------------------------------------------------- 1 | let debug = false; 2 | 3 | function random() { 4 | if (random.seed) { 5 | var x = Math.sin(random.seed++) * 10000; 6 | return x - Math.floor(x); 7 | } 8 | return Math.random(); 9 | } 10 | 11 | const copyToClipboard = str => { 12 | const el = document.createElement('textarea'); 13 | el.value = str; 14 | document.body.appendChild(el); 15 | el.select(); 16 | document.execCommand('copy'); 17 | document.body.removeChild(el); 18 | }; 19 | 20 | const download = () => { 21 | document.getElementById("download-link").click() 22 | } 23 | 24 | const copy = (el) => { 25 | let options = Object.fromEntries(new URLSearchParams(location.search)); 26 | 27 | let strings = [] 28 | let title = document.getElementById("t"); 29 | let text = "" 30 | if (options.t && options.t.length) { 31 | text = options.t + "\n" + decodeURIComponent(options.emoji); 32 | } else { 33 | let form = document.getElementById("form"); 34 | form.childNodes.forEach(node => { 35 | if (node.className == "ignore") return; 36 | if (node.options) { 37 | node = node.options[node.selectedIndex]; 38 | } 39 | let string = node.innerText || node.value || ""; 40 | console.log("child", node, string) 41 | 42 | string = string.trim(); 43 | if (string.length) 44 | strings.push(string.trim()); 45 | }) 46 | console.log("strings", strings) 47 | text = strings.join(" "); 48 | } 49 | text += "\n\n" 50 | text = location.href; 51 | copyToClipboard(text) 52 | document.getElementById("copy").innerText = "✓"; 53 | setTimeout(e => {document.getElementById("copy").innerText = "COPY";}, 5000) 54 | return false; 55 | } 56 | 57 | var timer; 58 | 59 | function updateForm() { 60 | let form = document.getElementById("form"); 61 | let paramsString = window.location.search.substring(1); 62 | let params = new URLSearchParams(paramsString); 63 | let entries = params.entries(); 64 | for (var [k, v] of entries) { 65 | let input = form.elements[k]; 66 | if (!input) continue; // URL-only param 67 | if (k == "emoji") v = decodeURIComponent(v) 68 | switch(input.type) { 69 | case 'checkbox': input.checked = !!v; break; 70 | default: input.value = v; break; 71 | } 72 | } 73 | } 74 | 75 | function updateURL() { 76 | let form = document.getElementById("form"); 77 | let params = new URLSearchParams(new FormData(form)); 78 | let options = Object.fromEntries(params); 79 | 80 | // omit empty/default params 81 | for (const [k, v] of Object.entries(options)) { 82 | console.log("keep", k,v) 83 | if (k == 'emoji') continue; // except emoji, let that be empty 84 | if (v == '' || v == null) { params.delete(k) } 85 | } 86 | 87 | // This works around a bug in apple's url detection that can't handle a whole mess of encoded unicode characters 88 | params.set("emoji", encodeURIComponent(params.get("emoji"))) 89 | history.replaceState(undefined, undefined, "?" + params.toString()) 90 | } 91 | 92 | function setColor(e) { 93 | console.log("e", e) 94 | let color = e.value || e.getAttribute("value"); 95 | console.log("setcolor", color) 96 | document.getElementById("colorPicker").value = color 97 | clearTimeout(timer); 98 | timer = setTimeout(render, 300); 99 | } 100 | 101 | function createCanvas(w, h) { 102 | var canvas = document.createElement('canvas'); 103 | canvas.setAttribute('width', w); 104 | canvas.setAttribute('height', h); 105 | return canvas; 106 | } 107 | 108 | var ua = navigator.userAgent; 109 | var isMac = /Macintosh/.test(ua) 110 | var isWin = /Windows/.test(ua) 111 | var iOS = /iPad|iPhone|iPod/.test(ua) 112 | var a = document.getElementById("download-link"); 113 | var c = document.getElementById("canvas"); 114 | var fullbleed = false; 115 | 116 | if (iOS) { 117 | //document.getElementById("textPicker").style.display = "none" 118 | } 119 | 120 | function render() { 121 | updateURL(); 122 | renderContent(); 123 | } 124 | 125 | document.fonts.onloadingdone = function (fontFaceSetEvent) { 126 | renderContent(); 127 | }; 128 | 129 | var startTime = undefined; 130 | function renderFrame(time) { 131 | if(!startTime){ startTime = time; } 132 | renderContent(time, 1); 133 | if (shouldAnimate) { 134 | setTimeout(() => { 135 | window.requestAnimationFrame(renderFrame); 136 | }, 1000 / 30); 137 | } 138 | } 139 | 140 | 141 | console.log() 142 | let emojiCanvas = []; 143 | function renderContent(time, seed) { 144 | if (seed) random.seed = seed; 145 | 146 | var elapsed = (time - startTime) || 0; 147 | let options = Object.fromEntries(new URLSearchParams(location.search)); 148 | 149 | let form = document.getElementById("form"); 150 | let data = new FormData(form); 151 | 152 | let color = data.get('color'); 153 | var lightColor = contrastColor(color, 10); 154 | var darkColor = contrastColor(color, -5); 155 | var textColor = contrastColor(color); 156 | var density = window.devicePixelRatio; 157 | var sh = screen.height, sw = screen.width; 158 | if (iOS && Math.abs(window.orientation) == 90) { 159 | [sw, sh] = [sh, sw] 160 | } 161 | 162 | sw *= density; 163 | sh *= density; 164 | 165 | let sizeString = document.querySelector('#sizePicker').value || "*"; 166 | localStorage.setItem("size", sizeString) 167 | 168 | if (sizeString == "page" || fullbleed) { 169 | sw = window.innerWidth; 170 | sh = window.innerHeight; 171 | sw *= density; 172 | sh *= density; 173 | } else if (sizeString != "*") { 174 | let sizes = sizeString.split("x"); 175 | sw = sizes[0] 176 | sh = sizes[1] || sw 177 | density = 2; 178 | } 179 | 180 | var filename = "emoji-" + color.replace("#",""); 181 | 182 | let title = options.t || ""; 183 | 184 | 185 | if (title.length) { 186 | filename = title 187 | document.title = title 188 | } else { 189 | document.title = "Emoji Wallpaper" 190 | } 191 | 192 | 193 | filename += "-" + sw / density + "x" + sh / density 194 | + (density != 1 ? "@" + density + "x" : "") + ".png" 195 | 196 | // a.innerHTML = name + "

" 197 | a.download = filename; 198 | 199 | if (c.getAttribute("height") != sh) c.setAttribute("height", sh); 200 | if (c.getAttribute("width") != sw) c.setAttribute("width", sw); 201 | 202 | var ctx = c.getContext('2d'); 203 | ctx.save() 204 | 205 | ctx.clearRect(0, 0, c.width/2, c.height); 206 | 207 | ctx.fillStyle = color || "#1e1e1e"; 208 | ctx.lineWidth = 2 * density; 209 | ctx.fillRect(0,0,c.width,c.height); 210 | 211 | // // Generate Linear Gradient 212 | // var grd=ctx.createLinearGradient(0,40,0, c.height); 213 | // grd.addColorStop(0,lightColor); 214 | // grd.addColorStop(0.5,color); 215 | // grd.addColorStop(1,darkColor); 216 | // ctx.fillStyle=grd; 217 | // ctx.fillRect(0,0,c.width,c.height); 218 | 219 | if (options.texture == 'flat') { 220 | ctx.fillStyle = color; 221 | } else { 222 | // Generate Raking Gradient 223 | var r2 = c.width * 2; 224 | var grd = ctx.createRadialGradient( 225 | c.width / 2, c.height - r2, 226 | r2, 227 | c.width / 2, 0 - r2, 228 | r2); 229 | grd.addColorStop(0,darkColor); 230 | grd.addColorStop(0.5,color); 231 | grd.addColorStop(1,lightColor); 232 | ctx.fillStyle = grd; 233 | } 234 | ctx.fillRect(0,0, c.width, c.height); 235 | 236 | 237 | let pattern = options.pattern || 'foam'; 238 | let size = Math.hypot(c.width, c.height) / 50; 239 | 240 | let scale = options.scale; 241 | size *= scale; 242 | let fontSize = parseFloat(options.fontSize) || size; 243 | 244 | let family = options.family; 245 | let font = "sans-serif"; 246 | if (options.noto && options.noto.length) family = "Noto Color Emoji"; 247 | else if (options.family && options.family.length) font = options.family; 248 | 249 | ctx.font = `${fontSize}px ${font}`; 250 | 251 | let margin = parseFloat(options.margin)/100 252 | if (isNaN(margin)) margin = 1.0; 253 | 254 | let marginX = size * 1.5 * margin; 255 | let marginY = size * 1.5 * margin; 256 | 257 | let width = c.width - marginX * 2; 258 | let height = c.height - marginY * 2; 259 | 260 | if (debug) ctx.strokeRect(marginX, marginY, width, height); 261 | if (debug) ctx.strokeRect(marginX, marginY, width, height); 262 | 263 | let spacingX = size; 264 | let spacingY = spacingX; 265 | 266 | ctx.lineWidth = 0.5 267 | ctx.textAlign = 'center' 268 | 269 | let order = options.order || 'random'; 270 | 271 | ctx.globalAlpha = 0.95; 272 | if (options.texture == 'monochrome') 273 | ctx.globalCompositeOperation = "luminosity"; 274 | ctx.shadowColor = 'rgba(0,0,0,0.05)'; 275 | ctx.shadowOffsetY = size / 8; 276 | ctx.shadowBlur = size / 8; 277 | 278 | var lastEmoji = undefined; 279 | 280 | const randomEmoji = () => { 281 | let i = Math.floor(random() * emojis.length); 282 | 283 | if (emojis.length > 3 && i == lastEmoji) { 284 | i += Math.ceil(random() * (emojis.length - 1)) 285 | i %= emojis.length; 286 | } 287 | lastEmoji = i; 288 | return emojis[i]; 289 | } 290 | 291 | const gridLayout = (emojis, options = {}) => { 292 | if (pattern == "diamond") { 293 | spacingX = size * 3 / 2; 294 | spacingY = spacingX / 3; 295 | } 296 | if (options.spacing && options.spacing.length) { 297 | spacingX *= options.spacing/100; 298 | spacingY *= options.spacing/100; 299 | } 300 | 301 | let cols = Math.round(width / (size + spacingX)); 302 | let rows = Math.round(height / (size + spacingY)); 303 | 304 | // Fit to the rect cleanly by fudging spacing 305 | spacingX *= (width -size/2) / (spacingX * cols); 306 | spacingY *= (height - size/2) / (spacingY * rows); 307 | 308 | if (pattern == "diamond" || pattern == "hex") { 309 | spacingX *= (width - (size * (cols-1))) / (spacingX * (cols-1)); 310 | spacingY *= (height - (size * (rows-1))) / (spacingY * (rows-1)); 311 | } else { 312 | spacingX *= (width - (size * (cols-1))) / (spacingX * (cols-1)); 313 | spacingY *= (height - (size * (rows-1))) / (spacingY * (rows-1)); 314 | } 315 | 316 | let stagger = pattern == "diamond" || pattern == "hex" || pattern == "random"; 317 | 318 | for (var y = 0; y < rows; y++) { 319 | for (var x = 0; x < cols; x++) { 320 | let staggerRow = stagger && y%2; 321 | if (staggerRow && x == cols - 1) continue; 322 | let emojiIndex = (x + y) % emojis.length 323 | let staggerX = staggerRow ? 0.5 : 0; 324 | let emoji = emojis[emojiIndex]; 325 | 326 | if (order == 'random') { 327 | emoji = randomEmoji() 328 | } 329 | 330 | let randomScale = pattern == "random" ? 0.5 : 0.00; 331 | let rx = (random() - 0.5) * randomScale; 332 | let ry = (random() - 0.5) * randomScale; 333 | 334 | ctx.globalAlpha = 0.95; 335 | if (options.texture == 'monochrome') 336 | ctx.globalCompositeOperation = "luminosity"; 337 | ctx.shadowColor = 'rgba(0,0,0,0.05)'; 338 | ctx.shadowOffsetY = size / 8; 339 | ctx.shadowBlur = size / 8; 340 | 341 | ctx.save() 342 | ctx.translate(marginX + (spacingX + size) * (x + rx + staggerX), 343 | marginY + (spacingY + size) * (y + ry)); 344 | 345 | let flip = false; 346 | if (order == 'alternating') { 347 | if (pattern == 'random') { 348 | flip = (random() < 0.5) 349 | } else { 350 | flip = y%2; 351 | } 352 | } 353 | 354 | if (flip) { 355 | ctx.scale(-1, 1); 356 | } 357 | 358 | if (pattern == 'random') { 359 | ctx.rotate((random() - 0.5) * Math.PI/5) 360 | } 361 | 362 | if (debug) { 363 | ctx.strokeRect(-size/2, -size/2, size, size); 364 | ctx.strokeRect(-2, -2, 4, 4); 365 | ctx.globalAlpha = 0.2 366 | ctx.strokeRect(size/2, size/2, spacingX, spacingY); 367 | } 368 | ctx.fillText(emoji, 0, + size/3); 369 | ctx.restore(); 370 | } 371 | } 372 | } 373 | 374 | // Warning: computationally expensive. 375 | const foamLayout = (emojis, options = {}) => { 376 | var r; 377 | var maxSize = size * 2.5; 378 | var bubbs = []; 379 | var spacing = parseFloat(options.spacing)/100 || 0.0; 380 | console.log(spacing); 381 | for (var j = 0; j < 10000; j++) { 382 | let emoji = emojis[j % emojis.length]; 383 | var x = random() * canvas.width; 384 | var y = random() * canvas.height; 385 | r = Math.min(x, canvas.width - x, y, canvas.height - y); 386 | // shrink radius by other extant bubble radii 387 | for (var i = 0; i < bubbs.length; i++) { 388 | r = Math.min(r, Math.hypot(x - bubbs[i].x, y - bubbs[i].y) - bubbs[i].r - size * spacing / 4); 389 | if (r < 5) break; 390 | } 391 | if (r < 5) { 392 | //if (debug) ctx.fillText("X", x, y); 393 | continue; 394 | } 395 | r = Math.min(maxSize, r); 396 | 397 | // we've got a good one 398 | bubbs.push({x:x, y:y, r:r, emoji:emoji}); 399 | } 400 | 401 | bubbs.reverse(); 402 | 403 | for (var i = 0; i < bubbs.length; i++) { 404 | var b = bubbs[i]; 405 | ctx.save() 406 | ctx.translate(b.x, b.y); 407 | if (debug) { 408 | ctx.beginPath(); 409 | ctx.arc(0, 0, b.r, 0, Math.PI*2, true); 410 | ctx.stroke(); 411 | ctx.strokeRect(-2, -2, 4, 4); 412 | ctx.globalAlpha = 0.2 413 | } 414 | 415 | ctx.font = `${1.75*b.r}px ${font}`; 416 | 417 | if (order == 'alternating' || order == 'random') { 418 | ctx.scale(1 - 2*(i%2), 1); 419 | } 420 | if (order == 'random') { 421 | ctx.rotate((random() - 0.5) * Math.PI/5) 422 | } 423 | 424 | if (options.pattern == 'many') { 425 | let scale = random() 426 | ctx.scale(1.5 + scale, 1.5 + scale); 427 | ctx.globalAlpha = 1.0; 428 | } 429 | 430 | ctx.fillText(b.emoji, 0, 0.6*b.r); 431 | ctx.restore(); 432 | } 433 | //console.log(bubbs); 434 | } 435 | 436 | // Conceptually splits the canvas into a grid and lays out 2x2, 3x3, 4x4 emojis randomly 437 | // and fills any empty spaces with 1x1 emojis. 438 | const packingLayout = (emojis, options = {}) => { 439 | let cols = Math.ceil(width / size); 440 | let rows = Math.ceil(height / size); 441 | 442 | const sizes = [2, 3, 4]; 443 | 444 | const bins = Array.from(Array(cols).keys()).map(i => { 445 | return new Array(rows).fill(false); 446 | }); 447 | 448 | const randomSize = () => { 449 | return sizes[Math.floor(random() * sizes.length)] 450 | } 451 | const checkFittable = (bins, size, x, y) => { 452 | for (let ix = x; ix < x + size; ix++) { 453 | for (let iy = y; iy < y + size; iy++) { 454 | if (ix >= bins.length) { 455 | return false; 456 | } 457 | if (iy >= bins[ix].length) { 458 | return false; 459 | } 460 | if (bins[ix][iy] == true) { 461 | return false 462 | } 463 | } 464 | } 465 | return true 466 | } 467 | 468 | const fillBin = (bins, size, x, y) => { 469 | for (let ix = x; ix < x + size; ix++) { 470 | for (let iy = y; iy < y + size; iy++) { 471 | bins[ix][iy] = true; 472 | } 473 | } 474 | } 475 | 476 | 477 | let baseFontSize = (parseFloat(options.fontSize) || size); 478 | 479 | ctx.textBaseline = "bottom"; 480 | ctx.textAlign = "left"; 481 | ctx.stroke = 'black' 482 | 483 | for (let x = 0; x < bins.length; x++) { 484 | for (let y = 0; y < bins[x].length; y++) { 485 | let occupied = bins[x][y] 486 | if (occupied) { continue; } 487 | 488 | let binSize = randomSize() 489 | let emoji = randomEmoji() 490 | 491 | // Try using the bin size that was randomly selected, if it doesn't fit 492 | // reduce size by one and try again. 493 | while (binSize > 0) { 494 | if (checkFittable(bins, binSize, x, y)) { 495 | fillBin(bins, binSize, x, y) 496 | const thisFontSize = baseFontSize * binSize; 497 | ctx.font = `${thisFontSize}px ${font}`; 498 | ctx.fillText(emoji, marginX + (x - 0.5) * size, marginY + (y + binSize) * size); 499 | //ctx.strokeRect(x * size, y * size, binSize * size, binSize * size) 500 | break 501 | } 502 | binSize -= 1 503 | } 504 | } 505 | } 506 | } 507 | 508 | // r = c√n
θ = i × 137.5° 509 | 510 | const spiralLayout = (emojis, options = {}, varyScale = false) => { 511 | 512 | var scale = size * 1.1, 513 | α = Math.PI * (3 - Math.sqrt(5)); 514 | 515 | var maxRadius = Math.hypot(canvas.width/2, canvas.height/2) 516 | 517 | var spacing = parseFloat(options.spacing)/100 || 1.0; 518 | 519 | for (var i = 0; i < 100000; i++) { 520 | let emojiIndex = (i) % emojis.length 521 | let emoji = emojis[emojiIndex]; 522 | if (order == 'random') { 523 | emoji = randomEmoji(); 524 | } 525 | 526 | var r = Math.sqrt(i); 527 | if (varyScale) r = Math.pow(r/10, 2)*6; 528 | 529 | var a = i * α - elapsed/100000; 530 | 531 | if (scale*r*spacing > maxRadius) { 532 | break; 533 | } 534 | 535 | let randomScale = pattern == "random" ? 1.0 : 0.00; 536 | let rx = (random() - 0.5) * randomScale; 537 | let ry = (random() - 0.5) * randomScale; 538 | 539 | var x = marginX + width / 2 + scale * r * Math.cos(a) * spacing + size * rx; 540 | var y = marginY + height / 2 + scale * r * Math.sin(a) * spacing + size * ry; 541 | 542 | if (x < -size || x > (width + marginX*2 + size)) continue; 543 | if (y < -size || y > (height + marginY*2 + size)) continue; 544 | 545 | ctx.save() 546 | if (i == 0) x+= size/2;//console.log(scale * r * Math.cos(a), scale * r * Math.sin(a)) 547 | ctx.translate(x, y); 548 | let flip = false; 549 | if (order == 'alternating') { 550 | if (pattern == 'random') { 551 | flip = (random() < 0.5) 552 | } else { 553 | flip = i%2; 554 | } 555 | } 556 | 557 | if (flip) { 558 | ctx.scale(-1, 1); 559 | } 560 | 561 | if (varyScale) { 562 | let rscale = Math.sqrt(r)/2; 563 | ctx.scale(rscale, rscale) 564 | ctx.globalAlpha = (r) / 10; 565 | ctx.rotate(a - Math.PI/2) 566 | } 567 | if (debug) { 568 | ctx.strokeRect(-size/2, -size/2, size, size); 569 | ctx.strokeRect(-2, -2, 4, 4); 570 | ctx.globalAlpha = 0.2 571 | } 572 | 573 | // ctx.fillText(emoji, 0, size * 0.375); 574 | 575 | ctx.drawImage(emojiCanvas[emojiIndex], -size, -size, size*2, size*2); 576 | 577 | ctx.restore(); 578 | } 579 | } 580 | 581 | let emojiString = document.querySelector('#emojiPicker').value || " "; 582 | let emojisRaw = runes(emojiString) 583 | let emojis = [] 584 | 585 | let skinTones = ['\u{1F3FB}', '\u{1F3FC}', '\u{1F3FD}', '\u{1F3FE}', '\u{1F3FF}'] 586 | for (var i = 0; i < emojisRaw.length; i++) { 587 | var emoji = emojisRaw[i]; 588 | var nextEmoji = emojisRaw[i+1]; 589 | if (skinTones.includes(nextEmoji)) { 590 | emoji = emoji + nextEmoji; 591 | i++; 592 | } 593 | emojis.push(emoji); 594 | } 595 | 596 | let iconSize = 160; 597 | emojiCanvas = emojis.map(emoji => { 598 | var imgcanvas = createCanvas(iconSize*2, iconSize*2); 599 | var imgctx = imgcanvas.getContext('2d'); 600 | imgctx.clearRect(0, 0, iconSize, iconSize); 601 | imgctx.font = `${iconSize}px ${font}`; 602 | imgctx.textAlign = 'center' 603 | imgctx.fillText(emoji, imgcanvas.width/2, imgcanvas.height/2 + .375 * iconSize); 604 | return imgcanvas; 605 | }) 606 | ctx.fillStyle = textColor; 607 | 608 | switch (pattern) { 609 | case 'hex': 610 | case 'diamond': 611 | case 'grid': 612 | case 'random': 613 | { 614 | gridLayout(emojis, options); 615 | break; 616 | } 617 | 618 | case 'spiral': 619 | { 620 | spiralLayout(emojis, options); 621 | break; 622 | } 623 | case 'scalespiral': 624 | { 625 | let t1 = performance.now(); 626 | spiralLayout(emojis, options, true); 627 | break; 628 | } 629 | case 'foam': 630 | case 'many': 631 | { 632 | foamLayout(emojis, options); 633 | break; 634 | } 635 | case 'stacks': { 636 | packingLayout(emojis, options); 637 | break; 638 | } 639 | 640 | 641 | 642 | default: { 643 | gridLayout(emojis, options) 644 | } 645 | } 646 | 647 | 648 | 649 | 650 | // Generate Noise 651 | // var dt = ctx.getImageData(0,0, c.width, c.height); 652 | // var dd = dt.data, dl = dt.width * dt.height; 653 | // var p = 0, i = 0; 654 | // var intensity = 4; 655 | // for (; i < dl; ++i) { 656 | // // var rand = Math.floor(random() * 2) - 1; 657 | // dd[p++] += Math.round((random() - 0.5) * intensity); 658 | // dd[p++] += Math.round((random() - 0.5) * intensity); 659 | // dd[p++] += Math.round((random() - 0.5) * intensity); 660 | // dd[p++] += 0 //255; 661 | // } 662 | // ctx.putImageData(dt, 0, 0); 663 | vingette = options.texture != 'flat'; 664 | if (vingette) { 665 | ctx.globalCompositeOperation = "hard-light"; 666 | grd = ctx.createRadialGradient( 667 | c.width / 2, c.height/4, 668 | c.width/4, 669 | c.width/2 , c.height/2 , 670 | Math.hypot(c.width/2, c.height/2)*1.1); 671 | // grd.addColorStop(0,darkColor); 672 | ctx.globalAlpha = 0.05 673 | grd.addColorStop(0.0,"rgba(0,0,0,0.0"); 674 | grd.addColorStop(0.9,"rgba(0,0,0,0.8"); 675 | grd.addColorStop(0.95,"rgba(0,0,0,0.9"); 676 | grd.addColorStop(1.0,"rgba(0,0,0,1.0"); 677 | ctx.fillStyle = grd; 678 | ctx.fillRect(0,0, c.width, c.height); 679 | } 680 | 681 | ctx.restore() 682 | 683 | if (fullbleed || time) return; 684 | 685 | c.toBlob(function(blob) { 686 | var date = new Date() 687 | window.URL.revokeObjectURL(blobURL); 688 | blobURL = window.URL.createObjectURL(blob); 689 | a.href = blobURL; 690 | }); 691 | } 692 | 693 | var blobURL = undefined 694 | 695 | function changeListeners() { 696 | updateForm(); 697 | let form = document.getElementById("form"); 698 | form.onchange = render; 699 | 700 | if (size = localStorage.getItem("size")) { 701 | form.elements["sizePicker"].value = size; 702 | } 703 | 704 | document.querySelectorAll('.swatch').forEach(e => { 705 | e.style.backgroundColor = e.value; 706 | e.addEventListener('click', e => { 707 | setColor(e.target); 708 | }) 709 | }); 710 | 711 | var emojiPicker = document.querySelector('#emojiPicker') 712 | emojiPicker.addEventListener('input', e => { 713 | render(); 714 | }) 715 | 716 | } 717 | 718 | var shouldAnimate = false; 719 | function startAnimation() { 720 | console.log("animate") 721 | shouldAnimate = !shouldAnimate; 722 | window.requestAnimationFrame(renderFrame); 723 | } 724 | 725 | function setFullbleed() { 726 | fullbleed = true; 727 | document.body.classList.add('fullscreen'); 728 | window.onresize = renderAfterDelay; 729 | } 730 | 731 | let renderTimeout; 732 | 733 | function renderAfterDelay() { 734 | clearTimeout(renderTimeout); 735 | renderTimeout = setTimeout(render, 100); 736 | } 737 | 738 | if (inIframe()) { 739 | setFullbleed(); 740 | } 741 | 742 | 743 | changeListeners() 744 | render(); 745 | document.body.classList.remove('loading'); 746 | 747 | if (window.obsstudio) { 748 | document.body.classList.add('fullscreen') 749 | startAnimation() 750 | } 751 | 752 | document.fonts.ready.then(function () { 753 | console.log("fonts loaded") 754 | }); 755 | 756 | function toggleAdvanced() { 757 | document.body.classList.toggle("advanced") 758 | } 759 | 760 | function inIframe() { 761 | try { 762 | return window.self !== window.top; 763 | } catch (e) { 764 | return true; 765 | } 766 | } 767 | 768 | 769 | 770 | // 771 | // Color Functions 772 | // 773 | 774 | function hexToRGB(hex) { 775 | var c = hex.substring(1); // strip # 776 | var rgb = parseInt(c, 16); // convert rrggbb to decimal 777 | var r = (rgb >> 16) & 0xff; // extract red 778 | var g = (rgb >> 8) & 0xff; // extract green 779 | var b = (rgb >> 0) & 0xff; // extract blue 780 | return [r,g,b]; 781 | } 782 | 783 | function luminance(rgb) { 784 | var luma = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; 785 | var luma2 = rgb2lab(rgb); 786 | return luma; 787 | } 788 | 789 | function contrastColor(hex, shift) { 790 | var rgb = hexToRGB(hex); 791 | var lab = rgb2lab(rgb); 792 | 793 | if (shift) { 794 | lab[0] += shift; 795 | } else if (lab[0]< 40) { 796 | lab[0] += 25; 797 | } else { 798 | lab[0] -= 25; 799 | } 800 | rgb = lab2rgb(lab); 801 | var hsl = rgbToHsl(rgb); 802 | rgb[0] = Math.round(rgb[0]); 803 | rgb[1] = Math.round(rgb[1]); 804 | rgb[2] = Math.round(rgb[2]); 805 | 806 | hsl[0] = Math.round(hsl[0] * 360); 807 | hsl[1] = Math.round(hsl[1] * 100); 808 | hsl[2] = Math.round(hsl[2] * 100); 809 | return "hsl(" + hsl[0] + ", " + hsl[1] + "%, " + hsl[2] + "%)" 810 | } 811 | 812 | function lab2rgb(lab){ 813 | var y = (lab[0] + 16) / 116, 814 | x = lab[1] / 500 + y, 815 | z = y - lab[2] / 200, 816 | r, g, b; 817 | 818 | x = 0.95047 * ((x * x * x > 0.008856) ? x * x * x : (x - 16/116) / 7.787); 819 | y = 1.00000 * ((y * y * y > 0.008856) ? y * y * y : (y - 16/116) / 7.787); 820 | z = 1.08883 * ((z * z * z > 0.008856) ? z * z * z : (z - 16/116) / 7.787); 821 | 822 | r = x * 3.2406 + y * -1.5372 + z * -0.4986; 823 | g = x * -0.9689 + y * 1.8758 + z * 0.0415; 824 | b = x * 0.0557 + y * -0.2040 + z * 1.0570; 825 | 826 | r = (r > 0.0031308) ? (1.055 * Math.pow(r, 1/2.4) - 0.055) : 12.92 * r; 827 | g = (g > 0.0031308) ? (1.055 * Math.pow(g, 1/2.4) - 0.055) : 12.92 * g; 828 | b = (b > 0.0031308) ? (1.055 * Math.pow(b, 1/2.4) - 0.055) : 12.92 * b; 829 | 830 | return [Math.max(0, Math.min(1, r)) * 255, 831 | Math.max(0, Math.min(1, g)) * 255, 832 | Math.max(0, Math.min(1, b)) * 255] 833 | } 834 | 835 | function rgb2lab(rgb){ 836 | var r = rgb[0] / 255, 837 | g = rgb[1] / 255, 838 | b = rgb[2] / 255, 839 | x, y, z; 840 | 841 | r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; 842 | g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; 843 | b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; 844 | 845 | x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047; 846 | y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000; 847 | z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883; 848 | 849 | x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116; 850 | y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116; 851 | z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116; 852 | 853 | return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)] 854 | } 855 | 856 | function rgbToHsl(rgb) { 857 | var r = rgb[0], g = rgb[1], b = rgb[2]; 858 | r /= 255, g /= 255, b /= 255; 859 | 860 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 861 | var h, s, l = (max + min) / 2; 862 | 863 | if (max == min) { 864 | h = s = 0; // achromatic 865 | } else { 866 | var d = max - min; 867 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 868 | 869 | switch (max) { 870 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 871 | case g: h = (b - r) / d + 2; break; 872 | case b: h = (r - g) / d + 4; break; 873 | } 874 | 875 | h /= 6; 876 | } 877 | 878 | return [ h, s, l ]; 879 | } -------------------------------------------------------------------------------- /docs/wallpaper/style.css: -------------------------------------------------------------------------------- 1 | 2 | canvas { 3 | background:gray; 4 | max-width:100%; 5 | max-height:100%; 6 | border-radius:4px; 7 | font-size:16px; 8 | margin-top:1.5em; 9 | } 10 | 11 | form { 12 | line-height:2.5em; 13 | padding:1em;margin: -1em; 14 | margin-bottom:1em; 15 | } 16 | 17 | body { 18 | margin:0 auto; 19 | padding:6vmin 6vmin; 20 | max-width:35em; 21 | word-wrap: break-word; 22 | font-size:16px; 23 | font-weight:300; 24 | line-height:160%; 25 | margin:0; 26 | max-width:inherit; 27 | } 28 | 29 | #title { 30 | font-weight: 400; 31 | font-size:150%; 32 | width:80%; 33 | max-width:500px; 34 | margin-bottom:1em; 35 | } 36 | 37 | body, input, select, label { 38 | font-weight:300; 39 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 40 | 41 | } 42 | 43 | .swatch { 44 | width: 1em; height:1em; 45 | border-radius:100%; 46 | margin-top:10px; 47 | display:inline-block; 48 | margin-right:0.2em; 49 | box-shadow:inset 0 0 0 1px rgba(0,0,0,0.2); 50 | } 51 | 52 | input, select { 53 | -webkit-appearance: none; 54 | background:white; 55 | border-radius:0; 56 | border:none; 57 | min-width:5em; 58 | padding-top:0; 59 | background:transparent; 60 | border-bottom:1px solid rgba(0,0,0,0.3); 61 | padding:0; 62 | margin:0; 63 | } 64 | 65 | input, select, label { 66 | height:1.5em; 67 | line-height:1.5em; 68 | font-size:16px; 69 | display:inline-block; 70 | 71 | } 72 | 73 | input[type="color"] { 74 | 75 | -webkit-appearance: none; 76 | background:transparent; 77 | border:0; 78 | padding:0; 79 | } 80 | input[type=color]::-webkit-color-swatch { 81 | border: none; 82 | padding: 0; 83 | -webkit-appearance: none; 84 | 85 | } 86 | 87 | input[type=color]::-webkit-color-swatch-wrapper { 88 | border: 1px solid black;; 89 | padding: 0; 90 | margin:0; 91 | height:1em; 92 | xwidth:5em; 93 | -webkit-appearance: none; 94 | 95 | } 96 | 97 | 98 | input#emojiPickker { 99 | font-family: monospace; 100 | } 101 | 102 | input#rows, input#cols { 103 | width: 2em; 104 | min-width: 2em; 105 | } 106 | 107 | .button { 108 | background-color: rgba(255,255,255,0.1); 109 | color: black; 110 | border-style: none; 111 | border-radius:1em; 112 | border: 1px solid rgba(0,0,0,0.3); 113 | padding: 0px 1em; 114 | min-width:5em; 115 | display:inline-block; 116 | text-align:center; 117 | text-transform:uppercase; 118 | font-weight:300; 119 | margin-right:0.5em; 120 | text-decoration:none; 121 | cursor:pointer; 122 | } 123 | 124 | .button:hover { 125 | background-color: rgba(0,0,0,0.9); 126 | color:white; 127 | background-color: rgba(0,0,0,0.9); 128 | color:white; 129 | box-shadow: 0px 0px 0.25em #0066ff; 130 | } 131 | 132 | .button:focus{ 133 | outline: none; 134 | box-shadow: 0px 0px 2px #0066ff; 135 | border-radius:1em; 136 | } 137 | 138 | a { 139 | text-decoration:none; 140 | } 141 | 142 | #advanced { 143 | font-style: oblique; 144 | opacity:0.6; 145 | } 146 | #advanced-toggle { 147 | position:fixed; 148 | bottom:0; 149 | right:0; 150 | width:2em; 151 | height:2em; 152 | line-height:2em; 153 | text-align:center; 154 | cursor:pointer; 155 | } 156 | 157 | body.advanced #advanced-toggle::before { 158 | content: '🥧' 159 | } 160 | 161 | #advanced-toggle::before { 162 | content: '🥧' 163 | } 164 | 165 | #advanced-toggle:not(:hover) { 166 | opacity:0.9; 167 | } 168 | body:not(.advanced) #advanced { 169 | display:none; 170 | } 171 | 172 | body.fullscreen { 173 | background-color: transparent; 174 | } 175 | body.fullscreen form { 176 | background:white; 177 | } 178 | body.fullscreen canvas{ 179 | position:absolute; 180 | top:0; 181 | left:0; 182 | bottom:0; 183 | right:0; 184 | z-index:-1; 185 | margin-top:0; 186 | border-radius:0; 187 | } 188 | 189 | body.fullscreen form, 190 | body.fullscreen .button, 191 | body.loading form, 192 | body.loading .button, 193 | body.loading canvas { 194 | display:none; 195 | } 196 | 197 | 198 | 199 | @media (prefers-color-scheme: dark) { 200 | body { background: #333; color: white; } 201 | input, select, a, button, .button {color:white;} 202 | input, select { 203 | border-bottom: 1px solid rgba(255,255,255,0.3); 204 | } 205 | } -------------------------------------------------------------------------------- /figma/kitchen/.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | *.log 3 | *.log.* 4 | node_modules 5 | 6 | out/ 7 | dist/ 8 | code.js 9 | -------------------------------------------------------------------------------- /figma/kitchen/Cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alcor/emoji-supply/c84b4dcc98af4acc6a77504050ff9d76c1bccea8/figma/kitchen/Cover.png -------------------------------------------------------------------------------- /figma/kitchen/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alcor/emoji-supply/c84b4dcc98af4acc6a77504050ff9d76c1bccea8/figma/kitchen/Icon.png -------------------------------------------------------------------------------- /figma/kitchen/README.md: -------------------------------------------------------------------------------- 1 | # Figma Emoji Kitchen Browser 2 | 3 | Figma Plugin that accesses https://emoji.supply/kitchen/ 4 | -------------------------------------------------------------------------------- /figma/kitchen/code.ts: -------------------------------------------------------------------------------- 1 | let url = "https://emoji.supply/kitchen"; 2 | url = "https://git.emoji.supply/kitchen/" 3 | // url = "http://localhost:8888/kitchen"; 4 | 5 | console.log("url", url); 6 | figma.showUI( 7 | ``, 8 | { themeColors: true, width: 360, height: 640 } 9 | ); 10 | 11 | figma.ui.onmessage = (message) => { 12 | console.log("got this from the UI", message) 13 | if (message.clickedImage) { 14 | dropIcon(message.clickedImage); 15 | } 16 | } 17 | 18 | let supress = false; 19 | figma.on('drop', (event: DropEvent) => { 20 | if (supress) { 21 | console.log("supressing duplicate drop") 22 | return; 23 | } 24 | 25 | const { items } = event; 26 | let fromBrowser = event.dropMetadata?.fromBrowser; 27 | console.log("Plugin got drop", event); 28 | 29 | let url = items.filter(item => item.type === 'text/uri-list')?.pop()?.data; 30 | 31 | if (url) { 32 | if (fromBrowser) { 33 | let position = figma.viewport.center; 34 | let userposition = figma.activeUsers[0]?.position; 35 | 36 | const { dropPosition, windowSize, offset, itemSize } = event.dropMetadata; 37 | 38 | console.log("client.xy", [dropPosition.x, dropPosition.y]); 39 | console.log("event.xy", [event.x, event.y]); 40 | console.log("event.absolute.xy", [event.absoluteX, event.absoluteY]); 41 | console.log("user.xy", [userposition?.x, userposition?.y]); 42 | console.log("offset.xy", [offset.x, offset.y]); 43 | 44 | const bounds = figma.viewport.bounds; 45 | const zoom = figma.viewport.zoom; 46 | const hasUI = Math.round(bounds.width * zoom) !== windowSize.width; 47 | const leftPaneWidth = windowSize.width - bounds.width * zoom - 240; 48 | console.log({hasUI, leftPaneWidth, zoom, bounds}) 49 | 50 | const xFromCanvas = hasUI ? dropPosition.x - leftPaneWidth : dropPosition.x; 51 | const yFromCanvas = hasUI ? dropPosition.y - 40 : dropPosition.y; 52 | console.log("canvas.xy", [xFromCanvas, yFromCanvas]); 53 | 54 | figma.notify("Adding emoji. Use the Figma app for more precise placement.") 55 | 56 | dropIcon(url); 57 | 58 | } else { 59 | dropIcon(url, event.absoluteX, event.absoluteY); 60 | } 61 | supress = true; 62 | setTimeout(() => { supress = false; }, 100); 63 | } 64 | }) 65 | 66 | const dropIcon = (url: string, x?: number, y?: number, size?: number) => { 67 | if (!size) { 68 | size = 64; 69 | size = Math.round(size / figma.viewport.zoom); 70 | } 71 | 72 | figma.createImageAsync(url).then(async (image: Image) => { 73 | const node = figma.createRectangle() 74 | node.resize(size, size) 75 | node.fills = [{ 76 | type: 'IMAGE', 77 | imageHash: image.hash, 78 | scaleMode: 'FILL' 79 | }] 80 | 81 | let position = figma.activeUsers[0]?.position; 82 | let viewportCenter = figma.viewport.center; 83 | node.name = "Emoji"; 84 | node.x = Math.round((x || position.x || viewportCenter.x) - size / 2); 85 | node.y = Math.round((y || position.y || viewportCenter.y) - size / 2); 86 | figma.currentPage.selection = [node]; 87 | 88 | }) 89 | } -------------------------------------------------------------------------------- /figma/kitchen/manifest-figma.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "emoji.supply", 3 | "name": "Emoji Kitchen Browser", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "id": "1270415471355558991", 7 | "editorType": ["figma", "figjam"], 8 | "permissions": ["activeusers"] 9 | } 10 | -------------------------------------------------------------------------------- /figma/kitchen/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Emoji Kitchen Browser", 3 | "id": "1269531050651505496", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "permissions": ["activeusers"] 8 | } 9 | -------------------------------------------------------------------------------- /figma/kitchen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Figma-Emoji-Kitchen-Browser", 3 | "version": "1.0.0", 4 | "description": "Browse the combinations from Emoji Kitchen in Figma.", 5 | "main": "code.js", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.json", 8 | "watch": "npm run build -- --watch" 9 | }, 10 | "author": "", 11 | "license": "", 12 | "devDependencies": { 13 | "@figma/plugin-typings": "^1.72.0", 14 | "typescript": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /figma/kitchen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "strict": true, 6 | "typeRoots": [ 7 | "./node_modules/@types", 8 | "./node_modules/@figma" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # [[edge_functions]] 2 | # path = "/kitchen/" 3 | # function = "metadata" 4 | 5 | [[redirects]] 6 | from = "/mix*" 7 | to = "/kitchen/" 8 | status = 301 9 | 10 | [[redirects]] 11 | from = "/emojikitchen/*" 12 | to = "https://www.gstatic.com/android/keyboard/emojikitchen/:splat" 13 | status = 200 14 | 15 | [[headers]] 16 | for = "/*" 17 | [headers.values] 18 | Content-Security-Policy = "frame-ancestors 'self' https://figma.com https://staging.figma.com" 19 | -------------------------------------------------------------------------------- /netlify/edge-functions/cache.js: -------------------------------------------------------------------------------- 1 | 2 | export default async (request, context) => { 3 | const ua = request.headers.get("user-agent"); 4 | let url = new URL(request.url); 5 | let query = url.search.substring(1); 6 | if (ua?.indexOf("Twitterbot") == -1) { 7 | return Response.redirect(query); 8 | } 9 | let res = await fetch(query) 10 | return new Response(res.body, { headers:res.headers }); 11 | } 12 | -------------------------------------------------------------------------------- /netlify/edge-functions/metadata.js: -------------------------------------------------------------------------------- 1 | function decodePrettyComponent(s) { 2 | let replacements = {'---': ' - ', '--': '-','-' : ' '} 3 | return decodeURIComponent(s.replace(/-+/g, e => replacements[e] ?? '-')) 4 | } 5 | 6 | const emojiUrl = (codePoint) => { 7 | let cp = codePoint.split("-").filter(x => x !== "fe0f").map(s => s.padStart(4,"0")).join("_"); 8 | return `https://raw.githubusercontent.com/googlefonts/noto-emoji/main/png/512/emoji_u${cp}.png` 9 | } 10 | 11 | const mixmojiUrl = (r, c, proxy, url) => { 12 | let padZeros = r < 20220500; // Revisions before 0522 had preceding zeros 13 | c[0] = c[0].split(/-/g).map(s => padZeros ? s.padStart(4,"0") : s).join("-u"); 14 | c[1] = c[1].split(/-/g).map(s => padZeros ? s.padStart(4,"0") : s).join("-u"); 15 | return `${proxy ? url.origin: 'https://www.gstatic.com/android/keyboard/'}/emojikitchen/${r}/u${c[0]}/u${c[0]}_u${c[1]}.png` 16 | } 17 | 18 | export default async (request, context) => { 19 | let url = new URL(request.url); 20 | try { 21 | if (!url.pathname.endsWith("/") || url.search.indexOf("&") != -1 ) { 22 | return; 23 | } 24 | 25 | const ua = request.headers.get("user-agent"); 26 | if (!ua) return; 27 | let metadataBots = [ "Twitterbot", "curl", "facebookexternalhit", "Slackbot-LinkExpanding", "Discordbot", "snapchat"] 28 | let isMetadataBot = metadataBots.some(bot => ua.indexOf(bot) != -1); 29 | if (!isMetadataBot) return; 30 | 31 | let search = url.search.substring(1); 32 | if (!search.length) return; 33 | let date; 34 | [search, date] = search.split("="); 35 | let components = search.split("+"); 36 | 37 | let chars = undefined 38 | components.map(c => Array.from(decodeURIComponent(c))); 39 | components = chars.map(c => c.map(a=>a.codePointAt(0).toString(16)).join("-")); 40 | 41 | let info = { 42 | s: "Emoji Kitchen Browser" 43 | } 44 | 45 | let isTwitter = ua.indexOf("Twitterbot") != -1 && ua.indexOf("facebookexternalhit") == -1; 46 | 47 | if (components.length > 1 && date) { 48 | date = 20200000 + parseInt(date,36); 49 | info.i = mixmojiUrl(date, components, isTwitter, url); 50 | } else { 51 | info.i = emojiUrl(components[0]); 52 | } 53 | info.title = chars.join(" + ").replace(",", "");// + " - " + info.s; 54 | 55 | console.log(chars.join("+")) 56 | 57 | let content = ['']; 58 | if (info.title) { content.push(`${info.title}`,``); } 59 | if (info.s) { content.push(``); } 60 | if (info.t) { content.push(``); } 61 | if (info.d) { content.push(``,``); } 62 | if (info.i) { 63 | if (!info.i.startsWith("http")) info.i = atob(info.i.replace(/=/g,'')); 64 | content.push(``); 65 | if (ua?.indexOf("Twitterbot") != -1) { 66 | content.push(``); 67 | } else { 68 | content.push(``); 69 | } 70 | if (info.iw) content.push(``); 71 | if (info.ih) content.push(``); 72 | } 73 | if (info.c) { content.push(``); } 74 | if (info.v) { 75 | if (!info.v.startsWith("http")) info.v = atob(info.v.replace(/=/g,'')); 76 | content.push(``); 77 | if (info.vw) content.push(``); 78 | if (info.vh) content.push(``); 79 | } 80 | if (info.f) { 81 | if (info.f.length > 9){ 82 | if (!info.f.startsWith("http")) info.f = atob(info.f.replace(/=/g,'')); 83 | content.push(``); 84 | } else { 85 | let codepoints = Array.from(info.f).map(c => c.codePointAt(0).toString(16)); 86 | content.push(``); 87 | } 88 | } 89 | return new Response(content.join("\n"), { 90 | headers: { "content-type": "text/html" }, 91 | }); 92 | } catch (e) {console.log(e, url)} 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "netlify-cli": "^11.5.1" 4 | }, 5 | "dependencies": { 6 | "netlify": "^13.1.11" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tools/fetchemoji.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | 3 | from urllib.parse import quote 4 | import json 5 | 6 | import requests 7 | 8 | api_url_format = "https://tenor.googleapis.com/v2/featured?key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ&contentfilter=high&media_filter=png_transparent&component=proactive&collection=emoji_kitchen_v6&q={}_{}" 9 | 10 | with open("fetchemoji.txt") as f: 11 | emojis = set(f.read().strip().splitlines()) 12 | emojis2 = emojis.copy() 13 | 14 | urls = set() 15 | multichar_ord = lambda s: '-'.join(map(lambda c: f"{ord(c):x}", (s))) 16 | 17 | def check(emoji1, emoji2): 18 | api_url = api_url_format.format(quote(emoji1), quote(emoji2)) 19 | data = requests.get(api_url).json() 20 | results = data["results"] 21 | if not results: 22 | print(f" {multichar_ord(emoji1)}_{multichar_ord(emoji2)} ({emoji1} + {emoji2})") 23 | return None 24 | print(f"✅ {multichar_ord(emoji1)}_{multichar_ord(emoji2)} ({emoji1} + {emoji2})") 25 | 26 | url = results[0]["media_formats"]["png_transparent"]["url"] 27 | fp = url.split('/')[-1] 28 | if fp in urls: 29 | print(" + duplicate") 30 | return url 31 | 32 | for emoji1 in emojis: 33 | for emoji2 in emojis2: 34 | try: 35 | url = check(emoji1, emoji2) 36 | if url: 37 | urls.add(url) 38 | except BaseException: 39 | url = check(emoji1, emoji2) 40 | if url: 41 | urls.add(url) 42 | 43 | emojis2.remove(emoji1) #remove the last checked emoji to avoid duplicates 44 | 45 | with open("fetchemoji.json", "w") as f: 46 | json.dump(list(urls), f, indent=2) 47 | -------------------------------------------------------------------------------- /tools/fetchemoji.txt: -------------------------------------------------------------------------------- 1 | ❤️ 2 | 🧡 3 | 💛 4 | 💚 5 | 🩵 6 | 💙 7 | 💜 8 | 🩷 9 | 🤎 10 | 🩶 11 | 🖤 12 | 🤍 13 | 🪄 14 | 😀 15 | 😃 16 | 😄 17 | 😁 18 | 😆 19 | 😅 20 | 🤣 21 | 😂 22 | 🙂 23 | 🙃 24 | 🫠 25 | 😉 26 | 😊 27 | 😇 28 | 🥰 29 | 😍 30 | 🤩 31 | 😘 32 | 😗 33 | ☺️ 34 | 😚 35 | 😙 36 | 🥲 37 | 😋 38 | 😛 39 | 😜 40 | 🤪 41 | 😝 42 | 🤑 43 | 🤗 44 | 🤭 45 | 🫢 46 | 🫣 47 | 🤫 48 | 🤔 49 | 🫡 50 | 🤐 51 | 🤨 52 | 😐 53 | 😑 54 | 😶 55 | 🫥 56 | 😶‍🌫️ 57 | 😏 58 | 😒 59 | 🙄 60 | 😬 61 | 😮‍💨 62 | 🤥 63 | 😌 64 | 😔 65 | 😪 66 | 🤤 67 | 😴 68 | 😷 69 | 🤒 70 | 🤕 71 | 🤢 72 | 🤮 73 | 🤧 74 | 🥵 75 | 🥶 76 | 🥴 77 | 😵 78 | 🤯 79 | 🤠 80 | 🥳 81 | 🥸 82 | 😎 83 | 🤓 84 | 🧐 85 | 😕 86 | 🫤 87 | 😟 88 | 🙁 89 | ☹️ 90 | 😮 91 | 😯 92 | 😲 93 | 😳 94 | 🥺 95 | 🥹 96 | 😦 97 | 😧 98 | 😨 99 | 😰 100 | 😥 101 | 😢 102 | 😭 103 | 😱 104 | 😖 105 | 😣 106 | 😞 107 | 😓 108 | 😩 109 | 😫 110 | 🥱 111 | 😤 112 | 😡 113 | 😠 114 | 🤬 115 | 😈 116 | 👿 117 | 💀 118 | 💩 119 | 🤡 120 | 👹 121 | 👺 122 | 👻 123 | 👽 124 | 🤖 125 | 🙈 126 | 💌 127 | 💘 128 | 💝 129 | 💖 130 | 💗 131 | 💓 132 | 💞 133 | 💕 134 | ❣️ 135 | 💔 136 | ❤️‍🩹 137 | 💋 138 | 💯 139 | 💥 140 | 💫 141 | 🕳️ 142 | 💬 143 | 🗯️ 144 | 👍 145 | 🧠 146 | 🫀 147 | 🫁 148 | 🦷 149 | 🦴 150 | 👀 151 | 👁️ 152 | 👄 153 | 🫦 154 | 🤷 155 | 🗣️ 156 | 👤 157 | 👥 158 | 🫂 159 | 👣 160 | 🐵 161 | 🐶 162 | 🐩 163 | 🐺 164 | 🦊 165 | 🦝 166 | 🐱 167 | 🦁 168 | 🐯 169 | 🦄 170 | 🦌 171 | 🐮 172 | 🐷 173 | 🐐 174 | 🦙 175 | 🐭 176 | 🐰 177 | 🦔 178 | 🦇 179 | 🐻 180 | 🐨 181 | 🐼 182 | 🦥 183 | 🐾 184 | 🐔 185 | 🐦 186 | 🐧 187 | 🕊️ 188 | 🦉 189 | 🦩 190 | 🪿 191 | 🐸 192 | 🐢 193 | 🐉 194 | 🐳 195 | 🐟 196 | 🦈 197 | 🐙 198 | 🐚 199 | 🪸 200 | 🐌 201 | 🦋 202 | 🐝 203 | 🪲 204 | 🐞 205 | 🦗 206 | 🪳 207 | 🕷️ 208 | 🦂 209 | 🦠 210 | 💐 211 | 🌸 212 | 💮 213 | 🪷 214 | 🏵️ 215 | 🌹 216 | 🥀 217 | 🌺 218 | 🌻 219 | 🌼 220 | 🌷 221 | 🪻 222 | 🌱 223 | 🪴 224 | 🌲 225 | 🌳 226 | 🌴 227 | 🌵 228 | 🌾 229 | 🌿 230 | 🍀 231 | 🍁 232 | 🍂 233 | 🍃 234 | 🪹 235 | 🍄 236 | 🍇 237 | 🍈 238 | 🍉 239 | 🍊 240 | 🍋 241 | 🍌 242 | 🍍 243 | 🥭 244 | 🍎 245 | 🍐 246 | 🍒 247 | 🍓 248 | 🫐 249 | 🥝 250 | 🍅 251 | 🫒 252 | 🥥 253 | 🥑 254 | 🥔 255 | 🥕 256 | 🌽 257 | 🌶️ 258 | 🫑 259 | 🥒 260 | 🥬 261 | 🥦 262 | 🧄 263 | 🧅 264 | 🥜 265 | 🫘 266 | 🌰 267 | 🫚 268 | 🫛 269 | 🍞 270 | 🥐 271 | 🫓 272 | 🥨 273 | 🥯 274 | 🥞 275 | 🧇 276 | 🧀 277 | 🍖 278 | 🍗 279 | 🥩 280 | 🥓 281 | 🍔 282 | 🍟 283 | 🌭 284 | 🥪 285 | 🌮 286 | 🌯 287 | 🫔 288 | 🥙 289 | 🧆 290 | 🍳 291 | 🥘 292 | 🍲 293 | 🫕 294 | 🥣 295 | 🥗 296 | 🍿 297 | 🧈 298 | 🧂 299 | 🥫 300 | 🍱 301 | 🍘 302 | 🍙 303 | 🍚 304 | 🍛 305 | 🍜 306 | 🍝 307 | 🍠 308 | 🍢 309 | 🍣 310 | 🍤 311 | 🍥 312 | 🍡 313 | 🥟 314 | 🥠 315 | 🥡 316 | 🦀 317 | 🦞 318 | 🦪 319 | 🍦 320 | 🍧 321 | 🍨 322 | 🍩 323 | 🍪 324 | 🎂 325 | 🍰 326 | 🧁 327 | 🥧 328 | 🍫 329 | 🍬 330 | 🍭 331 | 🍮 332 | 🍯 333 | 🍼 334 | 🥛 335 | ☕ 336 | 🫖 337 | 🍵 338 | 🍶 339 | 🍾 340 | 🍷 341 | 🍹 342 | 🥂 343 | 🫗 344 | 🥤 345 | 🧋 346 | 🧃 347 | 🧉 348 | 🧊 349 | 🥢 350 | 🍽️ 351 | 🏺 352 | 🌍 353 | 🏔️ 354 | ⛰️ 355 | 🌋 356 | 🏕️ 357 | 🏖️ 358 | 🏝️ 359 | 🏞️ 360 | 🏟️ 361 | 🏛️ 362 | 🧱 363 | 🪨 364 | 🪵 365 | 🏚️ 366 | 🏠 367 | 🏰 368 | 🗼 369 | 🌄 370 | 🌇 371 | ♨️ 372 | 🎠 373 | 🎡 374 | 🎢 375 | 🎪 376 | 🚂 377 | 🚇 378 | 🚌 379 | 🚕 380 | 🚗 381 | 🚚 382 | 🚛 383 | 🚜 384 | 🏎️ 385 | 🏍️ 386 | 🚲 387 | 🛴 388 | 🛹 389 | 🛼 390 | 🛣️ 391 | ⛽ 392 | 🚨 393 | 🚦 394 | 🛑 395 | 🚧 396 | ⚓ 397 | 🛟 398 | 🛶 399 | ✈️ 400 | 🪂 401 | 🚠 402 | 🚡 403 | 🚀 404 | 🛸 405 | 🧳 406 | ⌛ 407 | ⏳ 408 | ⌚ 409 | ⏰ 410 | 🕰️ 411 | 🌚 412 | 🌛 413 | 🌜 414 | 🌡️ 415 | 🌝 416 | 🌞 417 | 🪐 418 | ⭐ 419 | 🌟 420 | 🌌 421 | ☁️ 422 | ⛅ 423 | 🌧️ 424 | 🌩️ 425 | 🌪️ 426 | 🌬️ 427 | 🌀 428 | 🌈 429 | ☂️ 430 | ⚡ 431 | ⛄ 432 | ☄️ 433 | 🔥 434 | 💧 435 | 🌊 436 | 🎃 437 | 🎆 438 | 🎈 439 | 🎊 440 | 🎀 441 | 🎁 442 | 🎗️ 443 | 🎟️ 444 | 🎖️ 445 | 🏆 446 | 🏅 447 | 🥇 448 | 🥈 449 | 🥉 450 | ⚽ 451 | ⚾ 452 | 🥎 453 | 🏀 454 | 🏐 455 | 🏈 456 | 🏉 457 | 🎾 458 | 🥏 459 | 🎳 460 | 🏏 461 | 🏑 462 | 🏒 463 | 🥍 464 | 🏓 465 | 🏸 466 | 🥊 467 | 🥋 468 | 🥅 469 | ⛳ 470 | ⛸️ 471 | 🎣 472 | 🤿 473 | 🎽 474 | 🎿 475 | 🛷 476 | 🥌 477 | 🎯 478 | 🪀 479 | 🪁 480 | 🎱 481 | 🔮 482 | 🎮 483 | 🎰 484 | 🎲 485 | 🧩 486 | 🪩 487 | ♠️ 488 | ♥️ 489 | ♟️ 490 | 🃏 491 | 🀄 492 | 🎴 493 | 🎭 494 | 🖼️ 495 | 🎨 496 | 🧵 497 | 🪡 498 | 🧶 499 | 👕 500 | 🧦 501 | 👗 502 | 🪭 503 | 👜 504 | 🛍️ 505 | 👟 506 | 🥿 507 | 👠 508 | 🩰 509 | 🪮 510 | 👑 511 | 👒 512 | 🎓 513 | 💍 514 | 💎 515 | 🔈 516 | 📣 517 | 🔔 518 | 🎶 519 | 🎙️ 520 | 🎚️ 521 | 🎛️ 522 | 🎤 523 | 🎧 524 | 📻 525 | 🎷 526 | 🪗 527 | 🎸 528 | 🎹 529 | 🎺 530 | 🎻 531 | 🪕 532 | 🥁 533 | 🪘 534 | 🪇 535 | 🪈 536 | 📱 537 | ☎️ 538 | 📟 539 | 📠 540 | 🔋 541 | 🪫 542 | 🔌 543 | 💻 544 | 🖨️ 545 | 💾 546 | 💿 547 | 🎞️ 548 | 🎬 549 | 📺 550 | 📷 551 | 📼 552 | 🔎 553 | 💡 554 | 🪔 555 | 📚 556 | 📰 557 | 💸 558 | 📦 559 | 🗳️ 560 | ✏️ 561 | ✒️ 562 | 🖊️ 563 | 🖌️ 564 | 🖍️ 565 | 📈 566 | 📉 567 | 📊 568 | 🖇️ 569 | 📏 570 | ✂️ 571 | 🗃️ 572 | 🗑️ 573 | 🔒 574 | 🗝️ 575 | ⛏️ 576 | 🛠️ 577 | 🪃 578 | 🏹 579 | ⚙️ 580 | ⚖️ 581 | ⛓️ 582 | 🧲 583 | 🧪 584 | 🧬 585 | 🔬 586 | 🔭 587 | 🩸 588 | 🩺 589 | 🛋️ 590 | 🪤 591 | 🪒 592 | 🧹 593 | 🧺 594 | 🧼 595 | 🫧 596 | 🛒 597 | 🧿 598 | 🗿 599 | 🚮 600 | ⚠️ 601 | ☢️ 602 | ☣️ 603 | ☯️ 604 | ☮️ 605 | ♈ 606 | ♉ 607 | ♊ 608 | ♋ 609 | ♌ 610 | ♍ 611 | ♎ 612 | ♏ 613 | ♐ 614 | ♑ 615 | ♒ 616 | ♓ 617 | ⛎ 618 | 📴 619 | ✖️ 620 | ➕ 621 | ➖ 622 | ➗ 623 | ♾️ 624 | ⁉️ 625 | ❓ 626 | ❗ 627 | 〰️ 628 | ♻️ 629 | ✅ 630 | ➰ 631 | ➿ 632 | ©️ 633 | ®️ 634 | ™️ 635 | 🔢 636 | 🆒 637 | 🆓 638 | 🆕 639 | 🆗 640 | 🆘 641 | 🆙 642 | 🐍 643 | 🐎 644 | 👾 --------------------------------------------------------------------------------