Files
Meck/examples/companion_radio/ui-new/Emojisprites.h
2026-02-10 00:30:03 +11:00

410 lines
14 KiB
C

#pragma once
// Emoji sprites for e-ink display - dual size
// Large (12x12) for compose/picker, Small (10x10) for channel view
// MSB-first, 2 bytes per row
#include <stdint.h>
#ifdef ESP32
#include <pgmspace.h>
#endif
// Large sprites for compose screen and picker
#define EMOJI_LG_W 12
#define EMOJI_LG_H 12
// Small sprites for channel message view
#define EMOJI_SM_W 10
#define EMOJI_SM_H 10
#define EMOJI_COUNT 20
// Escape codes used in sanitized message text
// Bytes 0x01 through 0x14 map to emoji indices 0-19
#define EMOJI_ESCAPE_START 0x01
#define EMOJI_ESCAPE_END 0x14
#define EMOJI_PAD_BYTE 0x15 // Padding byte after escape to fill buffer to UTF-8 wire cost
// ======== LARGE 12x12 SPRITES ========
static const uint8_t emoji_lg_wireless[] PROGMEM = {
0x00,0x00, 0x3F,0xC0, 0x60,0x60, 0xC0,0x30,
0x0F,0x00, 0x19,0x80, 0x30,0xC0, 0x00,0x00,
0x06,0x00, 0x0F,0x00, 0x06,0x00, 0x00,0x00,
};
static const uint8_t emoji_lg_infinity[] PROGMEM = {
0x00,0x00, 0x00,0x00, 0x61,0x80, 0x92,0x40,
0x8C,0x40, 0x8C,0x40, 0x92,0x40, 0x61,0x80,
0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
};
static const uint8_t emoji_lg_trex[] PROGMEM = {
0x03,0xE0, 0x06,0xA0, 0x07,0xE0, 0x0C,0x00,
0x5C,0x00, 0x7C,0x00, 0x3C,0x00, 0x38,0x00,
0x3C,0x00, 0x36,0x00, 0x22,0x00, 0x33,0x00,
};
static const uint8_t emoji_lg_skull[] PROGMEM = {
0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0,
0x40,0x20, 0x49,0x20, 0x2F,0x40, 0x1F,0x80,
0x96,0x90, 0x66,0x60, 0x36,0xC0, 0x96,0x90,
};
static const uint8_t emoji_lg_cross[] PROGMEM = {
0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x3F,0xC0,
0x3F,0xC0, 0x3F,0xC0, 0x0F,0x00, 0x0F,0x00,
0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00,
};
static const uint8_t emoji_lg_lightning[] PROGMEM = {
0x03,0x00, 0x07,0x00, 0x0E,0x00, 0x1C,0x00,
0x3F,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00,
0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x00,0x00,
};
static const uint8_t emoji_lg_tophat[] PROGMEM = {
0x00,0x00, 0x1F,0x80, 0x3F,0xC0, 0x3F,0xC0,
0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0,
0x7F,0xE0, 0xFF,0xF0, 0xFF,0xF0, 0x00,0x00,
};
static const uint8_t emoji_lg_motorcycle[] PROGMEM = {
0x00,0x00, 0x00,0x00, 0x03,0x80, 0x1F,0xC0,
0x3F,0xC0, 0x7F,0xC0, 0xFF,0xE0, 0xDF,0x60,
0x51,0x40, 0xE0,0xE0, 0x40,0x40, 0x00,0x00,
};
static const uint8_t emoji_lg_seedling[] PROGMEM = {
0x00,0x00, 0x30,0x00, 0x79,0x80, 0x7B,0xC0,
0x33,0xC0, 0x1F,0x80, 0x06,0x00, 0x06,0x00,
0x06,0x00, 0x06,0x00, 0x00,0x00, 0x00,0x00,
};
static const uint8_t emoji_lg_flag_au[] PROGMEM = {
0x00,0x00, 0x32,0x40, 0x4A,0x40, 0x4A,0x40,
0x7A,0x40, 0x4A,0x40, 0x49,0x80, 0x00,0x00,
0xFF,0xF0, 0x00,0x00, 0xFF,0xF0, 0x00,0x00,
};
static const uint8_t emoji_lg_umbrella[] PROGMEM = {
0x06,0x00, 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0,
0xFF,0xF0, 0xDB,0x70, 0x06,0x00, 0x06,0x00,
0x06,0x00, 0x06,0x00, 0x46,0x00, 0x3C,0x00,
};
static const uint8_t emoji_lg_nazar[] PROGMEM = {
0x1F,0x80, 0x20,0x40, 0x4F,0x20, 0x99,0x90,
0xB6,0xD0, 0xB6,0xD0, 0xB6,0xD0, 0x99,0x90,
0x4F,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00,
};
static const uint8_t emoji_lg_globe[] PROGMEM = {
0x1F,0x80, 0x34,0xC0, 0x66,0x60, 0x4F,0x20,
0x8E,0x10, 0x86,0x10, 0x80,0x30, 0x46,0x60,
0x43,0xE0, 0x30,0xC0, 0x1F,0x80, 0x00,0x00,
};
static const uint8_t emoji_lg_radioactive[] PROGMEM = {
0x00,0x00, 0x22,0x40, 0x32,0xC0, 0x32,0xC0,
0x1B,0x40, 0x00,0x00, 0x0F,0x00, 0x0F,0x00,
0x00,0x00, 0x60,0x20, 0x39,0xC0, 0x0F,0x00,
};
static const uint8_t emoji_lg_cow[] PROGMEM = {
0x00,0x00, 0xC0,0x60, 0x6E,0xC0, 0x3F,0x80,
0x2A,0x80, 0x3F,0x80, 0x3F,0x80, 0x7F,0xC0,
0x5F,0x40, 0x5F,0x40, 0x11,0x00, 0x31,0x80,
};
static const uint8_t emoji_lg_alien[] PROGMEM = {
0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x76,0xE0,
0xF6,0xF0, 0x96,0x90, 0x7F,0xE0, 0x36,0xC0,
0x3F,0xC0, 0x16,0x80, 0x0F,0x00, 0x06,0x00,
};
static const uint8_t emoji_lg_invader[] PROGMEM = {
0x10,0x80, 0x09,0x00, 0x1F,0x80, 0x36,0xC0,
0x7F,0xE0, 0x5F,0xA0, 0x50,0xA0, 0x50,0xA0,
0x19,0x80, 0x19,0x80, 0x30,0xC0, 0x00,0x00,
};
static const uint8_t emoji_lg_dagger[] PROGMEM = {
0x00,0x20, 0x00,0x60, 0x00,0xC0, 0x01,0x80,
0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00,
0x38,0x00, 0x58,0x00, 0x28,0x00, 0x18,0x00,
};
static const uint8_t emoji_lg_grimace[] PROGMEM = {
0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0,
0x40,0x20, 0x40,0x20, 0x5F,0xA0, 0x55,0x40,
0x5F,0xA0, 0x20,0x40, 0x1F,0x80, 0x00,0x00,
};
static const uint8_t emoji_lg_telephone[] PROGMEM = {
0x00,0x00, 0x7F,0xE0, 0xC0,0x30, 0xC0,0x30,
0x60,0x60, 0x30,0xC0, 0x1F,0x80, 0x0F,0x00,
0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x00,0x00,
};
static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
emoji_lg_wireless, emoji_lg_infinity, emoji_lg_trex, emoji_lg_skull, emoji_lg_cross,
emoji_lg_lightning, emoji_lg_tophat, emoji_lg_motorcycle, emoji_lg_seedling, emoji_lg_flag_au,
emoji_lg_umbrella, emoji_lg_nazar, emoji_lg_globe, emoji_lg_radioactive, emoji_lg_cow,
emoji_lg_alien, emoji_lg_invader, emoji_lg_dagger, emoji_lg_grimace, emoji_lg_telephone,
};
// ======== SMALL 10x10 SPRITES ========
static const uint8_t emoji_sm_wireless[] PROGMEM = {
0x00,0x00, 0x7F,0x80, 0xC0,0xC0, 0x1E,0x00,
0x33,0x00, 0x21,0x00, 0x00,0x00, 0x0C,0x00,
0x0C,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_infinity[] PROGMEM = {
0x00,0x00, 0x00,0x00, 0xE7,0x00, 0x99,0x00,
0x99,0x00, 0xA5,0x00, 0x42,0x00, 0x00,0x00,
0x00,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_trex[] PROGMEM = {
0x07,0x80, 0x0F,0x80, 0x0F,0x80, 0x58,0x00,
0x78,0x00, 0x38,0x00, 0x38,0x00, 0x3C,0x00,
0x24,0x00, 0x26,0x00,
};
static const uint8_t emoji_sm_skull[] PROGMEM = {
0x3F,0x00, 0x61,0x80, 0x73,0x80, 0x40,0x80,
0x52,0x80, 0x3F,0x00, 0x3F,0x00, 0xED,0xC0,
0x6D,0x80, 0xAD,0x40,
};
static const uint8_t emoji_sm_cross[] PROGMEM = {
0x1E,0x00, 0x1E,0x00, 0x3F,0x00, 0x3F,0x00,
0x3F,0x00, 0x1E,0x00, 0x1E,0x00, 0x1E,0x00,
0x1E,0x00, 0x1E,0x00,
};
static const uint8_t emoji_sm_lightning[] PROGMEM = {
0x06,0x00, 0x0E,0x00, 0x1C,0x00, 0x3E,0x00,
0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00,
0x30,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_tophat[] PROGMEM = {
0x00,0x00, 0x3F,0x00, 0x3F,0x00, 0x3F,0x00,
0x3F,0x00, 0x3F,0x00, 0x7F,0x80, 0xFF,0xC0,
0xFF,0xC0, 0x00,0x00,
};
static const uint8_t emoji_sm_motorcycle[] PROGMEM = {
0x00,0x00, 0x00,0x00, 0x1F,0x00, 0x3F,0x00,
0x7F,0x00, 0xFF,0x80, 0xFF,0x80, 0xE3,0x80,
0xC1,0x80, 0x00,0x00,
};
static const uint8_t emoji_sm_seedling[] PROGMEM = {
0x00,0x00, 0x70,0x00, 0x77,0x00, 0x77,0x00,
0x3F,0x00, 0x0C,0x00, 0x0C,0x00, 0x0C,0x00,
0x00,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_flag_au[] PROGMEM = {
0x00,0x00, 0x75,0x00, 0x55,0x00, 0x75,0x00,
0x55,0x00, 0x53,0x00, 0x00,0x00, 0xFF,0xC0,
0xFF,0xC0, 0x00,0x00,
};
static const uint8_t emoji_sm_umbrella[] PROGMEM = {
0x0C,0x00, 0x3F,0x00, 0x7F,0x80, 0xFF,0xC0,
0xF7,0xC0, 0x0C,0x00, 0x0C,0x00, 0x0C,0x00,
0x4C,0x00, 0x78,0x00,
};
static const uint8_t emoji_sm_nazar[] PROGMEM = {
0x3F,0x00, 0x40,0x80, 0x9E,0x40, 0xBF,0x40,
0xAD,0x40, 0xBF,0x40, 0x9E,0x40, 0x4C,0x80,
0x3F,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_globe[] PROGMEM = {
0x3F,0x00, 0x69,0x80, 0x4C,0x80, 0x9C,0x40,
0x8C,0x40, 0x80,0xC0, 0x4D,0x80, 0x67,0x80,
0x3F,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_radioactive[] PROGMEM = {
0x00,0x00, 0x25,0x00, 0x25,0x00, 0x37,0x00,
0x00,0x00, 0x1E,0x00, 0x1E,0x00, 0x40,0x00,
0x73,0x80, 0x1E,0x00,
};
static const uint8_t emoji_sm_cow[] PROGMEM = {
0x00,0x00, 0xC1,0x80, 0x7F,0x00, 0x3F,0x00,
0x3F,0x00, 0x7F,0x00, 0x7F,0x00, 0x7F,0x00,
0x36,0x00, 0x23,0x00,
};
static const uint8_t emoji_sm_alien[] PROGMEM = {
0x3F,0x00, 0x7F,0x80, 0x7F,0x80, 0xED,0xC0,
0xAD,0x40, 0x7F,0x80, 0x3F,0x00, 0x3F,0x00,
0x1E,0x00, 0x0C,0x00,
};
static const uint8_t emoji_sm_invader[] PROGMEM = {
0x33,0x00, 0x1E,0x00, 0x3F,0x00, 0x7F,0x80,
0x7F,0x80, 0x61,0x80, 0x73,0x80, 0x33,0x00,
0x33,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_dagger[] PROGMEM = {
0x00,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00,
0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x70,0x00,
0x70,0x00, 0x30,0x00,
};
static const uint8_t emoji_sm_grimace[] PROGMEM = {
0x3F,0x00, 0x61,0x80, 0x73,0x80, 0x40,0x80,
0x40,0x80, 0x7F,0x80, 0x55,0x00, 0x7F,0x80,
0x3F,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_telephone[] PROGMEM = {
0x00,0x00, 0xFF,0xC0, 0xC0,0xC0, 0xC0,0xC0,
0x61,0x80, 0x3F,0x00, 0x1E,0x00, 0x3F,0x00,
0x7F,0x80, 0x00,0x00,
};
static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
emoji_sm_wireless, emoji_sm_infinity, emoji_sm_trex, emoji_sm_skull, emoji_sm_cross,
emoji_sm_lightning, emoji_sm_tophat, emoji_sm_motorcycle, emoji_sm_seedling, emoji_sm_flag_au,
emoji_sm_umbrella, emoji_sm_nazar, emoji_sm_globe, emoji_sm_radioactive, emoji_sm_cow,
emoji_sm_alien, emoji_sm_invader, emoji_sm_dagger, emoji_sm_grimace, emoji_sm_telephone,
};
// ---- Codepoint lookup for UTF-8 detection ----
struct EmojiCodepoint {
uint32_t cp;
uint32_t cp2;
uint8_t escape;
};
static const EmojiCodepoint EMOJI_CODEPOINTS[20] = {
{ 0x1F6DC, 0x0000, 0x01 }, { 0x267E, 0x0000, 0x02 }, { 0x1F996, 0x0000, 0x03 },
{ 0x2620, 0x0000, 0x04 }, { 0x271D, 0x0000, 0x05 }, { 0x26A1, 0x0000, 0x06 },
{ 0x1F3A9, 0x0000, 0x07 }, { 0x1F3CD, 0x0000, 0x08 }, { 0x1F331, 0x0000, 0x09 },
{ 0x1F1E6, 0x1F1FA, 0x0A }, { 0x2602, 0x0000, 0x0B }, { 0x1F9FF, 0x0000, 0x0C },
{ 0x1F30F, 0x0000, 0x0D }, { 0x2622, 0x0000, 0x0E }, { 0x1F404, 0x0000, 0x0F },
{ 0x1F47D, 0x0000, 0x10 }, { 0x1F47E, 0x0000, 0x11 }, { 0x1F5E1, 0x0000, 0x12 },
{ 0x1F62C, 0x0000, 0x13 }, { 0x260E, 0x0000, 0x14 },
};
// ---- Helper functions ----
static uint32_t emojiDecodeUtf8(const uint8_t* s, int remaining, int* bytes_consumed) {
uint8_t b0 = s[0];
if (b0 < 0x80) { *bytes_consumed = 1; return b0; }
if ((b0 & 0xE0) == 0xC0 && remaining >= 2) {
*bytes_consumed = 2;
return ((uint32_t)(b0 & 0x1F) << 6) | (s[1] & 0x3F);
}
if ((b0 & 0xF0) == 0xE0 && remaining >= 3) {
*bytes_consumed = 3;
return ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F);
}
if ((b0 & 0xF8) == 0xF0 && remaining >= 4) {
*bytes_consumed = 4;
return ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F);
}
*bytes_consumed = 1;
return 0xFFFD;
}
static void emojiSanitize(const char* src, char* dst, int dstLen) {
const uint8_t* s = (const uint8_t*)src;
int si = 0, di = 0;
int srcLen = strlen(src);
while (si < srcLen && di < dstLen - 1) {
uint8_t b = s[si];
if (b >= 0xE0) {
int consumed;
uint32_t cp = emojiDecodeUtf8(s + si, srcLen - si, &consumed);
if (cp == 0xFE0F) { si += consumed; continue; }
bool found = false;
for (int e = 0; e < 20; e++) {
if (EMOJI_CODEPOINTS[e].cp == cp) {
if (EMOJI_CODEPOINTS[e].cp2 != 0) {
int consumed2;
if (si + consumed < srcLen) {
uint32_t cp2 = emojiDecodeUtf8(s + si + consumed, srcLen - si - consumed, &consumed2);
if (cp2 == EMOJI_CODEPOINTS[e].cp2) {
dst[di++] = EMOJI_CODEPOINTS[e].escape;
si += consumed + consumed2;
found = true; break;
}
}
continue;
}
dst[di++] = EMOJI_CODEPOINTS[e].escape;
si += consumed;
if (si + 2 < srcLen && s[si] == 0xEF && s[si+1] == 0xB8 && s[si+2] == 0x8F) si += 3;
found = true; break;
}
}
if (!found) si += consumed;
} else {
dst[di++] = (char)b;
si++;
}
}
dst[di] = '\0';
}
static inline bool isEmojiEscape(uint8_t b) {
return b >= EMOJI_ESCAPE_START && b <= EMOJI_ESCAPE_END;
}
// Encode a Unicode codepoint as UTF-8 into dst, return bytes written
static int emojiEncodeUtf8(uint32_t cp, uint8_t* dst) {
if (cp < 0x80) {
dst[0] = (uint8_t)cp;
return 1;
} else if (cp < 0x800) {
dst[0] = 0xC0 | (cp >> 6);
dst[1] = 0x80 | (cp & 0x3F);
return 2;
} else if (cp < 0x10000) {
dst[0] = 0xE0 | (cp >> 12);
dst[1] = 0x80 | ((cp >> 6) & 0x3F);
dst[2] = 0x80 | (cp & 0x3F);
return 3;
} else {
dst[0] = 0xF0 | (cp >> 18);
dst[1] = 0x80 | ((cp >> 12) & 0x3F);
dst[2] = 0x80 | ((cp >> 6) & 0x3F);
dst[3] = 0x80 | (cp & 0x3F);
return 4;
}
}
// Reverse of emojiSanitize: convert escape bytes back to UTF-8 emoji sequences
// Used before sending over mesh/BLE so other devices and apps see real emoji
// dst must be large enough (worst case: srcLen * 8 for all flag emoji)
static void emojiUnescape(const char* src, char* dst, int dstLen) {
int si = 0, di = 0;
int srcLen = strlen(src);
while (si < srcLen && di < dstLen - 1) {
uint8_t b = (uint8_t)src[si];
if (b == EMOJI_PAD_BYTE) {
si++; // Skip padding bytes
continue;
}
if (isEmojiEscape(b)) {
int idx = b - EMOJI_ESCAPE_START;
if (idx < 20) {
uint8_t utf8[8];
int len = emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp, utf8);
// Encode second codepoint if present (flag sequences)
if (EMOJI_CODEPOINTS[idx].cp2 != 0) {
len += emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp2, utf8 + len);
}
if (di + len < dstLen) {
memcpy(dst + di, utf8, len);
di += len;
} else break; // No room
}
si++;
} else {
dst[di++] = src[si++];
}
}
dst[di] = '\0';
}
// Get large sprite (for compose/picker)
static inline const uint8_t* getEmojiSpriteLg(uint8_t escape_byte) {
if (!isEmojiEscape(escape_byte)) return nullptr;
return (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_LG[escape_byte - EMOJI_ESCAPE_START]);
}
// Get small sprite (for channel view)
static inline const uint8_t* getEmojiSpriteSm(uint8_t escape_byte) {
if (!isEmojiEscape(escape_byte)) return nullptr;
return (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_SM[escape_byte - EMOJI_ESCAPE_START]);
}
// Get the UTF-8 wire cost in bytes for an escape byte (4 for most emoji, 8 for flags)
static inline int emojiUtf8Cost(uint8_t escape_byte) {
if (!isEmojiEscape(escape_byte)) return 1; // not an emoji, regular char
int idx = escape_byte - EMOJI_ESCAPE_START;
uint32_t cp = EMOJI_CODEPOINTS[idx].cp;
int cost = (cp < 0x80) ? 1 : (cp < 0x800) ? 2 : (cp < 0x10000) ? 3 : 4;
if (EMOJI_CODEPOINTS[idx].cp2 != 0) {
uint32_t cp2 = EMOJI_CODEPOINTS[idx].cp2;
cost += (cp2 < 0x80) ? 1 : (cp2 < 0x800) ? 2 : (cp2 < 0x10000) ? 3 : 4;
}
return cost;
}