OpenEmoji 0.1: an emoji set as a folder (#196)
One openemoji.json that states coverage against a Unicode version, the licence, and who or what drew the set (made_by plus the W3C AI disclosure vocabulary), and glyphs named by fully-qualified codepoint sequence. Custom emoji get x- keys with shortcodes; skin tones name their base; rendered emoji keep the character as alt. Mapping from Mastodon, Slack, Discord and CLDR. Landing page shows 19 glyphs drawn by the reference implementation, `emoji` in profullstack/cli-tools. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
BIN
apps/logicsrc-web/public/openemoji/1f1e7-1f1f7.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f1ef-1f1f5.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f363.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f3b8.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f3d4-fe0f.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f419.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f431.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f44d-1f3fd.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f44d.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f469-1f3fe-200d-1f4bb.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f469-200d-1f4bb.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f4a1.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f525.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f600.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f602.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f680.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
apps/logicsrc-web/public/openemoji/1f979.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
apps/logicsrc-web/public/openemoji/267b-fe0f.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
apps/logicsrc-web/public/openemoji/2764-fe0f.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
292
apps/logicsrc-web/src/app/openemoji/page.tsx
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenEmoji · LogicSRC",
|
||||
description:
|
||||
"OpenEmoji is an emoji set as a folder: one openemoji.json that states coverage, licence and who or what drew it (made_by, W3C AI disclosure), and glyphs named by fully-qualified codepoint sequence. Standard and custom emoji, PNG, SVG and colour fonts, rendered as text with the character as alt.",
|
||||
alternates: { canonical: "/openemoji" }
|
||||
};
|
||||
|
||||
/** Drawn by the reference implementation, `emoji` in profullstack/cli-tools. */
|
||||
const SAMPLE: Array<[string, string, string]> = [
|
||||
["1f600", "😀", "grinning face"],
|
||||
["1f602", "😂", "face with tears of joy"],
|
||||
["1f979", "🥹", "face holding back tears"],
|
||||
["1f525", "🔥", "fire"],
|
||||
["2764-fe0f", "❤️", "red heart"],
|
||||
["1f44d", "👍", "thumbs up"],
|
||||
["1f44d-1f3fd", "👍🏽", "thumbs up: medium skin tone"],
|
||||
["1f469-200d-1f4bb", "👩💻", "woman technologist"],
|
||||
["1f469-1f3fe-200d-1f4bb", "👩🏾💻", "woman technologist: medium-dark skin tone"],
|
||||
["1f431", "🐱", "cat face"],
|
||||
["1f419", "🐙", "octopus"],
|
||||
["1f363", "🍣", "sushi"],
|
||||
["1f3d4-fe0f", "🏔️", "snow-capped mountain"],
|
||||
["1f680", "🚀", "rocket"],
|
||||
["1f4a1", "💡", "light bulb"],
|
||||
["1f3b8", "🎸", "guitar"],
|
||||
["267b-fe0f", "♻️", "recycling symbol"],
|
||||
["1f1ef-1f1f5", "🇯🇵", "flag: Japan"],
|
||||
["1f1e7-1f1f7", "🇧🇷", "flag: Brazil"]
|
||||
];
|
||||
|
||||
const EXAMPLE = `{
|
||||
"openemoji": "0.1",
|
||||
"name": "OpenEmoji",
|
||||
"license": "CC-BY-4.0",
|
||||
"unicode": "18.0",
|
||||
"made_by": "ai",
|
||||
"disclosure": "ai-generated",
|
||||
"ai_model": "gpt-image-2",
|
||||
"ai_prompt_url": "style.txt",
|
||||
"sizes": [16, 32, 64, 128, 512],
|
||||
"fonts": [{ "format": "cbdt", "path": "font/OpenEmoji-CBDT.ttf" }],
|
||||
"coverage": { "total": 3963, "drawn": 3963, "missing": [] },
|
||||
"emoji": [
|
||||
{ "key": "1f44d", "char": "👍", "name": "thumbs up",
|
||||
"png": "png/{size}/1f44d.png", "svg": "svg/1f44d.svg" },
|
||||
{ "key": "1f44d-1f3fd", "char": "👍🏽", "name": "thumbs up: medium skin tone",
|
||||
"base": "1f44d", "png": "png/{size}/1f44d-1f3fd.png" },
|
||||
{ "key": "x-shipit", "shortcodes": ["shipit"], "name": "ship it",
|
||||
"group": "Custom", "png": "png/{size}/x-shipit.png" }
|
||||
]
|
||||
}`;
|
||||
|
||||
const RULES: Array<[string, string]> = [
|
||||
["A set is a folder", "openemoji.json at the root, every path relative to it. Works on disk, in git, behind a CDN or in a package."],
|
||||
["Standard keys are codepoints", "The fully-qualified sequence, lowercase hex, hyphen-joined: 2764-fe0f, 1f469-1f3fe-200d-1f4bb. The key is the file name. Lookups retry without FE0F."],
|
||||
["Custom keys start with x-", "x-shipit, with shortcodes, a name and group Custom. Shortcodes belong to their set; nothing Unicode encodes gets an x- key."],
|
||||
["The descriptor says where files are", "png with a {size} placeholder, svg, optional webp, avif or apng. Fonts are listed once per set with their colour format."],
|
||||
["Coverage is stated", "total, drawn and missing against the named Unicode version. Missing falls back to the platform's emoji, never to a box."],
|
||||
["A variant names its base", "A skin-tone sequence carries base, so any picker can group tones and a model-drawn set can say its tones were derived."],
|
||||
["The set says what made it", "made_by human | ai | both, plus the W3C AI disclosure vocabulary: disclosure, ai_model, ai_provider, ai_prompt_url. Per set, overridable per emoji."],
|
||||
["Rendered emoji stay text", "img alt is the character itself and title its CLDR name, so copy, paste and screen readers keep the emoji. Fonts do the same with no images."]
|
||||
];
|
||||
|
||||
const MAPPING: Array<[string, string, string, string]> = [
|
||||
["shortcodes[0]", "shortcode", "the map's key", "name"],
|
||||
["png", "url / static_url", "the map's value", "CDN URL from id"],
|
||||
["group", "category", "none", "none"],
|
||||
["apng present", "url differs from static_url", "none", "animated"]
|
||||
];
|
||||
|
||||
const ABSENT: Array<[string, string]> = [
|
||||
["No registry", "A set is wherever its descriptor is. A directory is anyone reading /.well-known/openemoji.json."],
|
||||
["No shortcode authority", "Every platform's list already disagrees. CLDR names are the shared vocabulary, and name carries them."],
|
||||
["No drawing rules", "Style, palette and whether a flag waves are the set's business. The spec fixes names and facts, not looks."],
|
||||
["No animation rules in 0.1", "An apng or webp path may animate. Timing, loops and reduced motion come later."],
|
||||
["No required licence", "A set must say its licence. It need not be open."]
|
||||
];
|
||||
|
||||
export default function OpenEmojiPage(): ReactNode {
|
||||
return (
|
||||
<SiteShell active="OpenEmoji">
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<p className="eyebrow">LogicSRC standards surface</p>
|
||||
<h2>OpenEmoji</h2>
|
||||
<p>
|
||||
An emoji set as a folder: one descriptor that says what it covers and what made it, and
|
||||
glyphs named by the codepoints they draw. Any site can render any set.
|
||||
</p>
|
||||
</div>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
Every emoji a reader sees is drawn by whoever made their operating system, and a site that
|
||||
wants its own look has no agreed shape to ship one in. Custom emoji are worse: Slack,
|
||||
Discord and Mastodon each keep theirs behind their own API. And now that an image model can
|
||||
draw a whole set in an afternoon, a reader deserves to know whether a person, a model or
|
||||
both drew the thumbs-up in front of them.
|
||||
</p>
|
||||
<p style={{ color: "#5b6b7a" }}>
|
||||
Status: 0.1. Files first and <code style={mono}>made_by</code> on the work, like{" "}
|
||||
<Link href="/openwiki">OpenWiki</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>A set drawn to the spec</h2>
|
||||
<p>
|
||||
From the reference implementation: every glyph drawn by gpt-image-2 under one art
|
||||
direction. The skin tones are edits of their base, which is why the two thumbs are the
|
||||
same thumb.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(84px, 1fr))",
|
||||
gap: "0.5rem"
|
||||
}}
|
||||
>
|
||||
{SAMPLE.map(([key, char, name]) => (
|
||||
<figure
|
||||
key={key}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: "0.7rem 0.3rem 0.5rem",
|
||||
border: "1px solid #e3e6e0",
|
||||
borderRadius: "0.6rem",
|
||||
background: "#fff",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
className="openemoji"
|
||||
src={`/openemoji/${key}.png`}
|
||||
alt={char}
|
||||
title={name}
|
||||
width={56}
|
||||
height={56}
|
||||
loading="lazy"
|
||||
/>
|
||||
<figcaption
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
color: "#5b6b7a",
|
||||
marginTop: "0.3rem",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The shape</h2>
|
||||
<p>openemoji.json, trimmed to three entries: a glyph, its skin-tone variant, a custom emoji.</p>
|
||||
</div>
|
||||
<pre style={pre}>{EXAMPLE}</pre>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The eight rules</h2>
|
||||
<p>Every one of them degrades rather than fails.</p>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>Rule</th>
|
||||
<th style={th}>What it means</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{RULES.map(([rule, meaning], index) => (
|
||||
<tr key={rule}>
|
||||
<td style={td}>
|
||||
<strong>
|
||||
{index + 1}. {rule}
|
||||
</strong>
|
||||
</td>
|
||||
<td style={td}>{meaning}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Discovery</h2>
|
||||
<p>
|
||||
A <code style={mono}>{'<link rel="openemoji" href="/emoji/openemoji.json">'}</code> in a
|
||||
page's head, or <code style={mono}>/.well-known/openemoji.json</code> on a site, which is
|
||||
either a set or a list of the sets it offers. Served as JSON with CORS open, because the
|
||||
point is that other sites read it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Custom emoji, from where they live now</h2>
|
||||
<p>Importing is a fetch and a rename. Nothing in 0.1 has a field these cannot carry back.</p>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>OpenEmoji</th>
|
||||
<th style={th}>Mastodon</th>
|
||||
<th style={th}>Slack</th>
|
||||
<th style={th}>Discord</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{MAPPING.map(([ours, mastodon, slack, discord]) => (
|
||||
<tr key={ours}>
|
||||
<td style={td}>
|
||||
<code style={mono}>{ours}</code>
|
||||
</td>
|
||||
<td style={td}>{mastodon}</td>
|
||||
<td style={td}>{slack}</td>
|
||||
<td style={td}>{discord}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>What is deliberately absent</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{ABSENT.map(([what, why]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{why}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Draw a set</h2>
|
||||
<p>
|
||||
<code style={mono}>emoji</code> in{" "}
|
||||
<a href="https://github.com/profullstack/cli-tools">profullstack/cli-tools</a> draws every
|
||||
fully-qualified emoji in Unicode's list and writes the folder above: PNGs from 16 to
|
||||
512, SVGs, CBDT and sbix colour fonts, CSS and openemoji.json.
|
||||
</p>
|
||||
</div>
|
||||
<pre style={pre}>{`emoji generate --only 😀🔥🇯🇵👍🏽 # a handful, to judge the style
|
||||
emoji generate # all 3,963; resumable
|
||||
emoji build # sizes, SVGs, fonts, manifest`}</pre>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Where everything lives</h2>
|
||||
</div>
|
||||
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
|
||||
<li>
|
||||
<Link href="/docs/openemoji">Specification</Link>: the shape, eight rules, discovery, the
|
||||
mapping from Mastodon, Slack, Discord and CLDR
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/docs/openwebring">OpenWebring</Link> and <Link href="/openwiki">OpenWiki</Link>{" "}
|
||||
for made_by and the disclosure vocabulary; <Link href="/openprofile">OpenProfile.md</Link>{" "}
|
||||
for author
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
|
@ -96,6 +96,7 @@ export const FAMILIES: Family[] = [
|
|||
s("openaffiliate", "OpenAffiliate", "One file a merchant serves about the commission it pays"),
|
||||
s("openrecipe", "OpenRecipe.md", "One Markdown file that is a recipe, with schema.org derived from it and never the reverse"),
|
||||
s("opensong", "OpenSong", "One plain-text file that is a song: title, style, exclusions and lyrics as the blocks a generator takes, kept beside the audio"),
|
||||
s("openemoji", "OpenEmoji", "An emoji set as a folder: one file that states coverage, licence and whether a person or a model drew it, and glyphs named by the codepoints they draw"),
|
||||
s("openthreat", "OpenThreat", "One file a security tool serves about what it found in the open: public subjects only, secrets never located"),
|
||||
s("openrental", "OpenRental", "One file an operator serves about the agents and file swarms it rents out: members, metadata and rates through CoinPay", { landing: undefined, status: "draft" }),
|
||||
s("opensite", "OpenSite", "One record about a page or a site: the card a reader would draw, declared by the site or read from it, kept by an index"),
|
||||
|
|
|
|||
147
docs/openemoji.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# OpenEmoji
|
||||
|
||||
OpenEmoji is an emoji set as a folder: one `openemoji.json` at the top that says what the set covers, who or what drew it and under which licence, and image files named by the codepoints they draw. Any site, app or agent can render any set from it, standard Unicode emoji and a community's custom ones alike, without a vendor font and without a platform's private API. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface.
|
||||
|
||||
Status: **0.1**. Written in the spirit of [OpenWiki](/openwiki): files first, one small descriptor, and `made_by` on the work.
|
||||
|
||||
Slug: `openemoji`
|
||||
|
||||
## The problem
|
||||
|
||||
Every emoji a reader sees is drawn by whoever made their operating system. The same message shows Apple's 😂 on one phone, Google's on another, and a box with a question mark on a Linux server whose font is two Emoji versions old. A site that wants its own look has to ship a set, and there is no agreed shape for one: Twemoji, Noto, OpenMoji and Fluent each name their files differently, none of them says in a machine-readable way how much of Unicode it covers, and none of them says what made it.
|
||||
|
||||
Custom emoji are worse. Slack, Discord, Mastodon and Misskey each keep their own list behind their own API, with their own fields for the same five facts (a name, an image, a category, whether it animates, whether it is shown in the picker). A community that moves loses its emoji.
|
||||
|
||||
Image models make the gap wider. A complete, coherent set can now be drawn in an afternoon, and a reader deserves to know whether the thumbs-up in front of them was drawn by a person, a model, or both.
|
||||
|
||||
## The shape
|
||||
|
||||
```
|
||||
openemoji.json the descriptor (rule 1)
|
||||
png/<size>/<key>.png raster glyphs, one folder per size
|
||||
svg/<key>.svg vector glyphs, when the set has them
|
||||
font/<Family>-CBDT.ttf colour fonts, when the set has them
|
||||
openemoji.css @font-face and an img rule, optional
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"openemoji": "0.1",
|
||||
"name": "OpenEmoji",
|
||||
"version": "2026-09-24",
|
||||
"license": "CC-BY-4.0",
|
||||
"author": "Profullstack, Inc.",
|
||||
"unicode": "18.0",
|
||||
"made_by": "ai",
|
||||
"disclosure": "ai-generated",
|
||||
"ai_model": "gpt-image-2",
|
||||
"ai_provider": "OpenAI",
|
||||
"ai_prompt_url": "style.txt",
|
||||
"sizes": [16, 20, 32, 48, 64, 72, 96, 128, 136, 160, 256, 512],
|
||||
"formats": ["png", "svg", "cbdt", "sbix"],
|
||||
"fonts": [
|
||||
{ "format": "cbdt", "path": "font/OpenEmoji-CBDT.ttf" },
|
||||
{ "format": "sbix", "path": "font/OpenEmoji-sbix.ttf" }
|
||||
],
|
||||
"css": "openemoji.css",
|
||||
"coverage": { "total": 3963, "drawn": 3963, "missing": [] },
|
||||
"emoji": [
|
||||
{
|
||||
"key": "1f44d",
|
||||
"char": "👍",
|
||||
"name": "thumbs up",
|
||||
"group": "People & Body",
|
||||
"subgroup": "hand-fingers-closed",
|
||||
"unicode": "0.6",
|
||||
"svg": "svg/1f44d.svg",
|
||||
"png": "png/{size}/1f44d.png"
|
||||
},
|
||||
{
|
||||
"key": "1f44d-1f3fd",
|
||||
"char": "👍🏽",
|
||||
"name": "thumbs up: medium skin tone",
|
||||
"group": "People & Body",
|
||||
"subgroup": "hand-fingers-closed",
|
||||
"unicode": "1.0",
|
||||
"base": "1f44d",
|
||||
"svg": "svg/1f44d-1f3fd.svg",
|
||||
"png": "png/{size}/1f44d-1f3fd.png"
|
||||
},
|
||||
{
|
||||
"key": "x-shipit",
|
||||
"shortcodes": ["shipit", "squirrel"],
|
||||
"name": "ship it",
|
||||
"group": "Custom",
|
||||
"keywords": ["deploy", "release"],
|
||||
"png": "png/{size}/x-shipit.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
That is the set the reference implementation drew, trimmed to three entries, plus one custom emoji to show the shape.
|
||||
|
||||
## The rules
|
||||
|
||||
There are eight, and every one of them degrades rather than fails.
|
||||
|
||||
**1. A set is a folder with `openemoji.json` at its root.** Every path in the descriptor is relative to that file. The same folder works on a disk, in a git repository, behind a CDN, or inside an npm package. Top-level keys a reader should understand: `openemoji` (the spec version, required), `name` (required), `version`, `license` (an SPDX identifier), `author` (a name or an [OpenProfile.md](/openprofile) URL), `homepage`, `unicode` (the Emoji version the set was drawn against), `sizes`, `formats`, `fonts`, `css`, `coverage` and `emoji` (required). Unknown keys are kept and ignored.
|
||||
|
||||
**2. A standard emoji's key is its fully-qualified codepoint sequence.** Lowercase hexadecimal, joined by hyphens, exactly as Unicode's `emoji-test.txt` lists the fully-qualified form: `2764-fe0f` for ❤️, `1f469-1f3fe-200d-1f4bb` for 👩🏾💻, `1f1ef-1f1f5` for 🇯🇵. The key is also the file name. A reader looking up text a person typed tries the exact sequence first and then the same sequence with every `FE0F` removed from both sides, because people and keyboards leave the selector out. `char` holds the emoji itself and `name` its CLDR short name, both copied from Unicode, not paraphrased.
|
||||
|
||||
**3. A custom emoji's key starts with `x-`.** `x-` followed by lowercase letters, digits, `_` or `-`. It has no `char`, one or more `shortcodes` (without colons), a `name`, and `group: "Custom"` unless the set groups its own. A shortcode belongs to its set: two sets may both define `:shipit:`, and an app that loads both decides which wins. No `x-` key may be used for something Unicode has encoded; when Unicode encodes it, the set adds the standard key and may keep the custom one as an alias.
|
||||
|
||||
**4. Files are named by key, and the descriptor says where.** An entry's `png` is a path that may contain `{size}`, filled in from the set's `sizes`; `svg` is a path; a set may add `webp`, `avif` or `apng` the same way. A reader picks the smallest size at least as large as the rendered size times the device pixel ratio, and the largest when none is. Fonts are listed once for the set with their colour format (`cbdt`, `sbix`, `colrv1`, `ot-svg`), because a browser picks among them by what it supports, not per emoji.
|
||||
|
||||
**5. Coverage is stated, not implied.** `coverage.total` is the number of standard emoji in the Unicode version the set names, `coverage.drawn` how many of them it has, and `coverage.missing` their keys. A reader that meets an emoji the set does not have falls back to the platform's own, never to a blank or a box. A set that draws only faces is a valid set; it only has to say so.
|
||||
|
||||
**6. A variant names its base.** A skin-tone sequence carries `base`, the key of the same emoji without the tone (`1f44d-1f3fd` has `base: "1f44d"`). That one field is enough for any picker to group tones under their emoji and to offer a tone switch from any set, and it lets a set drawn by a model say that its variants were derived from one drawing rather than drawn five times.
|
||||
|
||||
**7. The set says what made it.** `made_by` is `human`, `ai` or `both`, the [OpenWebring](/docs/openwebring) vocabulary. `disclosure` is the W3C AI Content Disclosure vocabulary verbatim (`none`, `ai-assisted`, `ai-generated`, `autonomous`, and `mixed` at the set level), and `ai_model`, `ai_provider` and `ai_prompt_url` are the rest of it: which model, whose, and a file or page describing how it was asked. Any of the five may be repeated on an entry that differs from the set, for instance a hand-corrected flag in a set that is otherwise `ai-generated`. It is a self-declaration and nothing verifies it.
|
||||
|
||||
**8. Rendered emoji stay text.** An emoji shown as an image is `<img class="openemoji" src="…" alt="👍" title="thumbs up">`: `alt` is the character itself, so copying the text copies the emoji and a screen reader reads its name, and `title` is the CLDR name. When a set ships a font, `font-family` with the set's name first and the platform fonts after it does the same job with no images at all; `css` points at a stylesheet that declares both.
|
||||
|
||||
## Discovery
|
||||
|
||||
A set is found three ways, and a reader tries them in this order:
|
||||
|
||||
- A `<link rel="openemoji" href="/emoji/openemoji.json">` in a page's head: this page renders emoji with that set.
|
||||
- `/.well-known/openemoji.json` on a site: either a set itself, or a list of the sets the site offers, `{"openemoji": "0.1", "sets": [{"name": "…", "url": "…/openemoji.json"}]}`. A community server's custom emoji live here.
|
||||
- A URL someone hands over. The descriptor is plain JSON served as `application/json` with CORS open (`Access-Control-Allow-Origin: *`), because the point is that other sites read it.
|
||||
|
||||
## Mapping from what exists
|
||||
|
||||
| OpenEmoji | Mastodon `custom_emojis` | Slack `emoji.list` | Discord emoji | Unicode / CLDR |
|
||||
|---|---|---|---|---|
|
||||
| `key` | none (shortcode is the id) | none (name is the id) | `id` | codepoint sequence |
|
||||
| `shortcodes[0]` | `shortcode` | the map's key | `name` | none |
|
||||
| `png` | `url`, `static_url` | the map's value | CDN URL from `id` | none |
|
||||
| `group` | `category` | none | none | group, subgroup |
|
||||
| `name` | none | none | none | CLDR short name |
|
||||
| `keywords` | none | none | none | CLDR annotations |
|
||||
| `apng` present | `url` differs from `static_url` | none | `animated` | none |
|
||||
| absent from the picker | `visible_in_picker: false` | none | `available: false` | none |
|
||||
|
||||
Importing from any of them is a fetch and a rename: shortcode to `x-` key, image to `png/<size>/`. Exporting back is the reverse, and nothing in 0.1 has a field those four cannot carry.
|
||||
|
||||
## What is deliberately absent
|
||||
|
||||
- **No registry.** A set is wherever its descriptor is. A directory of sets can be built by anyone reading `/.well-known/openemoji.json`.
|
||||
- **No shortcode authority.** Shortcodes for standard emoji are not standardised here, because every platform's list already disagrees; CLDR names are the one shared vocabulary and `name` carries them.
|
||||
- **No drawing rules.** Style, palette, proportions and whether a flag waves are the set's business. The spec fixes names and facts, not looks.
|
||||
- **No animation rules in 0.1.** An `apng` or `webp` path may point at an animated file; timing, loops and reduced-motion are for a later version.
|
||||
- **No required licence.** A set must say its licence; it need not be open.
|
||||
|
||||
## Reference implementation
|
||||
|
||||
`emoji` in [profullstack/cli-tools](https://github.com/profullstack/cli-tools) draws a complete set with an image model and packs it as above:
|
||||
|
||||
```sh
|
||||
emoji generate --only 😀🔥🇯🇵👍🏽 # draw a handful, judge the style
|
||||
emoji generate # every fully-qualified sequence in emoji-test.txt
|
||||
emoji build # PNG sizes, SVGs, CBDT + sbix fonts, CSS, openemoji.json
|
||||
```
|
||||
|
||||
It follows rules 6 and 7 as a design choice, not only as fields. Six anchors are drawn first and every later glyph is an edit that sees them, so 3,963 glyphs share one light and one gloss. Every skin-tone variant is an edit of its `base` that changes the skin and nothing else. The descriptor it writes says `made_by: ai`, `disclosure: ai-generated`, the model, and points `ai_prompt_url` at the art direction it used.
|
||||
|
||||
Related: [OpenWebring](/docs/openwebring) and [OpenWiki](/docs/openwiki) for `made_by` and the disclosure vocabulary; [OpenProfile.md](/openprofile) for `author`; [OpenFile](/docs/openfile) for serving the folder.
|
||||