Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions app/patternfront.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
--text:#c0c0c0; /* text */
--dim:#7d7d7d; /* tab_normal_text */
--mute:#636d79; /* status_bar_text */
--accent:#2962ff; /* OpenFront's own blue, src/core/.../Colors.ts */
--accent-hi:#4b8fff;
--accent-ink:#ffffff;
--accent:#e1b85f; /* selected */
--accent-hi:#eec877; /* accent hover */
--accent-ink:#41444a; /* selected_text */
--link:#6e9adb; /* link_text */
--danger:#c75a68; /* flag_active */
--ok:#64c864; /* select_box_grid */
Expand Down Expand Up @@ -1774,17 +1774,17 @@
' map tiles. Repeats every '+(doc.w<<scale)+'×'+(doc.h<<scale)+'.';
if(doc.w>OF_MAXW||doc.h>OF_MAXH){
$('ofData').textContent='—';
$('ofStat').innerHTML='<span style="color:#e5674b">Canvas is '+doc.w+'×'+doc.h+
$('ofStat').innerHTML='<span style="color:var(--danger)">Canvas is '+doc.w+'×'+doc.h+
'. OpenFront allows at most '+OF_MAXW+'×'+OF_MAXH+'.</span>';
return null;}
const s=encodeOF(doc.w,doc.h,scale,to1bit());
$('ofData').textContent=s;
$('ofStat').innerHTML=s.length<=OF_MAXB64
?'<span style="color:#5cc98a">Valid · '+s.length+' / '+OF_MAXB64+' characters</span>'
:'<span style="color:#e5674b">Too long — '+s.length+' / '+OF_MAXB64+'</span>';
?'<span style="color:var(--ok)">Valid · '+s.length+' / '+OF_MAXB64+' characters</span>'
:'<span style="color:var(--danger)">Too long — '+s.length+' / '+OF_MAXB64+'</span>';
return s;}
function copyText(t,ok){
const done=()=>{$('ofStat').innerHTML='<span style="color:#5cc98a">'+ok+'</span>';};
const done=()=>{$('ofStat').innerHTML='<span style="color:var(--ok)">'+ok+'</span>';};
const fb=()=>{const ta=document.createElement('textarea');ta.value=t;
document.body.appendChild(ta);ta.select();
try{document.execCommand('copy');done();}
Expand Down
Binary file modified build/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 59 additions & 13 deletions docs/09-design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ Aseprite's own id is kept against each value so the mapping stays auditable.

Note Aseprite's dark accent is **gold**, not blue. Blue is reserved for links.

This one was reversed once and reversed back. A revision moved `--accent` to
OpenFront's own blue (`#2962ff`) on the reasoning that the tool is for OpenFront.
The result was two blues eight hue-degrees apart doing unrelated jobs — selection
and links — and a design document that disagreed with both the stylesheet and the
checker. Chrome belongs to the audience, which lives in Aseprite; OpenFront fidelity
is carried by the format, the codec and the map-scale preview, not by the accent.
`tools/verify-ui.py` now fails on `#2962ff` so the round trip is not repeated.

## Hard rules

- **Radius 0–2px.** Nothing rounder, anywhere.
Expand All @@ -51,26 +59,30 @@ Two deliberate exceptions, both checked for:
## Interface scale

Aseprite's native density is very tight and it ships a **UI Scale** preference for
exactly that reason, defaulting to 2× on high-DPI displays. PatternFront does the same.
exactly that reason, defaulting to 2× on high-DPI displays. PatternFront carries the
same preference at a lower default, for the reason given below.

Every length in the stylesheet is `rem`, and `html{font-size:calc(10px * var(--ui))}`
drives the lot. So `1.1rem` reads as "11px at 1×" and one token resizes the whole
interface. The artwork view scales too — `Z() = zoom * uiScale` — so the app scales as
a single piece rather than the chrome growing around a fixed canvas.

The control sits in the top bar (1× · 1.5× · 2× · 2.5× · 3×) and persists to
`localStorage`. **Default is 2×.**
`localStorage`. **Default is 1.5×** — 2× was the original default and crowded the
canvas once the preview took a column of its own.

Columns are left / preview / right.

| Scale | Bars | Buttons | Font | Rail | Columns |
|---|---|---|---|---|---|
| 1× | 24px | 18px | 11px | 26px | 152 / 190 |
| 1.5× | 36px | 27px | 16px | 39px | 228 / 285 |
| **2×** | **48px** | **36px** | **22px** | **52px** | **304 / 380** |
| 2.5× | 60px | 45px | 28px | 65px | 380 / 475 |
| 3× | 72px | 54px | 33px | 78px | 456 / 570 |
| 1× | 24px | 18px | 11px | 26px | 152 / 240 / 190 |
| **1.5×** | **36px** | **27px** | **16px** | **39px** | **228 / 360 / 285** |
| | 48px | 36px | 22px | 52px | 304 / 480 / 380 |
| 2.5× | 60px | 45px | 28px | 65px | 380 / 600 / 475 |
| 3× | 72px | 54px | 33px | 78px | 456 / 720 / 570 |

Above 2× the side columns start to crowd the canvas on a 1440px window — `Tab` collapses
both and is the intended companion to the larger scales.
Above 1.5× the columns start to crowd the canvas on a 1440px window — `Tab` collapses
them and is the intended companion to the larger scales.

## Density

Expand All @@ -90,16 +102,50 @@ Base values, scaled from Aseprite's own `dimensions` block, quoted at 1×.
```
top bar 24px
├ rail 26px · tools, always visible
├ left column 152px · colour, palette, canvas, options
├ left column 152px · colour, duotone, stamps, palette
├ stage · dominant, the only thing that grows
└ right column 190px · preview, layers
├ preview column 240px · preview and its controls
└ right column 190px · layers
dock 24px · frames + status on one line
```

`Tab` collapses both columns (`body.zen`) for a near-fullscreen canvas.
One continuous docked column per side — no floating cards, no gaps, no rounding;
The preview holds its own column rather than stacking under the layers panel. It is
the surface a pattern is actually judged on — it is the only place the duotone
appears at all — and sharing a column made it the first thing squeezed.

`Tab` collapses all three columns and their gutters (`body.zen`) for a near-fullscreen
canvas. Each boundary is a drag handle (`.gut`) that resizes the column it sits against
and resets on double-click; the widths live in `--wL` / `--wP` / `--wR` so a drag and a
scale change move the same numbers.

One continuous docked column per region — no floating cards, no gaps, no rounding;
sections separated by 1px rules.

## States

Tokens and density were specified from the start; states were not, and drifted into
inline hex. Every one below is a token, and `tools/verify-ui.py` fails on a literal
colour used where one of these belongs.

| State | Treatment | Token |
|---|---|---|
| Rest | `--raised` fill, `--edge-dim` border | — |
| Hover | fill and border both go to `--edge` | `--edge` |
| Active / pressed | fill drops to `--well` | `--well` |
| Selected / on | `--accent` fill, `--accent-ink` text | `--accent`, `--accent-ink` |
| Focus (keyboard) | 1px `--accent` outline, no offset | `--accent` |
| Disabled | text to `--well`, default cursor, no hover | `--well` |
| Success | text only, never a fill | `--ok` |
| Error | text only, never a fill | `--danger` |

Two rules that are easy to get wrong:

- **Status is text, not chrome.** A valid or over-length pattern says so in `--ok` or
`--danger` text. It never tints a panel, because a coloured panel competes with the
artwork, which is the only thing in the window whose colour carries meaning.
- **Focus is not hover.** Hover is a fill change; focus is an outline. A control that
showed focus by filling would be indistinguishable from selection.

## Anti-patterns

Every one of these was tried and rejected:
Expand Down
12 changes: 7 additions & 5 deletions tools/gen-icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@
# The app's own chrome, so the icon and the window agree.
PAPER = (0x20, 0x21, 0x25) # --well, the editor's background
INK = (0xE6, 0xE6, 0xE8) # the editor's foreground
DUO_A = (0x29, 0x62, 0xFF) # the accent, standing in for a duotone primary
DUO_B = (0xF1, 0x7A, 0x3C) # its warm complement, the secondary
ACCENT = (0xE1, 0xB8, 0x5F) # --accent, the same gold the app selects with
# Inside the band only the *ink* changes colour; the ground stays the same dark as
# everywhere else. Giving the band its own ground introduced a third grey that sat
# between paper and ink and made the whole tile muddy at small sizes.

# A diagonal, which is one of the patterns the editor ships, at the coarsest
# step that still reads as motion. Two cells of ink, two of paper, shifted one
Expand Down Expand Up @@ -133,10 +135,10 @@ def pixel(x: int, y: int):
cy = min(GRID - 1, max(0, int((y - inset) // cell)))
lit = ART[cy][cx] == "#"

if in_band(cx, cy):
rgb = DUO_B if lit else DUO_A
if lit:
rgb = ACCENT if in_band(cx, cy) else INK
else:
rgb = INK if lit else PAPER
rgb = PAPER

return (rgb[0], rgb[1], rgb[2], round(a * 255))

Expand Down
29 changes: 26 additions & 3 deletions tools/verify-ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,17 @@
"--text": "#c0c0c0", # text
"--dim": "#7d7d7d", # tab_normal_text
"--mute": "#636d79", # status_bar_text
"--accent": "#2962ff", # OpenFront's own blue
"--accent": "#e1b85f", # selected
"--accent-hi": "#eec877", # accent hover
"--accent-ink": "#41444a", # selected_text
"--link": "#6e9adb", # link_text
"--danger": "#c75a68", # flag_active
}
# NEUTRAL surfaces may not be lighter than --raised. Luma via Rec.709.
# The accent is exempt: Aseprite's own `selected` is a bright gold, and
# selection is meant to be loud. The rule is about chrome, not highlights.
MAX_SURFACE_LUMA = 0.30
ACCENT_FAMILY = {"2962ff", "4b8fff"}
ACCENT_FAMILY = {"e1b85f", "eec877"}

fails: list[str] = []

Expand Down Expand Up @@ -200,7 +202,28 @@ def main() -> int:
chk("picking a pair yields exactly two colours",
"doc.palette=[{r:0,g:0,b:0,a:0},hexRGB(prim),hexRGB(sec)]" in js)

chk("accent is no longer gold", "#e1b85f" not in css)
# Aseprite's dark accent is gold and blue is reserved for links, which is what
# keeps the two distinguishable. An earlier revision moved the accent to
# OpenFront's blue (#2962ff), leaving two near-identical blues doing different
# jobs. The tool's chrome belongs to the audience that lives in Aseprite;
# OpenFront fidelity is carried by the format and the preview, not the chrome.
chk("accent is gold, not OpenFront blue",
"#2962ff" not in css and "#4b8fff" not in css)

# States were the part of the system nobody wrote down, so they drifted into
# inline hex: four `style="color:#e5674b"` strings sat beside a --danger token
# that already held that job. See docs/09-design-system.md, "States".
print("\n=== states ===")
chk("status colour comes from tokens, never inline hex",
not re.search(r'style="color:#[0-9a-fA-F]', js), )
chk("focus is an outline in the accent, not a fill",
"outline:0.1rem solid var(--accent)" in css)
chk("hover changes fill and border to --edge",
".b:hover:not(:disabled){background:var(--edge);border-color:var(--edge)}" in css)
chk("pressed drops to --well", ".b:active:not(:disabled){background:var(--well)}" in css)
chk("disabled dims to --well", ".b:disabled{color:var(--well)" in css)
chk("selected is accent fill with accent ink",
'background:var(--accent);color:var(--accent-ink)' in css)

print("\n=== preview & colour input ===")
chk("preview starts fitted and tiled", "pvScale=0,pvTile=true" in js)
Expand Down
Loading