Skip to content

Commit 64d0d1d

Browse files
committed
Define Proposal 3 artwork contract
1 parent fe548ea commit 64d0d1d

21 files changed

Lines changed: 907 additions & 0 deletions

ui-portable/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Portable Proposal 3 artwork contract
2+
3+
This module owns the portable menu model and the future decoding/composition contract for Proposal
4+
3. Android and desktop adapters will only blit the ARGB result supplied by the portable compositor
5+
and map controller/touch input through the portable geometry; they must not reinterpret, crop,
6+
reflow, or stretch the visual contract.
7+
8+
## Provenance, fixtures, and license
9+
10+
The 14 source images are the original 1672x941 Proposal 3 compositions from
11+
`output/imagegen/proposal-3-menu-tree/`. The repository keeps those originals only in the existing
12+
untracked `output/` directory. This revision stores no artwork in `src/main/resources`, so it does
13+
not add the 22 MiB source set to an APK or desktop runtime artifact.
14+
15+
For contract tests, each source was transformed mechanically with
16+
`ffmpeg -vf crop=924:736:374:102` and losslessly optimized with `optipng -o2`. The resulting
17+
924x736 RGB, non-interlaced PNGs live under
18+
`src/test/resources/eu/rekawek/coffeegb/ui/menu/artwork/proposal3/source/`. Tests pin each encoded
19+
PNG SHA-256 and decode its PNG filters in pure Java to pin the raw RGB frame SHA-256 produced by the
20+
same ffmpeg crop. This is the lossless raw-pixel verification that guards the canonical crop and
21+
does not require ImageIO or a platform image library.
22+
23+
The repository is distributed under the MIT license in the root `LICENSE` file. This module does
24+
not add a separate artwork license or alter the source provenance; the imported fixtures retain the
25+
repository's applicable license and notices.
26+
27+
## Canonical visual authority
28+
29+
The original composition geometry is 1672x941. `MenuArtworkCatalog.SOURCE_VISIBLE_CROP` is
30+
`x=374, y=102, width=924, height=736`, and every packaged test fixture is already that crop. The
31+
fixture itself is the complete image to decode and blit; hosts must not crop it again at runtime.
32+
Each route also retains its original source filename for provenance.
33+
34+
Dynamic content is overlays-only: later compositor work may place the ROM name, sliders, focus
35+
state, or status values above the immutable artwork, but must not redraw or reflow the base visual.
36+
The Android file browser remains a native boundary. Open/import actions hand off to Android's native
37+
document picker; this portable UI never draws a filesystem browser.
38+
39+
## Integer aspect-fit placement
40+
41+
`MenuViewport` places the 924x736 image into any positive integer viewport with one aspect-fit
42+
placement. It uses long cross-products and floor rounding, centers with integer left/top offsets,
43+
and assigns odd remainder pixels to the right/bottom bars. `contentBounds()` is half-open:
44+
`[left,right) x [top,bottom)`. Inverse input mapping returns `Optional`/`OptionalInt` and rejects
45+
letterbox pixels and the right/bottom edges.
46+
47+
Portrait and desktop hosts must preserve this placement rather than reflowing or independently
48+
stretching either axis. The known portrait aperture `758x685` produces `[0,41,758,644)`; the
49+
remaining pixels are letterbox bars.
50+
51+
## Future runtime paths
52+
53+
The `source/` path is intentionally test-fixture-only. A later sanitized compositor PR will own
54+
the production runtime route and/or atlas paths in `src/main/resources`, decode and compose them in
55+
`ui-portable`, and expose only ARGB blit data plus the portable input mapping to Android and desktop
56+
adapters. No host adapter should depend on this fixture path or on raw resource streams.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package eu.rekawek.coffeegb.ui.menu.artwork;
2+
3+
import eu.rekawek.coffeegb.ui.menu.MenuRoute;
4+
5+
import java.io.InputStream;
6+
import java.util.Objects;
7+
8+
/**
9+
* Immutable metadata for one packaged canonical Proposal 3 menu crop.
10+
*
11+
* <p>The packaged image is already the complete 924x736 visible screen. The original 1672x941
12+
* composition and its {@link #sourceVisibleCrop()} are retained only as provenance. The portable
13+
* module owns the future decode/composition contract; platform adapters consume the resulting
14+
* ARGB pixels and map input through {@link MenuViewport}.
15+
*/
16+
public final class MenuArtwork {
17+
18+
private final MenuRoute route;
19+
private final String sourceFilename;
20+
private final String resourcePath;
21+
private final MenuRect sourceVisibleCrop;
22+
23+
MenuArtwork(MenuRoute route, String sourceFilename, String resourcePath, MenuRect sourceVisibleCrop) {
24+
this.route = Objects.requireNonNull(route, "route");
25+
this.sourceFilename = Objects.requireNonNull(sourceFilename, "sourceFilename");
26+
this.resourcePath = Objects.requireNonNull(resourcePath, "resourcePath");
27+
this.sourceVisibleCrop = Objects.requireNonNull(sourceVisibleCrop, "sourceVisibleCrop");
28+
if (sourceFilename.isEmpty()) {
29+
throw new IllegalArgumentException("sourceFilename must not be empty");
30+
}
31+
if (resourcePath.isEmpty() || resourcePath.charAt(0) != '/') {
32+
throw new IllegalArgumentException("Artwork resource paths must be absolute classpath paths");
33+
}
34+
}
35+
36+
public MenuRoute route() {
37+
return route;
38+
}
39+
40+
/** Returns the original Proposal 3 source filename associated with this crop. */
41+
public String sourceFilename() {
42+
return sourceFilename;
43+
}
44+
45+
/** Returns the crop coordinates in the original 1672x941 source composition. */
46+
public MenuRect sourceVisibleCrop() {
47+
return sourceVisibleCrop;
48+
}
49+
50+
/** Returns the dimensions of the already-cropped packaged image. */
51+
public int packagedWidth() {
52+
return MenuArtworkCatalog.PACKAGED_WIDTH;
53+
}
54+
55+
/** Returns the dimensions of the already-cropped packaged image. */
56+
public int packagedHeight() {
57+
return MenuArtworkCatalog.PACKAGED_HEIGHT;
58+
}
59+
60+
/* Fixture-only access; production runtime paths belong to the later compositor integration. */
61+
String resourcePath() {
62+
return resourcePath;
63+
}
64+
65+
/* Fixture-only access; callers receive a 924x736 image and must close the stream. */
66+
InputStream openStream() {
67+
InputStream stream = MenuArtwork.class.getResourceAsStream(resourcePath);
68+
if (stream == null) {
69+
throw new IllegalStateException("Missing menu artwork resource: " + resourcePath);
70+
}
71+
return stream;
72+
}
73+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package eu.rekawek.coffeegb.ui.menu.artwork;
2+
3+
import eu.rekawek.coffeegb.ui.menu.MenuRoute;
4+
5+
import java.util.Collections;
6+
import java.util.EnumMap;
7+
import java.util.HashSet;
8+
import java.util.Map;
9+
import java.util.Objects;
10+
import java.util.Set;
11+
12+
/**
13+
* The canonical Proposal 3 artwork catalog.
14+
*
15+
* <p>The source PNGs are complete 1672x941 compositions, but the test fixtures packaged by this
16+
* revision are already the fixed 924x736 crop declared by {@link #SOURCE_VISIBLE_CROP}. Runtime
17+
* route/atlas paths are reserved for the later sanitized compositor integration.
18+
*/
19+
public final class MenuArtworkCatalog {
20+
21+
/** Width of every original Proposal 3 composition in pixels. */
22+
public static final int SOURCE_WIDTH = 1672;
23+
24+
/** Height of every original Proposal 3 composition in pixels. */
25+
public static final int SOURCE_HEIGHT = 941;
26+
27+
/** Width of each lossless packaged canonical crop. */
28+
public static final int PACKAGED_WIDTH = 924;
29+
30+
/** Height of each lossless packaged canonical crop. */
31+
public static final int PACKAGED_HEIGHT = 736;
32+
33+
/** Original-composition crop retained as provenance for every packaged image. */
34+
public static final MenuRect SOURCE_VISIBLE_CROP = new MenuRect(374, 102, 924, 736);
35+
36+
/** The complete bounds of each packaged image. */
37+
public static final MenuRect PACKAGED_BOUNDS = new MenuRect(0, 0, PACKAGED_WIDTH, PACKAGED_HEIGHT);
38+
39+
private static final String FIXTURE_RESOURCE_ROOT =
40+
"/eu/rekawek/coffeegb/ui/menu/artwork/proposal3/source/";
41+
42+
private static final Map<MenuRoute, MenuArtwork> ARTWORK = createCatalog();
43+
44+
private MenuArtworkCatalog() {
45+
}
46+
47+
/** Returns the one canonical artwork entry for a route. */
48+
public static MenuArtwork artwork(MenuRoute route) {
49+
return ARTWORK.get(Objects.requireNonNull(route, "route"));
50+
}
51+
52+
/** Returns an immutable route-to-artwork view containing every menu route exactly once. */
53+
public static Map<MenuRoute, MenuArtwork> all() {
54+
return ARTWORK;
55+
}
56+
57+
private static Map<MenuRoute, MenuArtwork> createCatalog() {
58+
EnumMap<MenuRoute, MenuArtwork> catalog = new EnumMap<>(MenuRoute.class);
59+
add(catalog, MenuRoute.PAUSE_CONSOLE, "00-pause-console.png");
60+
add(catalog, MenuRoute.SAVE_STATES, "01-save-states.png");
61+
add(catalog, MenuRoute.SETTINGS, "02-settings.png");
62+
add(catalog, MenuRoute.AUDIO, "03-audio.png");
63+
add(catalog, MenuRoute.TOUCH_CONTROLS, "04-touch-controls.png");
64+
add(catalog, MenuRoute.CONTROLLER_MAPPING, "05-controller-mapping.png");
65+
add(catalog, MenuRoute.OPTIONAL_DEVICES, "06-optional-devices.png");
66+
add(catalog, MenuRoute.DATA_MEDIA, "07-data-media.png");
67+
add(catalog, MenuRoute.LIBRARY, "08-library.png");
68+
add(catalog, MenuRoute.CHOOSE_ROM, "09-choose-rom.png");
69+
add(catalog, MenuRoute.SYSTEM, "10-system.png");
70+
add(catalog, MenuRoute.ABOUT, "11-about.png");
71+
add(catalog, MenuRoute.CONFIRM_ACTION, "12-confirm-action.png");
72+
add(catalog, MenuRoute.PRINTER_PAPER, "13-printer-paper.png");
73+
74+
if (catalog.size() != MenuRoute.values().length) {
75+
throw new IllegalStateException("Proposal 3 artwork catalog does not cover every route");
76+
}
77+
Set<String> paths = new HashSet<>();
78+
for (MenuRoute route : MenuRoute.values()) {
79+
MenuArtwork artwork = catalog.get(route);
80+
if (artwork == null || !paths.add(artwork.resourcePath())) {
81+
throw new IllegalStateException("Proposal 3 artwork catalog contains a duplicate or missing route");
82+
}
83+
}
84+
return Collections.unmodifiableMap(catalog);
85+
}
86+
87+
private static void add(EnumMap<MenuRoute, MenuArtwork> catalog, MenuRoute route, String filename) {
88+
if (catalog.containsKey(route)) {
89+
throw new IllegalStateException("Duplicate artwork route: " + route);
90+
}
91+
MenuArtwork artwork = new MenuArtwork(route, filename, FIXTURE_RESOURCE_ROOT + filename,
92+
SOURCE_VISIBLE_CROP);
93+
for (MenuArtwork existing : catalog.values()) {
94+
if (existing.resourcePath().equals(artwork.resourcePath())) {
95+
throw new IllegalStateException("Duplicate artwork resource: " + artwork.resourcePath());
96+
}
97+
}
98+
catalog.put(route, artwork);
99+
}
100+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package eu.rekawek.coffeegb.ui.menu.artwork;
2+
3+
/** An immutable point in a menu source or destination coordinate space. */
4+
public record MenuPoint(double x, double y) {
5+
6+
public MenuPoint {
7+
if (!Double.isFinite(x) || !Double.isFinite(y)) {
8+
throw new IllegalArgumentException("Menu coordinates must be finite");
9+
}
10+
}
11+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package eu.rekawek.coffeegb.ui.menu.artwork;
2+
3+
/**
4+
* An immutable integer rectangle represented as {@code [x, right) x [y, bottom)}.
5+
*/
6+
public record MenuRect(int x, int y, int width, int height) {
7+
8+
public MenuRect {
9+
if (x < 0 || y < 0 || width <= 0 || height <= 0) {
10+
throw new IllegalArgumentException("A menu rectangle must be non-empty and non-negative");
11+
}
12+
if ((long) x + width > Integer.MAX_VALUE || (long) y + height > Integer.MAX_VALUE) {
13+
throw new IllegalArgumentException("A menu rectangle must fit in integer coordinates");
14+
}
15+
}
16+
17+
/** Returns the exclusive right edge. */
18+
public int right() {
19+
return x + width;
20+
}
21+
22+
/** Returns the exclusive bottom edge. */
23+
public int bottom() {
24+
return y + height;
25+
}
26+
27+
/** Returns whether an integer point is inside this half-open rectangle. */
28+
public boolean contains(int pointX, int pointY) {
29+
return pointX >= x && pointX < right() && pointY >= y && pointY < bottom();
30+
}
31+
}

0 commit comments

Comments
 (0)