diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 51f3d96..c0da344 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,11 +13,13 @@ on: - "platform/board/arduino/CHGame/libraries/**" - "platform/board/arduino/CHGame/platform.txt" - "docs/api/**" + - "docs/brand/**" - ".github/workflows/docs.yml" pull_request: paths: - "platform/board/arduino/CHGame/libraries/**" - "docs/api/**" + - "docs/brand/**" - ".github/workflows/docs.yml" workflow_dispatch: diff --git a/CLAUDE.md b/CLAUDE.md index 325eb78..7269820 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,6 +77,12 @@ changing that game. lists orphans and jaggies). Look at them all with `python tools/artsheet.py`; the README's galleries (`docs/cover-art.png`, `cover-art-defaults.png`) come from `python tools/artsheet.py --gallery`. + - The brand's pictures in `docs/brand/` (the README's banner, which shows + the box art, its buttons, caution and XOXO, the API reference's logo) come from + `python tools/brand/make.py`; the look, palette and rules are + [docs/brand/README.md](docs/brand/README.md), and the API reference's + stylesheet `docs/api/extra.css` follows them. After a box art changes, + remake the banner too. 5. **The shared `tools/` serve all 20 games.** After changing anything in `tools/chsim` or `tools/*.py`, run `chgame check` in several games and compare sim frames against a run from before the change. @@ -90,8 +96,8 @@ changing that game. (the style: [docs/api/README.md](docs/api/README.md)), and `doxygen docs/api/Doxyfile` must stay free of warnings. Two things to know: - `platform/board` is what the *next* board package release will contain. - Builds use the installed package for the core (0.2.4, or 0.3.0-local - when a staged build is installed), so an edit to the core, variant, + Builds use the installed package for the core (0.3.0, or a + `-local` version when a staged build is installed), so an edit to the core, variant, `platform.txt` or `boards.txt` has no effect on a build until it is released, staged and installed, or the installed copy is patched. - The three libraries in `platform/board/arduino/CHGame/libraries/` are @@ -164,22 +170,22 @@ pip install -e .[sim] # the tools (Pillow, pyserial, numpy) and the `chgame Manager), then `pip install -e .[sim]` in the repository root. **Notes:** -- **The Boards Manager URL** above is this repository's. Until 0.3.0, the - first release cut from here, is published, 0.2.4 is still served from - `https://github.com/bateske/CH32SerialBoot/releases/latest/download/package_chgame_index.json` - (use that URL and `CHGame:ch32v@0.2.4` meanwhile). The URL is given here, - in the README and in `platform/README.md`: change the three together. - To install 0.3.0 before it is published: `python tools/release/stage.py` - builds it as `0.3.0-local` and tests it as a new user, and +- **The Boards Manager URL** above is this repository's; 0.3.0 + (2026-10-07) is the first release from here, with every game as one + cart, `CHGame-Casino-0.3.0.chgame`, and its SD card's zip beside it. The + URL is given here, in the README and in `platform/README.md`: change the + three together. To install the next version before it is published: + `python tools/release/stage.py` builds it as `-local` and tests + it as a new user, and `python tools/release/serve.py` serves it to Boards Manager at `http://localhost:8765/package_chgame_index.json` ([platform/board/docs/trying-a-release.md](platform/board/docs/trying-a-release.md)). - The board package brings its own RISC-V GCC 8.2 and the `chgame-upload` tool, so nothing else is needed for device builds. -- **Linux:** the installed core 0.2.4's `ch32yyxx.h` includes +- **Linux:** core 0.2.4's `ch32yyxx.h` includes `core_riscv_cH32yyxx.h` with a capital H, which only resolves on - case-insensitive file systems. It is fixed in `platform/board` (ships with - 0.3.0); for 0.2.4: + case-insensitive file systems. Fixed in 0.3.0; if 0.2.4 is still + installed: `ln -s core_riscv_ch32yyxx.h ~/.arduino15/packages/CHGame/hardware/ch32v/0.2.4/cores/arduino/ch32/lib/core_riscv_cH32yyxx.h`. - **The libraries need no install.** `chgame build` compiles with `--library` for CHGfx, CHGame and CHSd from @@ -242,6 +248,7 @@ it, `python tools/chgame.py` is the same thing. The shared tools under | A picture's previews and the house checks, while painting it / a title's lettering from thousands of pixel fonts (needs Pixel Logo Lab beside the repository, or `$CHG_LOGOLAB`) | `python -m artkit show RECIPE.py` / `python -m artkit.fontscout scout "TITLE" OUT` | | The casino card's covers (`tools/sdcard/art/`, from `art/src/*.py`) / the menu's default pictures (`spec/assets/`, from `tools/art/menu/*.py`) / its built-in icons (`art/icons/` -> `src/icons.h`) | `python tools/sdcard/covers.py` / `python tools/menuart.py` / `python platform/bootloader/tools/icons.py` | | Every picture the menus show on one sheet (or before/after for some; or the README's galleries, `docs/cover-art*.png`, after a picture changes) | `python tools/artsheet.py [OUT]` / `python tools/artsheet.py --compare OUT NAME ...` / `python tools/artsheet.py --gallery` | +| The README's banner, buttons, caution and XOXO, and the API reference's logo (`docs/brand/`; one frame of the banner to look at) | `python tools/brand/make.py [banner\|buttons\|warning\|xoxo\|wordmark\|swatches]` / `python tools/brand/make.py banner --still out/banner.png` | | Build the uploader, `chgame-upload` (Go, five hosts, into `out/chgame-upload/`) | `python tools/release/build_uploader.py` | | The uploaders' parity tests (Python and Go against one vector file) | `python -m unittest discover -s platform/bootloader/test/protocol`; `go test ./...` in `host/go` | | Stage a release locally and test it as a new user (fresh arduino-cli in `out/newuser/`, every example compiled from the installed package, the casino cart and the SD card zip) | `python tools/release/stage.py [--quick] [--serve]` | diff --git a/README.md b/README.md index b2a66fc..db849e1 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,84 @@ -# CHGame + -[![API reference](https://img.shields.io/badge/docs-API%20reference-2e7d32)](https://bateske.github.io/CHGame/) +
-Everything for the **CHGame** handheld, in one repository: the Arduino board -package, the bootloader with its SD game menu, the graphics, SD-card and -sound code that games are built from, twenty casino games, and the PC tools -(simulator, uploader, packager, card builder). +CHGame, the lowest-cost game system: a handheld you program in Arduino, paging through the box art in its game menu -> ### ๐Ÿ“– [API reference: bateske.github.io/CHGame](https://bateske.github.io/CHGame/) -> -> Every function, class and constant of the libraries the board package -> installs (**CHGame**, **CHGfx**, **CHSd**, and the core's **SPI**, -> **Wire** and **EEPROM**), with what it does, its parameters and what it -> returns, in the style of the Arduboy2 library's reference. Start with the -> [CHGame library](https://bateske.github.io/CHGame/group__lib__chgame.html). +

+chgame.website  +Get started  +API docs  +Buy one  +Forum +

-**This repository is the source of truth for CHGame development.** It -replaces the separate repositories the pieces grew up in -(CH32SerialBoot, CHGfx and one repository per game). Those are frozen and -get no more updates; nothing here links to them at build time. +[![Board package](https://img.shields.io/github/v/release/bateske/CHGame?style=for-the-badge&label=board%20package&labelColor=252628&color=F9D84A)](https://github.com/bateske/CHGame/releases/latest) +[![Arduino](https://img.shields.io/badge/Arduino-IDE%20%26%20CLI-BDE2C7?style=for-the-badge&logo=arduino&logoColor=white&labelColor=252628)](#-get-started) +[![RISC-V](https://img.shields.io/badge/RISC--V-CH32X035-C8B6F3?style=for-the-badge&logo=riscv&logoColor=white&labelColor=252628)](docs/platform.md) +[![C++](https://img.shields.io/badge/language-C%2B%2B-B7DACC?style=for-the-badge&logo=cplusplus&logoColor=white&labelColor=252628)](https://bateske.github.io/CHGame/) -CHGame is a low-cost open colour handheld: a **WCH CH32X035** RISC-V -microcontroller (48 MHz, 62 KB flash, 20 KB SRAM), a **128x128 ST7735S** -colour LCD, a **microSD** slot on the same SPI bus as the screen, eight -buttons, a piezo speaker, a status LED and **USB-C**. It uploads over USB -with no button presses, like an Arduino Leonardo. +
-![The SD game menu](platform/bootloader/docs/menu.png) +In development: Rev0 hardware. CHGame is in active development. This board is Rev0, Rev1 will change the button wiring. -> **For AI agents and new developers:** read [CLAUDE.md](CLAUDE.md) first. -> It has the build, simulator and test commands, the rules of the codebase, -> and the hardware limits. -> -> **Coming from the Arduboy?** [docs/getting-started.md](docs/getting-started.md) -> maps the Arduboy2 calls to CHGame's, explains what happens behind the -> scenes, and walks through a first sketch. The -> [API reference](https://bateske.github.io/CHGame/) documents every call, -> and the CHGame library's [README](platform/board/arduino/CHGame/libraries/CHGame/README.md) -> explains how a game is put together. +
+ +
+ +**CHGame** is a small, open, low-cost handheld that you program in Arduino: +a 48 MHz RISC-V chip, a 128x128 colour screen, eight buttons, a speaker, a +microSD slot and a battery on one bare board. Plug in USB-C and press +Upload: no programmer, no driver, no buttons to hold. This repository is +everything that runs on it: the Arduino board package, the game menu built +into the bootloader, the libraries games are written with, twenty casino +games, and the PC tools (simulator, uploader, card builder). + +
+Contents + +1. [What it is](#-what-it-is) +2. [Get started](#-get-started): [install](#1-install-the-board-package), [a first sketch](#a-first-sketch), [the game menu](#put-the-game-menu-on-your-board) +3. [The games](#-the-games) +4. [The game menu](#-the-game-menu) +5. [For developers](#-for-developers) +6. [Documentation](#-documentation) +7. [Licences](#-licences) +8. [Credits](#-credits) + +
+ +## ๐ŸŽฎ What it is + +The CHGame Rev0 board running its demo + +| Board | CHGame Rev0 | +|---|---| +| **Chip** | 32-bit RISC-V at 48 MHz (WCH CH32X035) | +| **Memory** | 62 KB flash, 20 KB RAM, a microSD slot | +| **Screen** | 128x128 colour LCD, 16 colours of 4,096 | +| **Buttons** | eight: the D-pad, A, B, START, SELECT | +| **Sound** | a piezo speaker, and a status LED | +| **Power** | USB-C and a LiPo battery | + +It works like an Arduboy. Install one board package, write a sketch with +one include (`CHGame.h`) and upload it over USB. Switch it on and the +**game menu** in the bootloader lists the games on the SD card: pick one +and play, with no PC. -## What this repository is for +**What this repository holds**, and the board package delivers: + +| | Piece | Where | +|---|---|---| +| ๐Ÿ“ฆ | **The board package**: the core, the RISC-V toolchain, the `chgame-upload` uploader, the bootloaders and the libraries, with the games as examples. One install in the Boards Manager | [`platform/board/`](platform/board/) | +| ๐Ÿงญ | **The bootloader with the SD game menu**: a list or one picture at a time, and it installs games from the card | [`platform/bootloader/`](platform/bootloader/) | +| ๐Ÿ•น | **The CHGame library**, `#include `: buttons, frame pacing, the house palette, drawing, sound, saving, the debug protocol | [`libraries/CHGame/`](platform/board/arduino/CHGame/libraries/CHGame/) | +| ๐Ÿ–ผ | **CHGfx**, the graphics: a 16-colour framebuffer sent to the panel by DMA | [`libraries/CHGfx/`](platform/board/arduino/CHGame/libraries/CHGfx/) | +| ๐Ÿ’พ | **CHSd**, the SD card: a read-only FAT16/FAT32 reader | [`libraries/CHSd/`](platform/board/arduino/CHGame/libraries/CHSd/) | +| ๐ŸŽฐ | **Twenty casino games and three apps**, the library's examples | [`examples/`](platform/board/arduino/CHGame/libraries/CHGame/examples/) | +| ๐Ÿ“ | **`.chgame`**, the format games are shared in, and the SD card's layout | [`spec/`](spec/README.md) | +| ๐Ÿงฐ | **The PC tools**: simulator, uploader, packager, card builder, the art toolkit | [`tools/`](tools/README.md) | + +### Why one repository The aim is the Arduboy model, with one repository instead of several: @@ -57,49 +96,31 @@ The aim is the Arduboy model, with one repository instead of several: move together and are tested together. - **One place for the PC side.** The tools that talk to the device outside Arduino (simulator, uploader, serial and screenshot tools, `.CHG` packager, - SD card builder) live here too, not in separate repositories. + SD card builder) live here too. The ones an Arduino user needs are in + `chgame-upload`, which the board package installs; the developer tools + are Python and work from a clone. -### Where that stands today +**This repository is the source of truth for CHGame.** It replaces the +separate repositories the pieces grew up in (CH32SerialBoot, CHGfx and one +per game); those are frozen. [docs/roadmap.md](docs/roadmap.md) has what is +left to do. -Every piece is in this repository, and the board package built from it -delivers all of it. Release **0.3.0** is built and passes the new-user test -(below), but is **not published yet**: until it is, the URL under -*Installing* has nothing behind it, and -[platform/board/docs/trying-a-release.md](platform/board/docs/trying-a-release.md) -installs it from this machine instead. +

back to top โ†‘

-| Piece | In this repository | Delivered by the board package (0.3.0) | -|---|---|---| -| Core, variant, toolchain, `chgame-upload` | `platform/board/` | yes | -| Bootloader with the SD game menu | `platform/bootloader/` | yes: *Tools > Bootloader* (SD Text Menu, the list, or SD Graphic Menu, the pictures, each in Rainbow or Static; or USB Only), written by *Burn Bootloader* over USB with the programmer **CHGame USB**: no driver, no buttons | -| The CHGame library (`CHGame.h`: buttons, pacing, palette, drawing, sound, saving, debug protocol) | `platform/board/arduino/CHGame/libraries/CHGame/`; every game is built on it | yes, in the package's `libraries/`: nothing to install | -| CHGfx, the graphics library | `platform/board/arduino/CHGame/libraries/CHGfx/` (1.3.2) | yes, with its examples | -| CHSd, the SD/FAT reader | `platform/board/arduino/CHGame/libraries/CHSd/` (1.0.0) | yes | -| The casino games and three apps (CHStlView, CHSDtoUSB, CHSDtoSerial) as examples | `platform/board/arduino/CHGame/libraries/CHGame/examples/Games/`, `apps/` | yes: *File > Examples > CHGame > Games*, *Apps* | -| `.chgame`, the format games are shared in ([spec/chgame.md](spec/chgame.md)), and the SD menu's card ([spec/card.md](spec/card.md)) | `spec/`, `tools/chcart/` (`chgame export`, `chgame cart ...`) | the release carries every game as one cart, `CHGame-Casino-.chgame`, and its card as a zip. Every build also writes the menu's install file (`.chg`), and *Export Compiled Binary* puts it by the sketch | -| PC tools | `tools/`, `platform/bootloader/host/` | the ones an Arduino user needs, in `chgame-upload`: upload, burn the bootloader, pack for the SD menu. The developer tools (simulator, scripted runs, screenshots, GIFs, sound preview, `.chgame` carts and cards) are Python and work from a clone (`pip install -e .`) | - -`python tools/release/stage.py` builds the release as `0.3.0-local` and -checks it the way a new user would get it: a fresh `arduino-cli` installs -it from the one URL; the libraries, examples, bootloaders and programmers -are there; every game and app compiles from the installed package with no -`--library`; and the casino cart and its SD card are made from those builds. -[docs/roadmap.md](docs/roadmap.md) has what is left: publishing it. -[docs/chgame-library.md](docs/chgame-library.md) records why the library -is as it is, and [docs/unification.md](docs/unification.md) how the -games' twenty copies of their shared code became it. - -## Installing - -**The board package** (the toolchain, the uploader, the bootloaders, the -libraries and the games come with it). In the Arduino IDE 2.x: add +## ๐Ÿš€ Get started + +### 1. Install the board package + +In the Arduino IDE 2.x, add this URL under *File > Preferences > +Additional boards manager URLs*, then install **CHGame** from the Boards +Manager: ``` https://github.com/bateske/CHGame/releases/latest/download/package_chgame_index.json ``` -under *File > Preferences > Additional boards manager URLs*, then install -**CHGame** from the Boards Manager. With `arduino-cli`: +
+The same with arduino-cli ```bash arduino-cli config add board_manager.additional_urls https://github.com/bateske/CHGame/releases/latest/download/package_chgame_index.json @@ -107,105 +128,105 @@ arduino-cli core update-index arduino-cli core install CHGame:ch32v ``` -(Until 0.3.0 is published, 0.2.4 is still served from -`https://github.com/bateske/CH32SerialBoot/releases/latest/download/package_chgame_index.json`, -without the libraries, games and menu bootloader. To try 0.3.0 now: -[trying-a-release.md](platform/board/docs/trying-a-release.md).) +
-**Then, in the IDE:** *Tools > Board > CHGame Boards > CHGame Rev0*. The games are under *File > -Examples > CHGame > Games*; set *Tools > USB* to **Upload only** for them -(*Tools > Optimize* is **Smallest + LTO** by default, which they need too). -A new sketch only needs `#include `; *Hello* is the smallest one. +That is everything: the toolchain, the uploader, the bootloaders, the +libraries (nothing else to install) and the games under *File > Examples*. +The release, [v0.3.0](https://github.com/bateske/CHGame/releases/tag/v0.3.0), +also carries every game and app as one cart, `CHGame-Casino-0.3.0.chgame` +([spec/chgame.md](spec/chgame.md)), and that cart's SD card as a zip. +(Boards Manager URLs from before 0.3.0, the CH32SerialBoot repository's, +offer 0.2.4 only.) -**The menu bootloader** is installed over USB, through the bootloader a -board already has: no driver, no buttons. *Tools > Bootloader* **SD Text -Menu (Rainbow)** (or **(Static)**), *Tools > Programmer* -**CHGame USB**, then *Tools > Burn Bootloader*. Without the 0.3.0 package: -`chgame uploader selfupdate platform/bootloader/release/chgame_sdboot.bin` -([platform/bootloader](platform/bootloader/README.md#installing-it-on-a-board)). -The WCH driver and the BOOT button are only for recovery. +### 2. Pick the board -**The visual menu** is the other face of the same bootloader: one picture at -a time, no text, like the Arduboy FX. The card's cover at power-on, a cover -for each folder (LEFT/RIGHT), each game's box art (UP/DOWN), a bar over the -picture while a game installs. *Tools > Bootloader* **SD Graphic Menu -(Rainbow)** or **(Static)**; the same card works with both -([docs/visual-menu.md](docs/visual-menu.md)). +*Tools > Board > CHGame Boards > CHGame Rev0*. -![The visual menu on the casino card: the splash, a folder's cover, two games](platform/bootloader/docs/visual.png) +### 3. Upload -**The SD card.** Unzip `CHGame-sdcard-.zip` from the release page -onto a FAT32 card, or deploy `CHGame-Casino-.chgame` to it -(`chgame cart deploy ... --card E:\`; [docs/sd-menu.md](docs/sd-menu.md)). +Open *File > Examples > CHGame > Hello*, plug the board in and press +**Upload**. It resets itself, flashes in about half a second and starts. +For the games (*File > Examples > CHGame > Games*), set *Tools > USB* to +**Upload only**; *Tools > Optimize* stays at its default, **Smallest + +LTO**, which they need too. -**Your own menu picture.** Everything behind the menu's list, the CHGAME -logo included, is one 128x128 picture on the SD card, so you can redraw it -or replace it with anything you like: +### A first sketch -```bash -chgame background --template my-menu.png # the default picture, to edit in any paint program -chgame background my-menu.png --preview p.gif # see the menu on it (any image is converted to fit) -chgame background my-menu.png --card E:\ # put it on a mounted card -``` +A new sketch needs only `#include `: -The menu leaves the top 20 rows (the logo) and the bottom 8 (key hints) to -the picture, and anything painted in pure magenta (#FF00FF) turns through -the rainbow (or stays as painted, with the Static bootloader). [docs/menu-image.md](docs/menu-image.md) walks through it step -by step, including putting a picture into a `.chgame` cart. +```cpp +#include -**Pictures for the visual menu** are the same kind of 128x128 PNG: a game's -box art (`docs/cart.png` in its sketch), the card's cover, a folder's -cover, the about page. Draw yours in any paint program: +void setup() { + chgame.boot(); // buttons; START held 3 s goes back to the menu + gfx_begin(GFX_DIV2, GFX_12BPP); // the panel: 128x128, 16 colours on screen + pal::init(); // the house colours (INK, WHITE, FELT, GOLD ...) + chgame.setFrameRate(60); +} -```bash -chgame picture --template my-art.png # a blank picture with the menu's marks shown -chgame picture my-art.png --preview p.gif # as the visual menu shows it -chgame picture photo.jpg --out my-art.png # any image made to fit (scaled, colours reduced) +void loop() { + if (!chgame.nextFrame()) return; // 60 times a second + chgame.pollButtons(); + // ... game logic: chgame.pressed(LEFT_BUTTON), chgame.justPressed(A_BUTTON) ... + pal::tick(); + + gfx_wait(); // the last frame has gone out: draw the next + pal::commit(); + gfx_clear(FELT); + text35x2s(10, 9, "HELLO", WHITE); + gfx_flushAsync(); // sent by DMA while the next frame's logic runs +} ``` -Or paint it in code, as every picture in this repository is: a Python -recipe (a game's `tools/cart.py`) using `tools/artkit`, with shapes, light -and small 3D props in true colour, an ordered dither down to the 16 -colours, and a title set in a pixel font at its own size. -[docs/cover-art.md](docs/cover-art.md) is the house look and the method: +- **Coming from the Arduboy?** [docs/getting-started.md](docs/getting-started.md) + maps the Arduboy2 calls to CHGame's, explains what happens behind the + scenes, and walks through a first game. +- **Every call** is in the [API reference](https://bateske.github.io/CHGame/): + the CHGame, CHGfx and CHSd libraries and the core's SPI, Wire and EEPROM. + Start with the [CHGame library](https://bateske.github.io/CHGame/group__lib__chgame.html). +- **How a game is put together:** the CHGame library's + [README](platform/board/arduino/CHGame/libraries/CHGame/README.md). + +### Put the game menu on your board + +The menu bootloader goes on over USB, through the bootloader the board +already has: no driver, no buttons. *Tools > Bootloader* **SD Text Menu +(Rainbow)** (or **(Static)**), *Tools > Programmer* **CHGame USB**, then +*Tools > Burn Bootloader*. Or pick **SD Graphic Menu**, the +[visual menu](#-the-game-menu), which shows one picture at a time. Without +the IDE: +`chgame uploader selfupdate platform/bootloader/release/chgame_sdboot.bin` +([platform/bootloader](platform/bootloader/README.md#installing-it-on-a-board)). +The WCH driver and the BOOT button are only for +[recovery](platform/board/docs/recovery.md). -```bash -chgame boxart # in a game's folder: run its tools/cart.py, write docs/cart.png -python -m artkit show tools/cart.py # previews at 1x and 4x and as the menu shows it, and the house checks -python tools/artsheet.py # from the root: every picture the menus show on one sheet -``` +**The SD card:** unzip `CHGame-sdcard-.zip` from the +[release page](https://github.com/bateske/CHGame/releases/latest) onto a +FAT32 card, or deploy `CHGame-Casino-.chgame` to it +(`chgame cart deploy ... --card E:\`; [docs/sd-menu.md](docs/sd-menu.md)). +Holding **START** for 3 s in any game goes back to the menu. -**This repository** is for working on the platform and the games: clone it -and `pip install -e .[sim]` (see *Quick start* below). `chgame build` -compiles against the repository's own copies of the libraries, so an edit -there takes effect at once. +

back to top โ†‘

-## The games +## ๐ŸŽฐ The games Twenty casino and table games with one look: green felt, gold lettering, casino chips and a dealer. They are the platform's examples and its test -load: most are within 1 KB of filling the flash. They all fit on one SD card -behind the **game menu built into the bootloader**: switch on, pick a game, -play, with no PC, like an Arduboy FX ([docs/sd-menu.md](docs/sd-menu.md)). - -Each one, and each app, has its own box art for the visual menu, painted in -the manner of early-1990s game boxes, and the casino card has a cover and -a cover for each of its seven genre folders: +load (most are within 1 KB of filling the flash), and they all fit on one +SD card behind the game menu. Each one, and each app, has box art for the +visual menu, painted in the manner of early-1990s game boxes: ![The casino card's box art: its cover, the seven folders, the twenty games and the two apps](docs/cover-art.png) -The menu's own pictures, which every card falls back on (the default -cover and text-menu picture, the about page, installed, no picture, a -folder without a cover, the five errors), are in -[docs/cover-art-defaults.png](docs/cover-art-defaults.png). -[docs/cover-art.md](docs/cover-art.md) says how they are all made. +Each game is a sketch in +`platform/board/arduino/CHGame/libraries/CHGame/examples/Games//` +with its own README (rules, controls, design) and NOTES.md (status, +decisions, open items). The image column is the release build's size with +the 0.3.0 package (2026-10-07), against the **50,944 B** the bootloader +leaves for a sketch; up to 50,432 B a game keeps both of its save pages. -Each game is a standalone sketch in `platform/board/arduino/CHGame/libraries/CHGame/examples/Games//.ino` with its own -README (rules, controls, design) and NOTES.md (status, design decisions, -open items). The image column is the release build size with the 0.3.0 -package (2026-10-07), against the -**50,944 B** the bootloader leaves for a sketch; up to 50,432 B a game keeps -both of its save pages. +
+All twenty games: what each one is, and its size | Game | What it is | Image | |---|---|---| @@ -230,73 +251,115 @@ both of its save pages. | [CHWordWheel](platform/board/arduino/CHGame/libraries/CHGame/examples/Games/CHWordWheel) | WORD WHEEL, a word-puzzle game show: spin, call letters, solve | 49,832 B | | [CHYacht](platform/board/arduino/CHGame/libraries/CHGame/examples/Games/CHYacht) | YACHT DICE (five dice, thirteen boxes) with Craps's 3D dice | 44,704 B | -[docs/status.md](docs/status.md) lists what each game has been verified on -(the simulator or the device), its open items and the known issues. +
-## Repository map +The three apps sit beside them in `examples/Apps/`: **CHStlView** (a 3D +model viewer that streams STL files off the card), **CHSDtoUSB** (the board +as a USB card reader) and **CHSDtoSerial** (the card over serial, for the +website's tools). [docs/status.md](docs/status.md) lists what each game has +been verified on (the simulator or the device), its open items and the +known issues. -``` -CHGame/ -โ”œโ”€โ”€ CLAUDE.md start here: commands, rules, limits, gotchas -โ”œโ”€โ”€ platform/ the platform itself; these are the master copies -โ”‚ โ”œโ”€โ”€ board/ the Arduino board package (core, variant, linker scripts) + the board's docs -โ”‚ โ”œโ”€โ”€ bootloader/ the bootloader with the SD game menu: sources, PC test suite, binaries, -โ”‚ โ”‚ and the uploader's source (host/go) -โ”‚ โ”‚ โ””โ”€โ”€ arduino/CHGame/libraries/ CHGame (CHGame.h), CHGfx (graphics), CHSd (SD/FAT), beside SPI, Wire, EEPROM -โ”‚ โ”‚ โ””โ”€โ”€ CHGame/examples/ Hello, games/ (the 20 casino games), apps/ (CHStlView, CHSDtoUSB, CHSDtoSerial) -โ”‚ โ””โ”€โ”€ hardware/ Rev 0 schematic and netlist -โ”œโ”€โ”€ spec/ the .chgame format, the SD card's layout and the CHG file: the contract -โ”‚ with the emulator and web tools, with conformance fixtures -โ”œโ”€โ”€ tools/ the PC tools shared by every game (device.py, the simulator and script -โ”‚ driver, sound preview, size report, serial, chcart/ for .chgame carts -โ”‚ and cards, chgpack.py for CHG files, sdcard/ for the casino cart, -โ”‚ artkit/ for the box art) -โ””โ”€โ”€ docs/ platform knowledge: hardware, performance, SD card, how a game is built, - status, the roadmap to the first release, the CHGame library's decisions - and history, and a getting-started guide for Arduboy developers; - api/ builds the API reference (Doxygen) published to GitHub Pages -``` +

back to top โ†‘

-## Quick start for development +## ๐Ÿงญ The game menu -You need `arduino-cli` (or the Arduino IDE 2.x), Python 3.9+ and, for the -simulator and host tests, a C++ compiler. zig is the easiest; it comes with -pip. +The bootloader shows the games on the SD card at every power-on, in +folders, with the installed game preselected; starting it writes nothing, +and another game is checked completely before anything is erased. It has +two faces, from one source: + +| The list (SD Text Menu) | The pictures (SD Graphic Menu) | +|:---:|:---:| +| The list menu on the casino card, its highlight turning through the rainbow | The visual menu on the casino card: the splash, a folder's cover, two games | +| one picture behind a list of the card's games | one picture at a time, like the Arduboy FX: the card's cover, a cover for each folder (LEFT/RIGHT), each game's box art (UP/DOWN) | + +Both run the same card ([docs/sd-menu.md](docs/sd-menu.md) is the players' +guide, [docs/visual-menu.md](docs/visual-menu.md) the visual menu's). +Every picture is a 128x128 PNG on the card that you can redraw: ```bash -# 1. The board package: see "Installing" above +chgame background --template my-menu.png # the list menu's picture, to edit +chgame background my-menu.png --preview p.gif # the menu on your picture +chgame picture --template my-art.png # a blank visual-menu picture, marks shown +chgame picture photo.jpg --out my-art.png # any image, made to fit +chgame boxart # in a game's folder: paint docs/cart.png +``` + +
+More on the menu's pictures + +- **The list menu** leaves the top 20 rows (the logo) and the bottom 8 + (key hints) to the picture, and anything painted in pure magenta + (#FF00FF) turns through the rainbow (or stays as painted, with the + Static bootloader). [docs/menu-image.md](docs/menu-image.md) walks + through it step by step, including putting a picture into a `.chgame` + cart. +- **The visual menu's pictures** are a game's box art (`docs/cart.png` in + its sketch), the card's cover, a folder's cover and the about page. + `chgame picture my-art.png --card E:\` puts one on a mounted card. +- **Every picture in this repository is painted in code**: a Python recipe + (a game's `tools/cart.py`) using `tools/artkit`, with shapes, light and + small 3D props in true colour, an ordered dither down to the 16 colours, + and a title set in a pixel font at its own size. + [docs/cover-art.md](docs/cover-art.md) is the house look and the method; + `python -m artkit show tools/cart.py` previews one with the house checks, + and `python tools/artsheet.py` puts every picture the menus show on one + sheet. The menu's own pictures, which every card falls back on, are in + [docs/cover-art-defaults.png](docs/cover-art-defaults.png). + +
+ +

back to top โ†‘

+ +## ๐Ÿงฐ For developers + +> **For AI agents and new developers:** read [CLAUDE.md](CLAUDE.md) first. +> It has the build, simulator and test commands, the rules of the codebase, +> and the hardware limits. + +Clone this repository to work on the platform or the games. You need +`arduino-cli` (or the Arduino IDE 2.x) with the board package, Python 3.9+ +and, for the simulator and host tests, a C++ compiler; zig is the easiest, +and comes with pip. -# 2. Python tools, the `chgame` command and a compiler for the simulator +```bash +# 1. Python tools, the `chgame` command and a compiler for the simulator pip install -e .[sim] -# 3. Build a game (from its folder) against this repository's libraries +# 2. Build a game (from its folder) against this repository's libraries cd platform/board/arduino/CHGame/libraries/CHGame/examples/Games/CHFour -chgame build # release build + size report -chgame check --no-device # host tests + every sim script, twice (games that have check.py) -chgame sim # just the PC simulator +chgame build # release build + size report +chgame check --no-device # host tests + every sim script, twice +chgame sim # just the PC simulator chgame run tools/scripts/endings.txt out/endings # screenshots in out/endings -# 4. On a board (plugged in by USB) +# 3. On a board (plugged in by USB) chgame upload -# 5. Share a game, and make a card for the game menu +# 4. Share a game, and make a card for the game menu chgame export # build/CHFour.chgame chgame card # every game: out/CHGame-Casino.chgame and out/sdcard/ chgame cart deploy out/CHGame-Casino.chgame --card E:\ # onto a mounted card ``` -## The parts +`chgame build` compiles against the repository's own copies of the +libraries, so an edit there takes effect at once. The simulator runs a +sketch's real code on the PC, deterministically, and scripts drive it (and +the board) through the library's debug protocol. + +
+The parts: the board package, the libraries, the bootloader, the PC tools ### The board package: `platform/board/` The Arduino core for the board: package `CHGame`, architecture `ch32v`, -version **0.3.0** (built and tested, not yet published; 0.2.4 is the one -installed today). It is a fork of the WCH CH32 Arduino core. It adds the -CHGame variant (pin names such as `PIN_BTN_A` and `PIN_SD_CS`), the USB CDC -serial port, the app linker script (the sketch starts at 0x3000, above the -12 KB bootloader), and the `chgame-upload` tool, which uploads over USB in -about half a second with no button presses. This folder is its master copy; -releases of the board package are cut from here. +version **0.3.0** (released 2026-10-07). It is a fork of the WCH CH32 +Arduino core. It adds the CHGame variant (pin names such as `PIN_BTN_A` +and `PIN_SD_CS`), the USB CDC serial port, the app linker script (the +sketch starts at 0x3000, above the 12 KB bootloader), and the +`chgame-upload` tool, which uploads over USB in about half a second with +no button presses. Releases of the board package are cut from here. Its Tools menus matter for every game: @@ -326,20 +389,21 @@ CHGfx it gives: - `RAMFUNC`, which puts hot code in SRAM. It was built from the code the twenty games carried copies of, and every -game is now built on it ([docs/unification.md](docs/unification.md)). Every -call is in the [API reference](https://bateske.github.io/CHGame/group__lib__chgame.html), -its [README](platform/board/arduino/CHGame/libraries/CHGame/README.md) is the -guide, and `examples/Hello` the smallest complete sketch. +game is now built on it ([docs/unification.md](docs/unification.md); +[docs/chgame-library.md](docs/chgame-library.md) records why it is as it +is). Every call is in the [API reference](https://bateske.github.io/CHGame/group__lib__chgame.html), +its [README](platform/board/arduino/CHGame/libraries/CHGame/README.md) is +the guide, and `examples/Hello` the smallest complete sketch. ### CHGfx: `platform/board/arduino/CHGame/libraries/CHGfx/` -The graphics library, version **1.3.2**. A full 16-bit framebuffer would not -fit in 20 KB of RAM. CHGfx keeps a **4-bit indexed framebuffer** (8 KB, a -16-colour palette that can change every frame), converts it to the panel's -format in chunks, and streams it out by DMA at 24 MHz while the game draws -the next frame. It provides the drawing primitives, sprites (`sprite4`), -fonts, text effects, palette effects and partial updates. It also owns the -SPI bus that the SD card shares. +The graphics library, version **1.3.2**. A full 16-bit framebuffer would +not fit in 20 KB of RAM. CHGfx keeps a **4-bit indexed framebuffer** (8 KB, +a 16-colour palette that can change every frame), converts it to the +panel's format in chunks, and streams it out by DMA at 24 MHz while the +game draws the next frame. It provides the drawing primitives, sprites +(`sprite4`), fonts, text effects, palette effects and partial updates. It +also owns the SPI bus that the SD card shares. [docs/performance.md](docs/performance.md) explains the design and its measured limits; the [API reference](https://bateske.github.io/CHGame/group__lib__chgfx.html) documents every call. @@ -349,17 +413,18 @@ documents every call. A small read-only SD library: a polled SPI block driver plus a FAT16/FAT32 reader, about 1.7 KB of flash and 24 B of RAM. CHWords, CHCrossword and CHWordWheel use it to read their dictionary, puzzle packs and phrase bank -from the card, and the app CHStlView browses the card's folders and streams -3D models off it every frame (`sd::stream()`: one command, 24 MHz, DMA). -The games include it as a library (``, ``); the -simulator swaps its SPI driver for a pretend card (`$CHSD_CARD`). CHSDtoUSB has its own faster, read-write SD driver, which -is GPL-3.0 and stays inside that sketch. +from the card, and the app CHStlView browses the card's folders and +streams 3D models off it every frame (`sd::stream()`: one command, 24 MHz, +DMA). The games include it as a library (``, ``); the +simulator swaps its SPI driver for a pretend card (`$CHSD_CARD`). +CHSDtoUSB has its own faster, read-write SD driver, which is GPL-3.0 and +stays inside that sketch. [API reference](https://bateske.github.io/CHGame/group__lib__chsd.html). ### The bootloader and the SD game menu: `platform/bootloader/` The board's permanent bootloader (12 KB at 0x0000), with the game menu in -two faces, the list and the pictures ([docs/visual-menu.md](docs/visual-menu.md)): +two faces, the list and the pictures: - the menu appears at every power-on and lists `GAMES/*.CHG` from a FAT16/FAT32 card, in folders; - the installed game is preselected, and starting it writes nothing; @@ -370,10 +435,10 @@ two faces, the list and the pictures ([docs/visual-menu.md](docs/visual-menu.md) It has its own PC test suite, which runs the real C code against models of the flash, SD card and panel, including a power cut at every flash -operation of an install. Its README covers building, testing and installing -it; [docs/sd-menu.md](docs/sd-menu.md) is the players' guide and -[spec/chg.md](spec/chg.md) the developers' one-pager; what it reads from the -card is [spec/card.md](spec/card.md). +operation of an install. Its [README](platform/bootloader/README.md) +covers building, testing and installing it; [spec/chg.md](spec/chg.md) is +the developers' one-pager, and what it reads from the card is +[spec/card.md](spec/card.md). ### The PC tools: `tools/` @@ -398,13 +463,10 @@ The tools for working with the system outside the Arduino IDE: - the **casino cart** (`sdcard/mkcard.py`, `chgame card`): builds every game into one cart and its card; - the **box art** ([docs/cover-art.md](docs/cover-art.md)): `artkit/`, the - toolkit every menu picture is painted with (shapes and light, a small 3D - ray marcher, the ramp-aware dither, pixel finishing, titles, the house - checks), and `artkit.fontscout`, which sets a title in thousands of pixel - fonts to choose from. The recipes are each program's `tools/cart.py` - (`chgame boxart`), the casino card's `sdcard/art/src/` (`sdcard/covers.py`) - and the menu's defaults in `art/menu/` (`menuart.py`); `artsheet.py` puts - them all on one sheet; + toolkit every menu picture is painted with, and `artkit.fontscout`, which + sets a title in thousands of pixel fonts to choose from; +- the **brand** (`brand/make.py`, [docs/brand/](docs/brand/README.md)): this + README's banner and buttons, and the API reference's wordmark; - the **uploader**: `chgame upload` and `chgame uploader ...` go through the Python one (`platform/bootloader/host/py`, the package `chgame_upload`); `platform/bootloader/host/go` is the same tool in Go, `chgame-upload`, the @@ -419,10 +481,54 @@ The tools for working with the system outside the Arduino IDE: What is a game's own stays with it: its script commands (`chdrive.py`), its description for the shared checks (`game.py`), the tests and the asset -pipeline. [tools/README.md](tools/README.md) classifies every tool in -the repository. +pipeline. [tools/README.md](tools/README.md) classifies every tool in the +repository. + +
+ +
+Repository map + +``` +CHGame/ +โ”œโ”€โ”€ CLAUDE.md start here: commands, rules, limits, gotchas +โ”œโ”€โ”€ platform/ the platform itself; these are the master copies +โ”‚ โ”œโ”€โ”€ board/ the Arduino board package (core, variant, linker scripts) + the board's docs +โ”‚ โ”‚ โ””โ”€โ”€ arduino/CHGame/libraries/ CHGame (CHGame.h), CHGfx (graphics), CHSd (SD/FAT), beside SPI, Wire, EEPROM +โ”‚ โ”‚ โ””โ”€โ”€ CHGame/examples/ Hello, Games/ (the 20 casino games), Apps/ (CHStlView, CHSDtoUSB, CHSDtoSerial) +โ”‚ โ”œโ”€โ”€ bootloader/ the bootloader with the SD game menu: sources, PC test suite, binaries, +โ”‚ โ”‚ and the uploader's source (host/go) +โ”‚ โ””โ”€โ”€ hardware/ Rev 0 schematic and netlist +โ”œโ”€โ”€ spec/ the .chgame format, the SD card's layout and the CHG file: the contract +โ”‚ with the emulator and web tools, with conformance fixtures +โ”œโ”€โ”€ tools/ the PC tools shared by every game (device.py, the simulator and script +โ”‚ driver, sound preview, size report, serial, chcart/ for .chgame carts +โ”‚ and cards, chgpack.py for CHG files, sdcard/ for the casino cart, +โ”‚ artkit/ for the box art, brand/ for the README's banner) +โ””โ”€โ”€ docs/ platform knowledge: hardware, performance, SD card, how a game is built, + status, the roadmap, the CHGame library's decisions and history, a + getting-started guide for Arduboy developers, brand/ (the look); + api/ builds the API reference (Doxygen) published to GitHub Pages +``` + +
+ +

back to top โ†‘

+ +## ๐Ÿ“š Documentation + +| For | Read | +|---|---| +| Writing a game | [Getting started](docs/getting-started.md) (from the Arduboy) ยท [API reference](https://bateske.github.io/CHGame/) ยท [the CHGame library](platform/board/arduino/CHGame/libraries/CHGame/README.md) ยท [anatomy of a game](docs/game-anatomy.md) | +| What things cost | [platform.md](docs/platform.md) (pins, memory map, speeds) ยท [performance.md](docs/performance.md) ยท [hardware revisions](docs/hardware-revisions.md) | +| Playing | [the SD game menu](docs/sd-menu.md) ยท [the visual menu](docs/visual-menu.md) ยท [the SD card](docs/sd-card.md) | +| Pictures | [menu pictures](docs/menu-image.md) ยท [cover art](docs/cover-art.md) ยท [pixel art rules](docs/pixel-art.md) ยท [the brand](docs/brand/README.md) | +| Sharing games | [the `.chgame` format and the card](spec/README.md) ยท [the CHG file](spec/chg.md) ยท [the upload protocol](platform/board/docs/protocol.md) | +| The project | [roadmap](docs/roadmap.md) ยท [status of each game](docs/status.md) ยท [the library's decisions](docs/chgame-library.md) ยท [unification](docs/unification.md) ยท [CLAUDE.md](CLAUDE.md) | -## Licences +

back to top โ†‘

+ +## ๐Ÿ“œ Licences Each folder carries its own licence: @@ -440,13 +546,14 @@ Each folder carries its own licence: | `platform/hardware/` | No licence stated yet (schematic and netlist) | | `docs/`, root files | Apache-2.0 (`LICENSE`, `NOTICE`) | -The box art's titles are lettered in pixel fonts by other people. Each -title's text art (`tools/art/title.txt` and the like) names its font, author, -stated terms and source in its header, and -[docs/cover-art.md](docs/cover-art.md#credits-fonts-in-the-titles) lists -them all. +The box art's titles and the banner's lettering are set in pixel fonts by +other people. Each title's text art (`tools/art/title.txt` and the like) +names its font, author, stated terms and source in its header, and +[docs/cover-art.md](docs/cover-art.md#credits-fonts-in-the-titles) and +[docs/brand/README.md](docs/brand/README.md#credits) list them all. -## History +
+History The pieces were developed in separate repositories and brought together here on 2026-10-01, without their histories: @@ -460,3 +567,36 @@ here on 2026-10-01, without their histories: The collection was first assembled under the working name CHCasino (the name went from the code and comments on 2026-10-02). Those repositories are frozen. All development continues here. + +
+ +

back to top โ†‘

+ +## ๐Ÿ™Œ Credits + +CHGame stands on other people's open work. The licences above say what is +whose; these are the projects it leans on most: + +| Project | What CHGame owes it | +|---|---| +| [CH32_Arduino_Core](https://github.com/jobitjoseph/CH32_Arduino_Core) and [CH32X035_USBSerial](https://github.com/jobitjoseph/CH32X035_USBSerial) by jobitjoseph | the Arduino core the board package forks, and the USB serial port in the core and the bootloader | +| [arduino_core_ch32](https://github.com/openwch/arduino_core_ch32) by WCH | the upstream core, the chip's peripheral library and start-up code | +| [wchisp](https://github.com/ch32-rs/wchisp) by ch32-rs | factory programming and recovery | +| [Adafruit GFX](https://github.com/adafruit/Adafruit-GFX-Library) | the 5x7 font in CHGfx and the menu, and the drawing API CHGfx's compatibility layer follows | +| [SdFat / sdfatlib](https://github.com/greiman/SdFat) by Bill Greiman | the SD driver inside CHSDtoUSB | +| [go-serial](https://github.com/bugst/go-serial) by bugst | the serial port under the Go uploader | +| [Arduino](https://www.arduino.cc/) | the IDE, the CLI and the board package format that make one install enough | + +

back to top โ†‘

+ +## ๐Ÿ’› Special thanks + +XOXO, spelled in CHTicTacToe's spinning X and O pieces + +To [**Mr.Blinky**](https://github.com/MrBlinky/) and [**MLXXXp**](https://github.com/mlxxxp/), +for showing what is possible when you give developers the tools they need. +The Arduboy's homemade package, its FX flash cart and the Arduboy2 library +with its reference are the model for CHGame's board package, its game +menu and its API reference. + +

back to top โ†‘

diff --git a/docs/api/Doxyfile b/docs/api/Doxyfile index 9b3e4ed..78e7f0c 100644 --- a/docs/api/Doxyfile +++ b/docs/api/Doxyfile @@ -17,7 +17,7 @@ PROJECT_NAME = "CHGame" # Set by the workflow from platform.txt's version= (empty when run by hand). PROJECT_NUMBER = $(CHGAME_VERSION) PROJECT_BRIEF = "Libraries for the CHGame handheld" -PROJECT_LOGO = docs/api/logo.png +PROJECT_LOGO = docs/brand/logo.png OUTPUT_DIRECTORY = out/api-docs STRIP_FROM_PATH = platform/board/arduino/CHGame/libraries STRIP_FROM_INC_PATH = platform/board/arduino/CHGame/libraries/CHGame/src \ @@ -141,11 +141,14 @@ PREDEFINED = __riscv=1 \ #--------------------------------------------------------------------------- GENERATE_HTML = YES HTML_OUTPUT = html +# The brand's look (docs/brand/README.md): extra.css sets the colours, the +# hue only tints the few images Doxygen draws itself (folder icons). HTML_EXTRA_STYLESHEET = docs/api/extra.css -HTML_COLORSTYLE = AUTO_LIGHT -HTML_COLORSTYLE_HUE = 140 -HTML_COLORSTYLE_SAT = 90 -HTML_COLORSTYLE_GAMMA = 90 +HTML_EXTRA_FILES = docs/brand/wordmark.svg +HTML_COLORSTYLE = LIGHT +HTML_COLORSTYLE_HUE = 45 +HTML_COLORSTYLE_SAT = 200 +HTML_COLORSTYLE_GAMMA = 80 HTML_DYNAMIC_SECTIONS = NO HTML_INDEX_NUM_ENTRIES = 1 GENERATE_TREEVIEW = YES diff --git a/docs/api/README.md b/docs/api/README.md index 8fd83ef..9c18e17 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -12,7 +12,7 @@ to `main` that touches a library or this folder. | `mainpage.md` | the front page | | `groups.dox` | the top-level topics, one per library, with each library's overview | | `chgfx_fonts.dox` | CHGfx's bundled fonts (their headers are generated, so not commented) | -| `extra.css`, `logo.png` | the look | +| `extra.css` | the look: the brand's neobrutalist style ([docs/brand](../brand/README.md)); the logo and the wordmark come from `docs/brand/` | Everything else comes from the comments in the libraries' headers under `platform/board/arduino/CHGame/libraries/*/src/`. diff --git a/docs/api/extra.css b/docs/api/extra.css index f90229b..112f789 100644 --- a/docs/api/extra.css +++ b/docs/api/extra.css @@ -1,15 +1,1184 @@ -/* The CHGame reference's few changes to Doxygen's look. */ +/* The CHGame API reference's look: neobrutalism in the brand's palette + (docs/brand/README.md). Flat colours, ink outlines, hard shadows offset + down and right; nothing blurred or graded. Doxygen 1.9.8's own colours + are set through its variables, the rest is overridden below. */ + +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400..800;1,9..40,400..800&family=JetBrains+Mono:wght@400;600;700&family=Space+Grotesk:wght@500;700&display=swap'); + +html { + --ch-ink: #252628; + --ch-yellow: #F9D84A; + --ch-cream: #FFFDF6; + --ch-paper: #FBFAF5; + --ch-mint: #BDE2C7; + --ch-sage: #B7DACC; + --ch-teal: var(--ch-sage); /* the main accent; yellow is kept for the few big highlights */ + --ch-lavender: #C8B6F3; + --ch-coral: #F4A28C; + --ch-dots: #E6E2D3; + --ch-muted: #5F5E58; + --ch-line: 2px solid var(--ch-ink); + --ch-shadow: 4px 4px 0 var(--ch-ink); + --ch-shadow-sm: 3px 3px 0 var(--ch-ink); + --ch-radius: 8px; + --ch-sans: 'DM Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --ch-display: 'Space Grotesk', 'DM Sans', system-ui, sans-serif; + --ch-mono: 'JetBrains Mono', Consolas, Menlo, monospace; + + /* Doxygen's own variables */ + --page-background-color: var(--ch-paper); + --page-foreground-color: var(--ch-ink); + --page-link-color: var(--ch-ink); + --page-visited-link-color: var(--ch-ink); + --index-odd-item-bg-color: var(--ch-cream); + --index-even-item-bg-color: var(--ch-paper); + --index-header-color: var(--ch-ink); + --index-separator-color: var(--ch-ink); + --header-background-color: var(--ch-paper); + --header-separator-color: var(--ch-ink); + --header-gradient-image: none; + --group-header-separator-color: var(--ch-ink); + --group-header-color: var(--ch-ink); + --inherit-header-color: var(--ch-muted); + --footer-foreground-color: var(--ch-cream); + --title-background-color: var(--ch-paper); + --title-separator-color: var(--ch-ink); + --directory-separator-color: var(--ch-ink); + --separator-color: var(--ch-ink); + --blockquote-background-color: var(--ch-cream); + --blockquote-border-color: var(--ch-ink); + --scrollbar-thumb-color: var(--ch-ink); + --scrollbar-background-color: var(--ch-paper); + --icon-background-color: var(--ch-lavender); + --icon-foreground-color: var(--ch-ink); + --memdecl-background-color: var(--ch-cream); + --memdecl-separator-color: var(--ch-dots); + --memdecl-foreground-color: var(--ch-muted); + --memdecl-template-color: var(--ch-muted); + --memdef-border-color: var(--ch-ink); + --memdef-title-background-color: var(--ch-teal); + --memdef-title-gradient-image: none; + --memdef-proto-background-color: var(--ch-cream); + --memdef-proto-text-color: var(--ch-ink); + --memdef-proto-text-shadow: none; + --memdef-doc-background-color: var(--ch-cream); + --memdef-param-name-color: #7A3E9D; + --memdef-template-color: var(--ch-muted); + --table-cell-border-color: var(--ch-ink); + --table-header-background-color: var(--ch-teal); + --table-header-foreground-color: var(--ch-ink); + --label-background-color: var(--ch-mint); + --label-left-top-border-color: var(--ch-ink); + --label-right-bottom-border-color: var(--ch-ink); + --label-foreground-color: var(--ch-ink); + --nav-background-color: var(--ch-cream); + --nav-foreground-color: var(--ch-ink); + --nav-gradient-image: none; + --nav-gradient-hover-image: none; + --nav-gradient-active-image: none; + --nav-gradient-active-image-parent: none; + --nav-separator-image: none; + --nav-breadcrumb-image: none; + --nav-breadcrumb-border-color: var(--ch-ink); + --nav-splitbar-image: none; + --nav-text-normal-color: var(--ch-ink); + --nav-text-hover-color: var(--ch-ink); + --nav-text-active-color: var(--ch-ink); + --nav-text-normal-shadow: none; + --nav-text-hover-shadow: none; + --nav-text-active-shadow: none; + --nav-menu-button-color: var(--ch-ink); + --nav-menu-background-color: var(--ch-cream); + --nav-menu-foreground-color: var(--ch-ink); + --nav-menu-toggle-color: var(--ch-cream); + --nav-arrow-color: var(--ch-ink); + --nav-arrow-selected-color: var(--ch-ink); + --toc-background-color: var(--ch-cream); + --toc-border-color: var(--ch-ink); + --toc-header-color: var(--ch-ink); + --search-background-color: var(--ch-cream); + --search-foreground-color: var(--ch-ink); + --search-active-color: var(--ch-ink); + --search-filter-background-color: var(--ch-cream); + --search-filter-foreground-color: var(--ch-ink); + --search-filter-border-color: var(--ch-ink); + --search-filter-highlight-text-color: var(--ch-ink); + --search-filter-highlight-bg-color: var(--ch-yellow); + --search-results-foreground-color: var(--ch-ink); + --search-results-background-color: var(--ch-cream); + --search-results-border-color: var(--ch-ink); + --search-box-shadow: none; + --code-keyword-color: #6A3FC2; + --code-type-keyword-color: #1F6E52; + --code-flow-keyword-color: #B5482F; + --code-comment-color: #7D7A6E; + --code-preprocessor-color: #8A6100; + --code-string-literal-color: #8A6100; + --code-char-literal-color: #8A6100; + --code-link-color: var(--ch-ink); + --code-external-link-color: var(--ch-ink); + --fragment-foreground-color: var(--ch-ink); + --fragment-background-color: var(--ch-cream); + --fragment-border-color: var(--ch-ink); + --fragment-lineno-border-color: var(--ch-ink); + --fragment-lineno-background-color: var(--ch-paper); + --fragment-lineno-foreground-color: var(--ch-muted); + --tooltip-foreground-color: var(--ch-ink); + --tooltip-background-color: var(--ch-cream); + --tooltip-border-color: var(--ch-ink); + --tooltip-doc-color: var(--ch-muted); + --tooltip-declaration-color: #1F6E52; + --tooltip-link-color: var(--ch-ink); + --tooltip-shadow: var(--ch-shadow); + --font-family-normal: var(--ch-sans); + --font-family-monospace: var(--ch-mono); + --font-family-nav: var(--ch-sans); + --font-family-title: var(--ch-display); + --font-family-toc: var(--ch-sans); + --font-family-search: var(--ch-sans); + --font-family-tooltip: var(--ch-sans); +} + +/* ---------------------------------------------------------------- page */ + +body, table, div, p, dl { + font: 400 15px/1.6 var(--ch-sans); +} + +body { + background-color: var(--ch-paper); + color: var(--ch-ink); +} + +::selection { + background: var(--ch-yellow); + color: var(--ch-ink); +} + +a, a:visited, .contents a:visited { + color: var(--ch-ink); +} + +a.el, a.elRef, .contents a { + font-weight: 600; + text-decoration: underline; + text-decoration-color: var(--ch-teal); + text-decoration-thickness: 3px; + text-underline-offset: 3px; +} + +a.el:hover, a.elRef:hover, .contents a:hover { + background-color: var(--ch-teal); + text-decoration-color: var(--ch-ink); +} + +a.code, a.code:visited, a.line, a.line:visited { + color: inherit; + text-decoration: underline; + text-decoration-color: var(--ch-teal); + text-decoration-thickness: 2px; + text-underline-offset: 2px; +} + +a.code:hover { + background-color: var(--ch-teal); +} + +h1, h2, h3, h4, h5, h6, +h1.groupheader, h2.groupheader, h3.groupheader { + font-family: var(--ch-display); + font-weight: 700; + letter-spacing: -0.01em; + color: var(--ch-ink); +} + +.textblock h1, .contents > h1 { + font-size: 2em; +} + +.textblock h2 { + font-size: 1.55em; + margin-top: 1.6em; + padding-bottom: 4px; + border-bottom: 3px solid var(--ch-ink); +} + +.textblock h3 { + font-size: 1.2em; + margin-top: 1.4em; +} + +code, tt, kbd, samp { + font-family: var(--ch-mono); + font-size: 0.88em; +} + +p code, li code, td code, dd code, .memdoc code, .textblock code { + display: inline; + padding: 1px 5px; + background: var(--ch-cream); + border: 1.5px solid var(--ch-ink); + border-radius: 5px; + white-space: nowrap; +} + +/* a code chip that is a link (the header names in tables): a small + button, 1 px outline and a hard shadow; teal on hover */ +p code:has(> a.el), li code:has(> a.el), td code:has(> a.el), dd code:has(> a.el), +.textblock code:has(> a.el), .memdoc code:has(> a.el) { + display: inline-block; + border: 1px solid var(--ch-ink); + border-radius: 5px; + box-shadow: 2px 2px 0 var(--ch-ink); + background: var(--ch-cream); + padding: 1px 6px; + margin: 2px 4px 4px 0; +} + +code:has(> a.el):hover { + background: var(--ch-teal); +} + +code > a.el, code > a.el:hover { + text-decoration: none; + background: none; + font-weight: 700; +} + +hr { + height: 0; + border: none; + border-top: 2px dashed var(--ch-ink); +} + +/* ---------------------------------------------------------------- the header */ + +#top { + background: var(--ch-paper); +} + #titlearea { - padding: 10px 16px; + padding: 16px 24px 18px; + border-bottom: none; + background-color: var(--ch-paper); + background-image: radial-gradient(var(--ch-dots) 1.5px, transparent 1.6px); + background-size: 20px 20px; +} + +#projectrow { + height: auto; } + #projectlogo { - padding-right: 12px; + padding-right: 18px; } + #projectlogo img { - height: 56px; + height: 60px; width: auto; display: block; + filter: drop-shadow(4px 4px 0 var(--ch-ink)); } + #projectname { - font-size: 200%; + /* the pixel CHGAME logo (docs/brand/wordmark.svg) stands in for the text */ + font-size: 0; + line-height: 0; + padding: 0; + display: flex; + align-items: center; + gap: 14px; +} + +#projectname::before { + content: ""; + display: inline-block; + width: 219px; + height: 54px; + background: url('wordmark.svg') no-repeat left center / contain; +} + +#projectnumber { + font: 700 13px/1 var(--ch-mono); + color: var(--ch-ink); + background: var(--ch-mint); + border: var(--ch-line); + border-radius: 999px; + padding: 5px 10px 4px; + box-shadow: var(--ch-shadow-sm); + margin-left: 0; +} + +#projectbrief { + font: 500 15px/1.4 var(--ch-sans); + color: var(--ch-muted); + margin-top: 10px; +} + +/* the menu bar */ +#main-nav { + background: var(--ch-teal); + border-top: 3px solid var(--ch-ink); + border-bottom: 3px solid var(--ch-ink); +} + +.sm-dox { + background: var(--ch-teal); + padding: 0; +} + +#main-menu { + padding: 6px 14px; +} + +.sm-dox a, .sm-dox a:focus, .sm-dox a:active, .sm-dox a:hover, .sm-dox a.highlighted { + font: 700 14px/34px var(--ch-sans); + color: var(--ch-ink); + background: transparent; + border: 2px solid transparent; + border-radius: 7px !important; + margin: 0 3px; + text-shadow: none; +} + +.sm-dox > li > a:hover, .sm-dox > li > a.highlighted { + background: var(--ch-cream); + border-color: var(--ch-ink); + box-shadow: var(--ch-shadow-sm); + transform: translate(-1px, -1px); +} + +.sm-dox a span.sub-arrow { + border-color: var(--ch-ink) transparent transparent transparent; +} + +.sm-dox a:hover span.sub-arrow { + border-color: var(--ch-ink) transparent transparent transparent; +} + +.sm-dox ul { + border: var(--ch-line); + border-radius: var(--ch-radius) !important; + box-shadow: var(--ch-shadow); + background: var(--ch-cream); + padding: 6px; +} + +.sm-dox > li > ul:before, .sm-dox > li > ul:after { + display: none; +} + +.sm-dox ul a, .sm-dox ul a:hover, .sm-dox ul a:focus, .sm-dox ul a:active, .sm-dox ul a.highlighted { + font: 600 13px/30px var(--ch-sans); + color: var(--ch-ink); + border-radius: 6px !important; + margin: 0; +} + +.sm-dox ul a:hover, .sm-dox ul a.highlighted { + background: var(--ch-yellow); + color: var(--ch-ink); + text-shadow: none; +} + +.sm-dox ul a span.sub-arrow, .sm-dox ul a:hover span.sub-arrow { + border-color: transparent transparent transparent var(--ch-ink); +} + +.main-menu-btn-icon, .main-menu-btn-icon:before, .main-menu-btn-icon:after { + background: var(--ch-ink); +} + +/* the search box */ +#MSearchBox { + background: var(--ch-cream); + border: var(--ch-line); + border-radius: 999px; + box-shadow: var(--ch-shadow-sm); + padding: 2px 10px 2px 4px; +} + +#MSearchBox.MSearchBoxActive { + background: #fff; + box-shadow: 4px 4px 0 var(--ch-ink); +} + +#MSearchField { + font: 500 14px var(--ch-sans); + color: var(--ch-ink); + width: 10em; +} + +#MSearchField::placeholder { + color: var(--ch-muted); +} + +#MSearchSelectWindow, #MSearchResultsWindow { + border: var(--ch-line); + border-radius: var(--ch-radius); + box-shadow: var(--ch-shadow); + background: var(--ch-cream); +} + +a.SelectItem:hover, .SelectionMark { + background: var(--ch-yellow); + color: var(--ch-ink); +} + +.SRPage { + background: var(--ch-cream); +} + +.SRSymbol, .SRScope { + color: var(--ch-ink); + font-family: var(--ch-sans); +} + +.SRSymbol { + font-weight: 700; +} + +.SRSymbol, .SRScope, .SRStatus { + font-size: 13px; +} + +.SRResult { + padding: 2px 4px; +} + +/* ---------------------------------------------------------------- the tree */ + +#side-nav { + padding-right: 0; +} + +#nav-tree { + background: var(--ch-cream); + border-right: 3px solid var(--ch-ink); +} + +.ui-resizable-e { + background: transparent; + width: 8px; +} + +#nav-tree-contents { + margin-top: 10px; + padding-left: 8px; +} + +#nav-tree .label { + font: 500 14px/1.9 var(--ch-sans); +} + +#nav-tree .label a { + padding: 2px 6px; + border-radius: 5px; +} + +#nav-tree .item:hover .label a { + background: var(--ch-yellow); +} + +#nav-tree .selected { + background: none; + text-shadow: none; + margin: 2px 0 6px; /* air round the raised pill, so its shadow does not crowd the next row */ +} + +#nav-tree .selected .label a { + background: var(--ch-mint); + border: var(--ch-line); + box-shadow: var(--ch-shadow-sm); + font-weight: 700; + color: var(--ch-ink); +} + +/* The tree's arrows. Doxygen writes them as two different characters + (a right-pointing one, a down-pointing one) that have different shapes; + here the character is hidden and one triangle is drawn instead, turned a + quarter when the node is open. Every arrow, and every leaf's spacer, + keeps some air before its label. */ +#nav-tree .arrow { + margin-right: 6px; + vertical-align: top; +} + +#nav-tree a > span.arrow { + font-size: 0; + position: relative; + opacity: 1; +} + +#nav-tree a > span.arrow::before { + content: ""; + position: absolute; + right: 3px; + top: 50%; + margin-top: -5px; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 0 5px 8px; + border-color: transparent transparent transparent var(--ch-ink); + transition: transform 0.12s; +} + +#nav-tree li:has(> ul.children_ul:not([style*="display: none"])) > div.item > a > span.arrow::before { + transform: rotate(90deg); +} + +/* the tree's "sync with the page" toggle: syncing is on by default and + is all anyone wants, so the button only adds clutter */ +#nav-sync { + display: none; +} + +/* ---------------------------------------------------------------- the content */ + +#doc-content { + background: var(--ch-paper); +} + +div.header { + background: transparent; + border-bottom: none; + padding: 18px 26px 4px; +} + +div.headertitle { + padding: 0; +} + +div.header .title { + font: 700 2.15em/1.15 var(--ch-display); + letter-spacing: -0.02em; + color: var(--ch-ink); + margin: 6px 0 4px; +} + +div.ingroups { + font: 600 13px var(--ch-sans); + color: var(--ch-muted); +} + +div.ingroups a { + background: var(--ch-lavender); + border: 1.5px solid var(--ch-ink); + border-radius: 999px; + padding: 1px 9px; + text-decoration: none; +} + +div.summary { + font: 600 0/1.6 var(--ch-sans); /* hides Doxygen's " | " between the links */ + padding: 6px 0 0; +} + +div.summary a { + font-size: 13px; + margin-left: 6px; + display: inline-block; + background: var(--ch-cream); + border: 1.5px solid var(--ch-ink); + border-radius: 999px; + padding: 0 9px; + margin: 2px 0; + text-decoration: none; + white-space: nowrap; +} + +div.summary a:hover { + background: var(--ch-teal); +} + +div.contents { + margin: 0; + padding: 6px 26px 32px; + max-width: 1100px; +} + +div.textblock > p, div.textblock > ul, div.textblock > ol { + max-width: 78ch; +} + +/* the contents box on long pages */ +div.toc { + background: var(--ch-cream); + border: var(--ch-line); + border-radius: var(--ch-radius); + box-shadow: var(--ch-shadow); + padding: 12px 20px; +} + +div.toc h3 { + font: 700 13px var(--ch-display); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ch-ink); +} + +div.toc li { + background: none; + font: 500 13px var(--ch-sans); +} + +/* section headings of a topic / class page */ +h2.groupheader { + font-size: 1.35em; + color: var(--ch-ink); + border-bottom: 3px solid var(--ch-ink); + margin-top: 1.8em; + padding-bottom: 4px; +} + +tr.heading h2.groupheader { + margin-top: 1.2em; + border-bottom: none; + padding-bottom: 0; +} + +h2.groupheader a, h2.groupheader a:hover { + background: none; + text-decoration: none; +} + +/* ---------------------------------------------------------------- the summaries */ + +table.memberdecls { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.memberdecls td { + transition: none; +} + +.memItemLeft, .memItemRight, .memTemplItemLeft, .memTemplItemRight, .memTemplParams { + background-color: var(--ch-cream); + font-family: var(--ch-mono); + font-size: 13px; + padding: 7px 0 2px 12px; +} + +.memItemLeft { + border-left: var(--ch-line); + color: var(--ch-muted); +} + +.memItemRight { + border-right: var(--ch-line); + padding-right: 12px; +} + +.mdescLeft, .mdescRight { + background-color: var(--ch-cream); + color: var(--ch-muted); + font: 400 13.5px/1.5 var(--ch-sans); + padding: 0 12px 8px; +} + +.mdescLeft { + border-left: var(--ch-line); +} + +.mdescRight { + border-right: var(--ch-line); +} + +/* a box round each run of declarations */ +table.memberdecls tr.heading + tr > td, table.memberdecls tr.inherit_header + tr > td { + border-top: var(--ch-line); +} + +table.memberdecls tr.heading + tr > td:first-child { + border-top-left-radius: var(--ch-radius); +} + +table.memberdecls tr.heading + tr > td:last-child { + border-top-right-radius: var(--ch-radius); +} + +.memSeparator { + border-bottom: 1px dashed var(--ch-dots); + line-height: 1px; + padding: 0; +} + +table.memberdecls tr:last-child > td.memSeparator, table.memberdecls tr:has(+ tr.heading) > td.memSeparator { + border: var(--ch-line); + border-top: none; + border-radius: 0 0 var(--ch-radius) var(--ch-radius); + box-shadow: var(--ch-shadow); + line-height: 4px; + background: var(--ch-cream); +} + +.memItemLeft a.el, .memItemRight a.el, .mdescRight a.el { + font-weight: 700; +} + +/* ---------------------------------------------------------------- one member */ + +h2.memtitle { + display: inline-block; + background: var(--ch-teal); + border: var(--ch-line); + border-bottom: none; + border-radius: var(--ch-radius) var(--ch-radius) 0 0; + font: 700 15px/1.2 var(--ch-mono); + padding: 8px 14px 7px; + margin: 26px 0 0; + color: var(--ch-ink); + box-shadow: none; +} + +.permalink { + font-size: 11px; + margin-right: 4px; +} + +.permalink a { + text-decoration: none; + color: var(--ch-ink); +} + +div.memitem { + margin: 0 0 24px; + padding: 0; + border: var(--ch-line); + border-radius: 0 var(--ch-radius) var(--ch-radius) var(--ch-radius); + box-shadow: var(--ch-shadow); + background: var(--ch-cream); + display: block; + overflow: visible; +} + +div.memproto, dl.reflist dt { + background: var(--ch-paper); + border: none; + border-bottom: var(--ch-line); + border-radius: 0 6px 0 0; + box-shadow: none; + text-shadow: none; + padding: 10px 14px; + font-weight: 400; +} + +table.memname, table.memname td { + font: 500 13.5px/1.5 var(--ch-mono); + color: var(--ch-ink); +} + +td.memname { + font-weight: 700; +} + +.paramname, .paramname em { + color: #7A3E9D; + font-style: normal; + font-weight: 600; +} + +div.memdoc, dl.reflist dd { + background: var(--ch-cream); + border: none; + border-radius: 0 0 6px 6px; + box-shadow: none; + padding: 8px 16px 6px; +} + +.memdoc > p:first-child { + margin-top: 6px; +} + +/* labels such as static, inline */ +span.mlabel { + background: var(--ch-mint); + color: var(--ch-ink); + border: 1.5px solid var(--ch-ink); + border-radius: 999px; + font: 700 11px var(--ch-mono); + padding: 2px 8px; + margin-right: 4px; +} + +/* parameters */ +dl.params dt, dl.tparams dt, dl.retval dt, dl.exception dt { + font: 700 12px var(--ch-display); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +table.params, table.tparams, table.retval, table.exception { + border-collapse: separate; + border-spacing: 0; +} + +.params .paramname, .tparams .paramname, .retval .paramname, .exception .paramname { + font: 700 13px var(--ch-mono); + padding-right: 14px; +} + +.params .paramdir, .tparams .paramdir { + font: 600 12px var(--ch-mono); + color: var(--ch-muted); +} + +/* notes, warnings, see-also, return */ +dl.section { + margin: 12px 0; + padding: 8px 12px 8px 14px; + border: var(--ch-line); + border-radius: var(--ch-radius); + background: var(--ch-paper); +} + +dl.section dt { + font: 700 12px var(--ch-display); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 2px; +} + +dl.section dd { + margin: 0 0 2px; +} + +dl.note { + background: #FFF6CC; + box-shadow: var(--ch-shadow-sm); + margin-left: 0; + border-left: var(--ch-line); +} + +dl.warning, dl.attention, dl.bug { + background: #FCE3DB; + box-shadow: var(--ch-shadow-sm); + margin-left: 0; + border-left: var(--ch-line); + border-color: var(--ch-ink); +} + +dl.see, dl.return, dl.since, dl.pre, dl.post, dl.invariant, dl.user, dl.remark, dl.todo, dl.deprecated { + margin-left: 0; + border-left: var(--ch-line); + border-color: var(--ch-ink); +} + +dl.see { + background: #EFE9FD; +} + +dl.return { + background: #E8F5EC; +} + +dl.deprecated, dl.todo { + background: #FFF6CC; +} + +/* ---------------------------------------------------------------- code */ + +div.fragment { + background: var(--ch-cream); + border: var(--ch-line); + border-radius: var(--ch-radius); + box-shadow: var(--ch-shadow); + padding: 12px 14px; + margin: 10px 6px 18px 0; + overflow-x: auto; +} + +div.line { + font: 400 13px/1.55 var(--ch-mono); + min-height: 13px; +} + +pre.fragment { + font-family: var(--ch-mono); + border: var(--ch-line); + border-radius: var(--ch-radius); + box-shadow: var(--ch-shadow); + background: var(--ch-cream); + padding: 12px 14px; +} + +span.lineno { + border-right: 2px solid var(--ch-dots); + background: transparent; + color: var(--ch-muted); + padding-right: 6px; +} + +span.lineno a, span.lineno a:hover { + background: transparent; + color: var(--ch-muted); +} + +span.keyword, span.keywordflow, span.keywordtype { + font-weight: 600; +} + +/* ---------------------------------------------------------------- tables */ + +table.markdownTable, table.doxtable, table.fieldtable, table.directory { + border-collapse: separate; + border-spacing: 0; + border: var(--ch-line); + border-radius: var(--ch-radius); + box-shadow: var(--ch-shadow); + background: var(--ch-cream); + overflow: hidden; + margin: 10px 6px 18px 0; +} + +table.markdownTable td, table.markdownTable th, table.doxtable td, table.doxtable th { + border: none; + border-bottom: 1.5px solid var(--ch-ink); + border-right: 1.5px solid var(--ch-ink); + padding: 7px 12px; + vertical-align: top; + font-size: 14px; +} + +table.markdownTable tr > :last-child, table.doxtable tr > :last-child { + border-right: none; +} + +table.markdownTable tr:last-child > td, table.doxtable tr:last-child > td { + border-bottom: none; +} + +th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone, +table.doxtable th { + background: var(--ch-teal); + color: var(--ch-ink); + font: 700 13px var(--ch-display); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 8px 12px; +} + +tr.markdownTableRowEven, table.doxtable tr:nth-child(odd) td { + background: var(--ch-paper); +} + +/* the field tables (enum values, struct fields) */ +.fieldtable { + width: auto; +} + +.fieldtable th { + background: var(--ch-teal); + color: var(--ch-ink); + font: 700 13px var(--ch-display); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: var(--ch-line); + padding: 8px 12px; +} + +.fieldtable td { + border-color: var(--ch-ink); + border-width: 0 0 1.5px; + border-style: solid; +} + +.fieldtable td.fieldname, .fieldtable td.fieldtype { + font: 600 13px var(--ch-mono); + border-right: 1.5px solid var(--ch-ink); + padding: 6px 12px; +} + +.fieldtable td.fielddoc { + padding: 6px 12px; +} + +.fieldtable tr:last-child td { + border-bottom: none; +} + +.fieldtable td.fieldname em, .fieldtable td.fieldname { + color: #7A3E9D; + font-style: normal; +} + +/* the index tables (topics, classes, files) */ +div.directory { + border: none; + margin: 0; +} + +table.directory { + width: auto; + min-width: min(100%, 640px); + font: 400 14px var(--ch-sans); +} + +.directory tr.even, .directory tr.odd { + padding-left: 0; +} + +.directory tr.even td { + background: var(--ch-paper); +} + +.directory tr.odd td { + background: var(--ch-cream); +} + +.directory td { + border-bottom: 1px solid var(--ch-dots); + padding: 6px 10px; +} + +.directory td.entry { + white-space: nowrap; +} + +.directory td.entry a.el { + font-weight: 700; +} + +.directory td.desc { + color: var(--ch-muted); + border-left: 1px solid var(--ch-dots); +} + +.directory .levels { + font: 700 12px var(--ch-sans); + padding: 8px 0 8px; +} + +.directory .levels span { + display: inline-block; + min-width: 18px; + text-align: center; + border: 1.5px solid var(--ch-ink); + border-radius: 5px; + background: var(--ch-cream); + margin-left: 4px; + color: var(--ch-ink); +} + +.directory .levels span:hover { + background: var(--ch-teal); +} + +.icona .icon, .icon { + background: var(--ch-lavender); + color: var(--ch-ink); + border: 1.5px solid var(--ch-ink); + border-radius: 5px; + font: 700 11px/14px var(--ch-mono); + width: 16px; + height: 16px; +} + +.arrow { + color: var(--ch-ink); +} + +/* ---------------------------------------------------------------- tooltips */ + +div.ttc { + font-family: var(--ch-sans); +} + +div.ttc .tttitle, .tooltip { + border: var(--ch-line); + border-radius: var(--ch-radius); + box-shadow: var(--ch-shadow); +} + +/* ---------------------------------------------------------------- the footer */ + +.navpath ul { + background: var(--ch-ink); + border: none; + border-top: 3px solid var(--ch-ink); + height: 34px; + line-height: 34px; + font: 600 12px/34px var(--ch-sans); + color: var(--ch-cream); +} + +.navpath li.navelem { + background: none; + padding: 0 6px 0 12px; +} + +.navpath li.navelem a, .navpath li.navelem a:hover { + color: var(--ch-ink); + background: var(--ch-teal); + border: 1.5px solid var(--ch-ink); + border-radius: 999px; + padding: 2px 10px; + text-shadow: none; + text-decoration: none; + font-weight: 700; +} + +.navpath li.navelem + li.navelem::before { + content: "\203A"; + color: var(--ch-cream); + margin-right: 10px; +} + +.navpath li.footer, .navpath li.footer a { + color: var(--ch-cream); + font: 500 12px/34px var(--ch-sans); + text-shadow: none; +} + +img.footer { + filter: invert(1) grayscale(1) brightness(1.6); + height: 18px; + width: auto; + vertical-align: middle; +} + +/* ---------------------------------------------------------------- small screens */ + +@media (max-width: 767px) { + /* the tree would take most of a phone's width: the menu (the + hamburger) navigates instead, and the page scrolls as a whole */ + #side-nav { + display: none; + } + + #doc-content { + margin-left: 0 !important; + height: auto !important; + overflow: visible; + } + + #MSearchField { + width: 8em; + } + + .sm-dox a span.sub-arrow, .sm-dox a:hover span.sub-arrow { + border: 1.5px solid var(--ch-ink); + background: var(--ch-cream); + border-radius: 6px; + } + + #projectname::before { + width: 146px; + height: 36px; + } + + #projectlogo img { + height: 44px; + } + + div.contents { + padding: 6px 14px 28px; + } + + div.header { + padding: 14px 14px 4px; + } } diff --git a/docs/brand/README.md b/docs/brand/README.md new file mode 100644 index 0000000..26d63e6 --- /dev/null +++ b/docs/brand/README.md @@ -0,0 +1,120 @@ +# The CHGame look + +The CHGame logo + +How CHGame presents itself outside the device: the repository's +[README](../../README.md), the [API reference](https://bateske.github.io/CHGame/) +and anything else that sits beside them. The aim is **punchy and +friendly**: a style called neobrutalism (flat colours, thick ink outlines, +hard shadows), in pastel colours, with pixel lettering like the console's +own. + +The games keep their own look (green felt and gold, [cover-art.md](../cover-art.md)); +this one is for the pages around them. + +## The pieces + +| File | What it is | Made by | +|---|---|---| +| `logo.png` | the logo: CH on a yellow badge (153 x 143, transparent corners) | the owner; never redrawn | +| `../../tools/brand/logo-pixel.png` | the pixel CHGAME logo, the console's own (73 x 18): ink letters, a magenta shadow | the owner; never redrawn | +| `banner.gif` | the README's banner: both logos, what it is, and the handheld paging through the game menu's box art | `tools/brand/make.py banner` | +| `btn-*.svg` | the README's link buttons (website, get started, API docs, shop, forum) | `tools/brand/make.py buttons` | +| `warning.svg` | the README's caution: in development, Rev0 hardware | `tools/brand/make.py warning` | +| `xoxo.gif` | the special thanks' easter egg: XOXO in CHTicTacToe's spinning pieces | `tools/brand/make.py xoxo` | +| `wordmark.svg` | the pixel logo for the API reference's header, its magenta held in yellow (`docs/api/Doxyfile` copies it there) | `tools/brand/make.py wordmark` | +| `palette.svg` | the palette below | `tools/brand/make.py swatches` | +| `social.png` | GitHub's link preview, 1280 x 640: the banner's opening picture, the logo in its own magenta (uploaded by hand under Settings > Social preview) | `tools/brand/make.py social` | +| `board.webp` | the Rev0 board, from [chgame.website](https://chgame.website) | a photo | + +Everything but the logo and the photo is generated: change +`tools/brand/make.py` (or the art it reads), then run + +```bash +python tools/brand/make.py # all of it; or banner, buttons, warning, xoxo, wordmark, swatches, social +python tools/brand/make.py banner --still out/banner.png # one frame of the banner, to look at +``` + +The banner shows the casino card's cover and then each program's +`docs/cart.png`, so it follows the box art: after a cover changes +(`chgame boxart`), run `make.py banner` again. Each press of DOWN brings +the next picture up from the bottom in three steps, as the visual menu +does, and the pixel logo's magenta turns through the rainbow as it does on +the device. About 1.4 MB: the rainbow only moves while the screen is still, +so a frame stores either the logo or the screen, never a rectangle round +both. + +## The palette + +![The CHGame palette](palette.svg) + +| Name | Hex | Use | +|---|---|---| +| Ink | `#252628` | text, every outline, every shadow (the logo's own) | +| Yellow | `#F9D84A` | the special colour (the logo's own), kept for a few big highlights: the logo, the pixel logo's shadow in the API reference, the first button, the warning's hazard tape, a highlighted menu or tree item | +| Cream | `#FFFDF6` | cards, code, anything raised | +| Paper | `#FBFAF5` | the page behind them | +| Mint | `#BDE2C7` | what is selected or active, success | +| Sage (teal) | `#B7DACC` | **the main accent**: the API reference's menu bar, table and member headers, link underlines | +| Lavender | `#C8B6F3` | the topic a page belongs to, a third accent | +| Coral | `#F4A28C` | warnings in the API reference (sparingly, as a pale tint) | + +Mint, sage and lavender come from the companion app's shapes (the circle, +the rectangle and the triangle). Pale tints of yellow, coral, lavender and mint (`#FFF6CC`, +`#FCE3DB`, `#EFE9FD`, `#E8F5EC`) fill the API reference's note, warning, +see-also and return boxes. + +## The rules + +- **Outlines are ink**, 2-3 px on the web (`2px solid #252628`), 4-6 px in + the banner. +- **Shadows are hard**: ink, offset down and right, never blurred + (`box-shadow: 4px 4px 0 #252628`; 3 px for small things such as pills). +- **Corners are round but small**: 8 px for cards and boxes, fully round + for pills and tags. +- **Colour is flat.** No gradients, no glows, no transparency. +- **Pressed means flat**: a button that is pressed moves into its shadow, + as the banner's D-pad does. On the web, hovering only highlights (teal; + yellow in the menus and the tree). +- **Paper is dotted**: `#E6E2D3` dots on Paper, 2 px squares every 24 px + in the banner and the warning; the API reference's header has round + ones every 20 px. +- **Teal is the everyday accent, yellow the special one.** Most colour on a + page is teal; yellow marks only the logo and the one thing that is + highlighted right now. +- **Text on a colour is ink**, never white. + +## Type + +| Where | Face | +|---|---| +| The name | the pixel CHGAME logo (`tools/brand/logo-pixel.png`), at 3x and 4x: its magenta is the rainbow where things move (the banner) and yellow where they don't (the API reference) | +| The banner and the buttons | **Bitrimus** (`tools/art/fonts/about-bitrimus.json`), at 2x and 3x | +| The API reference | **Space Grotesk** for headings, **DM Sans** for text, **JetBrains Mono** for code (Google Fonts, loaded by `docs/api/extra.css`) | + +Pixel lettering is always drawn at a whole multiple of its size, never +smoothed. On GitHub the README's text is GitHub's own, so the pictures +carry the type. + +## Where it is used + +- **The README** opens with `banner.gif` (linked to chgame.website), the + buttons, shields.io badges in the palette + (`style=for-the-badge&labelColor=252628&color=`), + then `warning.svg`. `xoxo.gif` closes it, under *Special thanks*. +- **The API reference** (`docs/api/extra.css`) maps the palette onto + Doxygen's variables and gives its members, tables, notes and menus + outlines and hard shadows. It is light only (`HTML_COLORSTYLE = LIGHT`). + On a phone the tree is hidden and the menu button navigates. + +## Credits + +- Both logos: the project's own. +- **Bitrimus** by ggbot, CC0 ([ggbot.itch.io/bitrimus-font](https://ggbot.itch.io/bitrimus-font)). +- **Space Grotesk**, **DM Sans** and **JetBrains Mono**: SIL Open Font + License, served by Google Fonts. +- The board photo: [chgame.website](https://chgame.website). +- The X and O: CHTicTacToe's pieces (its `tools/art/pieces/`). +- The box art on the banner's screen: each program's `tools/cart.py` + ([cover-art.md](../cover-art.md), whose credits list the fonts of its + titles). diff --git a/docs/brand/banner.gif b/docs/brand/banner.gif new file mode 100644 index 0000000..393767b Binary files /dev/null and b/docs/brand/banner.gif differ diff --git a/docs/brand/board.webp b/docs/brand/board.webp new file mode 100644 index 0000000..ebbd7eb Binary files /dev/null and b/docs/brand/board.webp differ diff --git a/docs/brand/btn-docs.svg b/docs/brand/btn-docs.svg new file mode 100644 index 0000000..67512f1 --- /dev/null +++ b/docs/brand/btn-docs.svg @@ -0,0 +1 @@ +API docs diff --git a/docs/brand/btn-forum.svg b/docs/brand/btn-forum.svg new file mode 100644 index 0000000..15dfb65 --- /dev/null +++ b/docs/brand/btn-forum.svg @@ -0,0 +1 @@ +Forum diff --git a/docs/brand/btn-shop.svg b/docs/brand/btn-shop.svg new file mode 100644 index 0000000..53ca523 --- /dev/null +++ b/docs/brand/btn-shop.svg @@ -0,0 +1 @@ +Buy one diff --git a/docs/brand/btn-start.svg b/docs/brand/btn-start.svg new file mode 100644 index 0000000..5ef9ba1 --- /dev/null +++ b/docs/brand/btn-start.svg @@ -0,0 +1 @@ +Get started diff --git a/docs/brand/btn-website.svg b/docs/brand/btn-website.svg new file mode 100644 index 0000000..4c8fd64 --- /dev/null +++ b/docs/brand/btn-website.svg @@ -0,0 +1 @@ +chgame.website diff --git a/docs/api/logo.png b/docs/brand/logo.png similarity index 100% rename from docs/api/logo.png rename to docs/brand/logo.png diff --git a/docs/brand/palette.svg b/docs/brand/palette.svg new file mode 100644 index 0000000..2ab67b8 --- /dev/null +++ b/docs/brand/palette.svg @@ -0,0 +1 @@ +The CHGame palette diff --git a/docs/brand/social.png b/docs/brand/social.png new file mode 100644 index 0000000..cb1eec7 Binary files /dev/null and b/docs/brand/social.png differ diff --git a/docs/brand/warning.svg b/docs/brand/warning.svg new file mode 100644 index 0000000..762f331 --- /dev/null +++ b/docs/brand/warning.svg @@ -0,0 +1 @@ +IN DEVELOPMENT: REV0 HARDWARE. CHGame is in active development. This board is Rev0, Rev1 will change the button wiring. diff --git a/docs/brand/wordmark.svg b/docs/brand/wordmark.svg new file mode 100644 index 0000000..3fbc94e --- /dev/null +++ b/docs/brand/wordmark.svg @@ -0,0 +1 @@ +CHGame diff --git a/docs/brand/xoxo.gif b/docs/brand/xoxo.gif new file mode 100644 index 0000000..3259e19 Binary files /dev/null and b/docs/brand/xoxo.gif differ diff --git a/docs/getting-started.md b/docs/getting-started.md index 1789a70..9ab9a14 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -105,7 +105,7 @@ It uses 10.8 KB of flash and 11.9 KB of RAM, most of the RAM being the chgame --sketch Hello upload ``` - **Arduino IDE:** - 1. Install the CHGame board package, 0.3.0 or later ([README](../README.md#installing)). + 1. Install the CHGame board package, 0.3.0 or later ([README](../README.md#1-install-the-board-package)). The CHGame and CHGfx libraries come with it. (With 0.2.4, copy `platform/board/arduino/CHGame/libraries/CHGfx` and `.../CHGame` into your sketchbook's `libraries/`, and choose *Tools > Optimize > diff --git a/docs/roadmap.md b/docs/roadmap.md index c4b3ec7..9444295 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: from this repository to one board package -The goal ([README](../README.md#what-this-repository-is-for)): someone +The goal ([README](../README.md#why-one-repository)): someone installs **CHGame** in the Arduino Boards Manager and has everything: the core, the menu bootloader, one `CHGame.h` library, the casino games as examples. An update delivers all of it together. The PC tools live in the @@ -33,7 +33,7 @@ This page records what is in place and what each step involved. | | Where | State | |---|---|---| -| Arduino core, variant, linker scripts, Tools menus | `platform/board/arduino/CHGame` | 0.3.0, built and tested, not yet published (0.2.4 is the published one) | +| Arduino core, variant, linker scripts, Tools menus | `platform/board/arduino/CHGame` | 0.3.0, released 2026-10-07 | | Bootloader with the SD game menu | `platform/bootloader` | menu v2 (2026-10-03), Rainbow and Static (White until that evening, when colour 15 became the picture's magenta as painted; that build has not run on a board): the PC suite, and installed and checked on a board the same day (`test/hil/RESULTS-2026-10-03.md`; the first menu on 2026-10-01) | | The visual menu: the same bootloader, one picture at a time, no text ([visual-menu.md](visual-menu.md)) | `platform/bootloader` (`--ui=visual`), `tools/artkit`, each example's `tools/cart.py` | 2026-10-03: Rainbow and Static, in Burn Bootloader; the PC suite passes on test cards and the real casino card (which is now in genre folders, with box art for all 22 programs, repainted on 2026-10-04 with the menu's own pictures: [cover-art.md](cover-art.md)). Not yet run on a board (`platform/bootloader/HARDWARE.md`, "The visual menu") | | The `.chgame` format and the SD card's layout | `spec/`, `tools/chcart` | version 1 and layout 2 (with the visual menu's pictures: `menu.cover`, `menu.about`, folder covers, `cartImage` in each CHG file); reference implementation, 24 unit tests, 27 conformance fixtures; the repository's games and release use it | @@ -54,20 +54,18 @@ builds the uploader for the five hosts, the platform archive and the Boards Manager index, and publishes them with `gh` (`platform/board/docs/building.md`). `python tools/release/release.py --dry-run` makes the whole set in `out/dist/`. `platform.txt` says 0.3.0 -and the changelog has its section, headed "(not yet released)". +and the changelog has its section. Also done the same day: `stage.py` builds the release as `0.3.0-local` with localhost URLs, `serve.py` serves it to the Arduino IDE, and `acceptance.py` is the new-user test, which `release.py` now runs before it -publishes anything; it also packs the release's SD card zip. What is left -is the release itself: date the changelog heading, run it. - -- The package index then lives at - `https://github.com/bateske/CHGame/releases/latest/download/package_chgame_index.json`. -- Every place that gives the old URL changes with it: the root README's - *Installing* section, `CLAUDE.md`, `platform/README.md` and - `platform/board/arduino/CHGame/libraries/CHGame/examples/Apps/CHSDtoUSB/README.md`. The games' READMEs link to the root - README instead of repeating it. +publishes anything; it also packs the release's SD card zip. **0.3.0 was +released on 2026-10-07** (`v0.3.0`, with the casino cart and its SD card +zip). What is left: + +- (Done) The package index lives at + `https://github.com/bateske/CHGame/releases/latest/download/package_chgame_index.json`, + and the README, `CLAUDE.md` and `platform/README.md` give that URL only. - Decide how people on the old URL find the new one. An index is not redirected by itself; a last release on the old URL that says so is one way. diff --git a/platform/README.md b/platform/README.md index 1f3fc6c..4283816 100644 --- a/platform/README.md +++ b/platform/README.md @@ -7,7 +7,7 @@ here. The table's "Came from" column is history, not something to sync with. | Folder | What | Version | Came from | Licence | |---|---|---|---|---| -| `board/arduino/CHGame/` | The CHGame Arduino board package: core, variant, linker scripts, bootloader binary, `boards.txt` / `platform.txt` | 0.3.0 (not yet published; 0.2.4 is) | CH32SerialBoot tag `v0.2.4` (5de3006), folder `arduino/CHGame` | MIT (`board/LICENSE`, `board/THIRD-PARTY.md`) | +| `board/arduino/CHGame/` | The CHGame Arduino board package: core, variant, linker scripts, bootloader binary, `boards.txt` / `platform.txt` | 0.3.0 (released 2026-10-07) | CH32SerialBoot tag `v0.2.4` (5de3006), folder `arduino/CHGame` | MIT (`board/LICENSE`, `board/THIRD-PARTY.md`) | | `board/docs/` | The board's docs: hardware pin map, flash/RAM map, boot flow, upload protocol, recovery, CH32X035 gotchas, building the bootloader | 0.2.4 | same tag, folder `docs` | MIT | | `board/arduino/CHGame/libraries/CHGame/` | The CHGame library: `CHGame.h`, the one include of a sketch (buttons, pacing, palette, drawing, sound, saving, the debug protocol) | 0.1.0 | built here (2026-10-02) from the code the twenty games shared | Apache-2.0 (`LICENSE`, `NOTICE`) | | `board/arduino/CHGame/libraries/CHGfx/` | The graphics library | 1.3.2 | CHGfx tag `1.3.0` (838bbb0) | MIT (+ font notices in its `LICENSE`) | @@ -25,10 +25,10 @@ CHGfx's `LICENSE`. Everything: the core, the menu bootloader, the `CHGame` library (with CHGfx and CHSd beside it) and the casino games as its examples, so that installing or updating "CHGame" in the Boards Manager brings all of it at -once. That is 0.3.0, built from here and tested as a new user would get it -(`python tools/release/stage.py`), not yet published; -[../docs/roadmap.md](../docs/roadmap.md) has what is left. Until it is -published, the pieces are used from this repository as described below, +once. That is 0.3.0, released on 2026-10-07, built from here and tested +as a new user would get it (`python tools/release/stage.py`); +[../docs/roadmap.md](../docs/roadmap.md) has what is left. Between +releases, the pieces are used from this repository as described below, or from a staged install ([board/docs/trying-a-release.md](board/docs/trying-a-release.md)). @@ -44,11 +44,10 @@ arduino-cli core update-index arduino-cli core install CHGame:ch32v ``` -Until 0.3.0, the first release cut from this repository, is published, 0.2.4 -is still served from -`https://github.com/bateske/CH32SerialBoot/releases/latest/download/package_chgame_index.json` -(install `CHGame:ch32v@0.2.4` from there meanwhile, or stage 0.3.0 and -install it from this machine: [board/docs/trying-a-release.md](board/docs/trying-a-release.md)). +0.3.0 (2026-10-07) is the first release cut from this repository. The +CH32SerialBoot repository's URL offers 0.2.4 only. To try the next version +before it is published, stage it and install it from this machine: +[board/docs/trying-a-release.md](board/docs/trying-a-release.md). **What the copy here is:** - the source of the next release. A change made here does not reach a build @@ -73,7 +72,7 @@ install it from this machine: [board/docs/trying-a-release.md](board/docs/trying behaviour and memory report are described in [../README.md](../README.md) and [../CLAUDE.md](../CLAUDE.md). -**Fixed here, not yet released:** +**Fixed here, released in 0.3.0:** - **Linux builds failed.** `cores/arduino/ch32/lib/ch32yyxx.h` included `core_riscv_cH32yyxx.h`; the file is `core_riscv_ch32yyxx.h`, so it only resolved on case-insensitive file systems (Windows, default macOS). Fixed @@ -83,7 +82,7 @@ install it from this machine: [board/docs/trying-a-release.md](board/docs/trying 0x2000 (`link_chgame_app.ld`, `chgame_map.h`), and one in `boards.txt` naming a `tools/gen_ld.py` that does not exist (2026-10-02). -**Changed here, not yet released** (2026-10-02; all 20 games built from the +**Changed here, released in 0.3.0** (2026-10-02; all 20 games built from the staged package, sizes unchanged): - **Smallest + LTO is the default** *Optimize* option. FQBNs without `opt=` now build with `-flto`; with plain `-Os` the larger games do not fit. diff --git a/tools/README.md b/tools/README.md index ef36d57..d1212b2 100644 --- a/tools/README.md +++ b/tools/README.md @@ -48,6 +48,7 @@ about itself is in its `tools/game.py` (below), its `tools/chsim/chdrive.py` | `art/menu/`, `menuart.py`, `art/chglogo.py` | The menu's default pictures, every card's (`spec/assets/`): one recipe each in `art/menu/` (the cover, no picture and the folder share `sdscene.py`, the text menu's picture `listbg.py` with the casino's), drawn by `python tools/menuart.py`; `chglogo.py` is the CHGAME logo and its dressings. | | `sdcard/covers.py`, `sdcard/art/src/` | The casino card's own pictures: its cover, its seven folders (`folderkit.py`) and its text menu's picture, from the recipes in `sdcard/art/src/` to `sdcard/art/*.png` and `sdcard/menu.png`. | | `artsheet.py` | Every picture the menus show on one labelled sheet (`out/art-review.png`); `--compare OUT NAME ...` before (git HEAD) and after; `--gallery` the docs' two galleries, `docs/cover-art.png` and `docs/cover-art-defaults.png`. | +| `brand/make.py`, `brand/logo-pixel.png` | The brand's pictures in `docs/brand/` ([its README](../docs/brand/README.md): the palette and the rules): the root README's animated banner (the handheld paging through the casino card's cover and every program's `docs/cart.png`, the pixel logo turning through the rainbow), its link buttons, its Rev0 caution and the XOXO of its special thanks, and the pixel logo the API reference's header shows. `logo-pixel.png` is the owner's CHGAME logo; the lettering is Bitrimus (`art/fonts/about-bitrimus.json`). | | `pixkit.py` | A Python rendering of the library's drawing primitives, palette and sprites (checked against `chgame/Draw.cpp`), for mock-ups of screens before they are coded: CHRoulette's and CHWordWheel's `mockup.py` and previews. | | `music/composer.py` | The Playtune composer: note names and voices to the bytes the library's `audio::music` plays, and the writer of a game's `src/audio/Music.cpp`. The three `make_music.py` (CHBlackjack, CHRoulette, CHWordWheel) keep only their songs. | | `fonts/serif.py`, `fonts/font_preview.py` | The anti-aliased serif rasteriser behind CHCrossword's and CHWords' `tilefont.py` and CHDominoes' `aafont.py`, and the display-font preview sheet behind CHBackgammon's and CHFour's `font_preview.py`. | diff --git a/tools/brand/logo-pixel.png b/tools/brand/logo-pixel.png new file mode 100644 index 0000000..91aec59 Binary files /dev/null and b/tools/brand/logo-pixel.png differ diff --git a/tools/brand/make.py b/tools/brand/make.py new file mode 100644 index 0000000..95ed072 --- /dev/null +++ b/tools/brand/make.py @@ -0,0 +1,739 @@ +"""The CHGame brand's pictures, written to docs/brand/. The look and the +rules are docs/brand/README.md. + + python tools/brand/make.py # everything + python tools/brand/make.py banner # docs/brand/banner.gif, the README's banner + python tools/brand/make.py buttons # docs/brand/btn-*.svg, the README's link buttons + python tools/brand/make.py warning # docs/brand/warning.svg, the README's Rev0 caution + python tools/brand/make.py xoxo # docs/brand/xoxo.gif, the special thanks' X and O + python tools/brand/make.py wordmark # docs/brand/wordmark.svg (the API reference's header) + python tools/brand/make.py swatches # docs/brand/palette.svg + python tools/brand/make.py social # docs/brand/social.png, GitHub's link preview (upload it by hand) + python tools/brand/make.py banner --still out/banner.png # one frame, to look at + +Neobrutalism in the house palette: flat colours, ink outlines, hard +shadows offset down and right, nothing blurred. The lettering is pixel +art, like the console's own: the pixel CHGAME logo is the owner's +(logo-pixel.png: ink, and magenta where the device turns it through the +rainbow), everything else is Bitrimus (tools/art/fonts/about-bitrimus.json, +CC0). The badge logo is the owner's too (docs/brand/logo.png); both are +drawn as they are, never redrawn. + +The banner is the handheld with the visual menu on its screen: the casino +card's splash, then the games' box art (each program's docs/cart.png) +rising from the bottom at each press of DOWN, while the logo's magenta +turns through the rainbow. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys + +import numpy as np +from PIL import Image, ImageDraw + +ROOT = pathlib.Path(__file__).resolve().parents[2] +HERE = pathlib.Path(__file__).resolve().parent +OUT = ROOT / "docs" / "brand" +EXAMPLES = ROOT / "platform/board/arduino/CHGame/libraries/CHGame/examples" + +# The palette (docs/brand/README.md). INK and YELLOW are the logo's own. +INK = "#252628" +YELLOW = "#F9D84A" +CREAM = "#FFFDF6" +PAPER = "#FBFAF5" +MINT = "#BDE2C7" +SAGE = "#B7DACC" +LAVENDER = "#C8B6F3" +CORAL = "#F4A28C" +DOTS = "#E6E2D3" +PALETTE = [("Ink", INK), ("Yellow", YELLOW), ("Cream", CREAM), ("Paper", PAPER), + ("Mint", MINT), ("Sage", SAGE), ("Lavender", LAVENDER), ("Coral", CORAL)] + + +def rgb(h): + h = h.lstrip("#") + return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) + + +# ---------------------------------------------------------------- lettering + +class Face: + """A pixel face from a fontscout JSON, set on a fixed line (so every + string shares one baseline): top is the face's highest pixel, the + mask is `line` rows tall.""" + + def __init__(self, path, gap=0): + d = json.loads(pathlib.Path(path).read_text(encoding="utf-8")) + self.g = d["glyphs"] + self.space = d.get("space") or 3 + self.gap = gap + ys = [(g[3], g[3] + g[1]) for g in self.g.values() if g[0] and g[1]] + self.top = min(y for y, _ in ys) + self.line = max(y for _, y in ys) - self.top + + def mask(self, s): + cols = [] + for ch in s: + g = self.g.get(ch) + if g is None or not g[0]: + adv = g[4] if g and g[4] else self.space + cols.append((None, adv + self.gap)) + continue + cols.append((g, g[4] + self.gap)) + w = sum(a for _, a in cols) - self.gap + m = np.zeros((self.line, max(1, w)), dtype=bool) + pen = 0 + for g, adv in cols: + if g is not None: + gw, gh, gx, gy, _, rows = g + for j, r in enumerate(rows): + for i, c in enumerate(r): + if c in "#1": + m[gy - self.top + j, pen + gx + i] = True + pen += adv + return m + + +def art(s): + """An inline icon: rows of '#' and '.'.""" + rows = [r.strip() for r in s.strip().splitlines()] + return np.array([[c == "#" for c in r] for r in rows]) + + +BITRIMUS = ROOT / "tools/art/fonts/about-bitrimus.json" + +ICONS = { + "globe": art(""" + ...#####... + ..#..#..#.. + .#..#.#..#. + #...#.#...# + ########### + #...#.#...# + ########### + #...#.#...# + .#..#.#..#. + ..#..#..#.. + ...#####... + """), + "page": art(""" + ######... + #....##.. + #....#.#. + #....#### + #.#####.# + #.......# + #.#####.# + #.......# + #.####..# + #.......# + ######### + """), + "play": art(""" + ##....... + ####..... + ######... + ########. + ######### + ########. + ######... + ####..... + ##....... + """), + "pad": art(""" + ..#########.. + .#.........#. + #..#.....#..# + #.###...#.#.# + #..#.....#..# + #....###....# + .####...####. + """), + "basket": art(""" + ##......... + .#########. + .#########. + .########.. + ..#######.. + ..#........ + ..########. + ...##..##.. + ...##..##.. + """), + "chat": art(""" + .#########. + #.........# + #..#.#.#..# + #.........# + .##.######. + ..##....... + ..#........ + """), +} + + +def scaled(m, s): + return np.kron(m, np.ones((s, s), dtype=bool)) + + +# ---------------------------------------------------------------- drawing + +SS = 4 # supersampling for the smooth shapes + + +class Canvas: + """An RGB picture with neobrutalist shapes (supersampled, then reduced) + and pixel lettering (exact).""" + + def __init__(self, w, h, bg): + self.w, self.h = w, h + self.im = Image.new("RGB", (w, h), rgb(bg)) + + def shapes(self, paint): + """paint(draw, k) draws at k times the size on a transparent layer.""" + big = Image.new("RGBA", (self.w * SS, self.h * SS), (0, 0, 0, 0)) + paint(ImageDraw.Draw(big), SS) + small = big.resize((self.w, self.h), Image.Resampling.BOX) + self.im.paste(small, (0, 0), small) + + def mask(self, m, x, y, colour): + layer = Image.fromarray((m * 255).astype(np.uint8)) + self.im.paste(rgb(colour), (int(x), int(y)), layer) + + def image(self, im, x, y): + if im.mode == "RGBA": + self.im.paste(im, (int(x), int(y)), im) + else: + self.im.paste(im.convert("RGB"), (int(x), int(y))) + + +def card(d, k, x, y, w, h, fill, r=16, bw=5, shadow=10, border=INK): + """A neobrutalist card: hard shadow, ink border, flat fill.""" + if shadow: + d.rounded_rectangle([k * (x + shadow), k * (y + shadow), k * (x + w + shadow) - 1, + k * (y + h + shadow) - 1], radius=k * r, fill=rgb(INK)) + d.rounded_rectangle([k * x, k * y, k * (x + w) - 1, k * (y + h) - 1], radius=k * r, fill=rgb(border)) + if fill: + d.rounded_rectangle([k * (x + bw), k * (y + bw), k * (x + w - bw) - 1, k * (y + h - bw) - 1], + radius=k * max(0, r - bw), fill=rgb(fill)) + + +def disc(d, k, cx, cy, rad, fill, bw=5, shadow=8): + if shadow: + d.ellipse([k * (cx - rad + shadow), k * (cy - rad + shadow), k * (cx + rad + shadow) - 1, + k * (cy + rad + shadow) - 1], fill=rgb(INK)) + d.ellipse([k * (cx - rad), k * (cy - rad), k * (cx + rad) - 1, k * (cy + rad) - 1], fill=rgb(INK)) + d.ellipse([k * (cx - rad + bw), k * (cy - rad + bw), k * (cx + rad - bw) - 1, k * (cy + rad - bw) - 1], + fill=rgb(fill)) + + +# ---------------------------------------------------------------- the pixel logo + +PIXEL_LOGO = HERE / "logo-pixel.png" # the owner's: ink letters, a magenta shadow + + +def pixel_logo(): + """(ink, accent) masks of the pixel CHGAME logo: black is the ink, magenta + the colour that turns through the rainbow on the device.""" + a = np.array(Image.open(PIXEL_LOGO).convert("RGB")).astype(int) + ink = (a.sum(axis=2) < 128) + accent = (a[:, :, 0] > 200) & (a[:, :, 1] < 80) & (a[:, :, 2] > 200) + return ink, accent + + +def rainbow(t): + """The device's rainbow at full value: a hue from 0 to 1.""" + h = (t % 1.0) * 6 + i, f = int(h), h - int(h) + v = [(1, f, 0), (1 - f, 1, 0), (0, 1, f), (0, 1 - f, 1), (f, 0, 1), (1, 0, 1 - f)][i % 6] + return tuple(int(round(c * 255)) for c in v) + + +# ---------------------------------------------------------------- banner + +W, H = 1280, 480 +LOGO_S = 4 # the pixel logo's scale on the banner + +# The covers on the handheld's screen, in the order DOWN shows them. +COVERS = ["CHBlackjack", "CHChess", "CHSlots", "CHRoulette", "CHMahjong", "CHPoker", "CHCraps", + "CHWords", "CHBackgammon", "CHBoardwalk", "CHYacht", "CHSnakes", "CHDominoes", + "CHCrossword", "CHBingo", "CHFour", "CHCheckers", "CHSolitaire", "CHTicTacToe", + "CHWordWheel", "CHStlView"] + + +def cover_paths(): + out = [ROOT / "tools/sdcard/art/cover.png"] # the power-on splash + for name in COVERS: + hits = list(EXAMPLES.glob(f"*/{name}/docs/cart.png")) + if not hits: + sys.exit(f"no box art for {name}") + out.append(hits[0]) + return out + + +# The handheld: CHGame Rev0's layout (the expansion port's holes along the +# top, over the screen; a D-pad left, B and A right, two small buttons under +# the screen), in the brand's colours, every button white. +BX, BY, BW, BH = 640, 50, 584, 380 +SCR = 256 # 128 x 128 at 2x +BEZ = 14 +SX = BX + (BW - SCR) // 2 +SY = BY + 36 + BEZ +DPAD = (BX + (SX - BEZ - BX) // 2 + 2, SY + 112) # centre +KEY = 34 # D-pad key size +RIGHT_X = SX + SCR + BEZ + (BX + BW - SX - SCR - BEZ) // 2 +B_BTN = (RIGHT_X + 22, SY + 70) +A_BTN = (RIGHT_X - 20, SY + 140) +WHITE = "#FFFFFF" +HOLES, HOLE, HOLE_GAP = 8, 10, 16 + + +def dpad_key(d, k, cx, cy, pressed): + off = 4 if pressed else 0 + card(d, k, cx - KEY // 2 + off, cy - KEY // 2 + off, KEY, KEY, WHITE, r=8, bw=4, + shadow=0 if pressed else 4) + + +def draw_board(c: Canvas, pressed=None, led=False): + def paint(d, k): + card(d, k, BX, BY, BW, BH, YELLOW, r=48, bw=6, shadow=14) + # the screen's bezel + card(d, k, SX - BEZ, SY - BEZ, SCR + 2 * BEZ, SCR + 2 * BEZ, INK, r=12, bw=0, shadow=0) + # D-pad + cx, cy = DPAD + g = KEY + 2 + for name, (dx, dy) in (("up", (0, -g)), ("left", (-g, 0)), ("right", (g, 0)), ("down", (0, g))): + dpad_key(d, k, cx + dx, cy + dy, pressed == name) + # B and A + for name, (bx, by) in (("b", B_BTN), ("a", A_BTN)): + off = 4 if pressed == name else 0 + disc(d, k, bx + off, by + off, 25, WHITE, bw=5, shadow=0 if pressed == name else 5) + # the two small buttons under the screen + for i in (-1, 1): + card(d, k, BX + BW // 2 + i * 50 - 22, SY + SCR + BEZ + 14, 44, 18, WHITE, r=9, bw=4, shadow=3) + # status LED, and the expansion port's holes, centred over the screen + disc(d, k, RIGHT_X + 22, SY + 4, 9, MINT if led else CREAM, bw=3, shadow=0) + hx = SX + (SCR - (HOLES * HOLE_GAP - (HOLE_GAP - HOLE))) // 2 + for i in range(HOLES): + card(d, k, hx + i * HOLE_GAP, BY + 16, HOLE, HOLE, CREAM, r=2, bw=2, shadow=0) + c.shapes(paint) + + +LX, LY = 60, 52 # the left column's corner +COL_W = 560 # the left column's width + + +def draw_static(c: Canvas): + """Everything but the board, the screen and the logo's rainbow; returns + the rainbow's mask (full size).""" + # dotted paper + dots = np.zeros((H, W), dtype=bool) + for dy in (0, 1): + for dx in (0, 1): + dots[12 + dy::24, 12 + dx::24] = True + c.mask(dots, 0, 0, DOTS) + + # the badge, with a hard shadow + logo = Image.open(OUT / "logo.png").convert("RGBA") + a = np.array(logo)[:, :, 3] > 128 + c.mask(a, LX + 10, LY + 10, INK) + c.image(logo, LX, LY) + + # the pixel logo beside it: ink now, the rainbow per frame + ink, accent = (scaled(m, LOGO_S) for m in pixel_logo()) + wx = LX + logo.width + 26 + wy = LY + (logo.height - ink.shape[0]) // 2 + c.mask(ink, wx, wy, INK) + turning = np.zeros((H, W), dtype=bool) + turning[wy:wy + accent.shape[0], wx:wx + accent.shape[1]] = accent + + # what it is + face = Face(BITRIMUS) + y = LY + logo.height + 34 + for line in ("The lowest-cost game system.", "Program it in Arduino."): + m = scaled(face.mask(line), 3) + c.mask(m, LX, y, INK) + y += m.shape[0] + 10 + + # pills, in rows that fit the column + ph, gap = 50, 16 + pills, x, py = [], LX, y + 18 + for label, fill in (("RISC-V", LAVENDER), ("128x128", MINT), ("20 GAMES INCLUDED", SAGE)): + m = scaled(face.mask(label), 3) + w = m.shape[1] + 40 + if x > LX and x + w > LX + COL_W: + x, py = LX, py + ph + gap + pills.append((x, py, w, m, fill)) + x += w + gap + 2 + + def pill_shapes(d, k): + for x, py, w, m, fill in pills: + card(d, k, x, py, w, ph, fill, r=ph // 2, bw=4, shadow=5) + c.shapes(pill_shapes) + for x, py, w, m, fill in pills: + c.mask(m, x + 20, py + (ph - 21) // 2 - 3, INK) + + # a frame round it all + def frame(d, k): + d.rectangle([0, 0, k * W - 1, k * H - 1], outline=rgb(INK), width=k * 6) + c.shapes(frame) + return turning + + +def cover(path): + im = Image.open(path).convert("RGB") + if im.size != (128, 128): + sys.exit(f"{path}: {im.size}, not 128x128") + return im.resize((SCR, SCR), Image.Resampling.NEAREST) + + +# Timing (ms). DOWN brings the next game's box art up from the bottom, as the +# visual menu does (platform/bootloader/src/lcd.c, lcd_slide): the new picture +# rises over the old one in steps. +SPLASH_MS, HOLD_MS, STEP_MS = 2400, 1200, 60 +SLIDE = (32, 64, 96) # rows of the new picture shown (of 128) +TICK_MS = 150 # the rainbow moves this often +TURN_MS = 3800 # about one turn of the wheel, as on the device + + +def banner_frames(): + """[(RGB image, ms)]: the splash, then each cover rising after a press of + DOWN; the logo's magenta turns through the rainbow all the while.""" + base = Canvas(W, H, PAPER) + turning = draw_static(base) + states = {} + for pressed in (False, True): + c = Canvas(W, H, PAPER) + c.im = base.im.copy() + draw_board(c, pressed="down" if pressed else None, led=pressed) + states[pressed] = c.im + covers = [cover(p) for p in cover_paths()] + + # the timeline: (screen picture, DOWN held, ms) + steps = [] + + def hold(pic, ms): + while ms > 0: + steps.append((pic, False, min(TICK_MS, ms))) + ms -= TICK_MS + + hold(covers[0], SPLASH_MS) + order = list(range(1, len(covers))) + [0] + prev = covers[0] + for i in order: + new = covers[i] + for rows in SLIDE: + pic = prev.copy() + pic.paste(new.crop((0, 0, SCR, rows * 2)), (0, SCR - rows * 2)) + steps.append((pic, True, STEP_MS)) + steps.append((new, True, STEP_MS)) + steps.append((new, False, STEP_MS)) + hold(new, HOLD_MS - 2 * STEP_MS) + prev = new + + # The rainbow moves only while the screen is still: a frame that changed + # both the logo and the screen would be stored as one rectangle round + # both, text and all. + still = [i > 0 and steps[i][0] is steps[i - 1][0] and steps[i][1] == steps[i - 1][1] + for i in range(len(steps))] + total = sum(ms for (_, _, ms), s in zip(steps, still) if s) + turn = total / max(1, round(total / TURN_MS)) # a whole number of turns a loop + mask = Image.fromarray((turning * 255).astype(np.uint8)) + frames, t = [], 0 + for (pic, down, ms), moving in zip(steps, still): + if moving: + t += ms + im = states[down].copy() + im.paste(pic, (SX, SY)) + im.paste(rainbow(t / turn), (0, 0), mask) + frames.append((im, ms)) + return frames + + +def social_png(path): + """GitHub's social preview, 1280 x 640: the banner's opening picture (the + logo in its own magenta) on the same dotted paper, framed again. GitHub takes it only through the + repository's Settings, so it is uploaded by hand.""" + b = Canvas(W, H, PAPER) + turning = draw_static(b) + draw_board(b) + b.image(cover(cover_paths()[0]), SX, SY) + b.mask(turning, 0, 0, "#FF00FF") # the logo's own magenta, still + first = b.im + c = Canvas(W, 640, PAPER) + dots = np.zeros((640, W), dtype=bool) + for dy in (0, 1): + for dx in (0, 1): + dots[12 + dy::24, 12 + dx::24] = True + c.mask(dots, 0, 0, DOTS) + c.im.paste(first.crop((6, 6, W - 6, H - 6)), (6, 80 + 6)) + + def frame(d, k): + d.rectangle([0, 0, k * W - 1, k * 640 - 1], outline=rgb(INK), width=k * 6) + c.shapes(frame) + c.im.save(path, optimize=True) + + +def write_gif(frames, path): + """Each frame quantised on its own (none needs more than 256 colours); + Pillow writes only what changed.""" + ims, durs = [], [] + for im, ms in frames: + n = len(im.getcolors(1 << 16) or []) + if not n or n > 256: + p = im.quantize(256, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE) + else: + p = im.quantize(n, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE) + ims.append(p) + durs.append(ms) + ims[0].save(path, save_all=True, append_images=ims[1:], duration=durs, loop=0, optimize=False, + disposal=1) + + +# ---------------------------------------------------------------- buttons + +BUTTONS = [ + # file, label, icon, fill + ("btn-website", "chgame.website", "globe", YELLOW), + ("btn-start", "Get started", "play", LAVENDER), + ("btn-docs", "API docs", "page", MINT), + ("btn-shop", "Buy one", "basket", SAGE), + ("btn-forum", "Forum", "chat", CREAM), +] + + +def runs_path(m, ox, oy, s): + """An SVG path of a mask: one rectangle per horizontal run.""" + out = [] + for y in range(m.shape[0]): + row = m[y] + x = 0 + while x < len(row): + if row[x]: + x0 = x + while x < len(row) and row[x]: + x += 1 + out.append(f"M{ox + x0 * s} {oy + y * s}h{(x - x0) * s}v{s}h{-(x - x0) * s}z") + else: + x += 1 + return "".join(out) + + +def button_svg(label, icon, fill, s=2, h=44, bw=3, sh=4, r=10): + face = Face(BITRIMUS) + t = face.mask(label) + ic = ICONS[icon] + pad, gap = 12, 8 + w = pad + ic.shape[1] * s + gap + t.shape[1] * s + pad + ty = (h - t.shape[0] * s) // 2 + 1 + iy = (h - ic.shape[0] * s) // 2 + W_, H_ = w + sh, h + sh + return ( + f'{label}' + f'' + f'' + f'\n' + ) + + +def wordmark_svg(s=3): + """The pixel logo for the API reference's header: its ink, and the + device's magenta held still in the brand's yellow.""" + ink, accent = pixel_logo() + W_, H_ = ink.shape[1] * s, ink.shape[0] * s + return (f'CHGame' + f'' + f'\n') + + +# ---------------------------------------------------------------- the warning + +WARNING_TITLE = "IN DEVELOPMENT: REV0 HARDWARE" +WARNING_LINES = ["CHGame is in active development.", + "This board is Rev0, Rev1 will change the button wiring."] + +ICON_WARN = art(""" + .....##..... + ....####.... + ....####.... + ...##..##... + ...##..##... + ..###..###.. + ..###..###.. + .##########. + .####..####. + ############ +""") + + +def warning_svg(w=820): + """The README's caution panel: hazard tape along the top, dotted paper below + (the banner's), + ink outline and hard shadow, pixel lettering.""" + face = Face(BITRIMUS) + title = face.mask(WARNING_TITLE) + lines = [face.mask(t) for t in WARNING_LINES] + tape, pad, bw, sh, r = 18, 22, 3, 6, 12 + icon_s, ts, bs = 5, 3, 2 + icon_w = ICON_WARN.shape[1] * icon_s + tx = pad + icon_w + 24 + ty = tape + 18 + body_y = ty + title.shape[0] * ts + 10 + h = body_y + len(lines) * (lines[0].shape[0] * bs + 6) + 14 + W_, H_ = w + sh, h + sh + dots = (f'' + f'') # the banner's dotted paper + clip = dots + f'' + stripes = "".join(f'' + for x in range(-tape, w + tape, 28)) + body = runs_path(title, tx, ty, ts) + "".join( + runs_path(m, tx, body_y + i * (m.shape[0] * bs + 6), bs) for i, m in enumerate(lines)) + iy = (h + tape - ICON_WARN.shape[0] * icon_s) // 2 + alt = WARNING_TITLE + ". " + " ".join(WARNING_LINES) + return ( + f'{alt}{clip}' + f'' + f'' + f'' + f'{stripes}' + f'' + f'' + f'' + f'' + '\n') + + +# ---------------------------------------------------------------- XOXO + +TICTACTOE = EXAMPLES / "Games/CHTicTacToe/tools/art/pieces" +XOXO_S = 3 # pixel scale +XOXO_MS = 90 # a step of the spin +XOXO_PHASE = [0, 2, 1, 3] # each letter starts at a different step of its turn + + +def xoxo_frames(): + """CHTicTacToe's held X and O, spinning without stop as they do in the + glove (its Iso.cpp spin(): front, a quarter turn, edge on, the quarter + mirrored; each shape repeats every half turn), spelling XOXO, each + letter at its own step of the turn.""" + def load(n): + im = Image.open(TICTACTOE / f"{n}.png").convert("RGBA") + ax, ay = (int(v) for v in (TICTACTOE / f"{n}.anchor").read_text().split()) + return im, ax, ay + sets = {k: [load(f"{k}_l"), load(f"{k}_l1"), load(f"{k}_l2")] for k in "xo"} + + def step(k, i): + im, ax, ay = sets[k][1 if i == 3 else i] + if i == 3: + im, ax = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT), im.width - 1 - ax + return im, ax, ay + + # room for every frame: the highest top and the lowest bottom about the + # pieces' common base line, and a pixel to spare + up = max(ay for v in sets.values() for _, _, ay in v) + down = max(im.height - ay for v in sets.values() for im, _, ay in v) + pitch = 32 + base, h, w = up + 1, up + down + 2, 4 * pitch + 4 + frames = [] + for f in range(4): + c = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + for n, k in enumerate("xoxo"): + im, ax, ay = step(k, (f + XOXO_PHASE[n]) % 4) + c.alpha_composite(im, (pitch // 2 + 2 + n * pitch - ax, base - ay)) + frames.append(c.resize((c.width * XOXO_S, c.height * XOXO_S), Image.Resampling.NEAREST)) + return frames + + +def write_xoxo(path): + """A transparent GIF with one palette for every frame, so the + transparent colour is the same index throughout and never shows.""" + key = (1, 254, 3) + keyed = [] + for f in xoxo_frames(): + bg = Image.new("RGB", f.size, key) + bg.paste(f, (0, 0), f.split()[3].point(lambda a: 255 if a > 127 else 0)) + keyed.append(bg) + strip = Image.new("RGB", (keyed[0].width, keyed[0].height * len(keyed))) + for i, k in enumerate(keyed): + strip.paste(k, (0, i * k.height)) + pal = strip.quantize(255, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE) + ims = [k.quantize(palette=pal, dither=Image.Dither.NONE) for k in keyed] + flat = pal.getpalette()[:768] + t = next(i for i in range(len(flat) // 3) if tuple(flat[3 * i:3 * i + 3]) == key) + ims[0].save(path, save_all=True, append_images=ims[1:], duration=XOXO_MS, loop=0, + disposal=2, transparency=t, optimize=False) + + +def swatches_svg(): + face = Face(BITRIMUS) + cw, ch, gap, s = 132, 104, 18, 2 + W_ = len(PALETTE) * (cw + gap) - gap + 6 + H_ = ch + 6 + parts = [f'The CHGame palette'] + for i, (name, hexv) in enumerate(PALETTE): + x = i * (cw + gap) + parts.append(f'') + parts.append(f'') + fg = CREAM if name == "Ink" else INK + for j, s_ in enumerate((name, hexv.upper())): + m = face.mask(s_) + parts.append(f'') + parts.append("\n") + return "".join(parts) + + +# ---------------------------------------------------------------- main + +def main(): + ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + ap.add_argument("what", nargs="*", default=["banner", "buttons", "wordmark", "warning", "xoxo", "swatches", "social"]) + ap.add_argument("--still", help="write one frame of the banner as a PNG here instead") + a = ap.parse_args() + OUT.mkdir(parents=True, exist_ok=True) + if "banner" in a.what: + frames = banner_frames() + if a.still: + frames[0][0].save(a.still) + print(a.still) + else: + p = OUT / "banner.gif" + write_gif(frames, p) + print(f"{p.relative_to(ROOT).as_posix()}: {len(frames)} frames, {p.stat().st_size:,} B") + if "buttons" in a.what: + for name, label, icon, fill in BUTTONS: + p = OUT / f"{name}.svg" + p.write_text(button_svg(label, icon, fill), encoding="utf-8", newline="\n") + print(f"docs/brand/btn-*.svg: {len(BUTTONS)} buttons") + if "social" in a.what: + p = OUT / "social.png" + social_png(p) + print(f"{p.relative_to(ROOT).as_posix()}: {p.stat().st_size:,} B") + if "xoxo" in a.what: + p = OUT / "xoxo.gif" + write_xoxo(p) + print(f"{p.relative_to(ROOT).as_posix()}: {p.stat().st_size:,} B") + for what, name, svg in (("wordmark", "wordmark.svg", wordmark_svg), ("warning", "warning.svg", warning_svg), + ("swatches", "palette.svg", swatches_svg)): + if what in a.what: + p = OUT / name + p.write_text(svg(), encoding="utf-8", newline="\n") + print(p.relative_to(ROOT).as_posix()) + + +if __name__ == "__main__": + main() diff --git a/tools/release/chgame_upload_tool.json b/tools/release/chgame_upload_tool.json index 0a8ef04..2470f23 100644 --- a/tools/release/chgame_upload_tool.json +++ b/tools/release/chgame_upload_tool.json @@ -6,36 +6,36 @@ "host": "x86_64-mingw32", "url": "https://github.com/bateske/CHGame/releases/download/v0.3.0/chgame-upload-0.2.0-x86_64-mingw32.tar.bz2", "archiveFileName": "chgame-upload-0.2.0-x86_64-mingw32.tar.bz2", - "checksum": "SHA-256:ab8a3c080fb8a7fae448e423fce175dbedc1d1b8abae81e794ff3573ea811d4e", - "size": "1140238" + "checksum": "SHA-256:dce1723df37d74c60cdac9024fd2997b952fab39b462fc388cd8215ea6fae129", + "size": "1142265" }, { "host": "x86_64-pc-linux-gnu", "url": "https://github.com/bateske/CHGame/releases/download/v0.3.0/chgame-upload-0.2.0-x86_64-pc-linux-gnu.tar.bz2", "archiveFileName": "chgame-upload-0.2.0-x86_64-pc-linux-gnu.tar.bz2", - "checksum": "SHA-256:5e1f72d3d761a8620b5d5fc8065642a1772ec4d3970a7a0b46bef3d946f6fb91", - "size": "1051798" + "checksum": "SHA-256:19b5fc39aefde57ec98e92ac88940a5713436952780369115ca1c080a722ac78", + "size": "1055004" }, { "host": "aarch64-linux-gnu", "url": "https://github.com/bateske/CHGame/releases/download/v0.3.0/chgame-upload-0.2.0-aarch64-linux-gnu.tar.bz2", "archiveFileName": "chgame-upload-0.2.0-aarch64-linux-gnu.tar.bz2", - "checksum": "SHA-256:859a97aeef42049dd6e2028e99143fc540963ffa74bdf9dc043d5af01dd27fc4", - "size": "937633" + "checksum": "SHA-256:24d58a70d484a51ff7cd842d92753b7453faa831a548b685a9499a2b4e00757d", + "size": "939900" }, { "host": "x86_64-apple-darwin", "url": "https://github.com/bateske/CHGame/releases/download/v0.3.0/chgame-upload-0.2.0-x86_64-apple-darwin.tar.bz2", "archiveFileName": "chgame-upload-0.2.0-x86_64-apple-darwin.tar.bz2", - "checksum": "SHA-256:f0262bd43e4d5775fba87ca99eda8253ef0e5225b91e5386cac8e76db75e4721", - "size": "1035361" + "checksum": "SHA-256:f736014da70fa6a4718620d0c0239858a942c71a6185c6f356d6a3d5f91db835", + "size": "1037699" }, { "host": "arm64-apple-darwin", "url": "https://github.com/bateske/CHGame/releases/download/v0.3.0/chgame-upload-0.2.0-arm64-apple-darwin.tar.bz2", "archiveFileName": "chgame-upload-0.2.0-arm64-apple-darwin.tar.bz2", - "checksum": "SHA-256:4ea53d197bc0ad0f22aa2c8bf96183582cad9759f0a5bf73b8658b742943da12", - "size": "943335" + "checksum": "SHA-256:4a7ac7f4aeb8001af5710e2834d082c19ca7fff01be855d888f4452be6148a4a", + "size": "945652" } ] }