Pixel-Art Interfaces: Text Boxes, Fonts and Japanese
Pokémon Emerald’s dialogue box is a frame 30 tiles by 6, the full 240-pixel width of the screen and 30% of its height, around a text window of 27 by 4 tiles that holds two lines of a 16-pixel font; Crystal’s is 20 tiles by 6, two lines of 18 characters.123 Emerald prints a character every 8, 4 or 1 frames by its speed setting, 7.5, 15 or 60 a second at 60 hertz; Crystal’s fast setting and Final Fantasy VI also print one a frame.14 Emerald’s writers broke every line by hand: across 24,100 lines of script text the widest is 208 pixels against a 216-pixel window, and none spills.1 A Japanese pixel font that covers the standard set draws about seven thousand characters: JIS X 0208 has 6,879, Misaki draws all of them in an 8-by-8 cell, PixelMplus10 all but three code points on a 10-pixel grid, and the Final Fantasy Pixel Remaster’s producer put his team’s new Japanese font at “around 7,500 characters”.5678 On my Mac, with font smoothing switched off, Core Text drew the pixel TrueType fonts whose grid sits on the baseline with whole-pixel advances (Misaki, PixelMplus10 and Press Start 2P) with no partial pixels at whole multiples of their grid and with 19.4% to 82.2% of inked pixels smeared between them; PixelMplus12, whose grid sits off the baseline, Silkscreen, with stray outline points, and DotGothic16, with no grid, came out partial at every size measured, and k8x12 at every size once its glyphs were placed exactly; with the drawing context’s default smoothing, even PixelMplus10 at its own 10 pixels came out 178 of 272 inked pixels partial.79 At 3×, a 10-pixel em lands on whole device pixels at only one of iOS’s twelve Body sizes, 40 points.710 Kiradex World today sets every word in the system monospaced face, speaks English only, and gives VoiceOver the walk tests’ coordinates.11 So I measured the canon’s boxes, fonts and speeds from the decompilations, measured five Japanese and two Latin pixel fonts on Core Text, and wrote a brief: one licensed face for both languages, Dynamic Type rounded to the font’s grid, a forge-drawn frame, two lines and pages, and a stage VoiceOver can describe.
TL;DR
- The box is the bottom third and two lines. Emerald’s dialogue frame is 30 by 6 tiles (240 by 48 pixels, 30% of the screen’s height) around a 27-by-4-tile window of two 16-pixel lines; Crystal’s box is 20 by 6 tiles, 33% of the height, two lines of 18 characters, and Red’s is the same 20 by 6. Emerald’s box has no name field, and 934 of its 14,412 script paragraphs (6.5%) name the speaker in the text.12312
- Speed is a frame count. Emerald and FireRed take 8, 4 or 1 frames a character (7.5, 15 or 60 a second at 60 hertz), and one frame while A or B is held once it has been pressed during printing; Crystal and Red wait 5, 3 or 1 frame (12, 20 or 60); Final Fantasy VI draws field dialogue one letter a frame, with no setting for it (its six-step message speed is how long a battle message waits). Emerald’s advance arrow steps 0, 1, 2, 1 pixels from rest, a step every 9 frames, a 0.6-second cycle; Crystal’s ▼ blinks against the border tile.1131415164
- Five free fonts already draw the Japanese a game needs. Misaki draws all 6,879 JIS X 0208 characters in 8 by 8 with every kanji inside 7 by 7; k8x12 in 8 by 12; PixelMplus12 and PixelMplus10 draw all but three code points at 12 and 10 pixels; DotGothic16 draws them all but on no pixel grid. The three-sentence license of Misaki, k8x12 and PixelMplus grants use, copying and distribution “either commercially or noncommercially”.5671718
- A pixel font is crisp only at whole device pixels per font pixel, and only if the file is clean. Measured with Core Text on macOS 27.0, not on an iPhone, with font smoothing switched off: 0.0% partial pixels at whole multiples of a grid that sits on the baseline with whole-pixel advances (Misaki, PixelMplus10, Press Start 2P), 19.4% to 82.2% between them.7 With the drawing context’s default smoothing, PixelMplus10 at its own 10 pixels was 178 of 272 inked pixels partial, so the clean result at whole multiples holds only with smoothing off.9 PixelMplus12’s grid sits 0.39 pixel below the baseline, and with smoothing off it smears at every size; k8x12’s kana advance of 8.01 pixels smears once glyphs are placed exactly.719
- Dynamic Type has to be rounded, not followed. Of the twelve iOS Body sizes, 14 to 53 points, a 10-pixel em is whole at 3× only at 40 points, an 8-pixel em at 16 and 40, a 12-pixel em at 16, 28 and 40. Apple asks that a custom font implement the behaviors system fonts have, Dynamic Type among them, and, for an app’s text, that people ideally be able to enlarge it by at least 200 percent.71020
- Japanese line breaking is strict untagged and loose tagged. In Core Text on macOS, a 131-character paragraph broke with no line starting on a character from the probe’s line-start list, a partial list, mostly JLREQ’s characters with seven in their fullwidth forms, in any width measured, untagged; tagged
ja, a small kana began a line at 6 of 7 widths in Hiragino Sans, Misaki, PixelMplus and DotGothic16, the less strict rule the W3C’s Japanese layout requirements describe.721 - What this gives Kiradex. A brief, not a build: PixelMplus10 for every word drawn over the world (of the two measured faces with Japanese, a clean grid and whole-pixel advances, Misaki and PixelMplus10, the larger), sizes rounded to whole device pixels per font pixel (16.67 points at the default size on 3× iPhones, 15 on 2×), a forge-drawn frame of its own, two lines and pages, a 30-character-a-second reveal with a stepping mark, one-line bubbles sized to fit the narrowest iPhone the iOS 27.0 simulator lists when its sides have no insets, a line that cannot fit going to the box, Japanese lines reviewed by a Japanese speaker, and a stage VoiceOver describes in words instead of coordinates, each with a check a script, a log line or a capture can pass or fail, but for one manual VoiceOver check.2211
1. Emerald and FireRed: a screen-wide frame, two lines, written by hand
This series started with the world and has worked inward. Pixel-Art Worlds on iPhone set the tile system, the RealityKit recipe and a direction for text in one rule, “Text is art”, and one sentence of plan: “m5x7 at integer multiples in a 24-by-24 nine-slice box, two lines, typewriter reveal, a two-frame advance arrow.”23 The posts since built the collector, the buildings, the motion, the town’s own life, the rooms and the roads. Text was left at that sentence. This post measures where its numbers come from, finds four places where the world post is wrong or imprecise, and adds what it did not have: Japanese at 8, 10 and 12 dots, what Core Text actually does to a pixel TrueType font, how Dynamic Type and a pixel grid can agree, Japanese line breaking, and VoiceOver over a RealityKit stage.
Kiradex World’s text today is plain. Every word in it is SwiftUI in the theme’s system monospaced face. The bubbles over collectors’ heads are one line of the kit’s caption role in bold, on cream paper with a 2-point ink border, shown while the line is under 4 seconds old. The place’s line sits in a cream text box at the bottom of the screen. The app is English only, the preset lines live on the server in English, the forge’s sign is a 12-by-11 board with ink scribble and no letters, and VoiceOver reads the stage as one element whose value is the walk tests’ step trace.11242526272829 Section 10 has the detail; the brief in section 12 is written as a diff against it.
Every Pokémon number below was measured from shallow clones of pret’s decompilations at pokeemerald 731ad5b (October 1, 2026), pokefirered 037335f, pokecrystal 5beda23 and pokered d2704a6, by one Python script, measure_gen_text.py, whose output is saved beside it; Final Fantasy VI’s numbers come from a second script over the everything8215/ff6 disassembly. Both re-run in seconds, and the notes name the source file behind each figure.3014
The fonts
Emerald declares its fonts in one table, sFontInfos in src/text.c, each with a maximum letter width and height in pixels. The small font is 5 by 12; the normal font, which the dialogue box uses, 6 by 16; the short font 6 by 14, declared four times over (itself and three copies); the narrow font 5 by 16; the small narrow font 5 by 8; the bold font 8 by 8; and the braille font 8 by 16 with 8 pixels of line spacing. Every other font has a line spacing of 0: the 16-pixel cell is the line.113
The normal Latin font is proportional. Indexed through the game’s charmap.txt into gFontNormalLatinGlyphWidths in src/fonts.c, the lowercase letters a to z average 5.77 pixels wide, from 4 to 6; the capitals A to Z and the digits 0 to 9 are all 6; the space and the full stop are 3, and the exclamation mark 4.131 In running text that comes to 5.25 pixels a character on average, measured over every line of the game’s script below.1
Speed is a frame count
Text speed in Emerald is a delay in frames. The Options screen’s three speeds map through sTextSpeedFrameDelays in src/menu.c to delays of 8 (slow), 4 (mid) and 1 (fast). AddTextPrinter in src/text.c stores the delay less one as the printer’s speed; the printer sets its counter to that stored speed when a character prints and counts it down one a frame, and the next character prints on the frame after the counter reaches 0. So a character takes the delay itself: 8, 4 and 1 frames. At 60 hertz that is 7.5, 15 and 60 characters a second.1213 That count is my reading of the printer’s state machine (AddTextPrinter and the RENDER_STATE_HANDLE_CHAR case in src/text.c), which the script asserts rather than runs.1 The Game Boy Advance does not run at exactly 60 hertz: GBATEK gives its frame as “Total 228 lines, 16.743 ms, 280896 cycles - ca. 59.737 Hz”, 160 of those lines visible, each 240 dots wide, so the fast setting is about 59.7 characters a second on the hardware.3233
Holding a button speeds up the slower settings. A new press of A or B while a character’s delay is counting down, in a message that allows it (gTextFlags.canABSpeedUpPrint), sets hasPrintBeenSpedUp. From then on the printer checks JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp and, when it holds, drops the counter to 0: one character a frame, 60 a second, the fast setting’s own rate.131
Auto-scrolled text has its own delay. When gTextFlags.autoScroll is on, the printer sets the counter to 3 rather than to the stored speed, 4 frames a character, except in recorded battles: the test is if (!(gBattleTypeFlags & BATTLE_TYPE_RECORDED) && gTextFlags.autoScroll).131 Battle messages switch auto-scroll on in link battles and recorded battles, and the messageautoscroll script command switches it on and forces the mid speed.34 So the delay of 3 can apply to other battle windows in link battles and to scripted auto-scroll messages, and never in recorded battles, whose messages print at the speed the printer was given: for battle messages, the text speed saved with the recording, except in a recorded link battle, whose message window is given 1, like a link battle’s.341 On my reading the counter runs only while the printer’s stored speed is above 0, and a link battle’s message window is given the delay of 1, which leaves its text at a character a frame with auto-scroll on.1334
The three break codes do three different things. \n moves the printer down to the second line; \l waits under the advance arrow for a press, then scrolls the box up one line; \p waits the same way, then clears the box and starts again at the top.1335 In a scroll the 16-pixel line moves up at 1, 2 or 4 pixels a frame by the speed setting (sWindowVerticalScrollSpeeds), so a scroll takes 16, 8 or 4 frames.113
Text speed in every game measured, and the brief’s two proposed settings; drawn by figures/make_figures.py from the measurement outputs.
The arrow
When a page is full, the box waits under a small arrow. Emerald blits an 8-by-16 arrow cell at vertical offsets of { 0, 1, 2, 1 } pixels (sDownArrowYCoords) and redraws it when its delay, reset to 8, runs out. That is a step every 9 frames and a cycle of 36 frames, 0.60 second at 60 hertz.113 The arrow does not blink; it bobs, through three positions and back.
The box and its frame
Emerald’s field text box is defined by sStandardTextBox_WindowTemplates in src/menu.c. The text window sits at tile column 2, row 15, and is 27 by 4 tiles: 216 by 32 pixels, two lines of the 16-pixel normal font. Its frame, drawn by WindowFunc_DrawDialogueFrame and evaluated by the script for that window, runs from column 0 to 29 and row 14 to 19: 30 by 6 tiles, 240 by 48 pixels, the screen’s full width and 30% of its 160-pixel height.12
That frame is not a nine-slice. LoadMessageBoxGfx loads it from message_box.png, a strip of 56 by 16 pixels, 14 tiles, and the frame is drawn from eight tile positions, its bottom row the top row flipped vertically with BG_TILE_V_FLIP.1362 The script measures the frame’s extent and the PNG’s size; the flip is read from the fill calls.1
The player-chosen frames are for the other windows: the menus and the yes-or-no boxes. WINDOW_FRAMES_COUNT is 20, and the twenty frames are PNGs of 24 by 24 pixels, three by three tiles of 8, loaded by LoadUserWindowBorderGfx from the frame type saved by the Options screen’s FRAME item.136
That corrects the world post. Its rule 10 says “Pokémon Emerald’s text-window frame is a 24-by-24 image, three by three hardware tiles, which is a nine-slice”.23 The twenty 24-by-24 images in graphics/text_window/ are the selectable frames of the menu windows. The dialogue box, the text box players remember, is drawn from the 56-by-16-pixel message_box.png as a 30-by-6-tile frame with a mirrored bottom edge, not as a nine-slice of a 24-by-24 image.136 The nine-slice idea stands, and the menus use it; the attribution to the dialogue box does not.
The two dialogue boxes as tile grids, no game art; drawn by figures/make_figures.py, which asserts each extent against measure_gen_text.out.
The text was written for the box
To see how full the box runs, the script read every line of Emerald’s script text: 504 files, the 36 of data/text/*.inc and the 468 maps’ scripts.inc, 24,100 lines without a placeholder and 3,000 with one, split at the game’s three break codes, \n, \l and \p. The median line is 29 characters and 150 pixels; the 95th percentile 38 characters and 196 pixels; the longest 53 characters, the widest 208 pixels. None is wider than the 216-pixel window, and the average character is 5.25 pixels wide.137
At 5.25 pixels a character, the window holds 41; the writers stopped at 38 for 95% of lines.331 Every break is a hand-placed code. The game never wraps a line; it trusts the writer.
The box has one window and no name field. When the writers wanted a speaker, they typed the name and a colon into the line, as in Littleroot Town’s “MOM: {PLAYER}, we’re here, honey!” Of 14,412 paragraphs, 934, or 6.5%, open with a capitalized name and a colon.137 That there is no separate name field is my reading of the window template, which has room for the two text lines and nothing else.2
Japanese in the English game
The English Emerald still carries Japanese glyphs, for the names of creatures and trainers that come from Japanese cartridges. The printer switches glyph sets on the control code EXT_CTRL_CODE_JPN and back on EXT_CTRL_CODE_ENG, which such names carry (international_string_util.c, contest.c and battle_tower.c insert them). In that mode the normal font is 8 pixels wide for every character and 15 tall (DecompressGlyph_Normal, and GetGlyphWidth_Normal returns 8).11338
The sheets the English ROM builds from: japanese_normal.png is 128 by 512 pixels in cells of 8 by 16, 302 of its 512 cells drawn; japanese_small.png the same size, 307 drawn; japanese_bold.png 128 by 256, 246 of 256 drawn; and FireRed’s japanese_frlg_male.png, also in the Emerald tree, 256 by 512 pixels in cells of 16 by 16, 269 of 512 drawn, with a width table of 512 entries from 0 to 10 pixels, 10 in 408 of them.138 pret builds the English games, so these are the sheets for names in an English ROM; no Japanese ROM’s own box, speeds or font were read (section 11).30
So in the canon’s own terms, a Japanese character in a 16-pixel line is 8 pixels wide in Emerald and mostly 10 in FireRed’s sheet, against a Latin letter of 4 to 6.1
FireRed
FireRed keeps Emerald’s delays, 8, 4 and 1, stored less one by its own AddTextPrinter in src/text_printer.c as Emerald’s are, and a standard text window of 26 by 4 tiles at column 2 (src/new_menu_helpers.c), one tile narrower than Emerald’s.139 Its printer has Emerald’s hold too: if (JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp) drops the counter to 0, and a new press of A or B while a character’s delay counts down, in a message that allows it, sets hasPrintBeenSpedUp.14
2. Crystal and Red: eighteen characters and a blinking mark
The Game Boy box is the Game Boy Advance box’s ancestor at a smaller resolution. Crystal’s screen is 20 by 18 tiles (SCREEN_WIDTH 20, SCREEN_HEIGHT 18), 160 by 144 pixels. Its text box is TEXTBOX_WIDTH = SCREEN_WIDTH by TEXTBOX_HEIGHT 6: the full width and a third of the height, 33%. With BORDER_WIDTH 2 the inside is 18 by 4 tiles, and the 8-by-8 font gives 18 characters a line. Text starts two rows into the box, TEXTBOX_INNERY = TEXTBOX_Y + 2, and lines are two rows apart, so the box shows two lines with a blank row between them; the two-line layout is my reading of those constants.13 Red’s message box is db MESSAGE_BOX, 0, 12, 19, 17: the same 20 by 6 tiles at the bottom.112
Both games count speed in frames too. The delays are TEXT_DELAY_FAST 1, medium 3 and slow 5 frames a letter: 60, 20 and 12 letters a second at 60 hertz. Crystal’s PrintLetterDelay is plain about it: “The text speed setting in wOptions is actually a frame count”. While A or B is held it waits one frame a letter.115 So Crystal’s fast matches Emerald’s, 60 a second, and holding A or B in Emerald gives the same rate; Crystal’s middle and slow settings, 20 and 12, are faster than Emerald’s 15 and 7.5.1 Pan Docs gives the Game Boy’s frame as “One frame: 70224 dots @ 59.7 fps” and notes that a frame “takes ~16.74 ms instead of ~16.67”.40
Crystal lets the player choose among eight frames, FRAME_1 to FRAME_8, each TEXTBOX_FRAME_TILES, 6 tiles of 1 bit, stored as PNGs of 24 by 16 pixels; a ninth frame sits in the ROM unused. The font sheet, gfx/font/font.png, is 128 by 64 pixels, 128 cells of 8 by 8, and Red’s is the same size.141
The mark blinks
Crystal’s advance mark is part of the frame. WaitPressAorB_BlinkCursor waits for A or B while BlinkCursor swaps the tile at (18, 17), in the frame’s own bottom row, between '▼' and the border tile '─'.16 The two states are read from the code; the blink’s period was not derived (section 11).
That is the world post’s second imprecision. Its text plan asks for “a two-frame advance arrow”.23 A two-state mark is Crystal’s blink. Emerald’s arrow, the one the rest of that plan follows (the 16-pixel line, the box at the bottom), does not blink: it steps through three positions, 0, 1, 2 and 1 pixels from rest, on a 36-frame cycle (the code offsets the bitmap’s source rows, which on my reading lifts the wedge on screen; I have not run it).113 The brief in section 12 follows Emerald’s steps, not its direction.
3. Final Fantasy VI: a window you repaint, a font that wraps
Final Fantasy VI treats the window as the player’s. Its numbers here come from the everything8215/ff6 disassembly at 8130132 (September 14, 2026), which holds code and rip lists rather than the ROM’s graphics, so sizes come from byte ranges and from the code that reads them.304
Eight windows, seven colors each
The English and Japanese rip lists both give the window graphics as 0xED0000 to 0xED1BFF, 7,168 bytes: eight styles of 896 bytes, each 28 tiles of 8 by 8 at 4 bits a pixel. Their palettes follow at 0xED1C00 to 0xED1CFF, eight palettes of 16 colors.442 The config screen lists 8 window patterns (ConfigWindowNumPos).443
The window’s color is the player’s to set. LoadWindowPal reads the pattern’s colors from save memory at $1d57, 14 bytes a pattern, which is seven colors of two bytes, and the config screen edits them on R, G and B bars (ConfigColorBarTextList); one more color, for the font, sits at $1d55.443 The stride is read from the code and the three channels from the bar list.4
The config screen’s message speed has six settings, 1 to 6 (ChangeConfigOption_02 stops at $05), and a new save starts at “message speed = 3, battle speed = 3”, as the comment in menu_sram.asm puts it.443 It is the battle message speed, as config.asm names it in a comment, “battle speed, battle message speed, magic order, and window pattern”. Outside the config screen, the only code that reads its bits of the config byte, $1d4d, is mess_wait in src/btlgfx/gfx_cmd.asm, which looks the setting up in MsgSpeedTbl, “wait duration for each battle message speed”, and waits that many frames: 32, 48, 64, 80, 96 or 112 for settings 1 to 6. No file in the field code reads $1d4d at all, so field dialogue has no speed setting; it prints at the one rate below.444
A shaded window without a tile
The dialogue window is shaded down its height by the hardware’s color math. DlgWindowFixedColorTbl in src/field/hdma.asm is 200 bytes of $E0 to $E7, a fixed color on all three channels at intensities 0 to 7, and its first run is 72 values stepping 7, then down to 0, then back up to 7.445 The window opens near the top or the bottom of the screen, “dialog window y position (1 or 18)” in the comment of event.asm.45 That the table is consumed one value a scanline is read from comments, not traced, and what the gradient looks like was not seen, since the disassembly has no pixels (section 11).
The font
The dialogue font is 1 bit a pixel, 22 bytes a letter: 11 rows of 16 bits, a cell 16 pixels wide and 11 tall. Its drop shadow is not drawn: LoadLetterGfx makes it by shifting each row one pixel (lsr).446 The English widths, from FontWidth, are proportional: the capitals average 8.04 pixels (5 to 12), the lowercase 7.73 (4 to 12), the digits 7.80 (5 to 9), and the space is 5.446
The Japanese ROM’s dialogue font, at 0xC494C0 to 0xC4FFFF in its rip list, is 27,456 bytes: 1,248 letters of 22 bytes. Its width table has 1,280 entries, in runs of 1,056 at 12 pixels, 180 at 8 and 44 at 12, and the disassembly’s character tables list 1,010 kanji and 173 kana.447 The English large font’s range, 5,120 bytes, is not a whole number of 22-byte letters (232.7), which I cannot explain from the disassembly (section 11).4
A line, a wrap, a letter a frame
A dialogue line runs from x 4 to a maximum x of $e0, 224: 220 pixels, 18 Japanese letters at 12 pixels.446 English text wraps by word in code. CalcTextWidth adds the next word’s width, and NewLine is called when it would pass the maximum, so the English script needs no hand-placed breaks; the script asserts the calls, and the reading is of UpdateDlgTextOneLine.446
The text prints one letter a frame. The field loop waits for the vertical blank and calls UpdateDlgText once a pass, and UpdateDlgTextOneLine draws one letter and returns: 60 letters a second at 60 hertz.448 One pass per frame is my reading of the loop. Because one source builds both ROMs, the two languages share that loop, which on my reading means the same letters a second in English and Japanese.48
A map’s title is centered, ($e0 - width) / 2, and shown for 100 frames, 1.67 seconds.446
Set against Emerald, Final Fantasy VI shows the other way to build the same box: Emerald writes every break by hand and lets the player choose the menus’ frame, never the dialogue box’s; Final Fantasy VI wraps in code, lets the player repaint the window, and shades it in hardware. Emerald’s field box sits at the bottom, Final Fantasy VI’s at the top or the bottom, and both print at a fixed count of frames.
Chrono Trigger
I meant to set Chrono Trigger beside Final Fantasy VI and could not. No public disassembly was cloned, and Data Crystal’s ROM map for the game returned HTTP 403 when fetched, so nothing in this post about text boxes comes from it (section 11).49
4. Japanese at 8, 10 and 12 dots
Kiradex is a card collector’s app, and the Dex already shows Japanese card names with the English beneath, in the system’s fonts.50 The world’s text will need Japanese too, and a pixel font that cannot draw it is not a candidate. The question is how small a kanji can be, and who has already drawn seven thousand of them.
The size of the job
JIS X 0208, the character set Japanese pixel fonts are measured against, has 6,879 characters as the Unicode Consortium’s mapping table lists it: 524 non-kanji, 169 of them the kana in rows 4 and 5; 2,965 level-1 kanji in rows 16 to 47; and 3,390 level-2 kanji in rows 48 to 84.576 The table is version 2.0 and the consortium files it under its obsolete mappings; it is still the list a font’s coverage is easiest to count against.5
Misaki: 8 by 8
Misaki (美咲フォント), by Num Kadoma, is the smallest. Its page calls it “8×8 ドットの日本語ビットマップフォント” (an 8-by-8-dot Japanese bitmap font) that supports “JIS 第一・第二水準” (JIS levels 1 and 2), and says “一部の記号を除いた全ての文字は 7×7 ドットの範囲に収まっている” (every character but some symbols fits within 7 by 7 dots), so that, the page goes on, characters set side by side still keep their line and letter spacing.51 It was made for a Japanese display driver on the PC-E500 pocket computers, the page’s history says.51
Measured from its BDF files of the 2021-05-05 release, Misaki Gothic has 7,171 characters: 168 half width, 4 pixels wide, and 7,003 full width, 8 pixels wide, in an 8-by-8 cell with an ascent of 6 and a descent of 2. Of its 6,356 glyphs from U+4E00 to U+9FFF (a code range, so a different count from JIS’s 6,355 kanji in two levels), none fills the 8-dot cell in either direction, while 53 full-width glyphs, symbols, do. It draws all 6,879 JIS X 0208 characters.6 Misaki Mincho, whose page describes it as Misaki Gothic with the non-kanji redrawn in a Mincho style, measures the same on every count. Misaki Gothic 2nd, which its page says uses the full 3 by 7 pixels for half-width characters and 7 by 7 for full-width kana, has 7,170 characters (167 half width) and an ascent of 7 and descent of 1, and measures the same on the rest.651
k8x12: 8 by 12
k8x12, by the same author, keeps the 8-dot width and adds height: “8×12 ドットの日本語ビットマップフォント”, JIS levels 1 and 2. It has two variants: k8x12L, whose kana are drawn tall, and k8x12S, whose non-kanji are 8 dots tall, made, the page says, with the limits of old game consoles in mind: “8×8 ドット単位でグラフィックを扱うような、昔のゲーム機等の制限を意識しています” (conscious of the limits of old game machines that handle graphics in units of 8 by 8 dots).52
Measured from the BDF, k8x12 is an 8-by-12 cell with an ascent of 10 and a descent of 2: 7,170 characters, 168 at 4 pixels and 7,002 at 8. No kanji fills the cell; 53 full-width glyphs do in k8x12 and k8x12L and 45 in k8x12S. All three draw all 6,879 JIS X 0208 characters.6
PixelMplus: 10 and 12, as outlines
PixelMplus, by itouhiro (2013), is the other approach: TrueType outlines that look like a bitmap font, with no embedded bitmaps (“埋め込みビットマップは なし”), all the kanji of JIS levels 1 and 2 (“JIS第1・第2水準のすべての漢字を収録”) plus ISO-8859-1, in a regular and a bold weight and two sizes, PixelMplus12 and PixelMplus10, outlined from the M+ BITMAP FONTS.18 Measured with Core Text, each has 7,253 glyphs and draws 6,876 of JIS X 0208’s 6,879. The three it does not map are U+301C 〜, U+2016 ‖ and U+2212 −.7 Those are characters whose Unicode mapping differs between the consortium’s JIS0208.TXT and other vendors’ tables, so on my reading the fonts probably carry the glyphs at the other code points; that was not checked (section 11).5
DotGothic16: a dot font on no grid
DotGothic16, by Fontworks, is on Google Fonts under the SIL Open Font License 1.1 with no Reserved Font Name, listed with the subsets cyrillic, japanese, latin and latin-ext, and added on 2020-12-15.53 It has 9,362 glyphs and draws all 6,879 JIS X 0208 characters.7 But it is drawn as separate dots, not as pixels. In its A, the outline’s x edges come in pairs, 91 and 102, 153 and 163, 214 and 225, 276 and 287, 338 and 349, 400 and 410 units of 1,000: dots about 62 units apart with gaps of 10 or 11 units between them. No whole-pixel grid fits that, and the probe found none from 4 to 40 pixels per em.197 A pitch of about 62 units is close to 16 dots to the em, the number in the face’s name, so the probe draws it at a nominal 16 dots per em and at multiples of that; my choice of size, not a grid the face declares. Core Text on macOS 27.0 drew it 82.3% partial at 16 pixels and 37.3% at 64, and partial at every size measured (section 6).7
What the free fonts measure
The design grid in the table is the smallest number of pixels per em on which 95% of a sample of glyphs’ outline points lie within 0.04 of a pixel, each axis allowed one constant offset; the script that finds it, measure_coretext.swift, was compiled with swiftc -O and run on macOS 27.0 (build 26A428), so these are a Mac’s Core Text numbers, not an iPhone’s.7
| Font | Glyphs | Bitmap tables | Design grid (pixels per em) | Latin / kana advance (pixels) | JIS X 0208 drawn |
|---|---|---|---|---|---|
| Misaki Gothic | 7,174 | EBDT, EBLC | 8, on the baseline | 4 / 8 | 6,879 |
| k8x12 | 7,173 | EBDT, EBLC | 12, on the baseline | 4 / 8.01 | 6,879 |
| PixelMplus10 Regular and Bold | 7,253 each | none | 10, on the baseline | 5 / 10 | 6,876 |
| PixelMplus12 | 7,253 | none | 12, 0.39 pixel below the baseline | 6 / 12 | 6,876 |
| DotGothic16 | 9,362 | none | none (drawn at a nominal 16 dots) | 8 / 16 nominal dots | 6,879 |
On that measure PixelMplus10 Regular has 0 of 1,032 sampled outline coordinates off its 10-pixel grid and the Bold 0 of 1,000; Misaki Gothic 0 of 848; k8x12 0 of 988; PixelMplus12 0 of 1,192 once its offset of 0.39 pixel is allowed, and DotGothic16 all 1,544 off.7 PixelMplus12’s offset shows in its raw coordinates: its H runs from y −33 to 717 units of 1,000, on a grid of 83.3 units a pixel, so its baseline falls 0.396 of a pixel inside a grid row; the probe’s estimate, 0.39, comes from sorting the coordinates’ fractions into bins a fiftieth of a pixel wide, and is the figure the renders below were shifted by.19733
In every Japanese-capable font measured, a Japanese character is twice a Latin one: 8 against 4 in Misaki and k8x12, 10 against 5 and 12 against 6 in PixelMplus.7 The canon sits in the same range: Emerald’s Japanese is 8 pixels wide in a 16-pixel line, FireRed’s sheet mostly 10, Final Fantasy VI’s 12.14
The licenses
All of this is free in the plainest terms. The license for Misaki and k8x12, on the author’s font page, is three sentences: “These fonts are free software. Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially. THESE FONTS ARE PROVIDED “AS IS” WITHOUT WARRANTY.” Each archive’s readme carries the same terms in slightly different words, and the Misaki page names the license it uses as the M+ FONTS license.1751 PixelMplus is under the M+ FONT LICENSE, whose English text, LICENSE_E, is the same three sentences.18 DotGothic16 is under the SIL Open Font License 1.1, whose conditions are in its own OFL.txt.53 On my reading the three-sentence license asks for nothing back; crediting the authors is still right, and the brief does (section 12). These are readings, not legal advice.
What legibility costs
A kana in Misaki is 8 font pixels wide, in PixelMplus12 twelve. At the world’s own density on the iPhone 18 Pro Max, where a texel is 8 device pixels, a line of the box drawn in Misaki holds 36 Latin characters or 18 Japanese ones.11 The Japanese line carries more per character, so the box does not need to hold as many characters to say the same thing; that is my reading, not a measurement.
5. Latin pixel fonts, and the ones I could not measure
Two Latin pixel fonts from Google Fonts were measured alongside the Japanese ones.30
Press Start 2P, by CodeMan38, is under the SIL Open Font License 1.1 “with Reserved Font Name “Press Start 2P”“.54 It has 658 glyphs on an 8-pixel grid on the baseline, every Latin character 8 pixels wide, and all 95 printable ASCII characters. It has no kana or kanji: of JIS X 0208 it draws 150 non-kanji (its Greek, Cyrillic and symbols) and none of the 6,355 kanji.7
Silkscreen, by Jason Kottke, is under the SIL Open Font License 1.1 with no Reserved Font Name.55 It has 228 glyphs on an 8-pixel grid, with 10 of 588 sampled outline coordinates off it (its A and o have y values of 495 and 130 units on a 125-unit grid), and its A is 6 pixels wide. Of JIS X 0208 it draws 21 non-kanji.719
Three more fonts often recommended for pixel games were not measured, and I say so rather than describe them from memory.
Pixel Operator, by Jayvee Enaguas: the author’s notes on DaFont say “Changed licence to Creative Commons Zero (CC0) 1.0” and that the typeface is “licensed under a Creative Commons Zero (CC0) 1.0”. DaFont lists it as “Bitmap (16 px)”, in 15 font files, with 241 glyphs. The author’s own repository, which the notes give at NotABug, returned HTTP 404, and the font was not downloaded or measured. Everything here is from that one page.56
m5x7, by Daniel Linssen: its itch.io page says “use font size 16, 32, 48, etc” and “free to use but attribution appreciated”, lists “Asset license Creative Commons Zero v1.0 Universal”, and gives an average width of 5, an ascender of 7, a descender of 2 and an x-height of 5. It describes its characters as “uppercase and lowercase letters, including many accented letters, numbers and most common symbols”.57
m3x6, by the same author: “use font size 16, 32, 48, etc” and “free to use with attribution”; its page shows no asset-license field.58 Neither file was downloaded, because both sit behind a name-your-own-price download, so neither was measured.5758
That is the world post’s third imprecision. Its text plan names m5x7 as the face.23 By its own page’s description, m5x7 is a Latin font, and nothing on the page says it has kana or kanji; an app that will show Japanese cannot set its world text in it.57 The brief changes the face to PixelMplus10, one of two measured faces with Japanese, a clean grid and whole-pixel advances (Misaki is the other), and the larger of the two (sections 6 and 12). m5x7 itself remains unmeasured.
6. What Core Text does to a pixel font
The world post asserted that “Bitmap fonts render at integer multiples only”.23 That is the rule every pixel-art guide repeats, and I wanted to see it, with numbers, on Apple’s own text engine.
The measurement
Core Text is Apple’s layer to “Create text layouts, optimize font handling, and access font metrics and glyph data”, available since iOS 3.2 and macOS 10.8.59 measure_coretext.swift loads each candidate TrueType file, draws a sample line (“Nice card! Which set? 0123” for the Latin fonts, “カードを見せて! Nice card!” for the Japanese ones) into an 8-bit gray bitmap at sizes that are whole and fractional multiples of each font’s grid, and counts the inked pixels that are partial: between 8 and 247 of 255, more than 3% away from full or empty. It draws each size three ways: with Core Text’s default antialiasing, with antialiasing off, and with subpixel positioning on and quantization off, so that glyphs land exactly where their advances put them.7
Two limits matter before any number. The run is macOS 27.0, build 26A428, on my Mac, not iOS, and it draws through Core Text into a bitmap context, not through SwiftUI’s Text; what an iPhone draws is a prediction until the brief’s capture checks run on one (section 11).7 And the script draws with font smoothing switched off (setAllowsFontSmoothing(false)). When I drew the figure below with the bitmap context’s default instead, 178 of the 272 inked pixels of “Nice card!” in PixelMplus10 at 10 pixels came out partial, against 0 of 94 with smoothing off.9 Font smoothing is a macOS setting; whether anything like it applies on iOS is for the capture checks to decide.
Whole multiples are clean
At whole multiples of the grid, with antialiasing on (the context’s default) and font smoothing off, Misaki at 8, 16, 24 and 32 pixels, PixelMplus10 Regular and Bold at 10, 20, 30 and 40, and Press Start 2P at 8, 16, 24 and 32 all render with 0.0% partial pixels.7 Every pixel is ink or paper. That depends on the smoothing setting: with the bitmap context’s default smoothing, PixelMplus10 at its own 10 pixels drew 178 of the 272 inked pixels of “Nice card!” partial, so on the Mac a whole multiple alone did not make the text crisp.9
PixelMplus10 Regular drawn by Core Text on macOS 27.0 at one device pixel per font pixel, shown with antialiasing off. With antialiasing on and font smoothing off, 0 of the 94 inked pixels of “Nice card!” are partial; with the bitmap context’s default smoothing, 178 of 272 are. The font is under the M+ FONT LICENSE, which permits copying and distribution; drawn by figures/dump_glyphs.swift and figures/make_figures.py.
Between them, they smear
Between whole multiples the same fonts smear, with smoothing off as throughout the probe. Misaki is 57.7% partial at 12 pixels (1.5 times its grid), 39.9% at 20 (2.5 times) and 82.2% at 10 (1.25 times). PixelMplus10 is 60.1% partial at 15 pixels. Press Start 2P is 44.0% partial at 12 pixels, 29.1% at 20 and 21.7% at 28.7 Over all 40 renders at 1.25, 1.5, 1.75, 2.5 and 3.5 times the grid of the eight faces whose grid sits on the baseline (both Misakis, k8x12 and k8x12S, PixelMplus10 Regular and Bold, Press Start 2P and Silkscreen), the partial share runs from 19.4%, PixelMplus10 Bold at 35 pixels, to 82.2%, Misaki at 10.7 The smear does not fall steadily with size: in each of the eight, 1.75 times the grid smears more than 1.5 times (Misaki 65.7% at 14 pixels against 57.7% at 12). At the same fraction it does fall: at 1.5, 2.5 and 3.5 times the grid, each of the eight is lower at every step (Misaki 57.7%, 39.9% and 30.4%). In every render measured between whole multiples it never reaches zero; on my reading the fall at a fixed fraction is because a half-pixel error at a glyph’s edge is a smaller share of a bigger glyph.7
Turning antialiasing off (setShouldAntialias(false)) leaves every pixel full or empty at every size, but the shapes are resampled unevenly. Misaki’s 191 inked pixels at 8 pixels become 377 at 10; an even scale of 1.25 in each direction would give about 298.733 Some rows and columns are doubled and others are not, so strokes that were one pixel wide come out one or two.
Partial pixels by size, Core Text on macOS 27.0 with font smoothing off, not iOS; drawn by figures/make_figures.py from measure_coretext.out.
A clean size is not enough: the file has to be clean
Two quieter failures matter more for choosing a font than the multiples do.
A grid off the baseline smears at every size. PixelMplus12’s outlines sit 0.39 pixel below the baseline grid, and it renders 66.6% partial at its own 12 pixels, 40.0% at 24 and 29.8% at 36. Moving the baseline by the offset, on the context’s transform with subpixel positioning on, brings 12 pixels to 0.0% but leaves 14.3% at 24 and 19.0% at 36, even though the shift was scaled with the size (0.78 pixel at 24, 1.17 at 36).7 Why the scaled shift does not clean the larger sizes the output does not say, and I have not traced it.
An advance off the grid smears once glyphs are placed exactly. k8x12’s kana advance is 8.01 pixels, not 8. With the bitmap context’s default settings, which place each glyph on a whole pixel, it renders 0.0% partial at 12, 24 and 36 pixels. With subpixel positioning on and quantization off, the same sizes are 62.3%, 42.8% and 32.6% partial.7 On my reading, each kana then lands a hundredth of a pixel further from the grid than the last.
Stray outline points leave a little. Silkscreen’s ten off-grid coordinates show as 4.5% partial at 8 pixels and 1.2% at 32.7
A dot font never aligns. DotGothic16 has no grid to take multiples of, so the probe draws it at a nominal 16 dots per em and at 1.25 to 4 times that (section 4). With antialiasing on and smoothing off it is 82.3% partial at 16 pixels, 52.5% at 32, 48.8% at 48 and 37.3% at 64, and partial at every one of the nine sizes from 16 to 64 pixels, whole multiples of 16 included; its dots sit on no pixel grid (section 4).719
Misaki and k8x12 carry embedded bitmap tables (EBDT and EBLC, read with CTFontCopyAvailableTables). The measured results above are what Core Text drew; whether it drew those bitmaps or the outlines was not determined (section 11).7
Misaki, PixelMplus10 and Press Start 2P have neither quiet fault. The rule that follows is about the font file as much as the size: whole multiples, a grid on the baseline, and whole-pixel advances.
Dynamic Type and the grid
Apple’s Human Interface Guidelines give iOS a default text size of 17 points and a minimum of 11, and a table for every Dynamic Type size. Body runs, from xSmall to AX5, through 14, 15, 16, 17, 19, 21, 23, 28, 33, 40, 47 and 53 points; Caption 1 from 11 to 43.6010
On a 3× display, a font pixel is a whole number of device pixels only when the point size times 3, divided by the em in font pixels, is a whole number. Of the twelve Body sizes, a 12-pixel em lands whole at 3 of them (16, 28 and 40 points), an 8-pixel em at 2 (16 and 40), and a 10-pixel em at 1 (40). Of Caption 1’s twelve sizes, a 12-pixel em lands whole at 3 (12, 16 and 32).760
Which Body sizes land on whole device pixels at 3×, and the brief’s rounding; drawn by figures/make_figures.py from measure_coretext.out and measure_kiradex_text.out.
So a pixel font cannot simply follow Dynamic Type. If it scales continuously with the system’s sizes, it is off its grid at every size but the few whole ones: 11 of 12 Body sizes for a 10-pixel em, 9 of 12 for a 12-pixel em.733 Off the grid, Core Text on the Mac, with antialiasing on, smeared every face measured; whether SwiftUI on an iPhone does the same depends on its rendering and placement, which the brief’s captures decide. It has to take the size the system asks for and round it to the grid, which section 10 turns into an API recipe and section 12 into the brief’s rule.
7. Japanese line breaking
A short preset line never wraps. A box of text does, and Japanese has rules for where.
The rules
The W3C’s Requirements for Japanese Text Layout (a Working Group Note of 11 August 2020), in 3.1.7, sets the line-start rule: “In principle, no line should begin with closing brackets (cl-02), hyphens (cl-03), dividing punctuation marks (cl-04), middle dots (cl-05), full stops (cl-06), commas (cl-07), iteration marks (cl-09), a prolonged sound mark (cl-10), small kana (cl-11) or warichu closing brackets (cl-29)”. It also records the looser practice: “Not a small number of books adopt a less strict set of rules which allow IDEOGRAPHIC ITERATION MARK “々” (one of the iteration marks (cl-09)), prolonged sound mark (cl-10) and small kana (cl-11) to start a line.”21
Unicode’s line-breaking algorithm, UAX #14 (revision 57, for Unicode 18.0.0, dated 2026-09-01), puts the small hiragana and katakana and the prolonged sound mark in its class CJ, and says: “Treating characters of class CJ as class NS will give CSS strict line breaking; treating them as class ID will give CSS normal breaking.”61
What Core Text did
measure_coretext.swift, run on macOS 27.0 (not iOS), lays out one Japanese paragraph of 131 characters with CTFramesetter at widths of 8, 10, 12, 14, 16, 18 and 20 em, and checks the first character of every line after the first against its own list of line-start-prohibited characters: some of the closing brackets, the Japanese comma and full stop, the middle dot, the colon and semicolon, the question and exclamation marks, the prolonged sound mark, the small kana and 々.7 That list is not a subset of the classes JLREQ’s rule names, only an overlap with them. Seven of its 40 characters, ,.:;?! and ), are the fullwidth forms Japanese text sets where JLREQ lists the ASCII ones; the other 33 are JLREQ’s own. Of the closing brackets it holds 」』】〉》〕 and the fullwidth ), leaving out ’ ” ] } » 〟 ⦆ 〙 〗 and the ASCII ); of the 40 small kana it holds 22, leaving out ゕ, ゖ and ㇰ to ㇿ; and it leaves out the hyphens (cl-03), among them ‐, 〜, ゠ and the en dash, ‼ ⁇ ⁈ ⁉ of cl-04, and every iteration mark (cl-09) but 々, among them ヽ, ヾ, ゝ, ゞ and 〻.721 The results below are for the probe’s list, and the test paragraph’s text decides which characters ever reach a line start.
Untagged, no line began with a character from the probe’s list at any width, in Hiragino Sans (the system’s Japanese face, run as the control) or in any of the eight pixel faces: Misaki Gothic, Misaki Gothic 2nd, k8x12, k8x12S, PixelMplus10 Regular and Bold, PixelMplus12, and DotGothic16 at its nominal 16 dots.7
Tagged with kCTLanguageAttributeName set to ja, the rule loosened. In Hiragino Sans, both Misakis, both PixelMplus10s, PixelMplus12 and DotGothic16, a line began with a small kana (っ, ョ or ァ) at 6 of the 7 widths, every width but 18 em.7 In k8x12 and k8x12S, a line began with a small kana (っ, ョ or ょ) at 4 of the 7 widths, and with the prolonged sound mark ー at two, 10 em (beside ョ) and 18 em, a fifth width.7
On my reading of those counts against the two quotations, the ja tag gives the less strict rule JLREQ describes, the one UAX #14 calls CSS normal breaking, and the untagged run behaves strictly.2161 That is the opposite of what I expected, since the tag names the language, and it is why the brief does not tag box text without a capture.
Two notes on the measurement itself, both from reading the script against its output. The widths are in em, the font’s full size, so for k8x12, whose kana are 8 pixels in a 12-pixel em, each width holds one and a half times as many kana as its number says; k8x12’s paragraph took 12 lines at 8 em where the others took 17.7 And DotGothic16, which has no grid, was laid out at twice its nominal 16 dots, 32 pixels to the em; at that size it took the same number of lines as Hiragino Sans at every width, from 17 at 8 em to 7 at 20, and the same small kana began its lines when tagged.7
8. Pixel games since: boxes, bubbles, Japanese and Chinese
Stardew Valley: two forms of speech
Stardew Valley keeps both of the forms Kiradex already has. Its dialogue box pages with a command, $b, which the Stardew Valley Wiki’s modding page on dialogue (last edited 20 May 2026) describes as one that “Indicates pauses in dialogue, where the player will need to click for the next part to load in a new dialogue box”, and portrait commands that “set the portrait shown in the dialogue box for the current line of dialogue”.62 And it has speech bubbles, Strings\SpeechBubbles, for “dialogue spoken by NPCs through speech bubbles when the player enters a given location”.62 The box is for conversations, fronted by a face; the bubbles are what a town says as you walk in.
Its non-Latin languages use bitmap fonts. The wiki’s page on custom languages (last edited 3 January 2026) has a new language set UseLatinFont to false and supply a .fnt file, modeled on “the Chinese, Japanese, Korean, and Russian fonts in the Fonts folder”. Its FontPixelZoom is “A factor by which to multiply the font size. The recommended baseline is 1.5”, and FontApplyYOffset sets “Whether to shift the font up by four pixels”, to align “languages with larger characters like Chinese and Japanese”.63 A zoom of 1.5 is not a whole multiple, but the page gives it as the starting point for a language a modder adds, not as what the shipped Chinese, Japanese and Korean fonts use, and whether a zoom resamples on screen depends on the game’s scale to device pixels as well; so it says nothing certain about how Stardew draws its own Japanese.63
The Final Fantasy Pixel Remaster: the font was the work
The Pixel Remaster’s console release added a pixel-font option, and the producer, Naofumi Takuma, told Siliconera (Jenni Lada, June 2, 2023) how it was made. For the Western languages, the team found font data produced in-house and used it. For Japanese: “We didn’t have any alternative data saved on file in-house for the Japanese language font, so we decided to create a new one.” And: “While the Western language font required us to create only a few hundred characters, with the Japanese font, we had to create around 7,500 characters.”8
That is the world post’s fourth imprecision. It gave the same figure, “about 7,500 Japanese characters”, and cited GoNintendo’s report of the work; the primary source is the producer’s own interview with Siliconera, which says “around 7,500 characters”.238 Same number, better source.
Seven and a half thousand is roughly the size of JIS X 0208’s 6,879 characters, the usual coverage target; that comparison is mine.533 A game whose every line is fixed before it ships can draw fewer, as Final Fantasy VI’s Japanese ROM did with 1,248 dialogue letters (section 3), and then each string has to be checked against the font; a font that covers the whole set needs no such check for any string drawn from it.4 The lesson for a small app is the one the free fonts in section 4 teach from the other side: for text that will keep growing, Japanese pixel type is a solved problem only if someone has already drawn the seven thousand characters and the license lets you ship them.
What the modern pixel games ship
On Steam, on October 5, 2026, Stardew Valley lists 12 languages, among them Japanese, Simplified Chinese and Korean; Celeste 9, among them Japanese and Simplified Chinese; Sea of Stars 11, among them Japanese, Korean and both Simplified and Traditional Chinese; and Eastward 7, among them both Chinese and Japanese.64 Every one of the four ships Japanese.
How Celeste, Sea of Stars and Eastward draw their Japanese and Chinese text I could not find in a developer source, so this post says nothing about it beyond the language lists (section 11).
Two forms of speech in Kiradex
Kiradex already has both of Stardew’s forms: the WorldHUD box at the bottom of the screen and the plaza’s bubbles over heads.2625 What it lacks is the canon’s typography inside them, and a way for VoiceOver to hear either. The bubble’s own comment in the code says what it was reaching for: “A line said, as the old games drew it: a cream box with a dark border.”25
9. The craft, as rules with numbers
These are the rules the measurements support, each with where it came from. Where a rule turns a measured fact into a recommendation, the recommendation is mine and the rule says so by its verb.
| Element | Rule | Source |
|---|---|---|
| Box | The box is the bottom third of the screen and two lines. Put it at the bottom; let a game that needs the bottom move it to the top. | Emerald’s frame 30 by 6 tiles, the full width and 30% of the height, around a 27-by-4-tile window of two 16-pixel lines; Crystal’s 20 by 6, 33%, two lines of 18, and Red’s the same 20 by 6; Final Fantasy VI’s window at row 1 or 18 |
| Latin glyphs | 4 to 12 pixels wide in a cell 8 to 16 pixels tall. | Emerald’s 6-by-16 cell at 5.25 pixels a character in real text; Final Fantasy VI’s 4 to 12 in a 16-by-11 cell; Crystal’s fixed 8 by 8 |
| Japanese glyphs | 8 to 12 pixels, about twice a Latin letter in the same font. | Misaki 8 by 8 with 7-by-7 ink; Emerald 8 wide in a 16 line; FireRed’s sheet and PixelMplus10 10; Final Fantasy VI and PixelMplus12 12; 8 against 4, 10 against 5, 12 against 6 in every Japanese-capable font measured |
| Line length | 18 to 41 characters a line; write to the width rather than fill it. | Crystal 18; Emerald’s 216-pixel window holds 41 at 5.25 pixels, its writers stopped at 38 for 95% of lines; Final Fantasy VI’s 220-pixel line holds 18 Japanese at 12 pixels |
| Breaks | Either the writer breaks every line, or the code wraps by word; never both. | Emerald’s 24,100 hand-broken lines, none over 208 pixels; Final Fantasy VI’s CalcTextWidth and NewLine |
| Speed | Count characters in time: 60 a second as the ceiling (Emerald’s and Crystal’s fast setting, and Final Fantasy VI’s one letter a frame), 15 to 20 for a middle setting, 7.5 to 12 for slow. | Emerald 7.5, 15 and 60, 60 held; Crystal and Red 12, 20 and 60; Final Fantasy VI 60 |
| Advance mark | Move the mark on a slow clock: a step every 150 milliseconds, a cycle of about 0.6 second. | Emerald’s 0, 1, 2, 1 pixels, a step every 9 frames, 36 frames a cycle; Crystal’s ▼ blinking against the border tile |
| Frames | Frames are tiles, not drawings: a corner, an edge and a center, repeated. Let the player choose among a few. | Emerald’s 20 selectable 24-by-24 menu frames and its 14-tile dialogue strip with a mirrored bottom edge; Crystal’s 8 frames of 6 one-bit tiles; Final Fantasy VI’s 8 styles of 28 tiles with 7 editable colors |
| Crisp type | A pixel font is crisp only at whole device pixels per font pixel, and, on the Mac, only with font smoothing off; capture an iPhone to see which holds there. | Core Text on macOS 27.0 with font smoothing off: 0.0% partial pixels at whole multiples of a grid on the baseline with whole-pixel advances (Misaki, PixelMplus10, Press Start 2P), 19.4% to 82.2% between them; with the context’s default smoothing, PixelMplus10 at its own 10 pixels 178 of 272 inked pixels partial |
| Clean files | Check a font’s grid, its offset from the baseline and its advances before choosing it. | Core Text on macOS 27.0: PixelMplus12’s grid 0.39 pixel off the baseline, smeared at every size; k8x12’s 8.01-pixel kana advance, smeared once glyphs are placed exactly; Silkscreen’s 10 stray points, 1.2% to 4.5% |
| Dynamic Type | Honor the user’s size by rounding it to the grid, never by scaling continuously, and never below 11 points. | A 10-pixel em is whole at 1 of 12 Body sizes at 3×, a 12-pixel em at 3, an 8-pixel em at 2; the HIG’s 11-point minimum |
| Japanese breaks | Keep the strict line-start rule for box text; keep preset lines short enough never to wrap. | JLREQ 3.1.7; Core Text on macOS 27.0, untagged, kept the strict rule for the probe’s partial line-start list at every width measured, tagged ja let small kana start lines at 6 of 7 widths in most faces (4 of 7 in k8x12 and k8x12S) |
| Speakers | Name a speaker in the line, not in a separate field or over the head. | Emerald’s box has no name field and 6.5% of its paragraphs open with a name and a colon |
| Coverage | Cover the standard set, about seven thousand characters, before Japanese text the writers have not fixed can ship; a fixed script can ship fewer, checked string by string. | JIS X 0208’s 6,879; the Pixel Remaster’s “around 7,500”; Final Fantasy VI’s Japanese ROM’s 1,248 dialogue letters; Misaki, k8x12 and DotGothic16 draw all of JIS X 0208, PixelMplus all but three code points |
| Licenses | Ship a face only with its license text in hand and its terms read. | the three-sentence license of Misaki, k8x12 and PixelMplus; the SIL Open Font License of DotGothic16, Press Start 2P and Silkscreen, with Press Start 2P’s Reserved Font Name |
| Two forms | Keep both forms, a box and a bubble. Which says what is a game’s choice; Kiradex’s, mine, is the box for what a place says and the bubble for what a person says. | Stardew Valley has both: its box for conversations, its bubbles for what NPCs say as the player enters a location |
Every number in the table comes from sections 1 to 8 and carries its note there.1467921862
10. The Apple way: type, sizes, line breaks and VoiceOver
Kiradex World is a RealityKit scene used as a 2D renderer, with an orthographic camera at a whole number of device pixels per texel and SwiftUI over it for everything a person reads.23 The world post decided that dialogue text “lives in SwiftUI at UI scale rather than in the world”, and this section keeps that decision and fills in the APIs: what Apple asks of custom type, how a bundled pixel font gets in, how Dynamic Type can be honored by rounding, how Japanese breaks, and how VoiceOver can read a stage that is one RealityKit view. Every availability below is from Apple’s documentation pages, saved on October 5, 2026; none of the code has been built, and every cost in this section is unmeasured on a phone.65
What Apple asks of text
The Human Interface Guidelines’ Typography page gives iOS and iPadOS a default text size of 17 points and a minimum of 11, and asks apps to “Follow the recommended default and minimum text sizes for each platform”, for custom and system fonts both.1060 System fonts support Dynamic Type and accessibility features such as Bold Text on their own; for the rest the page says: “If you use a custom font, make sure it implements the same behaviors.”10
The Accessibility page sets the size of the job: “Ideally, give people the option to enlarge text by at least 200 percent (or 140 percent in watchOS apps).”20 The Games page adds legibility to it: “To keep text comfortably legible on each device, ensure that it contrasts well with the background and uses at least the recommended minimum text size in each platform.”66
Body’s range, 14 to 53 points, is from about 82% to about 312% of its default of 17; the box’s rounded sizes at 3×, 13.33 to 53.33 points, are 80% to 320% of their own default of 16.67, past the 200% the Accessibility page asks for.601133
Scaling a custom font
Apple has three ways to scale a value with Dynamic Type, and one to stop it.
UIFontMetrics, “A utility object for obtaining custom fonts that scale to support Dynamic Type”, available since iOS 11.0, withscaledFont(for:),scaledFont(for:maximumPointSize:)andscaledValue(for:), which “Scales an arbitrary layout value based on the current Dynamic Type settings”.67Font.custom(_:size:relativeTo:)in SwiftUI creates a custom font “that scales relative to the giventextStyle”, since iOS 14.0.68ScaledMetric, “A dynamic property that scales a numeric value”, since iOS 14.0.69Font.custom(_:fixedSize:)creates a custom font “that does not scale with Dynamic Type”, since iOS 14.0.68
A pixel font wants the first or third and the last together: read the size the system would use, round it to the font’s grid, and set the rounded size as a fixed size so the system does not scale it again. The rounding itself is arithmetic, in section 12’s brief: a font pixel becomes m device pixels, m = max(m₀, ⌊size × displayScale / 10 + 0.5⌋) for a 10-pixel em, the nearest whole number with a half rounding up, where m₀, the smallest m of at least 11 points, is 4 at 3× and 3 at 2×, and the point size is 10 × m / displayScale. At 3× that maps the twelve Body sizes to m of 4, 5, 5, 5, 6, 6, 7, 8, 10, 12, 14 and 16 (Small, 15 points, is the one exact half: 4.5 rounds up to 5): 16.67 points at the default size, 53.33 at AX5.11
To limit the size inside a view, dynamicTypeSize(_:) (iOS 15.0) sets or bounds the Dynamic Type size, and its page advises: “When limiting the Dynamic Type size, consider if adding a large content view with accessibilityShowsLargeContentViewer() would be appropriate.”70 That modifier, also iOS 15.0, carries its own warning: “Don’t use the large content viewer as a replacement for proper Dynamic Type support.”71 The brief limits only the bubbles, which must stay one line, and never the box.
Bundling the font
A font ships in the app bundle and is named in UIAppFonts, “App-specific font files located in the bundle and that the system loads at runtime” (iOS 3.2), or registered in code with CTFontManagerRegisterFontsForURL(_:_:_:) (iOS 4.1).7273
Drawing the frame and placing the baseline
A frame drawn from a few tiles is an image resized without smoothing. SwiftUI’s Image.interpolation(_:) sets the interpolation quality, since iOS 13.0, and Image.resizable(capInsets:resizingMode:), which “Sets the mode by which SwiftUI resizes an image to fit its space”, stretches or tiles everything but the corners, also since iOS 13.0.7475 The brief draws its frame in a Canvas instead, so that every source pixel is exactly m by m device pixels; on my reading resizable(capInsets:) with .tile would serve as well only if its tiling lands on whole device pixels, which nobody has measured.
Text.baselineOffset(_:), which “Sets the vertical offset for the text relative to its baseline” (iOS 13.0), is the tool for a face whose grid sits off the baseline, as PixelMplus12’s does; section 6 measured that a shift fixes such a face at one size only, which is one reason the brief does not use it.767
Japanese line breaking in SwiftUI and UIKit
typesettingLanguage(_:isEnabled:) (iOS 17.0) is for Text that “may contain text of a particular language which doesn’t match the device UI language. In that case it’s useful to specify a language so line height, line breaking and spacing will respect the script used for that language”. Its page adds: “this language does not affect text localization.”77
UIKit’s NSParagraphStyle.LineBreakStrategy (iOS 9.0) has three named strategies. standard (iOS 14.0) “uses the same configuration of line-break strategies that it uses for standard UI labels” and “may be unsuitable for large amounts of text”; pushOut (iOS 9.0) is there to avoid “an orphan word on the last line”; hangulWordPriority (iOS 14.0) is for Korean.78
Section 7 measured that Core Text on macOS, tagged ja, let small kana start lines at 6 of 7 widths in most faces (4 of 7 in k8x12 and k8x12S), and untagged did not.7 What SwiftUI’s Text does with Japanese when the device language is Japanese, and whether typesettingLanguage sets the same attribute the measurement set, were not measured (section 11). The brief therefore keeps box text untagged unless a capture shows the strict rule holding with the tag.
VoiceOver over a stage
SwiftUI’s accessibility modifiers describe a view: accessibilityLabel(_:) names it, and accessibilityValue(_:) adds its value, with a rule in its documentation: “Use this method to describe the value represented by a view, but only if that’s different than the view’s label.” Both are iOS 16.0 in the LocalizedStringResource form the pages list.79
A game’s stage needs more than a label. accessibilityDirectTouch(_:options:) (iOS 17.0) makes an area where “Direct touch areas passthrough touch events to the app rather than being handled through an assistive technology, such as VoiceOver”, after the user activates it.80 AccessibilityTraits.updatesFrequently (iOS 13.0) is for an element that changes: “Use this trait when you want an assistive technology to poll for changes when it needs updated information.”81 AccessibilityNotification.Announcement (iOS 17.0) posts a spoken announcement, with an accessibilitySpeechAnnouncementPriority attribute that sets how it queues against other speech.82 And accessibilityReduceMotion (iOS 13.0) reports the Reduce Motion setting, which, its page says, means “UI should avoid large animations”.83
RealityKit can describe the entities themselves. AccessibilityComponent (iOS 17.0) is “A component that stores accessibility information for an entity”, whose name “VoiceOver says aloud when it describes the entity”.84 Apple’s article “Improving the Accessibility of RealityKit Apps” says to “set the properties on each visible Entity in your scene”: Entity’s own accessibility properties, isAccessibilityElement, accessibilityLabel and accessibilityDescription, which the article dates to iOS 14 and whose pages are now marked deprecated, all but isAccessibilityElement (the page also lists accessibilityLabelKey, a label as a localized string key), an older route beside the component.85 RealityView, the SwiftUI view Kiradex’s stage uses, is iOS 18.0.86 Whether a RealityView with a virtual orthographic camera, as Kiradex sets it up, exposes those entities to VoiceOver is not documented on those pages and was not tested (section 11).
Testing it can now be automated. XCUIVoiceOverService “Provides programmatic control of VoiceOver for UI testing”, available from iOS 27.0, and is reached through the voiceOverService property on XCUIDevice.87
Kiradex today
Read from the repository on October 5, 2026, at commit 334fbfe with that day’s uncommitted edits, never run or changed by me; the app was being edited while I read it (section 11):11
- One face, the system’s. The theme’s font family is
.systemDesign(.monospaced), and the world’s views use the kit’s roles: caption at 10 call sites in four files, label at 3, body at 3 and title at 3.2411 - A custom-font path that scales continuously. The shared kit’s
Kit941.FontFamily.custom(named:)resolves toFont.custom(_:size:relativeTo:)at 34, 22, 17, 13 and 12 points for display, title, body, label and caption.88 With a pixel font that would scale through every Dynamic Type size, which section 6 counts as off its grid at 9 of 12 Body sizes for a 12-pixel em and 11 of 12 for a 10-pixel em.7 - The bubble.
Text(text).kit941Font(.caption, weight: .bold), ink onWorldInk.paper(an art surface with two fixed colors), padded by the kit’s small spacing across and extra-small down, with a 2-point ink border and.fixedSize(), so one line. It is placed a large spacing step aboverig.headPoint, shown while the line is under 4 seconds old, and labeled"Said: \(text)"inside an overlay with hit testing off. That overlay sits inside the stage, under the two overlays the stage lays over the whole scene: scanlines, a black line at 16% opacity every 3 points, and a vignette.2511 - The box.
WorldHUDputs the place’s title (caption, bold, uppercased) and status (caption, semibold) in a cream box with the same 2-point border at the bottom of the screen; half open, on the Duo, it keeps to the wider side of the fold. It takes no taps (.allowsHitTesting(false), “Taps go through to the ground under it”), and it is laid over the whole stage, above the scanlines and the vignette.26 - What is said. 23 preset lines on the server, 3 to 40 characters, a median of 16; the longest is “Anyone have a card from the first years?”. Handles are two words from lists of 20 and 20 and four digits, at most 20 characters (“Glacier Shuffle 0000”). Nothing is drawn over a collector’s head but the bubble; the handle shows in the panel a tap opens.288911 This series’ post on the town’s own life counted the same 23 lines and a server limit of one line every 2.0 seconds per player.9091
- No font, one language. No font file is bundled (the only
.ttfin the repository isscripts/screenshots/MartianMono-latin.ttf, for the screenshot tool); the project’sknownRegionsareBaseanden, and there is no.xcstringsor.lprojfile.1127 - VoiceOver hears coordinates. The stage is one element,
.accessibilityLabel("The plaza")and.accessibilityValue(stepTrace), wherestepTraceis the last step as “x,y facing”, kept because “the walks read it, since nothing else the simulator logs reaches them”. NoAccessibilityComponent,accessibilityDirectTouch,AccessibilityNotification,typesettingLanguage,UIFontMetrics,ScaledMetricordynamicTypeSizeappears anywhere in the app’s source.2511 - Signs without letters. The forge’s
sign()is “A hanging sign on a bracket, 12 × 11” whose face is two rows of ink marks, not letters; the plaza map has one sign tile,S. A finger on the ground walks the player to the tile under it at once and is followed across tiles; a tap on a collector opens their panel when the finger lifts, and one on the kiosk the town’s board; a sign tile is ground like any other, with no tap of its own.2992 - The world’s scale. By
PlazaRig.fit’s rule, the floor of the view’s pixel height over 320, a texel is 8 device pixels on the iPhone 18 Pro Max (440 by 956 points) and on the Duo’s inner display in portrait, 6 on the Duo’s outer display in portrait and the inner in landscape, and 4 on the outer in landscape: 2.67, 2.00 or 1.33 points.9311
Why the text stays out of the world
The world post’s arithmetic on this is right: “At k = 8 on a 3× display a five-texel font is 13.3 points and at k = 6 it is 10 points, under the HIG’s 11-point minimum”.23 It binds only fonts under 5.5 texels at k = 6 (11 points at 2 points a texel); an 8-texel em at k = 6 is 16 points.1133 The conclusion, text at UI scale in SwiftUI, holds for a different reason: at world density a 10-pixel em is 26.67 points on a phone at 8 device pixels a texel, which leaves 29 Latin characters to a line of the box on the 18 Pro Max.11 Measured in tiles, the canon’s box is narrow. A box 16 hardware tiles wide, 128 pixels, holds 16 characters of an 8-by-8 font, about 24 of Emerald’s Latin at its average of 5.25 pixels (21 at the worst case of 6), 16 of Emerald’s 8-pixel Japanese and 10 of Final Fantasy VI’s 12-pixel Japanese. Sixteen of Kiradex’s 16-texel tiles are 256 texels, which at the world’s own density hold 42 Latin characters and 21 kana of PixelMplus12, or 64 and 32 of Misaki.11 And a box measured in Kiradex’s own tiles does not fit three of the five views measured: 16 tiles of 16 texels are 683 points at 8 device pixels a texel, wider than the iPhone 18 Pro Max (440 points) and the Duo’s inner display in portrait (669), and 512 points at 6, wider than the Duo’s outer display in portrait (466). It fits only the two landscape views: 512 points in the inner display’s 951, and 341 points at 4 device pixels a texel in the outer display’s 678.11 A Kiradex box is sized in points, not tiles.
The same arithmetic at the brief’s size: PixelMplus10 at m = 5, 16.67 points, holds 47 Latin characters or 23 Japanese ones a line in the box on an 18 Pro Max in portrait, after today’s 48-point inset, 16 points of margin and 8 of padding a side, counted from the full width, with no side insets (assumed, not measured). The box sits inside the safe area, so on the Duo’s outer display in portrait the count depends on its side inset: 50 or 25 with none, and 40 or 20 with the 84-point trailing inset, the side rail, that this series measured on the closed display (section 11). Under the brief’s frame (section 12, item 3), which takes 8 font pixels a side in place of the padding and rounds the box down to whole font pixels, the same lines hold 45 or 22 on the 18 Pro Max, and 48 or 24, or 38 or 19 with the rail, on the Duo.119495
Costs
None of this has been measured on a device. Bundling a font of 7,253 glyphs adds its file to the app;7 drawing text through SwiftUI with a custom font costs what any Text costs; and the brief’s frame is a Canvas that redraws when its size changes. On my reading none of these is large next to a RealityKit scene, but that is a reading, and the brief’s captures are where it gets checked.
11. What is measured and what is not
Everything numbered in this post comes from scripts that re-run against saved sources, from pages saved on October 5, 2026, or from dated readings of the Kiradex source; the notes name which. This section is the list of what that does not cover.30
Measured from source, re-runnable. measure_gen_text.py against the four pret decompilations: Emerald’s font table (ten entries, seven fonts); text speed in frames and characters a second; the scroll; the advance arrow; the 27-by-4 window and the 30-by-6 frame; the 20 selectable frames; Latin glyph widths; 24,100 script lines in characters and pixels; 934 of 14,412 paragraphs naming a speaker; the Japanese glyph sheets; FireRed’s delays and window; Crystal’s and Red’s screens, boxes, delays, frames and font sheets.1 measure_ff6.py against the ff6 disassembly: window styles and palettes, the dialogue font’s cell and widths, the Japanese font’s size and width runs, the 220-pixel line, one letter a frame, the 100-frame title, word wrap, the config screen, and the window’s gradient table.4 measure_bdf.py against Misaki’s and k8x12’s BDF files.6 measure_coretext.swift and measure_outline_coords.swift, compiled and run on macOS 27.0, against ten TrueType files and Hiragino Sans.719 measure_kiradex_text.py against Kiradex as it stood on disk at commit 334fbfe (re-run on October 15 against the live tree, its earlier lines unchanged), and measure_figure_width.py against its body sheet.1196 measure_hig.py, measure_steam_languages.py and measure_apple_docs.py against the saved pages.606465 measure_ios27_iphones.py against the iOS 27.0 simulator runtime’s list of iPhones, and probe_line_start_glyphs.swift, compiled and run on macOS 27.0, against JLREQ’s class tables and PixelMplus10.9798 measure_cell_masks.py against the saved pret sheets: the brief’s cell-mask reference.99 That is thirteen scripts; a fourteenth, dump_glyphs.swift, kept with this post’s figures, drew PixelMplus10 at its own size with font smoothing on and off.9
Not measured, or weaker than it looks.
- Core Text on macOS stands in for iOS. Every rendering and line-breaking number in sections 6 and 7 is from macOS 27.0’s Core Text drawing into a bitmap context, with font smoothing switched off, not from SwiftUI on an iPhone. The brief’s capture checks are the measurement that counts; until they run, what an iPhone draws is a prediction.79
- Bitmap strikes. Misaki and k8x12 carry
EBDTandEBLCtables; whether Core Text drew those bitmaps or the outlines at 8 and 12 pixels was not determined.7 - SwiftUI’s own placement. Whether SwiftUI places
Textorigins on whole device pixels, and which line-breaking rule it applies to Japanese when the device language is Japanese, were not measured. - Weights. PixelMplus10 Bold is the only bold face measured; synthetic bold on any pixel face was not.7
- Legibility. Grids and coverage are measured; whether 10-by-10 kanji read well to a Japanese reader at 16.67 points is not, and needs a Japanese reader on a device.
- Japanese ROMs. pret builds the English games. The Japanese Emerald’s own box, speeds and font were not read; the Japanese sheets in section 1 are those the English ROM carries for names. Japanese Crystal and Red were not read.30
- Final Fantasy VI’s graphics. The disassembly has ranges and code, not pixels; the window tiles’ layout and what the gradient looks like were not seen. The English large font’s range, 5,120 bytes, is not a whole number of 22-byte letters, unexplained. That the gradient table is consumed one value a scanline is from comments, not traced.4
- Crystal’s blink period was not derived; its two states were read from the code.16
- Chrono Trigger was not measured at all. No public disassembly was cloned, and Data Crystal’s ROM map returned HTTP 403.49
- Celeste, Sea of Stars and Eastward. Only their Steam language lists; how they draw Japanese and Chinese text was not found in a developer source.64
- Pixel Operator, m5x7 and m3x6 were not downloaded or measured, and Pixel Operator’s own repository returned HTTP 404; their licenses are as their pages state them, Pixel Operator’s from DaFont alone.565758
- PixelMplus’s three unmapped JIS code points were not checked at their alternative code points.7
- DotGothic16’s size. It has no pixel grid, so it was drawn and laid out at a nominal 16 dots per em and multiples of that, my choice from its name and its dot pitch; no other nominal size was tried, and its numbers in sections 6 and 7 are at those sizes only.719
- Font smoothing. Section 6’s renders all disallow font smoothing; with the bitmap context’s default, only one face at one size was drawn, PixelMplus10 at 10 pixels, 178 of 272 inked pixels partial. The other faces and sizes were not drawn with smoothing on.9
- The app changed while I read it. Kiradex was being edited on October 5 while this post was written. Earlier that day the stage’s tap handling was rewritten and its lines moved by 30 to 144, and three of the world’s Swift files were not yet in any commit; fourteen minutes after my reading, the stage and the rig had changed again. The post reads commit
334fbfewith that day’s uncommitted edits as they stood when I read them: the app’s files at 12:28 Pacific (the server’s collectors and the town map at 12:33), the scripts that measure it at 12:34 and 12:36. The notes give each file’s lines at that reading; the brief names symbols rather than lines, and I checked at 12:42 that every symbol it names was still there.11 - The iPhones the app runs on. The list of 33 iPhones, three of them 2×, is the iOS 27.0 simulator runtime’s, read from Xcode on my Mac; Apple’s page of the iPhones iOS 27 supports was not fetched.97
- Safe-area insets. This series’ post on the iPhone Duo simulator measured the closed outer display, 466 points wide, with its content 382 points wide inside an 84-point trailing inset, the side rail, in a probe that showed the status bar, a toolbar and a tab bar.95 Kiradex’s world hides the status bar and the home indicator and shows no bars; the code puts the world’s chrome inside the safe area and says that keeps it clear of the cameras and the side rail, but neither the code nor that measurement says how wide the inset is with the status bar hidden, so the Duo’s numbers in sections 10 and 12 are given both ways, with no side inset and with the measured 84 points. The other iPhones’ side insets in portrait were not measured; the brief’s numbers for them assume none.94
- RealityKit accessibility in Kiradex’s setup.
AccessibilityComponentis documented for iOS 17.0; whether aRealityViewwith a virtual orthographic camera exposes entity elements to VoiceOver is unverified.84 - The brief’s numbers. The rounding of
m, the bubble one step smaller than the box and held under a ceiling each view sets, the 32-point margin, the box’s queue and its three lines, the frame’s 8 font pixels in place of the box’s padding, the 150-millisecond mark steps, four frames and six cells for announcements are proposals, not facts.
Where this post differs from the world post. Sections 1, 2, 5 and 8 each correct one statement in it, with the measurement; its size arithmetic is right (section 10). Three facts this post takes from the other posts in the series were checked against the app: the town’s 23 preset lines match lines.py; the characters post’s note that the bubble still takes its place from the cell’s height matches PlazaRig.headPoint, which adds half the sheet’s frame height and 4 to the walker’s position; and the figure under the bubble is the one that post’s update describes, 30 texels tall in a 32-by-40 cell, which the app’s manifest, rig.py and body sheet confirm. That post gives a width only for its earlier 26-texel figure, about 14 at the shoulders; section 12 measures the current figure’s width from the sheet.90100289396
Sources not reached. Data Crystal’s Chrono Trigger ROM map (HTTP 403); Pixel Operator’s repository at NotABug (HTTP 404); the m5x7 and m3x6 font files (behind a name-your-own-price download, not attempted); and a developer source for Celeste’s, Sea of Stars’s or Eastward’s Japanese and Chinese text (not found).49565758
12. The brief: what Kiradex builds and the checks it must pass
The app has moved on since. By commit fc9d4876 on October 15 the stage’s label and value are set in a StageAccessibility view modifier (PlazaStage.swift, applied at line 373, defined from line 520), still “The plaza” with the step trace as its value, and the element is absent while a place covers the stage. The checks below read the October 5 build, and every “today” means that build; item 8 applies to both.101
Author synthesis. What follows is my proposal, derived from sections 1 to 11 and written as a diff against Kiradex as it stood on October 5, 2026, at commit 334fbfe with that day’s uncommitted edits in its working tree (section 11): system monospaced text in every world view, a one-line bubble in caption bold, the WorldHUD box at the bottom, an English-only app with English preset lines on the server, a letterless sign sprite, and a stage VoiceOver reads as coordinates.2211 The world post’s direction stands: text is art; dialogue lives in SwiftUI at UI scale, not in the world’s texels; a box built from tiles; two lines; a typewriter; an advance mark.23 What changes is the face, for the reason section 5 gives, and the arithmetic, because Dynamic Type has to be rounded to the font’s grid.7 Nothing here draws a franchise’s glyph, frame, mark or word; everything is drawn by the forge or set in a font whose license is quoted in section 4. The legal notes are my readings, not legal advice. None of it has been built.
Every check below is one a script, a test’s log line or a capture can pass or fail, except one dated manual check on a device in item 8. Where a check’s outcome depends on the device, the text size, the line it picks or what else the box is showing, it names them. The brief names the app’s code by its symbols, not its line numbers, because the code moved while I read it; the notes give the lines as I read them, with the commit and the time. Where the evidence behind an item is a measurement on my Mac and the check runs on an iPhone or its simulator, the check decides it, not the Mac.
1. One face for both languages: PixelMplus10
What. Bundle PixelMplus10-Regular.ttf and PixelMplus10-Bold.ttf (by itouhiro, from the M+ BITMAP FONTS of the M+ FONTS PROJECT; license: M+ FONT LICENSE, https://github.com/itouhiro/PixelMplus/blob/master/misc/mplus_bitmap_fonts/LICENSE_E) through UIAppFonts, with LICENSE_E in the bundle, and add an acknowledgements section to Settings, which has none today, naming itouhiro and the M+ FONTS PROJECT.1872 Use it for every word drawn over the world: bubbles, the WorldHUD box, sign text, the quest board and the show-and-tell plates. The rest of the app, the Dex, the scanner and Settings, keeps the system face.
Why this one. Two measured faces have all three things the rules in section 9 ask for, Japanese, a clean grid and whole-pixel advances. Misaki Gothic and Misaki Gothic 2nd draw all 6,879 JIS X 0208 characters on an 8-pixel grid on the baseline, with 0 of 848 and 0 of 860 sampled outline coordinates off it and advances of 4 and 8. PixelMplus10 draws 6,876 of the 6,879, every kanji of both levels, on a 10-pixel grid on the baseline, 0 of 1,032 sampled outline coordinates off it in the Regular and 0 of 1,000 in the Bold, with advances of 5 for Latin and 10 for kana.7 The brief takes PixelMplus10, my choice: it is the larger of the two, 10 pixels to Misaki’s 8, and Misaki’s 8-by-8 kanji are the ones whose legibility to a Japanese reader is most in doubt (section 11); and it comes with a Bold that was measured, so bold text needs no synthesis. Refused, each with the measurement that refuses it:
- PixelMplus12 (M+ FONT LICENSE): its grid sits 0.39 pixel below the baseline, and Core Text on macOS 27.0 smeared it at every size.7
- k8x12 (the three-sentence license on littlelimit.net, https://littlelimit.net/font.htm#license): its kana advance is 8.01 pixels, and its Latin letters are 4 pixels wide.717
- Misaki (the same license, https://littlelimit.net/font.htm#license): it has all three, but it is the smallest kanji measured, 8 by 8, and the one whose legibility to a Japanese reader is most in doubt (section 11).617
- DotGothic16 (SIL Open Font License 1.1, https://github.com/google/fonts/blob/main/ofl/dotgothic16/OFL.txt): its dots are on no grid, and Core Text on macOS 27.0 drew it 37.3% to 82.3% partial at 16 to 64 pixels.753
- Press Start 2P (SIL Open Font License 1.1 with a Reserved Font Name, https://github.com/google/fonts/blob/main/ofl/pressstart2p/OFL.txt) and Silkscreen (SIL Open Font License 1.1, https://github.com/google/fonts/blob/main/ofl/silkscreen/OFL.txt): no Japanese.75455
- m5x7 (its page: “Creative Commons Zero v1.0 Universal”, https://managore.itch.io/m5x7) and m3x6 (its page: “free to use with attribution”, no license field, https://managore.itch.io/m3x6): not measured; Latin by their pages’ own descriptions; behind a name-your-own-price download.5758
- Pixel Operator (CC0 1.0 by its author’s notes on DaFont, https://www.dafont.com/pixel-operator.font): not measured; 241 glyphs on DaFont, far too few for Japanese.56
The kit. My apps’ shared kit routes every text style through .kit941Font(role), and its custom-font path scales a font continuously, which would put this face off its grid at 11 of 12 Body sizes, the condition under which Core Text on the Mac smeared it.887 The world is already a declared art surface, WorldInk, with its own two colors.25 Add a Kiradex-local PixelText modifier, declared as that art surface’s exception with a comment, that takes a kit role and applies item 2’s rounding; propose it upstream as a kit family case only after it ships.
No synthetic weights. Bold text uses PixelMplus10-Bold; no .weight(_:) on the pixel face, because what Core Text does to a weight it has to synthesize was not measured (section 11).
Checks.
- A unit test, since Core Text is on iOS, loads both bundled files and asserts: units per em 1,000; every glyph the world’s strings and the preset lines use, in both languages, has an advance that is a multiple of 100 units; the sample glyphs’ outline points lie within 4 units of the 100-unit grid (0.04 of a font pixel, the tolerance section 4’s probe used). A face that fails cannot ship.
- A test that every string the world can show (preset lines, the String Catalog’s world keys, sign texts) maps to glyphs in the face, with no fallback font.
- A build-phase script that finds
LICENSE_Ein the built app bundle, and a UI test that finds the font’s name, its author and its license in the new acknowledgements section of Settings.
2. Sizes: Dynamic Type, rounded to the grid
What. A font pixel is always a whole number m of device pixels. For a role, read the Dynamic Type size the system would use (UIFontMetrics(forTextStyle:).scaledValue(for:), or @ScaledMetric(relativeTo:) on the role’s base size), round that size to a hundredth of a point, so that a scaled value a hair under a table size, 14.999… for 15, rounds as the table’s size does, then set m = max(m₀, ⌊size × displayScale / 10 + 0.5⌋), the nearest whole number with a half rounding up, where m₀ = ⌈11 × displayScale / 10⌉ is the smallest m of at least 11 points, and pointSize = 10 × m / displayScale, and apply it with Font.custom(_:fixedSize:) so the system does not scale it again.676968 The box’s title and text move from the caption role they use today to the body role, so that at 3× the box maps the twelve Body sizes to m = 4, 5, 5, 5, 6, 6, 7, 8, 10, 12, 14 and 16: 16.67 points at the default size and 53.33 at AX5.2611 The smallest, m₀ = 4 at 3×, is 13.33 points, above the 11-point minimum.1033 At 2×, which the iOS 27.0 simulator lists for three iPhones (item 4), m₀ is 3, 15 points, and the twelve Body sizes give 3, 3, 3, 3, 4, 4, 5, 6, 7, 8, 9 and 11: 15 points at the default size.9711 Bubbles use one step less, never below m₀, and never above a ceiling each view sets (item 4).
Placement. A pixel face is crisp only if its baseline lands on a device pixel (section 6). Round every computed origin in the world overlay to the device-pixel grid, (x × scale).rounded() / scale, including the .position of PlazaStage.bubbles, and give text views widths in whole device pixels, and the box a width and height in whole font pixels (item 3).
Line pitch. PixelMplus10 is 9 font pixels up and 2 down; set the line pitch to 12 font pixels, one blank row between lines, as Emerald’s 15-pixel Japanese glyphs and 16-pixel Latin cell sit in a 16-pixel line.71 At m = 5 that is 20 points a line.33
Checks.
- A pure-function test of the rounding for every
DynamicTypeSizeat display scales 2 and 3: the result is a whole number of device pixels per font pixel, never under 11 points, and never smaller for a larger size. The test feeds the function the HIG table’s Body sizes directly, not values read fromscaledValue(for:), whose exact output I have not seen: at 3× the twelve give the twelve values above, Small’s 4.5 rounding up to 5, and at 2× the twelve 2× values; each size less a billionth of a point gives the samem, through the rounding to a hundredth.11 - Captures of the plaza with a bubble and the box at Large, xxxLarge and AX3 on the iPhone 18 Pro Max simulator, and at Large on the iPhone SE (3rd generation) simulator at 2×, with the bubble overlay moved above the stage’s scanline and vignette overlays (item 4), and a script that crops the text and counts partial pixels against the two colors, ink and paper: 0 inside the glyph boxes, in the bubble and in the box. Each capture needs the plaza server running (
KIRADEX_PLAZA=1) and is taken within 4 seconds of the player saying today’s longest line from the Say menu, since every line comes back from the room102 and a bubble shows only while its line is under 4 seconds old.25103 The same script, run on the current build, records the baseline it replaces. Section 6’s 0.0% is a Mac’s Core Text with font smoothing off; this capture is the check that decides whether an iPhone draws the same.
3. The box: a forge-drawn frame, two lines, pages
What. WorldHUD’s box keeps its place: the bottom, inside the safe area, and the wider side of the fold when the Duo is half open.26 Its 2-point stroked rectangle becomes a frame the forge draws: frame-paper.png, 24 by 24 font pixels, three by three tiles of 8 (a corner, an edge and a center), in WorldInk’s two colors plus one shade, original to Kiradex. A PixelFrame view draws it in a Canvas, every source pixel an m-by-m square of device pixels: the corner tiles at the four corners, and the edge and center tiles repeated outward from the top-left corner, the last tile cut where it meets the far side, so the frame’s pixels are the text’s pixels. For every source pixel to be a whole square, the box’s width is the safe area’s width, where the WorldHUD lays it out today, less 16 points a side, rounded down to whole font pixels, a multiple of m device pixels, and its height is whole font pixels too: on the 18 Pro Max in portrait at m = 5, with no side insets (assumed, not measured), 1,224 device pixels, which is 244.8 font pixels, round down to 1,220, 244 font pixels.1194 The text sits inside the frame, 8 font pixels from the box’s edge on every side, in place of today’s 8 points of padding (my choice), which leaves 228 font pixels a line there, 45 Latin characters or 22 full-width ones.11 The place’s title sits on its own line at the top of every page; below it the text shows two lines at a time, as in Emerald and Crystal.1 Text longer than two lines pages, each page waiting under the advance mark (item 6); where a page ends at a space, the space is dropped. WorldHUD’s box (in WorldHUD.swift) takes the pages in place of its one status string.
The queue. My rules, so that what the box shows never depends on how long the status is:
- The status’s pages are the box’s own. When the status changes (in the plaza, when anything in it changes: the connection’s state, the count of who is here, the time of day, the season, the ribbons earned), its pages are replaced and the box shows the new status from its first page, unless a page from the next rule is showing.
- A sign’s text (item 5) and a line said nearby at large sizes (item 4) are pages ahead of the status: shown at once, each after any such page already waiting, in the order they came, and, after the last of them, the status again from its first page. The box keeps the pages of at most the three most recent lines; when a fourth comes, the oldest still waiting that is not the player’s own is dropped (the oldest of the player’s own only when every line waiting is theirs), so a player who says one line never loses it. A new sign replaces a sign’s pages still waiting.
- The status text keeps the whole status as its accessibility label, whatever page is shown, as it does today, so VoiceOver reads it whole, and the existing UI tests that read
plaza-status,room-statusandhall-status(an opening phrase, a count of who is here, a room’s first words) read what they read now. The page shown, as far as it has revealed, is the value of a hidden element,plaza-pagein the plaza (<name>-page, my naming). It holds the page’s text lines without the place’s title, and it exists only under the walk tests’ launch argument,-worldTrace(item 8); the checks below read it, and each passes-worldTrace.103
Frames as a choice. Emerald lets the player pick its menus’ frame among 20 (its dialogue box keeps one frame), and Final Fantasy VI its window among 8 with editable colors.14 Offer four forge-drawn frames in Settings, all free, the first three from the start and the fourth at a level; no frame is ever sold. Whether a frame is ever a reward for supporters is a decision I have not made.
Checks. A forge check that frame-paper.png and each alternate is 24 by 24, uses only palette indices, and has identical opposite edges where the design says so; on the iPhone 18 Pro Max simulator, a capture of the box at m = 4 (xSmall) and m = 5 (the default size), in which a script finds the box’s width and height in device pixels to be multiples of m and compares its frame, the outer 8 font pixels on every side, pixel for pixel with a frame it composes itself from the PNG at the captured box’s size by the same rule (corners at the corners, edge and center tiles repeated from the top-left and cut at the far side), each source pixel an m-by-m square, leaving out any pixel the PNG leaves transparent, where the world shows through; and a UI test on the same simulator at the default size, without the plaza server, so that no line is said, launched with -worldTrace and -worldBoxSample and a status of three lines of 30 to 40 characters joined by line breaks, each inside the 45 characters a line holds there, that waits for plaza-page to equal the first two lines, whitespace aside, and only then, with the first page whole, taps the box and finds the third, with a capture of the first page in which a script finds the mark at the box’s bottom right.11 -worldBoxSample is a test argument of my naming, read in PlazaView as -plazaTell is (its tellOnArrival) and handed to the WorldHUD as its status in place of the plaza’s own.92
4. Bubbles: one line, no name, both languages
What. Keep the bubble’s form: cream, an ink border, over the head, under 4 seconds.25 Set it in the pixel face at the bubble’s m, with the frame’s corner pixels instead of a stroked rectangle, drawn inside the bubble’s 8 points of padding a side, so that, like today’s border, they add no width. No name in or over the bubble: a speaker is the head it sits over, and VoiceOver names them (item 8). A preset line never wraps; .fixedSize() stays.
Above the scanlines. Today the bubbles are an overlay inside the stage, under the stage’s scanlines, a black line at 16% opacity every 3 points (at 3×, a band of 3 device pixels in every 9), and its vignette, while the box is laid over the whole stage.2526 Under those layers a bubble’s paper is darkened in bands through its glyphs, so its text is not ink and paper at any size. The brief moves the bubble overlay above the scanline and vignette overlays, where the box already sits, rather than limiting item 2’s check to the box: my choice, from section 6’s rule that a pixel face is crisp only as whole device pixels of ink and paper, applied to both forms of speech alike. In PlazaStage.swift, the bubbles’ .overlay, the last modifier inside the stage’s GeometryReader, moves after the vignette’s .overlay (the RadialGradient), with a GeometryReader of its own for the view’s size.25
On screen. A bubble is centered over its speaker’s head today, with nothing that keeps it on the screen, so a speaker near an edge pushes part of it off.25 Clamp the bubble’s frame inside the safe area less 16 points on every side, the margin the width rule below leaves, moving it from the center over the head only as far as the clamp requires. The safe area, not the view: the stage ignores the safe area (.ignoresSafeArea()), so its own geometry runs under whatever the safe area keeps clear, which on the Duo is the cameras and the side rail, as the WorldHUD’s comment says of its own place inside the safe area.94 Nothing passes the stage its insets today. PlazaView reads them outside the stage and passes them in, as CardViewer already does for its card (proxy.safeAreaInsets into CardStage); the clamp is a pure function, BubblePlacement.frame(head:size:view:insets:), called from PlazaStage.bubbles(of:in:), and the same insets set the bubble’s ceiling below (the names are mine). The world hides the status bar and the home indicator (.statusBarHidden() and .persistentSystemOverlays(.hidden) on its root view); whether that narrows the insets is not in the code, so the app reads them rather than assuming them.94
The width rule. Every line, in both languages, fits as a one-line bubble on every iPhone the app can run on when the safe area has no side insets; that is what the server’s bound below guarantees. An inset can leave too little room for even m₀. For today’s longest line, of every width the iOS 27.0 simulator lists, that is every 3× width up to 393 points and both 2× widths with the Duo’s 84-point inset, and the 375-point 2× phones with 16 points a side. A line at the server’s bound fails in more cases: a Japanese one also on the 402-point phones with the Duo’s inset (286 points of room against 296 at m₀), and a Latin one there (302.67), on the 414-point 3× phone with the inset (298) and on the 360-point phones with 16 points a side (296): of the 36 cases of twelve widths and three insets, 7 for today’s line, 8 for a Japanese line at the bound and 10 for a Latin one. There such a line gets no bubble and goes to the box, while shorter lines keep theirs (below).11 The app is built for iPhone only, from iOS 27.0, and the iOS 27.0 simulator runtime installed with Xcode on my Mac lists 33 iPhones: the narrowest at 3× is 360 points wide (the iPhone 12 mini and 13 mini), and three are 2×, the iPhone SE of the 2nd and 3rd generations at 375 points and the iPhone 11 at 414.97 That is the SDK’s list of the devices it simulates; I did not fetch Apple’s own list of the iPhones iOS 27 supports. Room for a bubble is the safe area’s width less 32 points, the 16 a side the clamp keeps. On the phones in portrait, with no side insets (assumed; none was measured), that is 408 on the iPhone 18 Pro Max, the narrowest of the five views this series measured, but 328 on a 360-point phone and 343 on the 375-point 2× phones. On the Duo’s outer display in portrait it is 434 with no side inset, and 350 if the world keeps the 84-point trailing inset, the side rail, that this series measured on the closed display (section 11).11979533 Today’s longest line, “Anyone have a card from the first years?” at 40 characters, measures 283 points wide as a bubble at m = 4, 349 at 5, 416 at 6 and 549 at 8 at 3×, its glyphs plus today’s 8 points of padding a side; the 2-point border is drawn inside the bubble and adds no width. At 2× the same line is 316 points at m = 3 and 416 at 4.1125 So the bubble’s m has a ceiling on each view, the largest at which the longest preset line, in either language, fits the room. For today’s lines, all English, it is 5 on the 18 Pro Max (349 in 408) and on every 3× iPhone listed from 390 to 440 points, 4 on the 3× phones of 360 and 375 points, and 3, the smallest 2× allows, on the 2× phones; on the Duo’s outer display in portrait it is 6 with no side inset (416 in 434) and 5 with the measured rail (349.33 in 350). A Japanese line at the server’s bound below, 21 full-width characters, 210 font pixels of advances, would lower the Duo’s to 5 and 4 and the 390- and 393-point phones’ to 4 (seven of the listed iPhones are 390 points wide and four are 393), and leave the others as they are.1197 The app computes the ceiling from the safe width PlazaView passes in and the lines the server sends, so a narrower safe area or a longer line, a Japanese one among them, can lower it. The ceiling is one value over all the lines; what happens where there is none is decided per line, my choice: where not even m₀ fits the longest line, a line that fits at m₀ keeps its bubble at m₀, and a line that does not gets no bubble and goes to the box instead, as pages of its own, as at large sizes (below). That line’s bubble kept at m₀ would break the clamp: on a 360-point view with an 84-point trailing inset the room is 244 points, and today’s longest line at m₀ is 282.67.11 The server, in turn, holds every line to what fits the narrowest phone at each scale at the smallest size, with no side insets: at most 218 font pixels of advances, 43 Latin characters or 21 full-width ones, the lesser of (360 − 48) × 3 ÷ 4 = 234 at 3× and (375 − 48) × 2 ÷ 3 = 218 at 2×. A Japanese line at that bound is 296 points as a bubble at m = 4 at 3×, and 331 at m = 3 at 2×; a Latin one is 302.67 and 338.50; both fit the Duo’s 350 with the rail.1133 The phones’ side insets in portrait were not measured; the captures below, which assert the bubble inside the safe area, are where one would show.
The longest preset line as a bubble, by size, at 3×, against each view’s room; drawn by figures/make_figures.py from measure_kiradex_text.out.
Larger text. The bubble’s m follows the box’s, one step less, never below m₀ and never above the view’s ceiling: on the 18 Pro Max it reaches 5 when the box is at xLarge (m = 6), and from xxxLarge up, where the box’s m is 7 or more, it stays at 5. Wherever the ceiling holds the bubble two or more steps under the box (on the 18 Pro Max from xxxLarge up, on a 360-point phone from xLarge up), a reader gets each line said within six cells of the player in the box as well, as pages of its own at the box’s size: the speaker’s handle, a colon and the line, as Emerald names a speaker in the line (item 3 pages it; item 8 announces it). PlazaView, which holds the room’s lines and composes the box’s status today, composes those pages and puts them ahead of the status, by item 3’s queue: shown at once, after any line’s pages already waiting, at most three lines kept. It puts a line’s pages in the box’s queue in the same update that adds its bubble, so such a line’s bubble on screen means its pages are queued. They are not always one page. A handle runs from 14 to 20 characters, so “handle: line” runs from 19 to 62, and under item 3’s frame a line of the box on the 18 Pro Max holds 31 Latin characters at xxxLarge, 17 at AX3 and 12 at AX5: over every handle names.py can make and every preset line, one or two pages from xxxLarge to AX3 and up to three at AX4 and AX5, and up to four on the 360-point and 2× phones at the largest sizes and on the Duo’s outer display, with the measured rail, at AX4 and AX5.1189 The box itself is never limited; it wraps and pages. The dynamicTypeSize(_:) page suggests a large content view for a limited size; the brief does not rely on one, because that viewer’s own page warns against using it “as a replacement for proper Dynamic Type support”, and neither page says whether it opens from an overlay whose hit testing is off, as the bubbles’ is.707125 My research notes had proposed stopping the bubble at m = 8, but the longest line at 8 is 549 points wide (the notes give 553, counting the border that sits inside the bubble), wider than the 18 Pro Max (440 points) and the Duo’s outer display in portrait (466), and the notes measured the limit against the Duo’s outer display when the 18 Pro Max, at 440, is the narrower; the brief here follows the widths.2211
Checks.
- The app’s font test (item 1) also asserts that a table of PixelMplus10’s advances checked into the repository, one entry for each character the preset lines use in either language, matches the bundled font. The server’s test suite, which has no font parser (its requirements are FastAPI, uvicorn, websockets, httpx and pytest), reads that table, refuses a line containing a character the table lacks, and refuses a line, in either language, whose advances sum to more than 218 font pixels, the bound above.10411
- A unit test of the ceiling, a pure function of the safe width, the display scale and the lines’ advances: at 3× it gives, with today’s longest line, 5 for each listed width from 390 to 440 points, 4 for 360 and 375, 6 for 466 and 5 for 382 (the Duo’s outer display, without and with the measured rail), and at 2× 3 for 375 and 414; with a Japanese line of 210 font pixels among the lines, 5 for 466, 4 for 382 and 4 for 390 and 393; and none for a 244-point room (360 points less the 84-point inset and 32) at 3× with today’s longest line; and a test of the composer in
PlazaViewthat, in that room, draws no bubble for today’s longest line and puts its pages in the box, and draws “Hi!” as a bubble atm₀(36 points wide, in 244).11 - A unit test of
BubblePlacementputs a speaker’s head at each edge, each corner and the center of views 360, 375, 440 and 466 points wide, each with no safe-area insets, with 16 points on the leading and trailing sides, and with the Duo’s 84-point trailing inset, and bubbles of several widths up to that case’s safe width less 32 (from 244 points, 360 less 84 and 32, to 434), and finds every bubble inside the safe area less 16 points on every side and, wherever the head’s x lies inside that band, spanning the head’s x.11 - On the iPhone 18 Pro Max, iPhone 13 mini and iPhone SE (3rd generation) simulators, and on the iPhone Duo’s outer display in its simulator, four captures each (English and Japanese, each at the default size and at AX3), with the plaza server running (
KIRADEX_PLAZA=1) and the app launched with-worldTrace, each with that language’s longest line said by the player; in each, a script finds the bubble’s ink border and asserts it is one line of text tall, no wider than the safe area’s width less 32 points, and inside the safe area less 16 points on every side. The script takes the insets from the UI test that makes the capture, which reads them from a hidden element that-worldTraceturns on (item 8), wherePlazaViewwrites the insets it read (mine); the script also finds the bubble’smfrom its glyphs’ pixels and asserts it is the lesser of the box’smless one (never underm₀) and the ceiling for that safe width, computed from the build’s lines in both languages, so it equals the ceiling only where the ceiling limits it. At the default size that is 4 on the 3× phones and the Duo, whatever the ceiling, and 3 on the 2× phone; at AX3 it is the ceiling, and on the Duo the insets decide it: with today’s English lines, 6 with no side inset and 5 with the rail, and with a Japanese line at the server’s bound, 5 and 4.119597 - A UI test on the iPhone 18 Pro Max simulator at AX3, where the box’s
mis 12 and a line holds 17 Latin characters, needs the plaza server (KIRADEX_PLAZA=1) and no peer, and launches with-worldTraceand the text speed set to Instant (item 6) through theworld.textSpeedkey item 5 names, so every page shows whole the moment it opens and a tap on the box turns exactly one page. It reads the player’s handle from the Me tab, picks “Hi!” from the Say menu and waits, within the 5 seconds the existing company test allows, for the bubbleSaid: Hi!, so the line has come back from the room and, by item 4’s rule, its pages are in the box’s queue, ahead of the status, whatever the status’s length. A line of the town’s four collectors, who each say one at most every 20 seconds, may stand ahead of it; the queue keeps at most three lines and never drops a player’s one line (item 3), so at most two lines stand ahead, of at most two pages each there, and lines said later wait behind it. The test readsplaza-pagebefore each tap and taps the box only while the page is not the handle, a colon, a space and “Hi!”, a second apart and at most four times, and passes if a read finds that page, the last read a second after the fourth tap. It names the line because “handle: Hi!” is one page there for every handlenames.pycan make, while the longest line can take two. It logs every page it read.11103102
5. Signs and names
Signs. The sign art stays letterless, as kit.py draws it, so one atlas serves both languages.29 A tap on a sign tile opens the WorldHUD box with the sign’s text, as pages ahead of the status (item 3), instead of walking the player there as it does today. The stage reads touches through a DragGesture today: touched(_:in:) walks the player at once to the ground under a finger, except over a collector or the kiosk, which wait for the finger to lift, and lifted(_:in:) reads that lift as a tap. A sign tile joins the collector and the kiosk in both: it waits for the lift, and lifted(_:in:) calls an onSign closure for it as it calls onKiosk for the kiosk (the name is mine).92 Sign texts live with the map: town.json gains signs: { "7,10": { "en": ..., "ja": ... } }, keyed by cell, and the forge’s checks refuse a sign cell without both languages.
Names. No permanent name labels over heads. The canon’s box names its speaker in the line, Emerald in 6.5% of its paragraphs.1 Kiradex’s handles run to 20 characters; at the bubble’s smallest size, m = 4 at 3× and 3 at 2×, a 20-character handle is 133 points wide at 3× and 150 at 2×; at 3× that sits over a figure 16 texels wide facing the camera and 13 in profile (30 tall, in a 32-by-40 cell), which is 42.7 points, or 34.7, on the 18 Pro Max.119610033 A handle shows in the panel a tap opens, as now, and VoiceOver reads it (item 8).
Checks. A UI test cannot know where on the screen the sign’s cell is drawn, since the camera follows the player, so the test launches the plaza with -worldTrace and -plazaTapCell 7,10, a test argument of my naming read in PlazaStage’s body as -plazaFacingDemo is (in a .task), that sends the cell through the same handling, factored out of lifted(_:in:) into one method. On the iPhone 18 Pro Max simulator at the default size, without the plaza server, so that no line is said, the test also passes item 3’s -worldBoxSample with a one-page status, “Sample.”, and the text speed set to Instant (item 6) through its Settings key in the launch arguments (the key, world.textSpeed, is mine, beside the stick’s world.stick). It waits up to 5 seconds for plaza-page to change from “Sample.” to the sign’s first page, then reads it, taps the box and reads again, at most one tap for each word of the sign’s text (each character, in Japanese), plus one, until plaza-page reads “Sample.”, and finds the pages before it, joined in order, equal to the sign’s English text, whitespace aside, so a sign text long enough to page passes as well as a short one; relaunched with Japanese as the app’s language, the Japanese text.92 The forge check refuses a town.json with an S cell missing either language.
6. Text speed and the advance mark
What. A typewriter reveal whose count is time, not frames: shown = floor(elapsed × cps), a pure function of the elapsed time, the speed and two settings, Reduce Motion and VoiceOver running, so 60 and 120 hertz reveal at the same speed. Settings offers Slow, 12 characters a second, Crystal’s slow; Normal, 30, the default, my choice between the canon’s middle settings (Emerald’s 15, Crystal’s 20) and its fast setting of 60; and Instant. A tap while revealing finishes the page; a tap on a finished page turns it.1 For that the box takes taps while it is revealing or has a page after the one shown; on its last page, shown whole, it lets taps through to the ground, as it does all the time today.26 The same speed in both languages, as Final Fantasy VI prints both at one letter a frame.4 With Reduce Motion on, or VoiceOver running, the function returns the whole page at once; that is my choice, from the documentation’s line that with Reduce Motion “UI should avoid large animations”.83
The mark. A forge-drawn mark of Kiradex’s own, a downward wedge 5 by 3 font pixels, at the box’s bottom right, stepping 0, 1, 2 and 1 font pixels down (the direction is my choice; Emerald’s, on my reading, rises) every 150 milliseconds, a cycle of 600 milliseconds: Emerald’s 9-frame step at 60 hertz.133
Checks. A unit test of the reveal function at 30 characters a second: 0 characters at t = 0 and 30 at 1.000 second, with the elapsed time computed from a tick count, ticks divided by the rate, at 60 and at 120 simulated ticks a second, not summed from per-tick deltas (120 deltas of 1/120 summed in double precision come to 0.9999999999999989, which, times 30, is 29.999999999999968 and floors to 29); the same function with Reduce Motion on, and separately with VoiceOver running, returns the whole page at t = 0; a UI test on the iPhone 18 Pro Max simulator at the default size and Slow, without the plaza server, launched with -worldTrace and item 3’s -worldBoxSample status, whose first page is two lines of 30 to 40 characters, at least 60 characters and so at least 5 seconds of reveal at 12 a second, that a tap on the box one second into the reveal makes plaza-page the whole page within half a second, by which time the reveal alone would show 18 characters (12 at the tap); a unit test of the mark’s offset as a function of elapsed time, 0, 1, 2, 1 and 0 font pixels at 0, 150, 300, 450 and 600 milliseconds; and a screen recording of two seconds of the mark on the simulator in which its offsets, repeats removed, run 0, 1, 2, 1 in cyclic order.33
7. Japanese
What. Japanese for the world’s text first, the rest of the app later. server/app/lines.py keeps its index order, as its comment says, “add at the end only”, and each line becomes an English and a Japanese string; the server sends both, and each client shows the language its user reads, so a line said in a mixed plaza is read in two languages from one index.28 That is, on my reading, the best argument for preset lines in this app. World strings, the WorldHUD titles and statuses and quest titles, move into a String Catalog with ja; sign texts stay with the map (item 5). Every Japanese string is written or reviewed by a Japanese speaker; none ships machine-translated and unreviewed.
Line breaking. Preset lines never wrap (item 4). Box text is written to fit two lines a page at the default size, as Emerald’s script was broken by hand.1 Where it must wrap, keep the strict rule: do not tag box text with a ja typesetting language unless a capture shows the strict rule holding with it, because Core Text on macOS, tagged ja, let small kana start lines at 6 of 7 widths in most faces (4 of 7 in k8x12 and k8x12S).777 If a system default applies the relaxed rule anyway, the written fix is a U+2060 WORD JOINER before the small kana in the few strings that need it.
Checks. A test that every line and every world String Catalog key has both languages; item 4’s server test, which refuses a Japanese preset line past the 218-font-pixel bound, 21 full-width characters; and, on the iPhone 18 Pro Max simulator at the default size with the device language set to Japanese, without the plaza server and at Instant speed, launched with -worldTrace, a capture of each page of a test paragraph in the box, passed as item 3’s -worldBoxSample status and written so that at the 22 full-width characters a line holds there a break would fall before a small kana, in which no line after the paragraph’s first starts with a character from JLREQ’s line-start list or a compatibility form of one.11 That list is the full one, the 83 entries of the ten classes the 3.1.7 rule names (three characters sit in two classes), checked into the test target; section 7’s probe used a partial list (section 7). Japanese layout software mostly sets, in their place, the compatibility forms Unicode encodes for round trips with older standards, as JLREQ notes of the opening parenthesis: “the compatibility counterpart U+FF08 FULLWIDTH LEFT PARENTHESIS is used in the Japanese context in most Japanese text layout software”.21 So the list adds every character of Unicode’s Halfwidth and Fullwidth Forms block whose compatibility normalization (NFKC) is one of the 83, 24 of them, among them ! (U+FF01), ? (U+FF1F), ) (U+FF09), :, ;, , and ., and the halfwidth 。, 、, ー and small katakana from ァ (U+FF67); and ~ (U+FF5E), the fullwidth tilde, beside the wave dash 〜 of the same class, my addition, since its normalization is the ASCII tilde, which the list does not hold: 25 in all.98 PixelMplus10 maps 54 of the 83 entries and 24 of the 25 forms. It does not draw the other 29 entries, among them 〜 (U+301C), ゠, the en dash, ‼ and the small katakana from U+31F0, or the fullwidth ⦆ (U+FF60), and item 1’s coverage test keeps every one of those out of the world’s strings, so a capture can only ever show the 75 characters it draws (the 54 entries, three of which, ), 〕 and ], are listed in two classes, so 51 characters, and the 24 forms).98 A script compares the first glyph cell of each line, pixel for pixel, with the face’s glyphs for those 75 at the capture’s m, drawn from the bundled font with CTFontGetGlyphsForCharacters, and finds no match. Pixels can tell a listed character from the paragraph’s others only where the face draws them differently. Drawn at 10 pixels, two of the 75 have a pixel-identical twin in the face: ‐ (U+2010), identical to the non-breaking hyphen U+2011, and the colon, identical to the modifier letter colon U+A789; none of the 24 forms has one. So the test paragraph uses neither twin; ー (U+30FC) and the kanji 一 (U+4E00) differ, 200 inked device pixels against 250 at m = 5, so 一 may stand anywhere in it.9821 Section 7’s result is a Mac’s Core Text; this capture decides what SwiftUI on iOS does.
8. VoiceOver over the stage
Today. VoiceOver hears “The plaza” and then the walk tests’ trace, such as “12,4 down”.25 That value is for tests, not people.
What.
1. The stage’s label stays “The plaza”, or the room’s name, and its value becomes a sentence a person can use: where the collector stands, by landmark, who is near, and what they hold up (for example, By the pond. Three collectors nearby. Holding up a card.), by accessibilityValue’s rule that a value says what the label does not.79 Everything that rides stepTrace into the stage’s value today moves, whatever writes it: when I read PlazaStage.swift, the step trace from rig.onStep, the build-failure message from the RealityView’s make closure and the air probe’s report from the .task that reads -airProbe, and minutes later a fourth write, from a walk demo added that day. It moves to a hidden element that exists only when the app is launched with -worldTrace (my naming), which the walk tests pass, beside the box’s page (item 3) and the insets PlazaView read (item 4), and the tests that read the stage’s value, among them testTheCollectorFacesTheWayItWalks and testTheChimneysSmoke, which waits for value CONTAINS 'smoke 18 ', read that element instead.25103
2. accessibilityDirectTouch(_:options:) on the stage, so a VoiceOver user can tap to walk after activating it (iOS 17.0).80
3. Each collector’s entity gets an AccessibilityComponent with the handle as its label and what they hold as its value, so VoiceOver can describe them (iOS 17.0).84
4. A line said within six cells of the player is posted as an AccessibilityNotification.Announcement (for example, Glacier Shuffle 4471: Nice card!) at a low priority, at most one every two seconds, the server’s own rate for a line.8291
5. A box page that opens is announced whole, unless it repeats a line point 4 has already announced, as item 4’s large-text pages do.
Checks. A UI test that the stage’s value contains no coordinates without -worldTrace; on an iOS 27 simulator, a test with XCUIVoiceOverService that enables VoiceOver, moves it to the stage, and asserts that what currentSpeech() returns contains the stage’s label and no number pair in the step trace’s x,y form;87 a manual check on a device with VoiceOver on, dated and with the build number, that a collector entity is reachable and spoken, because whether a RealityView with a virtual camera exposes AccessibilityComponent elements is unverified (section 11); and a unit test of the announcement throttle, in which two lines inside 2 seconds yield one announcement.
9. Never, for type and windows
- No glyph, frame, arrow or window graphic from any game’s ROM or decompilation in the app or the forge. The pret and ff6 sheets are measured here and never copied. Kiradex’s frame, mark and sign are forge-drawn; its type is a licensed font.
- No franchise characters or ligatures. Emerald’s charmap carries special glyphs for franchise words; nothing like them is drawn, and no string uses the franchise’s names for things.31
- No font without its license file in hand: a face ships only with its license text quoted in section 4 or 5 and saved beside the evidence.
- No modified OFL face under its Reserved Font Name. Press Start 2P declares one; if any OFL face is ever modified, it is renamed.54
- No text baked into world art: signs, shop fronts and boards stay letterless, so no atlas is ever per language.
Check. A script computes two SHA-256 digests of every image the forge writes and the asset catalog holds, one of the file’s bytes and one of its decoded pixels, and compares them with the same two digests of each PNG measured here (pret’s graphics/fonts and graphics/text_window, Crystal’s frames, and Crystal’s and Red’s gfx/font/font.png). The list of those digests, not the images, is checked into the repository as scripts/forge/canon_digests.txt (a name of my choosing, beside the forge’s kit.py), so the check runs without the evidence folder, and any match fails the build. Its own test is a negative control that needs no game file: it adds the digests of one forge output to a copy of the list and asserts that the script then fails. The same script lists for review, without failing, any 8-by-8 cell of a forge output whose one-bit mask, or its inverse, matches a cell of those sheets. Whole-image digests cannot rebuild those cells, so the list also holds the SHA-256 digest of every distinct cell mask of the sheets, under one rule (mine), applied the same way to the forge’s outputs: a pixel is set where its color differs from the image’s top-left pixel; cells run 8 by 8 from the top-left corner, a partial cell at an edge dropped; a blank or solid cell is left out on both sides, since the sheets have many (japanese_normal.png alone has 430 blank cells of 1,024); and a mask is its 64 bits row by row, 8 bytes. Over the 50 sheets that is 7,746 cells kept and 1,484 distinct digests.99 A recolored, rescaled or redrawn copy passes the digests; rule 1 rests on the forge drawing every asset from code and on that review, not on the check alone. And the strings check the earlier briefs in this series propose is extended with the franchise’s abbreviations from Emerald’s charmap.105
Not in this brief
- A pixel face for the whole app. The Dex, the scanner and Settings keep the system face; pixel type is for the world.
- Chinese and Korean. All four modern pixel games checked ship Chinese, and two of them, Stardew Valley and Sea of Stars, ship Korean, but nothing here measured a pixel font for either.64
- Misaki or any 8-dot face for reading text. It is the smallest kanji measured; legibility at that size to a Japanese reader is unmeasured.6
- Name labels over heads and a name field in the box.1
- Machine-translated Japanese that nobody reviewed.
- Synthetic bold or italic on the pixel face.
- Frames for sale. Every frame is free; a supporters’ frame waits on a decision.
- Text drawn in the RealityKit scene. The world post’s reason and section 10’s arithmetic both keep it in SwiftUI.2311
Key Takeaways
If you draw the art
- Build frames from tiles: a corner, an edge and a center. Emerald’s menus choose among 20 frames of 24 by 24 pixels, three by three tiles; its dialogue box draws from a 14-tile strip with a mirrored bottom edge; Crystal’s frames are 6 one-bit tiles each.1
- Give the advance mark a slow clock. Emerald’s arrow steps 0, 1, 2 and 1 pixels from rest, a step every 9 frames, 0.6 second a cycle.1
- Keep words out of world art. A letterless sign serves every language from one atlas; the words go in the box.29
- Draw Japanese at 8 to 12 pixels and expect a kana to be twice a Latin letter: 8 against 4, 10 against 5, 12 against 6 in every Japanese-capable pixel font measured.7
If you build the engine
- Choose a pixel font by its file, not its name: check the grid, the offset from the baseline and the advances. PixelMplus12’s grid 0.39 pixel off the baseline smeared at every size in Core Text on macOS; k8x12’s 8.01-pixel kana advance smeared once glyphs were placed exactly.7
- Draw only at whole device pixels per font pixel: 0.0% partial pixels at whole multiples of a grid on the baseline with whole-pixel advances (Misaki, PixelMplus10, Press Start 2P), 19.4% to 82.2% between them, on a Mac’s Core Text with font smoothing off. With the context’s default smoothing, PixelMplus10 at its own size was 178 of 272 inked pixels partial, so a whole multiple is necessary, not sufficient; capture an iPhone before trusting either result there.79
- Honor Dynamic Type by rounding to the grid. A 10-pixel em is whole at 1 of the 12 Body sizes at 3×; round the scaled size to whole device pixels per font pixel and set it as a fixed size.768
- Count text speed in time, not frames, so 60 and 120 hertz reveal alike; 60 characters a second is the fastest rate in every game measured, and 15 to 20 the middle setting where a game has one.14
- Give VoiceOver a sentence, not a trace.
accessibilityValueis for what the label does not say,AccessibilityComponentdescribes an entity, andXCUIVoiceOverServicecan test what VoiceOver speaks from iOS 27.0.798487
If you design the loop
- Two lines at a time, at the bottom. Emerald’s box is 30% of the screen and Crystal’s 33%, each two lines.1
- Write to the width. Emerald’s writers kept 95% of 24,100 lines to 38 characters in a window that holds 41, and none past the window.1
- Name the speaker in the line, not over the head; Emerald’s box has no name field.1
- Plan Japanese from the start: four of four modern pixel games checked ship it. A font that covers JIS X 0208 draws about seven thousand characters; a game with a fixed script can ship fewer, as Final Fantasy VI’s Japanese ROM did with 1,248 letters, and then every new string has to be checked against the font.64584
FAQ
How big should a text box be in a pixel-art game?
In the handheld Pokémon games, the bottom third of the screen and two lines. Emerald’s dialogue frame is 30 by 6 tiles, 240 by 48 pixels, 30% of the Game Boy Advance screen’s height, around a text window of 27 by 4 tiles that holds two lines of a 16-pixel font. Crystal’s box is 20 by 6 tiles, 33% of the Game Boy screen, two lines of 18 characters, and Red’s is the same 20 by 6. Final Fantasy VI opens its window at the top or the bottom of the screen.12345
How fast should a typewriter text effect be?
The canon counts frames. Emerald and FireRed print a character every 8, 4 or 1 frames, 7.5, 15 or 60 a second at 60 hertz, and one a frame while A or B is held after a press during printing; Crystal and Red print at 12, 20 or 60 a second; Final Fantasy VI prints field dialogue one letter a frame, 60 a second, with no setting for it (its six-step message speed sets how long a battle message waits). On a modern phone, count characters in time rather than frames, so the speed does not change at 120 hertz. Every game measured tops out at 60 a second, and Emerald’s and Crystal’s middle settings are 15 and 20; the brief below proposes 30 as a default, which is my choice between those middle settings and the fast 60, not a measured setting.1144
Which pixel fonts support Japanese?
Of those measured, Misaki (8 by 8 dots), k8x12 (8 by 12) and DotGothic16 draw all 6,879 characters of JIS X 0208, and PixelMplus10 and PixelMplus12 all but three code points. Misaki, k8x12 and PixelMplus are under a three-sentence free license that allows use, copying and distribution “either commercially or noncommercially”; DotGothic16 is under the SIL Open Font License 1.1. Measured with Core Text on macOS 27.0, Misaki and PixelMplus10 are the two with a clean grid on the baseline and whole-pixel advances, at 8 and 10 pixels; PixelMplus12’s grid sits 0.39 pixel below the baseline, k8x12’s kana advance is 8.01 pixels, and DotGothic16’s dots sit on no pixel grid at all.67171853
Why does my pixel font look blurry on iPhone?
Usually because a font pixel is not a whole number of device pixels. In Core Text on macOS 27.0 with font smoothing switched off, the pixel TrueType fonts with a clean grid on the baseline and whole-pixel advances (Misaki, PixelMplus10 and Press Start 2P) drew with no partial pixels at whole multiples of their grid and with 19.4% to 82.2% of inked pixels smeared between them. With the drawing context’s default smoothing, even a whole multiple blurred: PixelMplus10 at its own 10 pixels came out 178 of 272 inked pixels partial.9 Two faults in the font file cause blur at every size: a grid that sits off the baseline, as PixelMplus12’s does by 0.39 pixel, and an advance that is not whole, as k8x12’s 8.01-pixel kana advance is once glyphs are placed exactly. The measurement was made on a Mac; a capture on the iPhone is the check that settles it there.7
Can a pixel font support Dynamic Type?
Only by rounding. Of iOS’s twelve Body sizes, 14 to 53 points, a 10-pixel em lands on whole device pixels at 3× only at 40 points, so a pixel font that scales continuously with Font.custom(_:size:relativeTo:) is off its grid at the other eleven; on the Mac’s Core Text, off the grid meant smeared, and a capture on the iPhone decides what SwiftUI draws there. Read the scaled size with UIFontMetrics or ScaledMetric, round it to a whole number of device pixels per font pixel, and set it with Font.custom(_:fixedSize:). Apple asks that a custom font implement the behaviors system fonts have, Dynamic Type among them, and that an app ideally let people enlarge its text by at least 200 percent.710206768
Does Core Text follow Japanese line-breaking rules?
In my measurement on macOS 27.0, yes, strictly, when the text was not tagged with a language: a 131-character paragraph broke at seven widths with no line starting on any character of the probe’s line-start list, a partial list, mostly JLREQ’s characters with seven in their fullwidth forms, in Hiragino Sans and eight pixel faces. Tagged ja, it relaxed to the looser rule the W3C’s Japanese layout requirements describe, and a small kana began a line at 6 of 7 widths in most faces. What SwiftUI does on an iPhone with a Japanese device language was not measured.721
How many characters does a Japanese pixel font need?
JIS X 0208, the usual coverage target, has 6,879: 524 non-kanji, 2,965 level-1 kanji and 3,390 level-2 kanji. The producer of the Final Fantasy Pixel Remaster told Siliconera that its Japanese pixel font took “around 7,500 characters”, against “a few hundred” for the Western languages. A game with a fixed script can ship fewer: Final Fantasy VI’s Japanese ROM has 1,248 letters in its dialogue font.584
Every number in this post comes from a script that re-runs against saved sources, from a page saved on October 5, 2026, or from a dated reading of the Kiradex source, and the notes name which; the Core Text numbers are from macOS 27.0, not iOS. The brief in section 12 is my proposal and labeled so; none of it has been built.
Related on this site: Pixel-Art Worlds on iPhone is the first post in this series, with the RealityKit recipe and the text direction this post measures; Pixel-Art People: Characters and a Creator on iPhone is the collector the bubbles sit over; Pixel-Art Structures: Houses, Halls and Interiors on iPhone has the forge whose sign stays letterless; Pixel-Art Motion: Walks, Cameras and Doors on iPhone has the 60 and 120 hertz clocks the reveal is timed against; A Pixel Town Alive When Nobody Is Online has the preset lines and their rate; Pixel-Art Rooms and Pixel-Art Routes are the briefs whose checks this one extends; Honoring the Old Pokémon Games Without Borrowing Them is why the canon is measured and never copied; and Xcode 27.1 Beta: Your App in the iPhone Duo Simulator covers the simulator the bubble capture runs on.
Sources
-
Author’s measurement, October 5, 2026:
measure_gen_text.pyin the author’s evidence folder for this post, outputmeasure_gen_text.out. Inputs from pret pokeemerald at731ad5b:src/text.c(sFontInfos, theRENDER_STATE_HANDLE_CHARcase,sWindowVerticalScrollSpeeds,sDownArrowYCoords, the Japanese glyph functions),src/menu.c(sTextSpeedFrameDelays,sStandardTextBox_WindowTemplates,WindowFunc_DrawDialogueFrameevaluated for the standard window),src/fonts.candcharmap.txt(glyph widths),graphics/text_window/*.png(sizes),include/text_window.h, every line ofdata/text/*.incanddata/maps/*/scripts.incsplit at\n,\land\pand measured through the width table (504 files, 24,100 lines without a placeholder, 3,000 with, 0 skipped), andgraphics/fonts/japanese_*.png(cells not blank). From pokefirered at037335f:src/new_menu_helpers.c. From pokecrystal at5beda23:constants/hardware.inc,constants/text_constants.asm,home/print_text.asm,gfx/font.asm, the frame PNGs andgfx/font/font.png. From pokered atd2704a6:data/text_boxes.asm,constants/ram_constants.asm,gfx/font/font.png. Characters a second are 60 divided by frames per character. The reading of Emerald’s printer (the delay stored less one byAddTextPrinter, then counted down, so a character takes the delay in frames), the auto-scroll delay’s conditions fromsrc/battle_message.candsrc/scrcmd.c, and the two-line reading of Crystal’s constants are asserted by the script, not run. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/menu.cat commit731ad5b(sTextSpeedFrameDelaysat line 77;sStandardTextBox_WindowTemplatesat line 84;LoadMessageBoxGfxandLoadUserWindowBorderGfxat lines 212 and 213;WindowFunc_DrawDialogueFramecalled at line 218, with its fills usingBG_TILE_V_FLIPfrom line 378), read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/src/menu.c. ↩↩↩↩↩↩↩ -
pret,
pokecrystal/constants/text_constants.asmat commit5beda23(lines 24 to 32:BORDER_WIDTH EQU 2,TEXTBOX_WIDTH EQU SCREEN_WIDTH,TEXTBOX_HEIGHT EQU 6,TEXTBOX_Y EQU SCREEN_HEIGHT - TEXTBOX_HEIGHT,TEXTBOX_INNERY EQU TEXTBOX_Y + 2) andconstants/hardware.inc(lines 922 and 923,SCREEN_WIDTH20 andSCREEN_HEIGHT18), read October 5, 2026, https://github.com/pret/pokecrystal/blob/master/constants/text_constants.asm. ↩↩↩↩ -
Author’s measurement, October 5, 2026:
measure_ff6.pyin the author’s evidence folder for this post, outputmeasure_ff6.out. Inputs from everything8215/ff6 at8130132:tools/rip_list_en.jsonandtools/rip_list_jp.json(byte ranges of the window graphics, palettes and fonts),src/menu/config.asm(ConfigWindowNumPos,LoadWindowPal,ConfigColorBarTextList,ChangeConfigOption_02),src/menu/menu_sram.asm,src/btlgfx/gfx_cmd.asm(MsgSpeedTbl,mess_wait), every file undersrc/that reads$1d4d,src/field/text.asm(LoadLetterGfx, the line’s x bounds,CalcTextWidth,NewLine,UpdateDlgTextOneLine, the map title),src/gfx/font_gfx.asm(FontWidth),src/field/reset.asm(the field loop),src/field/hdma.asm(DlgWindowFixedColorTbl) andtools/char_table/(kanji.json,kana.json). One letter a frame and the gradient’s one value a scanline are readings of the code, asserted, not run. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Unicode Consortium,
JIS0208.TXT, the JIS X 0208 to Unicode mapping table, version 2.0, filed under OBSOLETE/EASTASIA, fetched and saved October 5, 2026, https://www.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT. Counted bymeasure_coretext.swiftandmeasure_bdf.py: 6,879 characters; 524 non-kanji, 169 of them kana in rows 4 and 5; 2,965 level-1 kanji in rows 16 to 47; 3,390 level-2 kanji in rows 48 to 84. ↩↩↩↩↩↩↩↩ -
Author’s measurement, October 5, 2026:
measure_bdf.pyin the author’s evidence folder for this post, outputmeasure_bdf.out. Inputs: the BDF files of Misaki Gothic, Misaki Gothic 2nd and Misaki Mincho and of k8x12, k8x12L and k8x12S (2021-05-05 releases) and the savedJIS0208.TXT; it reports each font’s pixel size, bounding box, ascent and descent, character count, advance widths, the full-width glyphs whose ink fills the cell in a direction, the same for kanji (U+4E00 to U+9FFF), and the JIS X 0208 characters drawn. ↩↩↩↩↩↩↩↩↩↩↩ -
Author’s measurement, October 5, 2026:
measure_coretext.swiftin the author’s evidence folder for this post, compiled withswiftc -Oand run on macOS 27.0 (build 26A428), outputmeasure_coretext.out, not on iOS. Inputs: Misaki Gothic, Misaki Gothic 2nd, k8x12 and k8x12S (the 2021-05-05 TTF releases), PixelMplus10 Regular and Bold and PixelMplus12 Regular (PixelMplus atd89b95f), DotGothic16, Press Start 2P and Silkscreen (google/fonts at9710da1), Hiragino Sans W3 as the system control, and the savedJIS0208.TXT. It reports each font’s glyph count and bitmap tables (CTFontCopyAvailableTables); its design grid (the smallest pixels per em from 4 to 40 on which 95% of the sample glyphs’ outline points lie within 0.04 of a pixel, each axis allowed one constant offset); its Latin and kana advances; its coverage of ASCII and of JIS X 0208; the partial share of inked pixels (8 to 247 of 255) when a sample line is drawn into an 8-bit gray bitmap at 1 to 4 times the grid, with antialiasing on, off, and on with subpixel positioning on and quantization off, font smoothing not allowed; for PixelMplus12, the same with the baseline moved by the offset; the Body and Caption 1 sizes from the Human Interface Guidelines at which a font pixel is whole at 3×; and the first character of each line of a 131-character Japanese paragraph laid out byCTFramesetterat 8 to 20 em, untagged and withkCTLanguageAttributeName“ja”, against a list of line-start-prohibited characters. DotGothic16 has no design grid, so it is drawn at a nominal 16 dots per em (62.5 units a dot) and at 1.25 to 4 times that, and laid out at 32 pixels to the em, a size chosen from its name and the dot pitch inmeasure_outline_coords.out, which the output prints. Every render disallows font smoothing (setAllowsFontSmoothing(false)). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Jenni Lada, “Interview: Preparing Final Fantasy Pixel Remaster for the Switch and PS4,” Siliconera, June 2, 2023, the producer Naofumi Takuma on the new font option (“We didn’t have any alternative data saved on file in-house for the Japanese language font, so we decided to create a new one”; “around 7,500 characters” against “a few hundred”), fetched October 5, 2026, https://www.siliconera.com/interview-preparing-final-fantasy-pixel-remaster-for-the-switch-and-ps4/. ↩↩↩↩↩↩
-
Author’s measurement, October 5, 2026:
figures/dump_glyphs.swiftin the draft folder for this post, compiled withswiftc -Oand run on macOS 27.0 (26A428), outputfigures/glyphs.txt: PixelMplus10 Regular at 10 pixels drawn by Core Text, “Nice card!” with 0 of 94 inked pixels partial when font smoothing is not allowed (asmeasure_coretext.swiftdraws) and 178 of 272 with the bitmap context’s default; the Japanese sample line 0 of 164 and 287 of 451. ↩↩↩↩↩↩↩↩↩↩ -
Apple, Human Interface Guidelines, “Typography” (the default and minimum text sizes, 17 and 11 points on iOS and iPadOS; “Follow the recommended default and minimum text sizes for each platform”; “If you use a custom font, make sure it implements the same behaviors”; the iOS Dynamic Type tables), fetched October 5, 2026, https://developer.apple.com/design/human-interface-guidelines/typography. ↩↩↩↩↩↩↩
-
Author’s measurement, October 5, 2026:
measure_kiradex_text.pyin the author’s evidence folder for this post, outputmeasure_kiradex_text.out, run at 12:34 Pacific against Kiradex at commit334fbfewith that day’s uncommitted edits, a working tree being edited that day; the script reads the files whole, by symbol, not by line. Inputs, read only from the Kiradex repository as it stood on disk:server/app/lines.py(23 lines, 3 to 40 characters, median 16),server/app/names.py(handles of two words from lists of 20 and four digits, at most 20 characters), every.kit941Fontcall inKiradex/World/*.swift(caption 10, label 3, body 3, title 3, in four files), the theme’s font family, the bubble’s modifiers, the stage’s accessibility modifiers, a search of the app’s source forAccessibilityComponent,accessibilityDirectTouch,updatesFrequently,AccessibilityNotification,typesettingLanguage,UIFontMetrics,ScaledMetricanddynamicTypeSize(0 each),knownRegions(Base, en) and the count of.xcstringsand.lprojfiles (0); the world’s scale byPlazaRig.fit’s rule for the device sizes this series measured from the Xcode 27 simulator profiles (iPhone 18 Pro Max 440 by 956 points, iPhone Duo outer 466 by 678, inner 669 by 951, all at 3×); and, frommeasure_coretext.out, each candidate font’s em and advances, giving characters per line of the box after a 48-point inset, bubble widths for the longest preset line atm= 4, 5, 6 and 8 (283, 349, 416 and 549 points: its advances at that size plus the bubble’s 8 points of padding a side, the border being drawn inside the bubble), and the Body sizes rounded tomfor a 10-pixel em, a half rounding up. Added later on October 5, with the earlier script and output kept beside them: the rounding at 2× with the smallestmof at least 11 points (4 at 3×, 3 at 2×); the box under the brief’s frame (the width less 32 points rounded down to whole font pixels, 8 font pixels of frame a side; 1,224 device pixels to 1,220 on the 18 Pro Max atm= 5, 45 Latin or 22 full-width characters a line); the pages of “handle: line” for all 400 word pairs ofnames.pyand all 23 lines at the twelve Body sizes on five views (two lines a page, greedy breaks at spaces, every printable ASCII character 5 font pixels wide byprobe_line_start_glyphs.out); the bubble’s ceiling on each iPhone ofmeasure_ios27_iphones.out; the server’s bound of 218 font pixels; and the sum of 120 additions of 1/120. Added later that day, with the earlier script and output kept beside them and every earlier line of the output unchanged: the room and the ceiling on the safe width, with no side insets on the phones and, on the Duo’s outer display in portrait, both with none and with the 84-point trailing inset the series’ Duo post measured, parsed from that post (safe width 382, room 350, ceiling 5 at 349.33 points; the box at the default size 38 Latin or 19 full-width characters under the frame, 40 or 20 after today’s inset); the pages of “handle: line” on that safe width; the widths for the placement test; the server’s bound in that room (296 and 302.67 points atm= 4, in 350); and, fromserver/app/npcs.py, the town’s four collectors, their handles, their lines’ pages at AX3 on the 18 Pro Max (at most 2) and the tap bound for item 4’s test, (3 − 1) lines × 2 pages, one tap a page at Instant: 4. On October 15 that last line was recomputed for the test as it now stands, from the same inputs as far as the output shows (the stage file from the October 5 copy read at 12:28;lines.py,names.pyandnpcs.pyunchanged since October 2 and 3; the other inputs read from the live tree at commitfc9d4876, which printed the same lines), with the earlier script and output kept beside them; every other line of the output prints as before. Added later on October 15, with the earlier script and output kept beside them and every earlier line unchanged: the placement test’s cases where today’s longest line fits at no allowedm(360 and 375 points at 3× and 375 at 2× with the 84-point inset, and 375 at 2× with 16 a side: on the first, a 244-point room against 282.67 points atm₀); the ceiling with a Japanese line at the server’s bound, 21 × 10 = 210 font pixels (5 on the 18 Pro Max, 5 and 4 on the Duo without and with the rail, 4 at 360 points at 3×, 3 at 2×); and the bubble’smthe captures should find, the lesser of the box’smless one, never underm₀, and the ceiling over both languages (4 at the default size on the 3× views, 3 on the 2×; at AX3 the ceiling). Added the same evening, likewise: every widthmeasure_ios27_iphones.outlists, with no side insets, 16 points a side and the 84-point trailing inset, with today’s English ceiling, the ceiling with a Japanese line at the bound and both (no insets: 4 at 360 and 375, 4 at 390 and 393 with the Japanese line, 5 from 402 to 440, 3 at 2×); where today’s longest line fits at no allowedm(with the 84-point inset at 360, 375, 390 and 393 points at 3× and at 375 and 414 at 2×; with 16 a side at 375 at 2×); and “Hi!” atm₀, 36 points at 3× and 38.50 at 2×, which fits every case. Added after it: the same table with a Latin line at the bound, 43 × 5 = 215 font pixels, beside today’s line and the Japanese one, and the cases in which each fits at no allowedm: 7, 8 and 10 of the 36. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokered/data/text_boxes.asmat commitd2704a6, line 10 (db MESSAGE_BOX, 0, 12, 19, 17), read October 5, 2026, https://github.com/pret/pokered/blob/master/data/text_boxes.asm. ↩↩ -
pret,
pokeemerald/src/text.cat commit731ad5b(sFontInfosfrom line 119;sDownArrowYCoords{ 0, 1, 2, 1 }at line 75 andsWindowVerticalScrollSpeedsat line 76;AddTextPrintersettingtextSpeed = speedat line 281 and--sTempTextPrinter.textSpeed;at line 296; the arrow’sdownArrowDelay = 8at line 832; in theRENDER_STATE_HANDLE_CHARcase,if (JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp)at line 944, the countdownif (textPrinter->delayCounter && textPrinter->textSpeed)at line 947,canABSpeedUpPrintand a new A or B press settinghasPrintBeenSpedUpat lines 950 to 953, andif (!(gBattleTypeFlags & BATTLE_TYPE_RECORDED) && gTextFlags.autoScroll)setting the counter to 3, else totextSpeed, at lines 958 to 961;EXT_CTRL_CODE_JPNat line 1094;DecompressGlyph_Normalfrom line 1853 andGetGlyphWidth_Normalfrom line 1887), read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/src/text.c. ↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokefirered/src/text.cat commit037335f, lines 639 to 647 (in theRENDER_STATE_HANDLE_CHARcase,if (JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp)setting the counter to 0, andgTextFlags.canABSpeedUpPrint && JOY_NEW(A_BUTTON | B_BUTTON)settinghasPrintBeenSpedUpwhile a delay counts down), read October 5, 2026, https://github.com/pret/pokefirered/blob/master/src/text.c. ↩↩↩ -
pret,
pokecrystal/home/print_text.asmat commit5beda23(line 4, “The text speed setting in wOptions is actually a frame count”, andPrintLetterDelaywaiting one frame a letter while A or B is held) andpokered/constants/ram_constants.asmatd2704a6(lines 38 to 40:TEXT_DELAY_FAST1,TEXT_DELAY_MEDIUM3,TEXT_DELAY_SLOW5), read October 5, 2026, https://github.com/pret/pokecrystal/blob/master/home/print_text.asm. ↩↩ -
pret,
pokecrystal/home/joypad.asmat commit5beda23(WaitPressAorB_BlinkCursorat line 342;BlinkCursorat line 442, swapping the tile athlcoord 18, 17between'▼'and'─'), read October 5, 2026, https://github.com/pret/pokecrystal/blob/master/home/joypad.asm. The blink’s period was not derived. ↩↩↩ -
Num Kadoma, littlelimit.net, “font,” the license section for Misaki, k8x12 and the author’s other fonts (“These fonts are free software. Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.”), fetched October 5, 2026, https://littlelimit.net/font.htm#license. ↩↩↩↩↩
-
itouhiro, PixelMplus,
README.md(outline-only TrueType, “埋め込みビットマップは なし”; “JIS第1・第2水準のすべての漢字を収録” and ISO-8859-1; PixelMplus12 and PixelMplus10; built from the M+ BITMAP FONTS; license M+ FONT LICENSE) andmisc/mplus_bitmap_fonts/LICENSE_E(the M+ FONT LICENSE’s three sentences), at commitd89b95f, read October 5, 2026, https://github.com/itouhiro/PixelMplus/blob/master/misc/mplus_bitmap_fonts/LICENSE_E. ↩↩↩↩↩ -
Author’s measurement, October 5, 2026:
measure_outline_coords.swiftin the author’s evidence folder for this post, compiled and run on macOS 27.0, outputmeasure_outline_coords.out: the raw x and y outline coordinates ofH,A,oandあin PixelMplus12 Regular and DotGothic16 and ofH,Aandoin Silkscreen, in units of 1,000 to the em. ↩↩↩↩↩↩↩ -
Apple, Human Interface Guidelines, “Accessibility” (“Ideally, give people the option to enlarge text by at least 200 percent (or 140 percent in watchOS apps).”), fetched October 5, 2026, https://developer.apple.com/design/human-interface-guidelines/accessibility. ↩↩↩
-
W3C, “Requirements for Japanese Text Layout,” Working Group Note, 11 August 2020, section 3.1.7 (the line-start prohibition rule and the note on the less strict set of rules), fetched October 5, 2026, https://www.w3.org/TR/jlreq/. ↩↩↩↩↩↩↩↩
-
Author’s research dossier for this post, October 5, 2026 (private): the measurements above, the case studies, and a first draft of the brief for Kiradex. ↩↩↩
-
Blake Crosley, “Pixel-Art Worlds on iPhone: What the 16-Bit Masters Knew,” blakecrosley.com, October 3, 2026: rule 10 (“Text is art”, the m5x7 page’s “use font size 16, 32, 48, etc”, and the sentence on Emerald’s 24-by-24 text-window frame), the size arithmetic on text at k = 8 and k = 6, the Pixel Remaster paragraph citing GoNintendo for about 7,500 Japanese characters, and the text plan (“m5x7 at integer multiples in a 24-by-24 nine-slice box, two lines, typewriter reveal, a two-frame advance arrow”), read October 5, 2026, https://blakecrosley.com/blog/pixel-art-world-on-iphone. That post is quoted here and not changed. ↩↩↩↩↩↩↩↩↩↩
-
Kiradex source,
Kiradex/KiradexTheme.swift, line 12 (let fontFamily: Kit941.FontFamily = .systemDesign(.monospaced)), read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository). ↩↩ -
Kiradex source,
Kiradex/World/PlazaStage.swift, read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository): lines 146, 155 and 189 (stepTraceset to a build failure in theRealityView’s make closure, to each step inrig.onStep, and, under-airProbe, to the air’s report in a.task; a read of the same file at 12:42 showed a fourth,stepTrace = "lap done"under a new-plazaLapDemo), lines 58 to 61 (stepTrace, “The player’s last step, “x,y facing”, as the stage’s accessibility value: the walks read it, since nothing else the simulator logs reaches them.”), lines 313 to 315 (.accessibilityElement(),.accessibilityLabel("The plaza"),.accessibilityValue(stepTrace)), lines 289 to 293 (the bubbles, the last.overlayinside the stage’sGeometryReaderof lines 121 to 294), lines 295 to 309 (the stage’s two overlays over it: scanlines,.black.opacity(0.16)one point tall every 3 points, and aRadialGradientvignette), lines 402 to 418 (bubbles(of:in:): a line shown whiletimeline.date.timeIntervalSince(said.at) < 4, positioned at the head’s view point lessKit941.Spacing.lgwith no clamping, hit testing off), and lines 444 to 459 (theBubbleview,.padding(.horizontal, Kit941.Spacing.sm)then.overlay(Rectangle().strokeBorder(...)), its comment “A line said, as the old games drew it: a cream box with a dark border.”, caption bold,WorldInk.paper, aKit941.Spacing.xxsink border,.fixedSize(),.accessibilityLabel("Said: \(text)")). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Kiradex source,
Kiradex/World/WorldHUD.swift, read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository); the file was dated October 4 and did not change during the reading: lines 31 to 38 (WorldInk, “The world’s own colours, as the old games drew a text box: cream paper”, and its paper color), lines 40 to 44 (the place’s line “in a text box at the bottom. Half open, the box keeps to the wider side of the fold.”), lines 61 to 79 (the chrome in aGeometryReader, inset by.padding(Kit941.Spacing.md), the box across that width, or, half open, the wider pane’s width lessKit941.Spacing.mda side), and lines 84 to 101 (the box: title in caption bold, uppercased; status in caption semibold, identifier<name>-status,plaza-statusin the plaza; paper background; aKit941.Spacing.xxsink border;.allowsHitTesting(false)under the comment “Taps go through to the ground under it.”); andKiradex/World/PlazaStage.swiftlines 581 and 582, wherePlazaViewlays theWorldHUDover the whole stage withwordsas its status, and lines 725 to 736 (words: the time of day’s phrase, the count of who is here and a line of advice, the ribbons earned and the season’s phrase). The kit’s spacing,Sources/Kit941/Tokens/Spacing.swiftin 941Kit:xxs2,xs4,sm8,md16,lg24 points on iPhone. ↩↩↩↩↩↩↩ -
Kiradex source,
Kiradex.xcodeproj/project.pbxproj, lines 1138 to 1141 (knownRegions = (Base, en)), read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository); the file had gained lines by 12:42. ↩↩ -
Kiradex source,
server/app/lines.py, read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository): the docstring, “The preset lines: everything anyone can say in the plaza.”, the 23 entries ofLINES(from line 5), and the comment at line 20, before index 14, “Indices are fixed; add at the end only, the app shows them by number.” ↩↩↩↩ -
Kiradex source, read at 12:28 Pacific on October 5, 2026, at commit
334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository):scripts/forge/kit.py, lines 529 and 530 (def sign():, “A hanging sign on a bracket, 12 × 11.”),Kiradex/World/TileMap.swift, line 52 (the map letter"S": .sign), andKiradex/Resources/World/town.json(oneScell, at x 7, row 10, read with Python’sjsonmodule at 12:33). ↩↩↩↩ -
Method. pret decompilations, shallow clones read at pokeemerald
731ad5b(October 1, 2026), pokefirered037335f(September 26, 2026), pokecrystal5beda23(September 29, 2026) and pokeredd2704a6(September 22, 2026), https://github.com/pret. Final Fantasy VI from the everything8215/ff6 disassembly at813013276c952fdd27edcf7b8b86f17291542cbf(September 14, 2026), which holds code and rip lists, not the ROM’s graphics, https://github.com/everything8215/ff6. PixelMplus shallow-cloned atd89b95f04eeb95bf98bb09cb9234a2f99d186df5(June 2, 2013); Misaki and k8x12, the 2021-05-05 releases in TTF and BDF, downloaded from the author’s site; Press Start 2P, Silkscreen and DotGothic16 (font files,OFL.txtandMETADATA.pb) fromgoogle/fontsat9710da1(September 30, 2026). Web pages fetched and saved on October 5, 2026. Thirteen scripts in the author’s evidence folder for this post, ten in Python and three in Swift, and one more Swift script with this post’s figures,dump_glyphs.swift, produce every measured number, each with its output saved beside it. pret builds the English games; no Japanese ROM was read. ↩↩↩↩↩↩ -
pret,
pokeemerald/src/fonts.c(gFontNormalLatinGlyphWidthsfrom line 148) andpokeemerald/charmap.txt(lines 50 to 52, special glyphs for franchise words) at commit731ad5b, read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/src/fonts.c. ↩↩ -
Martin Korth, GBATEK, “LCD Dimensions and Timings” (“Total 228 lines, 16.743 ms, 280896 cycles - ca. 59.737 Hz”; 160 visible lines; 240 dots a visible line), fetched October 5, 2026, https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm. ↩
-
Author’s arithmetic on the measured numbers: 216 ÷ 5.25 = 41 characters; 59.737 ÷ 1 = 59.7 characters a second; 191 × 1.25² = 298 pixels; PixelMplus12’s −33 units ÷ 83.33 units a pixel = −0.396 pixel (the probe reports −0.39, the center of its bin); 7,500 against 6,879; 11 of 12 and 9 of 12 Body sizes off the grid, by complement of 1 and 3 whole; 14 ÷ 17 = 82% and 53 ÷ 17 = 312%; 13.33 ÷ 16.67 = 80% and 53.33 ÷ 16.67 = 320%; 11 points ÷ 2 points a texel = 5.5 texels; 4 × 10 ÷ 3 = 13.33 points; 12 font pixels × 5 ÷ 3 = 20 points; 440 − 32 = 408, 360 − 32 = 328 and 375 − 32 = 343 points; 40 × 5 × 3 ÷ 2 + 16 = 316 points at 2×; ⌈11 × 3 ÷ 10⌉ = 4 and ⌈11 × 2 ÷ 10⌉ = 3, and 10 × 3 ÷ 2 = 15 points; (360 − 48) × 3 ÷ 4 = 234 and (375 − 48) × 2 ÷ 3 = 218 font pixels, and 218 ÷ 5 = 43 Latin and 218 ÷ 10 = 21 full-width characters; 21 × 10 × 4 ÷ 3 + 16 = 296 and 21 × 10 × 3 ÷ 2 + 16 = 331 points; (440 − 32) × 3 = 1,224 device pixels, ÷ 5 = 244.8, so 244 font pixels (1,220 device pixels), less 2 × 8 = 228, ÷ 5 = 45 Latin and ÷ 10 = 22 full-width characters; 15 × 3 ÷ 10 = 4.5, rounded up to 5; 120 additions of 1/120 in double precision = 0.9999999999999989, × 30 = 29.999999999999968, floored to 29; 1.5 seconds × 12 = 18 characters; 20 × 5 × 4 ÷ 3 = 133 points and 20 × 5 × 3 ÷ 2 = 150 points at 2×; 466 − 84 = 382 and 382 − 32 = 350 points, 466 − 32 = 434; 40 × 5 × 5 ÷ 3 + 16 = 349.33 points; (382 − 32) × 3 ÷ 5 = 210 font pixels, less 2 × 8 = 194, ÷ 5 = 38 Latin and ÷ 10 = 19 full-width characters; (382 − 48) ÷ (5 × 5 ÷ 3) = 40.08 and ÷ (10 × 5 ÷ 3) = 20.04; 43 × 5 × 4 ÷ 3 + 16 = 302.67 points; 360 − 84 − 32 = 244 points; (3 − 1) × 2 × 1 = 4 taps; 16 texels × 2.67 points = 42.7 points and 13 × 2.67 = 34.7; 9 frames ÷ 60 = 150 milliseconds, four steps 600 milliseconds; 3 points × 3 = 9 device pixels between scanlines, each 1 point × 3 = 3 device pixels tall. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
pret,
pokeemerald/src/battle_message.cat commit731ad5b(lines 3002 and 3003: auto-scroll on whengBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED); line 3010,speed = 1for the message window in link battles; line 3012, a recorded battle’sspeed = sRecordedBattleTextSpeeds[GetTextSpeedInRecordedBattle()], the table{8, 4, 1, 0}at line 1957) andpokeemerald/src/scrcmd.c(ScrCmd_messageautoscrollfrom line 1286, settingautoScrollandforceMidTextSpeedat lines 1292 and 1293), read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/src/battle_message.c. ↩↩↩ -
pret,
pokeemerald/charmap.txtat commit731ad5b, lines 1087 to 1089 ('\l' = FA @ scroll up window text,'\p' = FB @ new paragraph,'\n' = FE @ new line), andinclude/constants/characters.h, lines 175 and 176 (CHAR_PROMPT_SCROLL, “waits for button press and scrolls dialog”;CHAR_PROMPT_CLEAR, “waits for button press and clears dialog”), withsrc/text.clines 968 to 971 (CHAR_NEWLINEmovingcurrentYdown a line) and 1102 to 1109 (the two prompts enteringRENDER_STATE_CLEARandRENDER_STATE_SCROLL_START), read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/charmap.txt. ↩ -
pret,
pokeemerald/src/text_window.c(message_box.pngat line 53;LoadMessageBoxGfxat line 93;LoadUserWindowBorderGfxat line 110),pokeemerald/include/text_window.h(#define WINDOW_FRAMES_COUNT 20at line 4),pokeemerald/src/option_menu.c(the FRAME option) andpokeemerald/graphics/text_window/at commit731ad5b, read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/src/text_window.c. ↩↩↩ -
pret,
pokeemerald/data/text/and each map’sscripts.incat commit731ad5b, among themdata/maps/LittlerootTown/scripts.inc, line 854 (.string "MOM: {PLAYER}, we're here, honey!\p"), read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/data/maps/LittlerootTown/scripts.inc. ↩↩ -
pret,
pokeemerald/src/international_string_util.c,src/contest.candsrc/battle_tower.c(theEXT_CTRL_CODE_JPNandEXT_CTRL_CODE_ENGcodes in names) andgraphics/fonts/(japanese_normal.png,japanese_small.png,japanese_bold.png,japanese_frlg_male.png) at commit731ad5b, read October 5, 2026, https://github.com/pret/pokeemerald/blob/master/src/international_string_util.c. ↩↩ -
pret,
pokefirered/src/new_menu_helpers.cat commit037335f(sStandardTextBox_WindowTemplatesfrom line 34,.width = 26at line 40, and the text speed delays) andpokefirered/src/text_printer.c(AddTextPrinter,textSpeed = speedat line 78 and--sTempTextPrinter.textSpeed;at line 93) at commit037335f, read October 5, 2026, https://github.com/pret/pokefirered/blob/master/src/new_menu_helpers.c. ↩ -
Pan Docs, “Rendering” (“One frame: 70224 dots @ 59.7 fps”; a frame “takes ~16.74 ms instead of ~16.67”), gbdev.io, fetched October 5, 2026, https://gbdev.io/pandocs/Rendering.html. ↩
-
pret,
pokecrystal/gfx/font.asmat commit5beda23(the framesFRAME_1toFRAME_8,TEXTBOX_FRAME_TILES6 tiles of 1 bit each, a ninth frame unused) andgfx/font/font.png, read October 5, 2026, https://github.com/pret/pokecrystal/blob/master/gfx/font.asm. ↩ -
everything8215, ff6,
tools/rip_list_en.jsonandtools/rip_list_jp.jsonat commit8130132(assets/gfx/window/window_%s.4bppat0xED0000to0xED1BFF;window_%s.palat0xED1C00to0xED1CFF), read October 5, 2026, https://github.com/everything8215/ff6/blob/master/tools/rip_list_en.json. ↩ -
everything8215, ff6,
src/menu/config.asm(ConfigWindowNumPos,LoadWindowPalreading from$1d57, the font color at$1d55,ConfigColorBarTextList,ChangeConfigOption_02) andsrc/menu/menu_sram.asm(line 102, “message speed = 3, battle speed = 3”) at commit8130132, read October 5, 2026, https://github.com/everything8215/ff6/blob/master/src/menu/config.asm. ↩↩↩ -
everything8215, ff6, at commit
8130132:src/btlgfx/gfx_cmd.asmlines 1101 to 1103 (MsgSpeedTbl, “wait duration for each battle message speed”,$20,$30,$40,$50,$60,$70,$80,$90) and lines 1151 to 1165 (mess_wait, reading bits 4 to 6 of$1d4das the index and callingWaitFramethat many times);src/menu/config.asmline 1499 (“battle speed, battle message speed, magic order, and window pattern”) andChangeConfigOption_02from line 933;src/menu/menu_text.en.incline 346 (the settings drawn as 1 to 6). Files whose code touches$1d4d:src/battle/init.asm(the command setting, battle mode and battle speed),src/btlgfx/gfx_cmd.asm,src/menu/config.asm,src/menu/field_menu.asmandsrc/menu/menu_sram.asm; none undersrc/field/. Read October 5, 2026, https://github.com/everything8215/ff6/blob/master/src/btlgfx/gfx_cmd.asm. ↩ -
everything8215, ff6,
src/field/hdma.asm(DlgWindowFixedColorTbl) andsrc/field/event.asm(line 1591, “dialog window y position (1 or 18)”) at commit8130132, read October 5, 2026, https://github.com/everything8215/ff6/blob/master/src/field/hdma.asm. ↩↩↩ -
everything8215, ff6,
src/field/text.asm(LoadLetterGfx, the dialogue line from x 4 to$e0,CalcTextWidth,NewLine,UpdateDlgTextOneLine, the centered map title shown for 100 frames) andsrc/gfx/font_gfx.asm(FontWidth) at commit8130132, read October 5, 2026, https://github.com/everything8215/ff6/blob/master/src/field/text.asm. ↩↩↩↩↩ -
everything8215, ff6,
tools/rip_list_jp.json(large_font.jp.1bppat0xC494C0to0xC4FFFF) andtools/char_table/at commit8130132, read October 5, 2026, https://github.com/everything8215/ff6/tree/master/tools/char_table. ↩ -
everything8215, ff6,
src/field/reset.asm(the field loop: wait for the vertical blank, callUpdateDlgTextonce a pass) at commit8130132, read October 5, 2026, https://github.com/everything8215/ff6/blob/master/src/field/reset.asm. The disassembly builds both the English and the Japanese ROM from one source. ↩↩ -
Data Crystal, “Chrono Trigger/ROM map,” datacrystal.tcrf.net, fetched October 5, 2026: HTTP 403, not read, https://datacrystal.tcrf.net/wiki/Chrono_Trigger/ROM_map. ↩↩↩
-
Kiradex,
docs/TestFlight.md, step 5 (lines 192 to 194: in the Dex, choosing Japanese shows sets and cards with their Japanese names and English beneath), read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository). ↩ -
Num Kadoma, littlelimit.net, “8×8ドット日本語フォント「美咲フォント」” (the overview’s 8-by-8 cell, JIS levels 1 and 2 and 7-by-7 ink; Misaki Mincho’s and Misaki Gothic 2nd’s descriptions; the M+ FONTS license note; the account of its making for the PC-E500’s Japanese display driver), fetched October 5, 2026, https://littlelimit.net/misaki.htm. ↩↩↩↩
-
Num Kadoma, littlelimit.net, “8×12ドット日本語フォント「k8x12」” (the 8-by-12 cell, JIS levels 1 and 2, the L and S variants, the S variant’s note on old consoles’ 8-by-8 graphics), fetched October 5, 2026, https://littlelimit.net/k8x12.htm. ↩
-
Google Fonts,
ofl/dotgothic16/OFL.txt(SIL Open Font License 1.1, Copyright 2020 The DotGothic16 Project Authors, no Reserved Font Name) andMETADATA.pb(designer Fontworks Inc.; subsets cyrillic, japanese, latin, latin-ext; added 2020-12-15) at9710da1, read October 5, 2026, https://github.com/google/fonts/blob/main/ofl/dotgothic16/OFL.txt. ↩↩↩↩ -
Google Fonts,
ofl/pressstart2p/OFL.txt(SIL Open Font License 1.1, “with Reserved Font Name “Press Start 2P”“) andMETADATA.pb(designer CodeMan38) at9710da1, read October 5, 2026, https://github.com/google/fonts/blob/main/ofl/pressstart2p/OFL.txt. ↩↩↩ -
Google Fonts,
ofl/silkscreen/OFL.txt(SIL Open Font License 1.1, no Reserved Font Name) andMETADATA.pb(designer Jason Kottke) at9710da1, read October 5, 2026, https://github.com/google/fonts/blob/main/ofl/silkscreen/OFL.txt. ↩↩ -
DaFont, “Pixel Operator Font” (by Jayvee Enaguas; “Bitmap (16 px)”; 15 font files; 241 glyphs; the author’s notes, “Changed licence to Creative Commons Zero (CC0) 1.0” and “licensed under a Creative Commons Zero (CC0) 1.0”), fetched October 5, 2026, https://www.dafont.com/pixel-operator.font. The repository the notes name, https://notabug.org/HarvettFox96/ttf-pixeloperator, returned HTTP 404 on October 5, 2026. Single source. ↩↩↩↩
-
Daniel Linssen, “m5x7,” itch.io (“use font size 16, 32, 48, etc”; “free to use but attribution appreciated”; “Asset license Creative Commons Zero v1.0 Universal”; average width 5, ascender 7, descender 2, x-height 5; its characters described as “uppercase and lowercase letters, including many accented letters, numbers and most common symbols”; a name-your-own-price download), fetched October 5, 2026, https://managore.itch.io/m5x7. The font file was not downloaded. ↩↩↩↩↩↩
-
Daniel Linssen, “m3x6,” itch.io (“use font size 16, 32, 48, etc”; “free to use with attribution”; no asset-license field; a name-your-own-price download), fetched October 5, 2026, https://managore.itch.io/m3x6. The font file was not downloaded. ↩↩↩↩↩
-
Apple Developer Documentation, “Core Text” (“Create text layouts, optimize font handling, and access font metrics and glyph data”; iOS 3.2, macOS 10.8), fetched October 5, 2026, https://developer.apple.com/documentation/coretext. ↩
-
Author’s measurement, October 5, 2026:
measure_hig.pyin the author’s evidence folder for this post, outputmeasure_hig.out: the tables of the saved Typography page, the default and minimum sizes and every iOS Dynamic Type table from xSmall to AX5 (Body 14, 15, 16, 17, 19, 21, 23, 28, 33, 40, 47 and 53 points; Caption 1 11, 11, 11, 12, 14, 16, 18, 22, 26, 32, 37 and 43). ↩↩↩↩↩ -
Unicode Consortium, “Unicode Standard Annex #14: Unicode Line Breaking Algorithm,” revision 57, Unicode 18.0.0, 2026-09-01 (the CJ class and “Treating characters of class CJ as class NS will give CSS strict line breaking; treating them as class ID will give CSS normal breaking.”), fetched October 5, 2026, https://www.unicode.org/reports/tr14/. ↩↩
-
Stardew Valley Wiki, “Modding:Dialogue,” last edited 20 May 2026 (
$b; portrait commands;Strings\SpeechBubbles), fetched October 5, 2026, https://stardewvalleywiki.com/Modding:Dialogue. ↩↩↩ -
Stardew Valley Wiki, “Modding:Custom languages,” last edited 3 January 2026 (
UseLatinFont, the.fntfile,FontPixelZoomwith a recommended baseline of 1.5,FontApplyYOffset), fetched October 5, 2026, https://stardewvalleywiki.com/Modding:Custom_languages. ↩↩ -
Author’s measurement, October 5, 2026:
measure_steam_languages.pyin the author’s evidence folder for this post, outputmeasure_steam_languages.out, reading the language tables of the saved Steam store pages for Stardew Valley (https://store.steampowered.com/app/413150/), Celeste (https://store.steampowered.com/app/504230/), Sea of Stars (https://store.steampowered.com/app/1244090/) and Eastward (https://store.steampowered.com/app/977880/), fetched October 5, 2026. ↩↩↩↩↩ -
Author’s measurement, October 5, 2026:
measure_apple_docs.pyin the author’s evidence folder for this post, outputmeasure_apple_docs.out: the title, declaration, abstract, platforms and first paragraphs of each saved Apple documentation page cited in section 10. ↩↩ -
Apple, Human Interface Guidelines, “Designing for games” (“To keep text comfortably legible on each device, ensure that it contrasts well with the background and uses at least the recommended minimum text size in each platform.”), fetched October 5, 2026, https://developer.apple.com/design/human-interface-guidelines/designing-for-games. ↩
-
Apple Developer Documentation, “UIFontMetrics” (“A utility object for obtaining custom fonts that scale to support Dynamic Type”; iOS 11.0), “scaledFont(for:)”, “scaledFont(for:maximumPointSize:)” and “scaledValue(for:)” (“Scales an arbitrary layout value based on the current Dynamic Type settings”; iOS 11.0), fetched October 5, 2026, https://developer.apple.com/documentation/uikit/uifontmetrics. ↩↩↩
-
Apple Developer Documentation, “custom(:size:relativeTo:)” (a custom font “that scales relative to the given
textStyle”; iOS 14.0), https://developer.apple.com/documentation/swiftui/font/custom(:size:relativeto:), and “custom(:fixedSize:)” (a custom font “that does not scale with Dynamic Type”; iOS 14.0), https://developer.apple.com/documentation/swiftui/font/custom(:fixedsize:), fetched October 5, 2026. ↩↩↩↩↩ -
Apple Developer Documentation, “ScaledMetric” (“A dynamic property that scales a numeric value”; iOS 14.0), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/scaledmetric. ↩↩
-
Apple Developer Documentation, “dynamicTypeSize(:)” (iOS 15.0; “When limiting the Dynamic Type size, consider if adding a large content view with
accessibilityShowsLargeContentViewer()would be appropriate.”), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/view/dynamictypesize(:). ↩↩ -
Apple Developer Documentation, “accessibilityShowsLargeContentViewer()” (iOS 15.0; “Don’t use the large content viewer as a replacement for proper Dynamic Type support.”), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/view/accessibilityshowslargecontentviewer(). ↩↩
-
Apple Developer Documentation, “UIAppFonts” (“App-specific font files located in the bundle and that the system loads at runtime”; iOS 3.2), fetched October 5, 2026, https://developer.apple.com/documentation/bundleresources/information-property-list/uiappfonts. ↩↩
-
Apple Developer Documentation, “CTFontManagerRegisterFontsForURL(:::)” (iOS 4.1), fetched October 5, 2026, https://developer.apple.com/documentation/coretext/ctfontmanagerregisterfontsforurl(:::). ↩
-
Apple Developer Documentation, “interpolation(:)” (Image; iOS 13.0), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/image/interpolation(:). ↩
-
Apple Developer Documentation, “resizable(capInsets:resizingMode:)” (“Sets the mode by which SwiftUI resizes an image to fit its space”; iOS 13.0), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/image/resizable(capinsets:resizingmode:). ↩
-
Apple Developer Documentation, “baselineOffset(:)” (Text; “Sets the vertical offset for the text relative to its baseline”; iOS 13.0), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/text/baselineoffset(:). ↩
-
Apple Developer Documentation, “typesettingLanguage(:isEnabled:)” (iOS 17.0; the passage on text in a language that does not match the device UI language; “this language does not affect text localization.”), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/view/typesettinglanguage(:isenabled:). ↩↩
-
Apple Developer Documentation, “NSParagraphStyle.LineBreakStrategy” (iOS 9.0) and its properties
standard(iOS 14.0),pushOut(iOS 9.0) andhangulWordPriority(iOS 14.0), fetched October 5, 2026, https://developer.apple.com/documentation/uikit/nsparagraphstyle/linebreakstrategy-swift.struct. ↩ -
Apple Developer Documentation, “accessibilityValue(:)” (“Use this method to describe the value represented by a view, but only if that’s different than the view’s label.”; iOS 16.0 in the
LocalizedStringResourceform), https://developer.apple.com/documentation/swiftui/view/accessibilityvalue(:), and “accessibilityLabel(:)” (iOS 16.0), https://developer.apple.com/documentation/swiftui/view/accessibilitylabel(:), fetched October 5, 2026. ↩↩↩ -
Apple Developer Documentation, “accessibilityDirectTouch(:options:)” (iOS 17.0; “Direct touch areas passthrough touch events to the app rather than being handled through an assistive technology, such as VoiceOver.”; the area must be activated first), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/view/accessibilitydirecttouch(:options:). ↩↩
-
Apple Developer Documentation, “updatesFrequently” (AccessibilityTraits; iOS 13.0; “Use this trait when you want an assistive technology to poll for changes when it needs updated information.”), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/accessibilitytraits/updatesfrequently. ↩
-
Apple Developer Documentation, “AccessibilityNotification.Announcement” (iOS 17.0; the
accessibilitySpeechAnnouncementPriorityattribute), fetched October 5, 2026, https://developer.apple.com/documentation/accessibility/accessibilitynotification/announcement. ↩↩ -
Apple Developer Documentation, “accessibilityReduceMotion” (iOS 13.0; “If this property’s value is true, UI should avoid large animations”), fetched October 5, 2026, https://developer.apple.com/documentation/swiftui/environmentvalues/accessibilityreducemotion. ↩↩
-
Apple Developer Documentation, “AccessibilityComponent” (RealityKit; “A component that stores accessibility information for an entity”; the name “VoiceOver says aloud when it describes the entity”; iOS 17.0), fetched October 5, 2026, https://developer.apple.com/documentation/realitykit/accessibilitycomponent. ↩↩↩↩
-
Apple Developer Documentation, “Improving the Accessibility of RealityKit Apps” (“set the properties on each visible
Entityin your scene”), fetched October 5, 2026, https://developer.apple.com/documentation/realitykit/improving-the-accessibility-of-realitykit-apps. ↩ -
Apple Developer Documentation, “RealityView” (iOS 18.0), fetched October 5, 2026, https://developer.apple.com/documentation/realitykit/realityview. ↩
-
Apple Developer Documentation, “XCUIVoiceOverService” (“Provides programmatic control of VoiceOver for UI testing”; iOS 27.0), fetched October 5, 2026, https://developer.apple.com/documentation/xcuiautomation/xcuivoiceoverservice. ↩↩↩
-
941Kit source,
Sources/Kit941/Tokens/Typography/FontFamily.swiftat commit50bc657(September 28, 2026), lines 21 to 63 (case custom(named: String), rendered throughFont.custom(_:size:relativeTo:)at intrinsic sizes of 34, 22, 17, 13 and 12 points for display, title, body, label and caption), read October 5, 2026; private repository. ↩↩ -
Kiradex source,
server/app/names.py(lines 7 to 14,FIRSTSandSECONDS, 20 words each, and the handle pattern of two capitalized words and four digits), read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository). ↩↩ -
Blake Crosley, “A Pixel Town Alive When Nobody Is Online: NPCs and Ghosts,” blakecrosley.com, October 5, 2026 (the 23 preset lines, 14 for players and 9 for the town’s own collectors, and one line every 2.0 seconds per player), https://blakecrosley.com/blog/pixel-art-town-alive-when-nobody-is-online. ↩↩
-
Kiradex,
docs/WORLD.md, section 5, “The stack,” lines 524 and 525 (rate limits: “eight moves a second, a line every two seconds”), read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository). ↩↩ -
Kiradex source, read at 12:28 Pacific on October 5, 2026, at commit
334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository):Kiradex/World/PlazaStage.swift, lines 199 to 203 (the stage’s.gesture(DragGesture(minimumDistance: 0)), callingtouchedas it changes andliftedas it ends), lines 329 to 359 (touched(_:in:): without the thumbstick, the ground under the finger is walked to at once and followed across tiles, while a touch on a collector or on the kiosk’s tiles returns and waits for the lift), lines 364 to 380 (lifted(_:in:): a touch that did not walk or steer is a tap; on another collector it setstapped, on the kiosk’s tiles it callsonKiosk, and otherwise it runsrig.walk(to: tile)), lines 31 and 32 (onKiosk, “The kiosk tapped: the town’s board.”), line 82 (stickKey,"world.stick"), the test launch argument-plazaFacingDemo(the.taskat lines 255 to 270), andPlazaView’stellOnArrival, which reads-plazaTell(lines 515 to 522);KiradexUITests/KiradexUITests.swift, lines 384 and 387, where the walk test taps the stage at fractions of its frame. ↩↩↩↩ -
Kiradex source,
Kiradex/World/PlazaRig.swift, read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository):fit(viewSize:displayScale:tilesTall:fitsWhole:)at line 432 (pixelScalethe floor of the view’s pixel height overtilesTall × TileMap.tileSize, 20 × 16 = 320, line 439;TileMap.tileSizeis 16 atTileMap.swiftline 60) andheadPoint(of:)at line 479 (the walker’s position plus half the sheet’s frame height, plus 4, lines 482 and 483). The file changed again by 12:42; both functions were unchanged then. ↩↩ -
Kiradex source, read at 12:28 Pacific on October 5, 2026, at commit
334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository):Kiradex/World/PlazaStage.swiftline 311 (.ignoresSafeArea()on the stage, whoseGeometryReader, lines 121 to 294, holds the bubbles’ overlay), and no read of the safe-area insets anywhere in the file;Kiradex/World/WorldHUD.swiftlines 26 and 27 (.statusBarHidden()and.persistentSystemOverlays(.hidden)onWorldView, the world’s root) and lines 40 to 42 (the chrome is “over its stage and inside the safe area, which on the Duo keeps it clear of the cameras and the side rail”); andKiradex/Views/Card/CardViewer.swiftlines 19 to 24, where aGeometryReaderoutside the card’s stage passesproxy.safeAreaInsetsintoCardStage, which ignores the safe area, so “the card rests inside the safe area, clear of the Duo’s outer camera and side rail”, with the same two modifiers at lines 56 and 57. A search ofKiradex/forsafeAreaInsetsfound only theCardViewerline. ↩↩↩↩↩ -
Blake Crosley, “Xcode 27.1 Beta: Your App in the iPhone Duo Simulator,” blakecrosley.com, September 21, 2026, updated October 2, 2026 (running an app on the iPhone Duo simulator, outer and inner displays; closed, linked against the 27.1 SDK, a 466-by-678-point window whose content inside a navigation stack measured 382 by 562 points, with safe-area insets of 82 at the top, 34 at the bottom and 84 on the trailing side, the rail, in a probe with a toolbar and a tab bar on that edge and the status bar shown), https://blakecrosley.com/blog/xcode-27-1-beta-iphone-duo-simulator. ↩↩↩↩
-
Author’s measurement, October 5, 2026:
measure_figure_width.pyin the author’s evidence folder for this post, outputmeasure_figure_width.out, re-run at 12:36 Pacific against Kiradex at commit334fbfewith that day’s uncommitted edits, output unchanged. Inputs, read only from Kiradex:Kiradex/Resources/World/characters.json("cell": [32, 40], the frames and facings),scripts/forge/rig.py(line 53,CELL_W, CELL_H = 32, 40) and the body sheetfig-body.png(1,376 by 160 pixels, 43 frames by 4 facings). In the standing frameidle_a, the figure is 30 texels tall in each facing, 16 wide at its widest row facing down or up, and 13 facing left or right. ↩↩↩ -
Author’s measurement, October 5, 2026:
measure_ios27_iphones.pyin the author’s evidence folder for this post, outputmeasure_ios27_iphones.out, re-run at 12:36 Pacific, output unchanged. It readsxcrun simctl list runtimes -jfor the iOS 27.0 simulator runtime (24A434) installed with Xcode 27.0 on the author’s Mac, and each iPhone device type’scapabilities.plist(main-screen-width,main-screen-height,main-screen-scale): 33 iPhones, from the iPhone 12 mini and 13 mini (1,080 by 2,340 pixels at 3×, 360 by 780 points) to the iPhone 16, 17 and 18 Pro Max (440 by 956 points), with three at 2×, the iPhone SE (2nd and 3rd generation, 375 by 667 points) and the iPhone 11 (414 by 896). This is the simulator’s list of devices, not Apple’s compatibility page, which was not fetched. Also from the Kiradex project file at commit334fbfe, read only: the app target’sTARGETED_DEVICE_FAMILY = 1(iPhone) in both its configurations (lines 1559 and 1705 at the 12:28 reading) andIPHONEOS_DEPLOYMENT_TARGET = 27.0in the project’s two configurations (lines 1608 and 1670), which the app target does not override. ↩↩↩↩↩↩↩ -
Author’s measurement, October 5, 2026:
probe_line_start_glyphs.swiftin the author’s evidence folder for this post, compiled withswiftc -Oand run on macOS 27.0 (build 26A428), not iOS, outputprobe_line_start_glyphs.out. It reads the ten line-start classes of JLREQ 3.1.7 from the saved specification’s appendix tables (83 entries: cl-02 16, cl-03 4, cl-04 6, cl-05 3, cl-06 2, cl-07 2, cl-09 6, cl-10 1, cl-11 40, cl-29 3), asks Core Text which PixelMplus10 Regular maps (CTFontGetGlyphsForCharacters, no fallback): 54, with 〜 U+301C, ゠ U+30A0 and the en dash of cl-03, ⦆, 〙 and 〗 of cl-02, ‼ ⁇ ⁈ ⁉ of cl-04, 〻 of cl-09 and 18 small kana of cl-11 (ゕ, ゖ and U+31F0 to U+31FF) unmapped. It draws all 7,223 code points the face maps at 10 pixels, antialiasing and font smoothing off, and groups pixel-identical cells: of the 54, ‐ U+2010 matches U+2011 and : U+003A matches U+A789, and no other has a twin. At 50 pixels (m= 5), antialiasing on and smoothing off, ー U+30FC draws 200 inked device pixels with a hook at its left end and 一 U+4E00 250, not identical, both with 0 partial pixels. Every printable ASCII character advances 5 font pixels. Extended later on October 5, with the earlier script and output kept beside them and every earlier line of the output unchanged: the 24 characters of the Halfwidth and Fullwidth Forms block, U+FF01 to U+FFEF, whose compatibility normalization (NFKC) is one of the listed characters (! ) , . : ; ? ] } ⦆ 。 」 、 ・ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー), and ~ U+FF5E, added by the author: the face maps 24 of the 25, all but ⦆ U+FF60, the fullwidth ones 10 pixels wide and the halfwidth ones 5, and none of the 24 has a pixel-identical twin among the face’s code points, so the comparison set is 75 characters: the 54 entries hold 51 characters, since ), 〕 and ] are listed in both cl-02 and cl-29, and the 24 forms make 75 (the probe’s own last line, 54 + 24 = 78, counts entries). ↩↩↩↩ -
Author’s measurement, October 15, 2026:
measure_cell_masks.pyin the author’s evidence folder for this post, outputmeasure_cell_masks.outand the digest listcanon_cell_masks.txt, run on the pret files saved for this post (pokeemeraldgraphics/fonts, 17 sheets, andgraphics/text_window, 21; pokecrystalgfx/frames, 10; pokecrystal’s and pokered’sgfx/font/font.png). Each sheet is decoded to RGBA; a pixel is set where it differs from the sheet’s top-left pixel; cells are 8 by 8 from the top-left corner; blank and solid cells are left out; a mask’s digest is the SHA-256 of its 8 row bytes, the leftmost pixel the high bit.japanese_normal.png, 128 by 512 pixels: 1,024 cells, 430 blank, 594 kept, 478 distinct. All 50 sheets: 7,746 cells kept, 1,484 distinct digests. ↩↩ -
Blake Crosley, “Pixel-Art People: Characters and a Creator on iPhone,” blakecrosley.com, October 3, 2026, read October 5, 2026: for the figure of builds 21 to 33, “The cell is 32 by 32; the figure is 26 tall to the crown of the head, 27 with most hair (28 with buns, spikes or the puff), and about 14 wide at the shoulders”; in its update for build 34, “The cell is 32 by 40 now, with eight rows of headroom and two under the feet, and the figure 30 pixels”; and the manifest’s head and hand anchors for the bubble and the 3D card, “though both still take their place from the cell’s height for now”, https://blakecrosley.com/blog/pixel-art-characters-on-iphone. ↩↩
-
Author’s reading of the Kiradex working tree at commit
fc9d4876(refs/heads/main), October 15, 2026 at 22:29 Pacific:Kiradex/World/PlazaStage.swift(modified 17:13 that day), line 373 (.modifier(StageAccessibility(covered: covered, trace: stepTrace))), line 520 (private struct StageAccessibility: ViewModifier), lines 511 to 513 (the comment: the stage is one accessibility element, “The plaza” with the player’s last step as its value, or none while a place is laid over it), line 525 (content.accessibilityRepresentation {), line 526 (if !covered {), line 533 (.accessibilityLabel("The plaza")) and line 534 (.accessibilityValue(trace)). The post’s other Kiradex readings are of October 5 and are dated. ↩ -
Kiradex source,
server/app/npcs.py, read at 12:33 Pacific on October 5, 2026, at commit334fbfe(the file dated October 3; private repository): the four town collectors ofDEFINITIONS(lines 36 to 58), Moss Keeper 1999, Prism Scout 2024, Comet Pocket 3030 and Dawn Sprout 4040, whose lines are indices intoLINES; each says its first line 8 to 20 seconds in (line 77) and then one every 20 to 40 seconds (line 108).server/app/main.pyline 231 broadcasts a player’s line to the whole room, the player included (broadcast’sexcept_idis unset;rooms.pyline 182), read at 12:33. ↩↩ -
Kiradex source,
KiradexUITests/KiradexUITests.swift, read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository); the file was dated 10:32 and did not change during the reading: line 228 (the Me tab’sme-handleholds the handle, two words and a number),testTheCollectorFacesTheWayItWalksfrom line 432 (reads the stage’s value for each facing),testThePlazaKeepsTimefrom line 502 (readsplaza-statusand checks its prefix, line 509),testTheChimneysSmokefrom line 523 (launched with-airProbe, waits for the stage’svalue CONTAINS 'smoke 18 ', line 531),testVisitingARoomIsAVisit(lines 469 to 472) andtestYourRoomHasYourBinders(lines 604 to 607), which wait onroom-status’s first words,testTheShowHallHasEveryonesCards(lines 490 to 494), which waits onhall-status,testTheTownHasCollectors(lines 672 to 676), which waits onplaza-statusfor a count of five or more “here”, andtestThePlazaHasCompanyfrom line 689 (skipped unlessKIRADEX_PLAZA=1, the server, and by its skip message it needs a peer too, lines 690 and 691; waits onplaza-statusfor the count, picks “Hi!” from the Say menu and waits 5 seconds for the bubbleSaid: Hi!, line 714). ↩↩↩↩ -
Kiradex source,
server/requirements.txt(fastapi,uvicorn[standard],websockets,httpxandpytest, no font library) andserver/tests/(test_rooms.py), read at 12:28 Pacific on October 5, 2026, at commit334fbfewith that day’s uncommitted edits, from a working tree being edited that day (private repository). ↩ -
Blake Crosley, “Pixel-Art Routes: Map Connections and Districts on iPhone,” blakecrosley.com, October 5, 2026, section 7, check 13 (a deny list in the repository, with its test, that every area name, sign and string clears), https://blakecrosley.com/blog/pixel-art-world-beyond-the-plaza. ↩