Detecting emoji that Windows 10 can't draw, and swapping in images without breaking copy
I run CopyCute, a small site of cute symbols and kaomoji. I started it after watching a friend search all over the place just to find a few emoji they wanted. Almost everything on the site is plain text, which is the whole point: you tap an item, it goes to your clipboard, and you paste it into a bio or a nickname.
Plain text has one catch. I don't control how a character is drawn. The visitor's fonts do. For most symbols that's fine, but emoji have a hard cutoff on Windows 10, and I wanted to handle it without making the site worse for everyone else.
The problem
Windows 10's last emoji update was the May 2019 Update (version 1903), which added Emoji 12.0. Anything newer, like 🩷 pink heart (Emoji 15) or 🫧 bubbles (Emoji 14), only shipped with Windows 11. Windows also has no flag emoji at all; it shows two letters instead.
My development PC runs Windows 10, so I hit this myself while building the site: some emoji showed up as empty boxes. On my site that affects 10 different emoji out of 2,862 items. Not many, but they're in popular sections like hearts and flowers.
I didn't want to just delete them. They look fine on phones and on Windows 11, so I wanted to see how far I could get with compatibility instead. I also didn't want to replace them with images for everyone, because then most visitors would see pictures that don't match their own emoji.
So the plan was: find the risky emoji at build time, test them on the visitor's device, and only swap in an image where the test fails.
Step 1: list every emoji Windows 10 can't draw
Unicode publishes emoji-test.txt, which lists every emoji with the version it was added in. A small script keeps the fully-qualified sequences newer than Emoji 12.0, plus every flag:
const m = line.match(/^([0-9A-F ]+?)\s*;\s*fully-qualified\s*#\s*\S+\s+E(\d+\.\d+)\s+(.*)$/);
if (!m) continue;
const v = Number(m[2]), name = m[3].trim();
const flag = subgroup === 'country-flag' || subgroup === 'subdivision-flag';
if (!flag && v <= 12.0) continue;
// the Emoji 12.1 skin tones of "people holding hands" did reach Windows 10
if (!flag && v === 12.1 && /^people holding hands/.test(name)) continue;
With Emoji 18.0 data that gives 1,204 sequences, 262 of them flags. The script writes them to a JS file keyed by code points, with FE0F (the emoji presentation selector) removed so matching doesn't depend on it.
At build time, every item on every page is scanned for those sequences, longest match first, and each match is wrapped:
<span class="emo" data-emo="1fa77">🩷</span>
Step 2: test on the device, not the user agent
Checking the user agent doesn't work: Windows 11 still reports "Windows NT 10.0". Chromium can tell them apart with navigator.userAgentData.getHighEntropyValues(['platformVersion']), but Firefox can't, and the font installed matters more than the OS version anyway.
So the page draws each risky emoji on a small canvas and looks at the pixels. The trick is to draw it twice, in two different fill colours. A colour emoji glyph ignores fillStyle. An empty box, or a black-and-white fallback glyph, is painted in it.
const draw = (colour) => {
ctx.clearRect(0, 0, SIZE * 2, SIZE * 2);
ctx.fillStyle = colour;
ctx.fillText(ch, 0, 4);
return ctx.getImageData(0, 0, SIZE * 2, SIZE * 2).data;
};
const a = draw('#f00'), b = draw('#00f');
let ink = false;
for (let i = 0; i < a.length; i += 4) {
if (a[i + 3] > 0) ink = true;
if (Math.abs(a[i] - b[i]) > 8 || Math.abs(a[i + 2] - b[i + 2]) > 8) return false; // took the fill colour
}
if (!ink) return false; // nothing drawn at all
Same pixels in both colours, and some ink, means a real colour emoji.
ZWJ sequences need one more check. A font that doesn't know a sequence like ❤️🔥 draws its parts side by side, and each part passes the colour test. So if the sequence is much wider than its first emoji, I treat it as unsupported:
if (ch.includes('\u200d')) {
const first = String.fromCodePoint(ch.codePointAt(0));
if (ctx.measureText(ch).width > ctx.measureText(first).width * 1.5) return false;
}
The test waits for document.fonts.ready and runs in requestIdleCallback, so it doesn't block the first paint. Results are cached in localStorage, keyed by the user agent, so it only runs once per browser version.
Step 3: swap the picture, keep the text
When the test fails, the emoji is replaced on screen by a PNG from Google's open-source Noto Emoji set (Apache 2.0). The original character stays in the DOM, visually hidden, so selecting and copying the line still gives the real emoji. The copy buttons read from a data-copy attribute anyway, so what you paste is never an image.
The section also shows a short note: some emoji here are too new for this device, so they're shown as pictures, and the copied text is still the real emoji.
One trade-off: on a first visit, the box can show for a moment before the test runs. After that the result is cached.
Step 4: make the build fail when I forget
The hardest part wasn't the code. It was checking the result on Windows 10, Windows 11, Android and iOS. So I made the build check what it can by itself. After astro build, a script reads the generated HTML and fails if:
- a Windows 10 emoji appears outside a wrapped item, so it would have no fallback
- a .emo wrapper is around an emoji Windows 10 can draw
- a section has wrapped emoji but no note
- a fallback PNG or the Noto licence file is missing
A separate Python script uses fontTools to check every non-ASCII character on the site (1,767 right now) against 8 common fonts. The latest run found none without a glyph. That doesn't prove every phone draws them, so the site says so instead of promising they work everywhere.
What I'd still like to improve
- The box can flash briefly on the first visit. I could hide wrapped emoji until the test finishes, but that hides them for most people who don't need it.
- The colour test is a heuristic. If you know a case where a colour glyph changes with fillStyle, or a box glyph doesn't, I'd like to hear it.
- I want to keep adding emoji and symbols while keeping each page clean, so people can still find what they need quickly.
The list of 1,204 sequences is in a small open dataset: cute-symbols-data.
So what's missing? If there's an emoji or symbol you use all the time and can't find on the site, tell me in the comments.
Source credits and setup:
github.com/QasimAliSidhu/3-friends-dairy-storefro…
Font sources: Manrope and DM Serif Display from Google Fonts.
googlefonts/manrope
googlefonts/dm-fonts
1 like · 6 views
Free Font Downloader to get started (PowerShell)(Windows)
A simple PowerShell script to fetch a curated collection of free/open-source fonts (great for fresh installs or lightweight suites). If the script confuses you or you don't trust it you can find links to each font at the end of the post.
For people who don't have a lot of fonts or on a machine that has not had Office or Adobe installed before ArtCraft:
I put together a little downloader of my favorite free fonts. This script will download around 30 of my favorite free fonts directly from their sources for you into a little folder named Phobic Favorite Free Fonts on your desktop.
How to Run It (No Admin Rights Needed!)
- Press the Windows Key, type PowerShell, and open Windows PowerShell (or Terminal).
- Copy the script code provided below.
- Right-click inside the PowerShell window to paste the code, then press Enter.
- The script will automatically create a Phobic Favorite Free Fonts folder on your Desktop, download all the fonts directly from their official sources, and pop open the folder when it's done!
- From the folder, you can review and install the fonts normally.
All these fonts are free but some have different licenses. See bottom of the post. In particular Heatwave, Another Danger are free for personal use commercial use requires license, and Clash Grotesk liscence requires direct individual download which this script does (technically correct, though possibly not how it was originally intended).
<#
===============================================================================
PHOBIC FAVORITE FREE FONTS DOWNLOADER - LICENSE & USAGE BREAKDOWN
===============================================================================
SIL Open Font License 1.1 (Free for personal & commercial use):
- Inter, Montserrat, Open Sans, Poppins, JetBrains Mono, Fira Code,
Merriweather, Playfair Display, Fraunces, Lora, Bricolage Grotesque,
Overused Grotesk, Road Rage, Rubik Wet Paint, Sedgwick Ave Display,
Jost, Manrope
Apache License 2.0 (Free for personal & commercial use):
- Roboto, Permanent Marker
Vic Fieger Freeware (Free for personal & commercial use, not open-source):
- Edo SZ
ITF Free Font License (Free for personal & commercial use):
- Clash Grotesk
- Note: Fontshare terms require direct individual download, which this
script executes directly. Do not re-host or redistribute the font file.
Personal Use Only / Demo (Free for personal use / memes only):
- Another Danger (Demo)
- Commercial license sold separately by The Branded Quotes.
Free Font (Personal use only):
- Heatwave Free Font
- Commercial license available via Maulana Creative.
===============================================================================
#>
$out = Join-Path ([Environment]::GetFolderPath("Desktop")) "Phobic Favorite Free Fonts"
$licOut = Join-Path $out "Licenses"
New-Item -ItemType Directory -Force -Path $out, $licOut | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$web = New-Object System.Net.WebClient
$web.Headers.Add("User-Agent", "Mozilla/5.0")
$web.Headers.Add("Referer", "dafont.com")
# ---------- FONT FILES ----------
$fonts = @(
@{ File = "Montserrat-Regular.ttf"; Url = "github.com/googlefonts/montserrat/raw/master/font…" },
@{ File = "Montserrat-Bold.ttf"; Url = "github.com/googlefonts/montserrat/raw/master/font…" },
@{ File = "OpenSans-Regular.ttf"; Url = "github.com/googlefonts/opensans/raw/main/fonts/tt…" },
@{ File = "OpenSans-Bold.ttf"; Url = "github.com/googlefonts/opensans/raw/main/fonts/tt…" },
@{ File = "Roboto-Regular.ttf"; Url = "github.com/googlefonts/roboto/raw/main/src/hinted…" },
@{ File = "Roboto-Bold.ttf"; Url = "github.com/googlefonts/roboto/raw/main/src/hinted…" },
@{ File = "Poppins-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/poppins/Popp…" },
@{ File = "Poppins-Bold.ttf"; Url = "github.com/google/fonts/raw/main/ofl/poppins/Popp…" },
@{ File = "JetBrainsMono-Regular.ttf"; Url = "github.com/JetBrains/JetBrainsMono/raw/master/fon…" },
@{ File = "PermanentMarker-Regular.ttf"; Url = "github.com/google/fonts/raw/main/apache/permanent…" },
@{ File = "EdoSZ.ttf"; Url = "github.com/adam-lynch/fonts/raw/master/edosz.ttf" },
@{ File = "Merriweather-Regular.ttf"; Url = "github.com/SorkinType/Merriweather/raw/master/fon…" },
@{ File = "PlayfairDisplay-Regular.ttf"; Url = "github.com/fonts/Playfair-Display/raw/master/TTF/…" },
@{ File = "Fraunces72pt-Regular.ttf"; Url = "github.com/googlefonts/fraunces/raw/master/fonts/…" },
@{ File = "Lora-Regular.ttf"; Url = "github.com/cyrealtype/Lora/raw/main/fonts/ttf/Lor…" },
@{ File = "BricolageGrotesque-Regular.ttf"; Url = "github.com/ateliertriay/bricolage/raw/main/fonts/…" },
@{ File = "BricolageGrotesque-Bold.ttf"; Url = "github.com/ateliertriay/bricolage/raw/main/fonts/…" },
@{ File = "OverusedGrotesk-Regular.ttf"; Url = "github.com/RandomMaerks/Overused-Grotesk/raw/HEAD…" },
@{ File = "OverusedGrotesk-Bold.ttf"; Url = "github.com/RandomMaerks/Overused-Grotesk/raw/HEAD…" },
@{ File = "RoadRage-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/roadrage/Roa…" },
@{ File = "RubikWetPaint-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/rubikwetpain…" },
@{ File = "SedgwickAveDisplay-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/sedgwickaved…" },
@{ File = "Jost-Regular.ttf"; Url = "github.com/indestructible-type/Jost/raw/master/fo…" },
@{ File = "Jost-Bold.ttf"; Url = "github.com/indestructible-type/Jost/raw/master/fo…" },
@{ File = "Manrope-Regular.ttf"; Url = "github.com/googlefonts/manrope/raw/master/fonts/t…" },
@{ File = "Manrope-Bold.ttf"; Url = "github.com/googlefonts/manrope/raw/master/fonts/t…" }
)
foreach ($font in $fonts) {
Write-Host "Downloading $($font.File)..."
try {
$web.DownloadFile($font.Url, (Join-Path $out $font.File))
} catch {
Write-Warning "Failed: $($font.File) - $($_.Exception.Message)"
}
}
# ---------- ZIPPED FONTS ----------
$zips = @(
@{ Url = "github.com/rsms/inter/releases/download/v4.1/Inte…"; Files = @("Inter-Regular.ttf", "Inter-Bold.ttf") },
@{ Url = "github.com/tonsky/FiraCode/releases/download/6.2/…"; Files = @("FiraCode-Regular.ttf") },
@{ Url = "dl.dafont.com"; Files = @("Another Danger - Demo.otf", "Another Danger Slanted - Demo.otf") },
@{ Url = "dl.dafont.com"; Files = @("Heatwave Free Font.ttf") },
@{ Url = "api.fontshare.com"; Files = @("ClashGrotesk-Regular.otf", "ClashGrotesk-Bold.otf") }
)
foreach ($zip in $zips) {
Write-Host "Downloading $($zip.Files -join ', ')..."
$zipPath = Join-Path $env:TEMP ([guid]::NewGuid().ToString() + ".zip")
$extract = Join-Path $env:TEMP ([guid]::NewGuid().ToString())
try {
$web.DownloadFile($zip.Url, $zipPath)
Expand-Archive -LiteralPath $zipPath -DestinationPath $extract -Force
foreach ($name in $zip.Files) {
$found = Get-ChildItem -LiteralPath $extract -Recurse -Filter $name | Select-Object -First 1
if ($found) {
Copy-Item -LiteralPath $found.FullName -Destination (Join-Path $out $name) -Force
} else {
Write-Warning "Could not find $name inside the zip"
}
}
# Also grab any license/readme text files from the zip
Get-ChildItem -LiteralPath $extract -Recurse -Include .txt,.md,LICENSE*,OFL* -ErrorAction SilentlyContinue |
ForEach-Object {
$destName = $_.Name
if ($destName -match "license|ofl|readme" -or $_.Extension -eq ".txt") {
Copy-Item $_.FullName (Join-Path $licOut $destName) -Force -ErrorAction SilentlyContinue
}
}
} catch {
Write-Warning "Failed zip: $($zip.Url) - $($_.Exception.Message)"
} finally {
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $extract -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ---------- LICENSE FILES ----------
$licenses = @(
@{ Name = "Montserrat-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "OpenSans-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Roboto-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Poppins-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "JetBrainsMono-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "PermanentMarker-Apache.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Merriweather-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "PlayfairDisplay-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Fraunces-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Lora-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "BricolageGrotesque-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "OverusedGrotesk-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "RoadRage-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "RubikWetPaint-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "SedgwickAveDisplay-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Jost-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Manrope-OFL.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "Inter-LICENSE.txt"; Url = "raw.githubusercontent.com" },
@{ Name = "FiraCode-LICENSE.txt"; Url = "raw.githubusercontent.com" }
)
Write-Host "`nDownloading license files..."
foreach ($lic in $licenses) {
Write-Host " $($lic.Name)"
try {
$web.DownloadFile($lic.Url, (Join-Path $licOut $lic.Name))
} catch {
Write-Warning " Failed: $($lic.Name) - $($_.Exception.Message)"
}
}
$web.Dispose()
Write-Host "`nDone."
Write-Host "Fonts -> $out"
Write-Host "Licenses -> $licOut"
explorer $out
Included Fonts & License Breakdown
- SIL Open Font License 1.1 (Free for personal & commercial use):
- Inter, Montserrat, Open Sans, Poppins, JetBrains Mono, Fira Code, Merriweather, Playfair Display, Fraunces, Lora, Bricolage Grotesque, Overused Grotesk, Road Rage, Rubik Wet Paint, Sedgwick Ave Display, Jost, Manrope.
- Apache License 2.0 (Free for personal & commercial use):
- Roboto, Permanent Marker.
- Vic Fieger Freeware (Free for personal & commercial use, not open-source):
- Edo SZ.
- ITF Free Font License (Free for personal & commercial use; direct download via script):
- Clash Grotesk (Note: Fontshare terms require direct individual download which this script does (technically correct but possibly not how it was intended); do not redistribute the file directly).
- Personal Use Only / Demo (Free for personal use / memes only):
- Another Danger (Demo) (Commercial license sold separately by The Branded Quotes).
- Free Font (Commercial use requires a license):
- Heatwave Free Font (Commercial license available via Maulana Creative).
Included Fonts & Licenses
- Inter — SIL Open Font License 1.1 (GitHub)
- Montserrat — SIL Open Font License 1.1 (Google Fonts)
- Open Sans — SIL Open Font License 1.1 (Google Fonts)
- Poppins — SIL Open Font License 1.1 (Google Fonts)
- JetBrains Mono — SIL Open Font License 1.1 (GitHub)
- Fira Code — SIL Open Font License 1.1 (GitHub)
- Merriweather — SIL Open Font License 1.1 (Google Fonts)
- Playfair Display — SIL Open Font License 1.1 (Google Fonts)
- Fraunces — SIL Open Font License 1.1 (Google Fonts)
- Lora — SIL Open Font License 1.1 (Google Fonts)
- Bricolage Grotesque — SIL Open Font License 1.1 (Google Fonts)
- Overused Grotesk — SIL Open Font License 1.1 (GitHub)
- Road Rage — SIL Open Font License 1.1 (Google Fonts)
- Rubik Wet Paint — SIL Open Font License 1.1 (Google Fonts)
- Sedgwick Ave Display — SIL Open Font License 1.1 (Google Fonts)
- Jost — SIL Open Font License 1.1 (Google Fonts)
- Manrope — SIL Open Font License 1.1 (Google Fonts)
- Roboto — Apache License 2.0 (Google Fonts)
- Permanent Marker — Apache License 2.0 (Google Fonts)
- Edo SZ — Freeware: Free for personal & commercial use, not open-source (DaFont)
- Clash Grotesk — ITF Free Font License: Direct individual download via script (technically correct, though possibly not how it was originally intended; do not redistribute directly) (Fontshare)
- Another Danger (Demo) — Personal Use Only / Demo: Commercial license sold by The Branded Quotes (DaFont)
- Heatwave — Free for personal use: Commercial use requires a license from Maulana Creative (DaFont)
Also if you're on old hardware and find the launcher to be laggy check out my little powershell Artcraft App Updater: reddit.com
20 points · 7 comments
Grok botからM5PaperMonoに情報を表示させよう
みんな、こんにちは!電子ペーパーにパーソナライズされた予定やニュースが毎日自動で出てたら便利だと思わない?でも、表示する内容を毎回自分で作って送るのは面倒だよね?そんなみんなのために、ボクが大好きなGrok botにお願いすることでM5PaperMonoっていう電子ペーパー搭載のマイコンモジュールの表示を毎朝・毎晩自動で更新してもらう方法をまとめたよ。間にFirebaseのRealtime Database(RTDB)を置くだけで、意外とお手軽にできるから、今回やってみた内容を紹介するよ。
x.com
この先の内容について
過去の記事ではソースコードや設定とか載せていたけど、AI時代にそれらを載せる意味がとっても薄れてきたから、この記事では仕組みとかやり方はまとめているけど、具体的なソースコードや詳細な設定などは載せていないよ。自前で実装するかAIに指示して作ってみてね。
やること
M5StackのPaperMonoに次のような内容を毎日自動で表示するよ。ボクはこの構成でやってみたけど、好きなデバイスやデータの置き場所でやってみてね。
毎朝:今日の予定と、準備しておくこと
毎晩:明日の予定、近い期限、今日のAI・電子工作のニュース
作ったものの動きはこんな感じ:
Grok botが決まった時刻に、Googleカレンダーやウェブから情報を集めて表示内容を作成
Grok botがFirebase RTDBの /display/info.json に文章を書き込む
M5PaperMonoがRTDBのデータを読み込んで画面に表示する
環境
今回ボクが使った環境は以下の通りだよ。
表示用デバイスと開発環境
デバイス:
M5Stack PaperMono(ESP32-S3/Flash 16MB)
開発ツール:
PlatformIO
フレームワーク:
Arduino
主要ライブラリ:
M5Unified 0.2.20、M5GFX 0.2.27、ArduinoJson 6.21.5、OpenFontRender、M5PM1
バックエンド:
Firebase Realtime Database、Firebase Authentication(メール/パスワード認証)
通信方式:
Wi-Fi経由のHTTPS REST API
セットアップ、コーディング:
Codex
AIエージェント
Grok bot(定期実行の「ルーチン」機能を使用)
対象読者
電子ペーパーにAIが作った情報を自動で表示したい
M5StackでIoTっぽいことをやりたい
AIエージェントから外のデータベースに書き込む方法を知りたい
全体の構成
やってることはとってもシンプル。真ん中にFirebase RTDBの小さなJSONを置いて、Grok botが書いて、M5PaperMonoが読むだけだよ。この構成の良いところは、Grok botもFirebaseもクラウド上にあるから、ローカルのPCで情報をまとめたり、ブリッジみたいな環境を用意しなくても済むところ。M5PaperMonoがネットに繋がる環境にあればそれでOKってとこだね。
Grok botってなに?
Grok botは、SpaceXAI社が提供するGrokサービスの一部で、チャットでお願いするだけで色々な作業をしてくれるAIエージェントだよ。今回使ったのはGrok botの「ルーチン」という機能で、やってほしいことを文章で登録しておくと、決めた時刻に自動で実行してくれるよ。Grok botはGoogleカレンダーを読んだり、ウェブを検索したりだけじゃなくて、実はWebhookでトリガーしたりREST APIを呼んだりもできるよ。今回はこのREST APIという機能をGrok botから使うことでFirebase RTDBへの書き込みを実現しているよ。
grok.com
Firebase RTDBってなに?
FirebaseはGoogleが提供するクラウドサービスで、Firebase Realtime Database(RTDB)はFirebaseのサービスの一部で、JSON形式のデータをクラウドに置いておける仕組みだよ。このFirebase RTDBはREST APIで読み書きできるから、M5PaperMonoのようなマイコンからもGrok botのようなAIエージェントからも、HTTPリクエストを送るだけで使えるよ。
firebase.google.com
M5PaperMonoってなに?
M5PaperMonoは、M5Stack社が出している電子ペーパーの開発ボードだよ。3.97インチ・480×800のタッチ対応の白黒電子ペーパー(4階調グレースケール)に、Wi-Fiが使えるESP32-S3と1150mAhのバッテリーを内蔵しているよ。電子ペーパーは書き換えるときにしか電気を使わないから、電源をOFFしても一度表示した内容はそのまま残ってくれるよ。今回はこのM5PaperMonoにFirebase RTDBから読み込んだ情報を表示するよ。
docs.m5stack.com
PoCの進め方
PoC (Proof of Concept) は、小さく作って、うまく動くか確かめることだよ。ボクがこれを作った時の進め方を書いているよ。特に今回のように複数のデバイスやシステムが関係している場合、いきなり全部つなげると、うまく動かないときにどこが悪いのか分からなくなっちゃうよね。だから、ボクはこんな順番で1つずつ確認しながら進めてみたよ。
CodexにFirebaseのセットアップをお願いする
Grok bot/M5PaperMono用の各アカウントで、ルール通りにJSONを読み書きできるか確認
Grok botから試しに書き込んでみる
M5PaperMonoの表示部分を作る
準備完了!Grok botのルーチンで毎日動かす
順番に確認して各ステップを1つずつクリアしていくことで、次のステップに問題を持ち越さなくて済むからどこが問題なのかをすぐ切り分けられるよ。ここからは、この順番で説明していくよ。
Firebaseをセットアップしよう
まずはステップ1と2、Firebaseにデータの置き場所を作って、権限通りに読み書きできるところまで確認するよ。
Firebaseのセットアップ手順
ボクはこの手順のほとんどはCodexにお願いしてやってもらったよ。任せられるところはどんどんAIに任せちゃおう。
Firebaseコンソールでプロジェクトを作る
「Realtime Database」を開いて、データベースを作る(最初はロックモード)
「Authentication」でメール/パスワード認証をオンにして、Grok bot用とM5PaperMono用のユーザーを作る
Firebase CLIを入れて、ルールを反映する
Firebase CLIは以下のコマンドでインストールしてログインできるよ。これも含めてCodexに任せてもいいよね。
npm install -g firebase-tools
firebase login
補足だよ。
RTDBのロケーションは日本が選べないから、近いシンガポール(asia-southeast1)がおすすめだよ
データベースのURLは https://<プロジェクト名>-default-rtdb.<リージョン>.firebasedatabase.app の形で、コンソールのデータ画面に出てくるよ
Firebase CLIを使うには、先にNode.jsを入れておいてね
ルールは database.rules.json に書いて、中身は下の「セキュリティルール」で説明するね
表示用のデータ
今回の表示用データは、こんな小さなJSONにしているよ。
{
"title": "...",
"message": "1行目\n2行目\n...",
"updatedAt": 1760000000000,
"value": 0
}
message:画面に出す文章。改行は \n
updatedAt:更新時刻(Unixミリ秒の数値)。M5PaperMono側で取得したとき、情報が「新しくなったか」を判断するために使用するよ
title と value:テスト用に入れているだけで、実際には使ってないよ
セキュリティルール
Grok botには、書き込みが必要なパスだけ許可しているよ。ルールを設定しておけば、もしAIが書き込む情報の構造などを間違えても、ほかのデータは壊れないから安心だね。
/display/info.json だけ書き込み可
形も固定(title と message は文字列、updatedAt と value は数値)
認証
Firebase Authenticationのメール+パスワードで、Grok bot専用のユーザーとM5PaperMono用の2つ作ったよ。それぞれ以下のように設定しているよ。パスワードはGrok botでルーチンの設定をお願いするときに安全に保存してもらえるよ。
M5PaperMono用アカウント:/display/info の読み取りのみ
Grok bot用アカウント:/display/info の読み取り・書き込み
未認証のアクセス:読み取り・書き込みともに拒否
権限通りに読み書きできるか確認しよう
Grok botをつなぐ前に、それぞれのアカウントでIDトークンを取って、curlで読み書きを試しておくよ。ボクは全てCodexに確認をお願いしたよ。
Grok bot用のアカウントで /display/info.json に書き込めること
Grok bot用のアカウントで、ほかの場所には書き込めないこと(拒否されればOK)
M5デバイス用のアカウントで /display/info.json を読めること
ここで期待通りの結果になれば、Firebase側の設定は完了だよ。
Grok botにお願いしよう
ステップ3、Grok botからFirebaseに書き込んでみるよ。基本的にはGrok botにやって欲しいことを伝えるだけでOKだよ。Grok botから更新するのは message と updatedAt の2つだけ。Grok botにFirebase RTDBのURLを教えてあげると、APIキーとか聞かれるからCodexに聞いて必要な情報をもらってGrok botへ入力するよ。
Grok botに教えてあげる情報をまとめるとこんな感じ:
Firebase RTDBのURL(FirebaseでRTDBを設定したときに生成されたhttps://<プロジェクト名>-default-rtdb.<リージョン>.firebasedatabase.app)
許可しているパスは/display/info.jsonだけであること
FirebaseのAPIキー(FirebaseをセットアップしたCodexに教えてもらう)
Grok bot用のメールアドレスとパスワード
書き込んだあとは、Grok botから書き込んだ内容を読み出して、さっき書き込んだ値がちゃんと反映されたか確認してもらってね。
M5PaperMonoの制約に合わせて文章を作る
M5PaperMonoの画面に合わせて、こんなルールで文章を作ってもらったよ。
最大16行
1行は全角18文字以内(超えると端で折り返されて、別の行になっちゃう)
絵文字は使わない
送る前に、行数と各行の文字数をプログラムで数えて確認する
AIに「18文字以内で」ってお願いしても守れないことがあるから、最後はコードで数えるのがおすすめだよ。あと、アスキーアートを出したいときは、半角文字だと実機でずれちゃうから、全角文字だけで作る必要があったよ。これはM5PaperMono側で使うフォントにも依存するかな。
M5PaperMonoで表示してみよう
ステップ4、いよいよFirebase RTDBに入った文章をM5PaperMonoに表示するよ。今回は全てCodexに任せて以下のような仕様でM5PaperMono用のファームを作ってもらったよ。
M5PaperMonoの動作
起動後、Wi-Fiに接続して時刻を同期
Firebase AuthenticationでM5PaperMono用アカウントにログイン
取得したIDトークンを使い、HTTPSでJSONを取得
本文と、日本時間に変換した更新日時を表示
内蔵タイマーを設定して電源を落とし、1時間後に自動起動
日本語の表示用に「Zen丸ゴシック」というフォントを使用したよ。
googlefonts/zen-marugothic
毎日動かしてみよう
最後にステップ5。Grok botのルーチンで毎日自動でFirebaseの情報を更新してもらえるようにするよ。これもGrok botにお願いしてルーチンを作ってもらうだけだよ。M5PaperMonoで表示したい内容をお願いするだけで毎日ちゃんと動いてくれるのがいい感じだよ。例えばボクはこんな感じで朝と夜に情報を更新してもらっているよ。
朝(7:30ごろ)
Googleカレンダーを読む(読むだけで、予定は作ったり変えたりしない)
今日の予定、今週の残り、2〜3週間先の準備が必要な予定をまとめてチャットで送る(これはスマホとかで直接見る用に送ってもらってるよ)
今日の予定と、準備しておくことを電子ペーパーに表示する
夜(21:00ごろ)
明日の予定と、近い期限・準備が必要なことをカレンダーから拾う
AI・M5Stack・マイコン・電子工作・電子ペーパーの今日のニュースを、ウェブ検索と公式Xから3〜5件集める(確認できたものだけ)
「明日の予定」「共有したいこと」「今日の動き」の順に並べて、行が余ったら一言を添える
M5PaperMonoで手動更新するかGrok botのルーチンの時間が過ぎるのを待ってM5PaperMonoの画面を確認してみてね。画面の表示が更新されて最新の予定とかを表示することができたかな?予定だけじゃなくてタスクの一覧とか買い物メモとかを自動的に表示しておくとかにも使えそうだよね!
まとめ
今回は、Grok botを使って日々の予定をM5PaperMonoに表示してもらう方法をまとめたよ。真ん中にFirebase RTDBの小さなJSONを置くだけで、Grok botとM5PaperMonoを簡単に繋げられたね。ほしい情報が毎日勝手に電子ペーパーに出てくるのは、とっても便利だね!みんなも、普段使ってるAIエージェントに予定とかまとめてもらって電子ペーパーに表示してみてね。
じゃあ、またね!
It's not "some obscure module" that is written in Rust. Just some examples I know of:
- Chrome currently uses googlefonts/fontations to implement its font handling, and text is the main thing on most web pages.
- Windows' DirectWriteCore for font handling is also now written in Rust.
- Firefox's whole CSS engine and GPU 2D rendering engine are written in Rust.
あまりにも神ライブラリすぎる
googlefonts/fontations
23 views
@chetellik @0bNARA Yea that’s what they’re doing. Same with Arabic. For most of those Chinese languages the character sets aren’t even defined. For example Wu: github.com/googlefonts/lang/blob/main/Lib/gflangu…
As opposed to say Yue github.com/googlefonts/lang/blob/main/Lib/gflangu… t.co
3 likes · 25 views
Mentions
Detecting emoji that Windows 10 can't draw, and swapping in images without breaking copy I run CopyCute, a small site of cute symbols and kaomoji. I started it after watching a friend search all over the place just to find a few emoji they wanted. Almost everything on the site is plain text, which is the whole point: you tap an item, it goes to your clipboard, and you paste it into a bio or a nickname. Plain text has one catch. I don't control how a character is drawn. The visitor's fonts do. For most symbols that's fine, but emoji have a hard cutoff on Windows 10, and I wanted to handle it without making the site worse for everyone else. The problem Windows 10's last emoji update was the May 2019 Update (version 1903), which added Emoji 12.0. Anything newer, like 🩷 pink heart (Emoji 15) or 🫧 bubbles (Emoji 14), only shipped with Windows 11. Windows also has no flag emoji at all; it shows two letters instead. My development PC runs Windows 10, so I hit this myself while building the site: some emoji showed up as empty boxes. On my site that affects 10 different emoji out of 2,862 items. Not many, but they're in popular sections like hearts and flowers. I didn't want to just delete them. They look fine on phones and on Windows 11, so I wanted to see how far I could get with compatibility instead. I also didn't want to replace them with images for everyone, because then most visitors would see pictures that don't match their own emoji. So the plan was: find the risky emoji at build time, test them on the visitor's device, and only swap in an image where the test fails. Step 1: list every emoji Windows 10 can't draw Unicode publishes emoji-test.txt, which lists every emoji with the version it was added in. A small script keeps the fully-qualified sequences newer than Emoji 12.0, plus every flag: const m = line.match(/^([0-9A-F ]+?)\s*;\s*fully-qualified\s*#\s*\S+\s+E(\d+\.\d+)\s+(.*)$/); if (!m) continue; const v = Number(m[2]), name = m[3].trim(); const flag = subgroup === 'country-flag' || subgroup === 'subdivision-flag'; if (!flag && v <= 12.0) continue; // the Emoji 12.1 skin tones of "people holding hands" did reach Windows 10 if (!flag && v === 12.1 && /^people holding hands/.test(name)) continue; With Emoji 18.0 data that gives 1,204 sequences, 262 of them flags. The script writes them to a JS file keyed by code points, with FE0F (the emoji presentation selector) removed so matching doesn't depend on it. At build time, every item on every page is scanned for those sequences, longest match first, and each match is wrapped: <span class="emo" data-emo="1fa77">🩷</span> Step 2: test on the device, not the user agent Checking the user agent doesn't work: Windows 11 still reports "Windows NT 10.0". Chromium can tell them apart with navigator.userAgentData.getHighEntropyValues(['platformVersion']), but Firefox can't, and the font installed matters more than the OS version anyway. So the page draws each risky emoji on a small canvas and looks at the pixels. The trick is to draw it twice, in two different fill colours. A colour emoji glyph ignores fillStyle. An empty box, or a black-and-white fallback glyph, is painted in it. const draw = (colour) => { ctx.clearRect(0, 0, SIZE * 2, SIZE * 2); ctx.fillStyle = colour; ctx.fillText(ch, 0, 4); return ctx.getImageData(0, 0, SIZE * 2, SIZE * 2).data; }; const a = draw('#f00'), b = draw('#00f'); let ink = false; for (let i = 0; i < a.length; i += 4) { if (a[i + 3] > 0) ink = true; if (Math.abs(a[i] - b[i]) > 8 || Math.abs(a[i + 2] - b[i + 2]) > 8) return false; // took the fill colour } if (!ink) return false; // nothing drawn at all Same pixels in both colours, and some ink, means a real colour emoji. ZWJ sequences need one more check. A font that doesn't know a sequence like ❤️🔥 draws its parts side by side, and each part passes the colour test. So if the sequence is much wider than its first emoji, I treat it as unsupported: if (ch.includes('\u200d')) { const first = String.fromCodePoint(ch.codePointAt(0)); if (ctx.measureText(ch).width > ctx.measureText(first).width * 1.5) return false; } The test waits for document.fonts.ready and runs in requestIdleCallback, so it doesn't block the first paint. Results are cached in localStorage, keyed by the user agent, so it only runs once per browser version. Step 3: swap the picture, keep the text When the test fails, the emoji is replaced on screen by a PNG from Google's open-source Noto Emoji set (Apache 2.0). The original character stays in the DOM, visually hidden, so selecting and copying the line still gives the real emoji. The copy buttons read from a data-copy attribute anyway, so what you paste is never an image. The section also shows a short note: some emoji here are too new for this device, so they're shown as pictures, and the copied text is still the real emoji. One trade-off: on a first visit, the box can show for a moment before the test runs. After that the result is cached. Step 4: make the build fail when I forget The hardest part wasn't the code. It was checking the result on Windows 10, Windows 11, Android and iOS. So I made the build check what it can by itself. After astro build, a script reads the generated HTML and fails if: - a Windows 10 emoji appears outside a wrapped item, so it would have no fallback - a .emo wrapper is around an emoji Windows 10 can draw - a section has wrapped emoji but no note - a fallback PNG or the Noto licence file is missing A separate Python script uses fontTools to check every non-ASCII character on the site (1,767 right now) against 8 common fonts. The latest run found none without a glyph. That doesn't prove every phone draws them, so the site says so instead of promising they work everywhere. What I'd still like to improve - The box can flash briefly on the first visit. I could hide wrapped emoji until the test finishes, but that hides them for most people who don't need it. - The colour test is a heuristic. If you know a case where a colour glyph changes with fillStyle, or a box glyph doesn't, I'd like to hear it. - I want to keep adding emoji and symbols while keeping each page clean, so people can still find what they need quickly. The list of 1,204 sequences is in a small open dataset: cute-symbols-data. So what's missing? If there's an emoji or symbol you use all the time and can't find on the site, tell me in the comments.
Source credits and setup: github.com/QasimAliSidhu/3-friends-dairy-storefro… Font sources: Manrope and DM Serif Display from Google Fonts. googlefonts/manrope googlefonts/dm-fonts
Free Font Downloader to get started (PowerShell)(Windows) A simple PowerShell script to fetch a curated collection of free/open-source fonts (great for fresh installs or lightweight suites). If the script confuses you or you don't trust it you can find links to each font at the end of the post. For people who don't have a lot of fonts or on a machine that has not had Office or Adobe installed before ArtCraft: I put together a little downloader of my favorite free fonts. This script will download around 30 of my favorite free fonts directly from their sources for you into a little folder named Phobic Favorite Free Fonts on your desktop. How to Run It (No Admin Rights Needed!) - Press the Windows Key, type PowerShell, and open Windows PowerShell (or Terminal). - Copy the script code provided below. - Right-click inside the PowerShell window to paste the code, then press Enter. - The script will automatically create a Phobic Favorite Free Fonts folder on your Desktop, download all the fonts directly from their official sources, and pop open the folder when it's done! - From the folder, you can review and install the fonts normally. All these fonts are free but some have different licenses. See bottom of the post. In particular Heatwave, Another Danger are free for personal use commercial use requires license, and Clash Grotesk liscence requires direct individual download which this script does (technically correct, though possibly not how it was originally intended). <# =============================================================================== PHOBIC FAVORITE FREE FONTS DOWNLOADER - LICENSE & USAGE BREAKDOWN =============================================================================== SIL Open Font License 1.1 (Free for personal & commercial use): - Inter, Montserrat, Open Sans, Poppins, JetBrains Mono, Fira Code, Merriweather, Playfair Display, Fraunces, Lora, Bricolage Grotesque, Overused Grotesk, Road Rage, Rubik Wet Paint, Sedgwick Ave Display, Jost, Manrope Apache License 2.0 (Free for personal & commercial use): - Roboto, Permanent Marker Vic Fieger Freeware (Free for personal & commercial use, not open-source): - Edo SZ ITF Free Font License (Free for personal & commercial use): - Clash Grotesk - Note: Fontshare terms require direct individual download, which this script executes directly. Do not re-host or redistribute the font file. Personal Use Only / Demo (Free for personal use / memes only): - Another Danger (Demo) - Commercial license sold separately by The Branded Quotes. Free Font (Personal use only): - Heatwave Free Font - Commercial license available via Maulana Creative. =============================================================================== #> $out = Join-Path ([Environment]::GetFolderPath("Desktop")) "Phobic Favorite Free Fonts" $licOut = Join-Path $out "Licenses" New-Item -ItemType Directory -Force -Path $out, $licOut | Out-Null [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $web = New-Object System.Net.WebClient $web.Headers.Add("User-Agent", "Mozilla/5.0") $web.Headers.Add("Referer", "dafont.com") # ---------- FONT FILES ---------- $fonts = @( @{ File = "Montserrat-Regular.ttf"; Url = "github.com/googlefonts/montserrat/raw/master/font…" }, @{ File = "Montserrat-Bold.ttf"; Url = "github.com/googlefonts/montserrat/raw/master/font…" }, @{ File = "OpenSans-Regular.ttf"; Url = "github.com/googlefonts/opensans/raw/main/fonts/tt…" }, @{ File = "OpenSans-Bold.ttf"; Url = "github.com/googlefonts/opensans/raw/main/fonts/tt…" }, @{ File = "Roboto-Regular.ttf"; Url = "github.com/googlefonts/roboto/raw/main/src/hinted…" }, @{ File = "Roboto-Bold.ttf"; Url = "github.com/googlefonts/roboto/raw/main/src/hinted…" }, @{ File = "Poppins-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/poppins/Popp…" }, @{ File = "Poppins-Bold.ttf"; Url = "github.com/google/fonts/raw/main/ofl/poppins/Popp…" }, @{ File = "JetBrainsMono-Regular.ttf"; Url = "github.com/JetBrains/JetBrainsMono/raw/master/fon…" }, @{ File = "PermanentMarker-Regular.ttf"; Url = "github.com/google/fonts/raw/main/apache/permanent…" }, @{ File = "EdoSZ.ttf"; Url = "github.com/adam-lynch/fonts/raw/master/edosz.ttf" }, @{ File = "Merriweather-Regular.ttf"; Url = "github.com/SorkinType/Merriweather/raw/master/fon…" }, @{ File = "PlayfairDisplay-Regular.ttf"; Url = "github.com/fonts/Playfair-Display/raw/master/TTF/…" }, @{ File = "Fraunces72pt-Regular.ttf"; Url = "github.com/googlefonts/fraunces/raw/master/fonts/…" }, @{ File = "Lora-Regular.ttf"; Url = "github.com/cyrealtype/Lora/raw/main/fonts/ttf/Lor…" }, @{ File = "BricolageGrotesque-Regular.ttf"; Url = "github.com/ateliertriay/bricolage/raw/main/fonts/…" }, @{ File = "BricolageGrotesque-Bold.ttf"; Url = "github.com/ateliertriay/bricolage/raw/main/fonts/…" }, @{ File = "OverusedGrotesk-Regular.ttf"; Url = "github.com/RandomMaerks/Overused-Grotesk/raw/HEAD…" }, @{ File = "OverusedGrotesk-Bold.ttf"; Url = "github.com/RandomMaerks/Overused-Grotesk/raw/HEAD…" }, @{ File = "RoadRage-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/roadrage/Roa…" }, @{ File = "RubikWetPaint-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/rubikwetpain…" }, @{ File = "SedgwickAveDisplay-Regular.ttf"; Url = "github.com/google/fonts/raw/main/ofl/sedgwickaved…" }, @{ File = "Jost-Regular.ttf"; Url = "github.com/indestructible-type/Jost/raw/master/fo…" }, @{ File = "Jost-Bold.ttf"; Url = "github.com/indestructible-type/Jost/raw/master/fo…" }, @{ File = "Manrope-Regular.ttf"; Url = "github.com/googlefonts/manrope/raw/master/fonts/t…" }, @{ File = "Manrope-Bold.ttf"; Url = "github.com/googlefonts/manrope/raw/master/fonts/t…" } ) foreach ($font in $fonts) { Write-Host "Downloading $($font.File)..." try { $web.DownloadFile($font.Url, (Join-Path $out $font.File)) } catch { Write-Warning "Failed: $($font.File) - $($_.Exception.Message)" } } # ---------- ZIPPED FONTS ---------- $zips = @( @{ Url = "github.com/rsms/inter/releases/download/v4.1/Inte…"; Files = @("Inter-Regular.ttf", "Inter-Bold.ttf") }, @{ Url = "github.com/tonsky/FiraCode/releases/download/6.2/…"; Files = @("FiraCode-Regular.ttf") }, @{ Url = "dl.dafont.com"; Files = @("Another Danger - Demo.otf", "Another Danger Slanted - Demo.otf") }, @{ Url = "dl.dafont.com"; Files = @("Heatwave Free Font.ttf") }, @{ Url = "api.fontshare.com"; Files = @("ClashGrotesk-Regular.otf", "ClashGrotesk-Bold.otf") } ) foreach ($zip in $zips) { Write-Host "Downloading $($zip.Files -join ', ')..." $zipPath = Join-Path $env:TEMP ([guid]::NewGuid().ToString() + ".zip") $extract = Join-Path $env:TEMP ([guid]::NewGuid().ToString()) try { $web.DownloadFile($zip.Url, $zipPath) Expand-Archive -LiteralPath $zipPath -DestinationPath $extract -Force foreach ($name in $zip.Files) { $found = Get-ChildItem -LiteralPath $extract -Recurse -Filter $name | Select-Object -First 1 if ($found) { Copy-Item -LiteralPath $found.FullName -Destination (Join-Path $out $name) -Force } else { Write-Warning "Could not find $name inside the zip" } } # Also grab any license/readme text files from the zip Get-ChildItem -LiteralPath $extract -Recurse -Include .txt,.md,LICENSE*,OFL* -ErrorAction SilentlyContinue | ForEach-Object { $destName = $_.Name if ($destName -match "license|ofl|readme" -or $_.Extension -eq ".txt") { Copy-Item $_.FullName (Join-Path $licOut $destName) -Force -ErrorAction SilentlyContinue } } } catch { Write-Warning "Failed zip: $($zip.Url) - $($_.Exception.Message)" } finally { Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $extract -Recurse -Force -ErrorAction SilentlyContinue } } # ---------- LICENSE FILES ---------- $licenses = @( @{ Name = "Montserrat-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "OpenSans-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Roboto-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Poppins-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "JetBrainsMono-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "PermanentMarker-Apache.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Merriweather-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "PlayfairDisplay-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Fraunces-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Lora-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "BricolageGrotesque-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "OverusedGrotesk-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "RoadRage-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "RubikWetPaint-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "SedgwickAveDisplay-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Jost-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Manrope-OFL.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "Inter-LICENSE.txt"; Url = "raw.githubusercontent.com" }, @{ Name = "FiraCode-LICENSE.txt"; Url = "raw.githubusercontent.com" } ) Write-Host "`nDownloading license files..." foreach ($lic in $licenses) { Write-Host " $($lic.Name)" try { $web.DownloadFile($lic.Url, (Join-Path $licOut $lic.Name)) } catch { Write-Warning " Failed: $($lic.Name) - $($_.Exception.Message)" } } $web.Dispose() Write-Host "`nDone." Write-Host "Fonts -> $out" Write-Host "Licenses -> $licOut" explorer $out Included Fonts & License Breakdown - SIL Open Font License 1.1 (Free for personal & commercial use): - Inter, Montserrat, Open Sans, Poppins, JetBrains Mono, Fira Code, Merriweather, Playfair Display, Fraunces, Lora, Bricolage Grotesque, Overused Grotesk, Road Rage, Rubik Wet Paint, Sedgwick Ave Display, Jost, Manrope. - Apache License 2.0 (Free for personal & commercial use): - Roboto, Permanent Marker. - Vic Fieger Freeware (Free for personal & commercial use, not open-source): - Edo SZ. - ITF Free Font License (Free for personal & commercial use; direct download via script): - Clash Grotesk (Note: Fontshare terms require direct individual download which this script does (technically correct but possibly not how it was intended); do not redistribute the file directly). - Personal Use Only / Demo (Free for personal use / memes only): - Another Danger (Demo) (Commercial license sold separately by The Branded Quotes). - Free Font (Commercial use requires a license): - Heatwave Free Font (Commercial license available via Maulana Creative). Included Fonts & Licenses - Inter — SIL Open Font License 1.1 (GitHub) - Montserrat — SIL Open Font License 1.1 (Google Fonts) - Open Sans — SIL Open Font License 1.1 (Google Fonts) - Poppins — SIL Open Font License 1.1 (Google Fonts) - JetBrains Mono — SIL Open Font License 1.1 (GitHub) - Fira Code — SIL Open Font License 1.1 (GitHub) - Merriweather — SIL Open Font License 1.1 (Google Fonts) - Playfair Display — SIL Open Font License 1.1 (Google Fonts) - Fraunces — SIL Open Font License 1.1 (Google Fonts) - Lora — SIL Open Font License 1.1 (Google Fonts) - Bricolage Grotesque — SIL Open Font License 1.1 (Google Fonts) - Overused Grotesk — SIL Open Font License 1.1 (GitHub) - Road Rage — SIL Open Font License 1.1 (Google Fonts) - Rubik Wet Paint — SIL Open Font License 1.1 (Google Fonts) - Sedgwick Ave Display — SIL Open Font License 1.1 (Google Fonts) - Jost — SIL Open Font License 1.1 (Google Fonts) - Manrope — SIL Open Font License 1.1 (Google Fonts) - Roboto — Apache License 2.0 (Google Fonts) - Permanent Marker — Apache License 2.0 (Google Fonts) - Edo SZ — Freeware: Free for personal & commercial use, not open-source (DaFont) - Clash Grotesk — ITF Free Font License: Direct individual download via script (technically correct, though possibly not how it was originally intended; do not redistribute directly) (Fontshare) - Another Danger (Demo) — Personal Use Only / Demo: Commercial license sold by The Branded Quotes (DaFont) - Heatwave — Free for personal use: Commercial use requires a license from Maulana Creative (DaFont) Also if you're on old hardware and find the launcher to be laggy check out my little powershell Artcraft App Updater: reddit.com
Grok botからM5PaperMonoに情報を表示させよう みんな、こんにちは!電子ペーパーにパーソナライズされた予定やニュースが毎日自動で出てたら便利だと思わない?でも、表示する内容を毎回自分で作って送るのは面倒だよね?そんなみんなのために、ボクが大好きなGrok botにお願いすることでM5PaperMonoっていう電子ペーパー搭載のマイコンモジュールの表示を毎朝・毎晩自動で更新してもらう方法をまとめたよ。間にFirebaseのRealtime Database(RTDB)を置くだけで、意外とお手軽にできるから、今回やってみた内容を紹介するよ。 x.com この先の内容について 過去の記事ではソースコードや設定とか載せていたけど、AI時代にそれらを載せる意味がとっても薄れてきたから、この記事では仕組みとかやり方はまとめているけど、具体的なソースコードや詳細な設定などは載せていないよ。自前で実装するかAIに指示して作ってみてね。 やること M5StackのPaperMonoに次のような内容を毎日自動で表示するよ。ボクはこの構成でやってみたけど、好きなデバイスやデータの置き場所でやってみてね。 毎朝:今日の予定と、準備しておくこと 毎晩:明日の予定、近い期限、今日のAI・電子工作のニュース 作ったものの動きはこんな感じ: Grok botが決まった時刻に、Googleカレンダーやウェブから情報を集めて表示内容を作成 Grok botがFirebase RTDBの /display/info.json に文章を書き込む M5PaperMonoがRTDBのデータを読み込んで画面に表示する 環境 今回ボクが使った環境は以下の通りだよ。 表示用デバイスと開発環境 デバイス: M5Stack PaperMono(ESP32-S3/Flash 16MB) 開発ツール: PlatformIO フレームワーク: Arduino 主要ライブラリ: M5Unified 0.2.20、M5GFX 0.2.27、ArduinoJson 6.21.5、OpenFontRender、M5PM1 バックエンド: Firebase Realtime Database、Firebase Authentication(メール/パスワード認証) 通信方式: Wi-Fi経由のHTTPS REST API セットアップ、コーディング: Codex AIエージェント Grok bot(定期実行の「ルーチン」機能を使用) 対象読者 電子ペーパーにAIが作った情報を自動で表示したい M5StackでIoTっぽいことをやりたい AIエージェントから外のデータベースに書き込む方法を知りたい 全体の構成 やってることはとってもシンプル。真ん中にFirebase RTDBの小さなJSONを置いて、Grok botが書いて、M5PaperMonoが読むだけだよ。この構成の良いところは、Grok botもFirebaseもクラウド上にあるから、ローカルのPCで情報をまとめたり、ブリッジみたいな環境を用意しなくても済むところ。M5PaperMonoがネットに繋がる環境にあればそれでOKってとこだね。 Grok botってなに? Grok botは、SpaceXAI社が提供するGrokサービスの一部で、チャットでお願いするだけで色々な作業をしてくれるAIエージェントだよ。今回使ったのはGrok botの「ルーチン」という機能で、やってほしいことを文章で登録しておくと、決めた時刻に自動で実行してくれるよ。Grok botはGoogleカレンダーを読んだり、ウェブを検索したりだけじゃなくて、実はWebhookでトリガーしたりREST APIを呼んだりもできるよ。今回はこのREST APIという機能をGrok botから使うことでFirebase RTDBへの書き込みを実現しているよ。 grok.com Firebase RTDBってなに? FirebaseはGoogleが提供するクラウドサービスで、Firebase Realtime Database(RTDB)はFirebaseのサービスの一部で、JSON形式のデータをクラウドに置いておける仕組みだよ。このFirebase RTDBはREST APIで読み書きできるから、M5PaperMonoのようなマイコンからもGrok botのようなAIエージェントからも、HTTPリクエストを送るだけで使えるよ。 firebase.google.com M5PaperMonoってなに? M5PaperMonoは、M5Stack社が出している電子ペーパーの開発ボードだよ。3.97インチ・480×800のタッチ対応の白黒電子ペーパー(4階調グレースケール)に、Wi-Fiが使えるESP32-S3と1150mAhのバッテリーを内蔵しているよ。電子ペーパーは書き換えるときにしか電気を使わないから、電源をOFFしても一度表示した内容はそのまま残ってくれるよ。今回はこのM5PaperMonoにFirebase RTDBから読み込んだ情報を表示するよ。 docs.m5stack.com PoCの進め方 PoC (Proof of Concept) は、小さく作って、うまく動くか確かめることだよ。ボクがこれを作った時の進め方を書いているよ。特に今回のように複数のデバイスやシステムが関係している場合、いきなり全部つなげると、うまく動かないときにどこが悪いのか分からなくなっちゃうよね。だから、ボクはこんな順番で1つずつ確認しながら進めてみたよ。 CodexにFirebaseのセットアップをお願いする Grok bot/M5PaperMono用の各アカウントで、ルール通りにJSONを読み書きできるか確認 Grok botから試しに書き込んでみる M5PaperMonoの表示部分を作る 準備完了!Grok botのルーチンで毎日動かす 順番に確認して各ステップを1つずつクリアしていくことで、次のステップに問題を持ち越さなくて済むからどこが問題なのかをすぐ切り分けられるよ。ここからは、この順番で説明していくよ。 Firebaseをセットアップしよう まずはステップ1と2、Firebaseにデータの置き場所を作って、権限通りに読み書きできるところまで確認するよ。 Firebaseのセットアップ手順 ボクはこの手順のほとんどはCodexにお願いしてやってもらったよ。任せられるところはどんどんAIに任せちゃおう。 Firebaseコンソールでプロジェクトを作る 「Realtime Database」を開いて、データベースを作る(最初はロックモード) 「Authentication」でメール/パスワード認証をオンにして、Grok bot用とM5PaperMono用のユーザーを作る Firebase CLIを入れて、ルールを反映する Firebase CLIは以下のコマンドでインストールしてログインできるよ。これも含めてCodexに任せてもいいよね。 npm install -g firebase-tools firebase login 補足だよ。 RTDBのロケーションは日本が選べないから、近いシンガポール(asia-southeast1)がおすすめだよ データベースのURLは https://<プロジェクト名>-default-rtdb.<リージョン>.firebasedatabase.app の形で、コンソールのデータ画面に出てくるよ Firebase CLIを使うには、先にNode.jsを入れておいてね ルールは database.rules.json に書いて、中身は下の「セキュリティルール」で説明するね 表示用のデータ 今回の表示用データは、こんな小さなJSONにしているよ。 { "title": "...", "message": "1行目\n2行目\n...", "updatedAt": 1760000000000, "value": 0 } message:画面に出す文章。改行は \n updatedAt:更新時刻(Unixミリ秒の数値)。M5PaperMono側で取得したとき、情報が「新しくなったか」を判断するために使用するよ title と value:テスト用に入れているだけで、実際には使ってないよ セキュリティルール Grok botには、書き込みが必要なパスだけ許可しているよ。ルールを設定しておけば、もしAIが書き込む情報の構造などを間違えても、ほかのデータは壊れないから安心だね。 /display/info.json だけ書き込み可 形も固定(title と message は文字列、updatedAt と value は数値) 認証 Firebase Authenticationのメール+パスワードで、Grok bot専用のユーザーとM5PaperMono用の2つ作ったよ。それぞれ以下のように設定しているよ。パスワードはGrok botでルーチンの設定をお願いするときに安全に保存してもらえるよ。 M5PaperMono用アカウント:/display/info の読み取りのみ Grok bot用アカウント:/display/info の読み取り・書き込み 未認証のアクセス:読み取り・書き込みともに拒否 権限通りに読み書きできるか確認しよう Grok botをつなぐ前に、それぞれのアカウントでIDトークンを取って、curlで読み書きを試しておくよ。ボクは全てCodexに確認をお願いしたよ。 Grok bot用のアカウントで /display/info.json に書き込めること Grok bot用のアカウントで、ほかの場所には書き込めないこと(拒否されればOK) M5デバイス用のアカウントで /display/info.json を読めること ここで期待通りの結果になれば、Firebase側の設定は完了だよ。 Grok botにお願いしよう ステップ3、Grok botからFirebaseに書き込んでみるよ。基本的にはGrok botにやって欲しいことを伝えるだけでOKだよ。Grok botから更新するのは message と updatedAt の2つだけ。Grok botにFirebase RTDBのURLを教えてあげると、APIキーとか聞かれるからCodexに聞いて必要な情報をもらってGrok botへ入力するよ。 Grok botに教えてあげる情報をまとめるとこんな感じ: Firebase RTDBのURL(FirebaseでRTDBを設定したときに生成されたhttps://<プロジェクト名>-default-rtdb.<リージョン>.firebasedatabase.app) 許可しているパスは/display/info.jsonだけであること FirebaseのAPIキー(FirebaseをセットアップしたCodexに教えてもらう) Grok bot用のメールアドレスとパスワード 書き込んだあとは、Grok botから書き込んだ内容を読み出して、さっき書き込んだ値がちゃんと反映されたか確認してもらってね。 M5PaperMonoの制約に合わせて文章を作る M5PaperMonoの画面に合わせて、こんなルールで文章を作ってもらったよ。 最大16行 1行は全角18文字以内(超えると端で折り返されて、別の行になっちゃう) 絵文字は使わない 送る前に、行数と各行の文字数をプログラムで数えて確認する AIに「18文字以内で」ってお願いしても守れないことがあるから、最後はコードで数えるのがおすすめだよ。あと、アスキーアートを出したいときは、半角文字だと実機でずれちゃうから、全角文字だけで作る必要があったよ。これはM5PaperMono側で使うフォントにも依存するかな。 M5PaperMonoで表示してみよう ステップ4、いよいよFirebase RTDBに入った文章をM5PaperMonoに表示するよ。今回は全てCodexに任せて以下のような仕様でM5PaperMono用のファームを作ってもらったよ。 M5PaperMonoの動作 起動後、Wi-Fiに接続して時刻を同期 Firebase AuthenticationでM5PaperMono用アカウントにログイン 取得したIDトークンを使い、HTTPSでJSONを取得 本文と、日本時間に変換した更新日時を表示 内蔵タイマーを設定して電源を落とし、1時間後に自動起動 日本語の表示用に「Zen丸ゴシック」というフォントを使用したよ。 googlefonts/zen-marugothic 毎日動かしてみよう 最後にステップ5。Grok botのルーチンで毎日自動でFirebaseの情報を更新してもらえるようにするよ。これもGrok botにお願いしてルーチンを作ってもらうだけだよ。M5PaperMonoで表示したい内容をお願いするだけで毎日ちゃんと動いてくれるのがいい感じだよ。例えばボクはこんな感じで朝と夜に情報を更新してもらっているよ。 朝(7:30ごろ) Googleカレンダーを読む(読むだけで、予定は作ったり変えたりしない) 今日の予定、今週の残り、2〜3週間先の準備が必要な予定をまとめてチャットで送る(これはスマホとかで直接見る用に送ってもらってるよ) 今日の予定と、準備しておくことを電子ペーパーに表示する 夜(21:00ごろ) 明日の予定と、近い期限・準備が必要なことをカレンダーから拾う AI・M5Stack・マイコン・電子工作・電子ペーパーの今日のニュースを、ウェブ検索と公式Xから3〜5件集める(確認できたものだけ) 「明日の予定」「共有したいこと」「今日の動き」の順に並べて、行が余ったら一言を添える M5PaperMonoで手動更新するかGrok botのルーチンの時間が過ぎるのを待ってM5PaperMonoの画面を確認してみてね。画面の表示が更新されて最新の予定とかを表示することができたかな?予定だけじゃなくてタスクの一覧とか買い物メモとかを自動的に表示しておくとかにも使えそうだよね! まとめ 今回は、Grok botを使って日々の予定をM5PaperMonoに表示してもらう方法をまとめたよ。真ん中にFirebase RTDBの小さなJSONを置くだけで、Grok botとM5PaperMonoを簡単に繋げられたね。ほしい情報が毎日勝手に電子ペーパーに出てくるのは、とっても便利だね!みんなも、普段使ってるAIエージェントに予定とかまとめてもらって電子ペーパーに表示してみてね。 じゃあ、またね!
It's not "some obscure module" that is written in Rust. Just some examples I know of: - Chrome currently uses googlefonts/fontations to implement its font handling, and text is the main thing on most web pages. - Windows' DirectWriteCore for font handling is also now written in Rust. - Firefox's whole CSS engine and GPU 2D rendering engine are written in Rust.
あまりにも神ライブラリすぎる googlefonts/fontations
@chetellik @0bNARA Yea that’s what they’re doing. Same with Arabic. For most of those Chinese languages the character sets aren’t even defined. For example Wu: github.com/googlefonts/lang/blob/main/Lib/gflangu… As opposed to say Yue github.com/googlefonts/lang/blob/main/Lib/gflangu… t.co