diff --git a/docs/launcher.md b/docs/launcher.md index 3c883870..cdb2334b 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -38,13 +38,14 @@ Customize Launcher (on the home screen) makes the look the player's own: - Color Scheme: Forest (the colours above), Volcano, Lake, Desert, Temple and Shadow. A scheme is the first one with its woods, accents, backdrop and glow turned to other hues (`art.THEMES`, `art.recolor`), so each keeps the same light and dark steps and the same readable text. The window is drawn again in the new colours at once. The names - are plain words, not the game's. Custom, the last one, takes two colours of the player's: Background Color for the - backdrop and Button Color for the buttons, the panels and the title band. Each opens a picker in the description + are plain words, not the game's. Custom, the last one, takes three colours of the player's: Background Color for + the backdrop, Panel Color for the list panel, the description box and the title band, and Button Color for the + rows' cells and the orange choices in them. Each opens a picker in the description box's place: a square of the hue's colours (saturation across, brightness up and down) and a hue bar under it, to click or drag on, Hue, Saturation and Brightness rows for the arrows and the gamepad, and Hex Code, where a code such as `#3A7BD5` is typed. The picked colour stands in for one colour of the first scheme (`art.CUSTOM_SLOTS`) and - the rest of its group follows it, lighter and darker as in the first scheme (`art.follow`); light buttons get dark - text. The window takes the new colour as the mouse moves; Reset brings back the first scheme's colour. + the rest of its part of the window follows it, lighter and darker as in the first scheme (`art.follow`); text on + a light button or panel turns dark. The window takes the new colour as the mouse moves; Reset brings back the first scheme's colour. - Background: a picture of the player's own behind the panels instead of the scheme's backdrop (`launcher/ui/picture.py`: PNG, JPEG, WebP, BMP, GIF or TIFF, read with Pillow). It fills the window below the title band, so none of it is hidden under the band: cut to that shape around its middle and scaled evenly, never stretched, and kept as @@ -75,8 +76,8 @@ Customize Launcher (on the home screen) makes the look the player's own: starts the launcher of `app/current.txt` with `--play`: the game starts as Play starts it, and the process waits for it, so Steam counts the game as running while it is. -The choices are `theme`, `custom_background`, `custom_buttons`, `background`, `background_dim`, `animations` and -`sounds` in `data/launcher.json`. +The choices are `theme`, `custom_background`, `custom_panel`, `custom_buttons`, `background`, `background_dim`, +`animations` and `sounds` in `data/launcher.json`. Navi (`art.NAVI`, 22 x 21 pixels, drawn in whole pixels at every size) is also the icon of `NX2navi.exe`, `NX2navi-Setup.exe`, the window and its taskbar button (`art.navi_ico`, written into the release by @@ -86,8 +87,8 @@ identity (`NX2navi.Launcher`), so the taskbar groups and pins it under NX2navi r Screens (`launcher/ui/app.py`): first run (dump, install folder, Desktop Shortcut, Check for Updates at Start, Update Channel, Begin Installation); the steps of an install, update or rebuild with their progress, the build log's last lines and Cancel, and after a failure Try Again and Copy Report; home (Play, Updates, Game Folder, Saves, -Rebuild the Game, Customize Launcher, Settings); customize (Color Scheme, with Custom Background Color and Button -Color, Background, with a picture set Background Dimming and Remove Background, Animations, Sounds, Add to Steam on +Rebuild the Game, Customize Launcher, Settings); customize (Color Scheme, with Custom Background Color, Panel Color and +Button Color, Background, with a picture set Background Dimming and Remove Background, Animations, Sounds, Add to Steam on Windows, Back); the colour picker (Hue, Saturation, Brightness, Hex Code, Reset, Done); saves (Export Save, Add Log and Settings, Export To, Import a Save, Import a Save Folder, Open Saves Folder, Back); settings (update check, Install Updates Automatically, Hide Launcher While Playing, Keep Build Cache, Update Channel, the folders, Open Logs Folder, Uninstall, Back); uninstall (keep or remove the built game). diff --git a/docs/releases/v0.7.2.md b/docs/releases/v0.7.2.md index af6d3c71..474fdbe3 100644 --- a/docs/releases/v0.7.2.md +++ b/docs/releases/v0.7.2.md @@ -21,8 +21,8 @@ Draft notes for the release after 0.7.1. The version number is not decided yet. - **Add to Steam** in Customize Launcher (Windows): adds the game to your Steam library as *The Legend of Zelda Ocarina of Time*, with Navi as its icon and the title screen's artwork and logo. Starting it from Steam starts the game. Steam restarts once while it is added. -- **Custom color scheme** in Customize Launcher: pick your own background color and button color on a color square - with a hue bar, or type a hex code. +- **Custom color scheme** in Customize Launcher: pick your own background, panel and button colors on a color + square with a hue bar, or type a hex code. ## Fixed diff --git a/launcher/settings.py b/launcher/settings.py index a725c6ad..97020f13 100644 --- a/launcher/settings.py +++ b/launcher/settings.py @@ -17,7 +17,8 @@ DEFAULTS = { 'export_with_log': True, # Export Save adds the game's latest log and nx2navi.ini (for a bug report) 'theme': 'forest', # the colour scheme (launcher/ui/art.py THEMES) 'custom_background': '', # the Custom scheme's backdrop colour, #rrggbb; empty: the first scheme's - 'custom_buttons': '', # the Custom scheme's colour of the buttons, panels and title band + 'custom_panel': '', # the Custom scheme's colour of the panels and the title band + 'custom_buttons': '', # the Custom scheme's colour of the buttons 'background': '', # the picture the player chose as the background; its fitted copy is data/background.npy 'background_dim': 40, # how much that picture is darkened, in percent 'animations': True, # the cursor glides, screens slide in, sparks drift, Navi bobs diff --git a/launcher/ui/app.py b/launcher/ui/app.py index 507f4e17..c7688a70 100644 --- a/launcher/ui/app.py +++ b/launcher/ui/app.py @@ -60,7 +60,9 @@ DEPTH = {'first_run': 0, 'home': 0, 'job': 1, 'saves': 1, 'settings': 1, 'custom PICKER_BOX = (614, 100, 336, 452) SQUARE = (667, 160, 897, 390) BAR = (667, 408, 897, 430) -CUSTOM_LABELS = {'background': 'Background Color', 'buttons': 'Button Color'} +CUSTOM_LABELS = {'background': 'Background Color', 'panel': 'Panel Color', 'buttons': 'Button Color'} +CUSTOM_DESC = {'background': 'The color behind the menus.', 'panel': 'The color of the panels and the top bar.', + 'buttons': 'The color of the buttons.'} DIMS = (0, 20, 40, 60, 80) # Background Dimming, in percent SOUND_LEVELS = (('off', 'Off'), ('low', 'Low'), ('medium', 'Medium'), ('high', 'High')) @@ -980,13 +982,9 @@ class App: desc=('The launcher\'s colours. Custom lets you pick your own.',)), ] if art.theme == 'custom': - rows += [ - Row('value', CUSTOM_LABELS['background'], art.CUSTOM['background'], 'swatch', - on=lambda: self.open_picker('background'), desc=('The color behind the menus.',)), - Row('value', CUSTOM_LABELS['buttons'], art.CUSTOM['buttons'], 'swatch', - on=lambda: self.open_picker('buttons'), - desc=('The color of the buttons, the panels and the top bar.',)), - ] + rows += [Row('value', CUSTOM_LABELS[slot], art.CUSTOM[slot], 'swatch', + on=lambda sl=slot: self.open_picker(sl), desc=(CUSTOM_DESC[slot],)) + for slot in art.CUSTOM_SLOTS] rows += [ Row('value', 'Background', Path(self.settings['background']).name if custom else 'Choose a Picture', 'arrow', on=self.choose_background, desc=bg_desc), diff --git a/launcher/ui/art.py b/launcher/ui/art.py index 36b9746d..dd6bd1e6 100644 --- a/launcher/ui/art.py +++ b/launcher/ui/art.py @@ -7,9 +7,9 @@ shape is a picture; flat rectangles and text stay canvas items. Colours follow the game's options menu (docs/launcher.md, "Look"). Sizes are in logical pixels times the display scale s. PALETTE holds the colours of the colour scheme in use: set_theme fills it from THEMES, each scheme the -first one with its woods, accents, backdrop and glow turned to other hues. The Custom scheme takes two colours the -player picks on a colour square (color_square, hue_bar): one for the backdrop, one for the buttons, panels and title -band. +first one with its woods, accents, backdrop and glow turned to other hues. The Custom scheme takes three colours the +player picks on a colour square (color_square, hue_bar): one for the backdrop, one for the panels and the title band, +one for the buttons. """ import base64 import colorsys @@ -81,11 +81,21 @@ THEMES = { } DEFAULT_THEME = 'forest' -# The Custom scheme: each colour the player picks takes the place of one colour of the first scheme, and the rest of -# that colour's group follows it, lighter and darker as in the first scheme (follow). Its two colours start as the -# first scheme's. -CUSTOM_SLOTS = {'background': ('scene', 'tint_mid'), 'buttons': ('wood', 'label_mid')} -CUSTOM_DEFAULT = {slot: BASE[ref] for slot, (_, ref) in CUSTOM_SLOTS.items()} +# The Custom scheme: each colour the player picks takes the place of one colour of the first scheme (ref), and the +# rest of that colour's part of the window follows it, lighter and darker as in the first scheme (follow). Each part: +# (ref, the colours that follow it, the text on it, which turns dark when the picked colour is light). The buttons are +# the rows' label cells, their value cells and the orange choices in them; the panel is the list panel, the +# description box, the dividers and the title band. The colours start as the first scheme's. +CUSTOM_SLOTS = { + 'background': ('tint_mid', GROUPS['scene'], ()), + 'panel': ('panel', ('panel', 'desc', 'desc_border', 'line', 'dot', 'filigree', 'button_hover', 'wood_0', 'wood_1', + 'wood_2', 'wood_3', 'band_line_dark', 'band_line_light', 'band_shadow', 'frame'), + ('desc_text', 'muted', 'band_text', 'status', 'log_text')), + 'buttons': ('label_mid', ('label_top', 'label_mid', 'label_bottom', 'label_border', 'ring', 'value', 'value_border', + 'text_shadow') + GROUPS['accent'], + ('text', 'text_bright', 'value_text', 'on_text', 'icon_frame', 'icon_fill', 'chevron_dim')), +} +CUSTOM_DEFAULT = {slot: BASE[ref] for slot, (ref, _, _) in CUSTOM_SLOTS.items()} CUSTOM = dict(CUSTOM_DEFAULT) HEX = re.compile(r'^#[0-9a-f]{6}$') @@ -134,20 +144,22 @@ def luminance(hexcolor): def custom_palette(colors): """The Custom scheme's colours from {slot: '#rrggbb'} (CUSTOM_SLOTS); a slot left out or not a colour keeps the - first scheme's. Light buttons get dark text, so the labels stay readable on them.""" + first scheme's. Text on a light button or panel turns dark, so it stays readable, and its shadow light.""" pal = dict(BASE) - for slot, (group, ref) in CUSTOM_SLOTS.items(): + for slot, (ref, keys, text) in CUSTOM_SLOTS.items(): c = str((colors or {}).get(slot) or '').lower() if not HEX.match(c) or c == BASE[ref]: # the first scheme's own colour: its colours as written continue - for k in GROUPS[group]: + for k in keys: pal[k] = follow(BASE[k], BASE[ref], c) - if slot == 'buttons' and luminance(c) > 0.5: + if text and luminance(c) > 0.5: h = hls(c)[0] * 360 - for k in GROUPS['text']: - pal[k] = recolor(BASE[k], h, 0.5, 0.18) - pal['text_shadow'] = follow(BASE['label_top'], BASE[ref], c) - pal['chevron_dim'] = recolor(BASE['chevron_dim'], h, 0.5, 0.3) + for k in text: + pal[k] = recolor(BASE[k], h, 0.5, 0.3 if k == 'chevron_dim' else 0.18) + shadow = 'text_shadow' if slot == 'buttons' else 'band_shadow' + pal[shadow] = follow(BASE['label_top'], BASE[ref], c) + if slot == 'buttons' and luminance(pal['orange']) > 0.65: # the choices come out lighter than the cells + pal['on_text'] = recolor(BASE['on_text'], hls(c)[0] * 360, 0.5, 0.18) return pal diff --git a/launcher/ui/selftest.py b/launcher/ui/selftest.py index 38e832b4..7eeec6c1 100644 --- a/launcher/ui/selftest.py +++ b/launcher/ui/selftest.py @@ -205,8 +205,8 @@ def states(app): def custom_picker(): # the Custom scheme with two picked colours, and its picker on the button colour from . import art - was = {k: s.get(k) for k in ('theme', 'custom_background', 'custom_buttons')} - s.update(theme='custom', custom_background='#3a1d5c', custom_buttons='#2f6fb0') + was = {k: s.get(k) for k in ('theme', 'custom_background', 'custom_panel', 'custom_buttons')} + s.update(theme='custom', custom_background='#3a1d5c', custom_panel='#24324a', custom_buttons='#2f6fb0') app.apply_look() app.open_picker('buttons') s.update(was) diff --git a/tests/launcher_test.py b/tests/launcher_test.py index a997f7fd..12b55009 100644 --- a/tests/launcher_test.py +++ b/tests/launcher_test.py @@ -2713,23 +2713,44 @@ class LookTest(Tmp): def test_the_custom_scheme(self): art = self.art self.assertEqual(art.custom_palette({}), art.BASE) # nothing picked yet: the first scheme's colours - pal = art.custom_palette({'background': '#3A7BD5', 'buttons': '#802020'}) + slots = art.CUSTOM_SLOTS + self.assertEqual(list(slots), ['background', 'panel', 'buttons']) + parts = [k for _, keys, _ in slots.values() for k in keys] + self.assertEqual(len(parts), len(set(parts)), 'a colour in two parts') + self.assertEqual(set(parts), set(art.GROUPS['scene'] + art.GROUPS['wood'] + art.GROUPS['accent'])) + pal = art.custom_palette({'background': '#3A7BD5', 'panel': '#203040', 'buttons': '#802020'}) self.assertEqual(pal['tint_mid'], '#3a7bd5') # the picked colours exactly where they stand in + self.assertEqual(pal['panel'], '#203040') self.assertEqual(pal['label_mid'], '#802020') - self.assertEqual(pal['orange'], art.BASE['orange']) # the other groups stay - for group, ref in (('scene', 'tint_mid'), ('wood', 'label_mid')): - for k in art.GROUPS[group]: # lighter than the picked colour where the first scheme's is lighter + only = art.custom_palette({'buttons': '#802020'}) # Button Color: the buttons and nothing else + self.assertLessEqual({k for k in art.BASE if only[k] != art.BASE[k]}, set(slots['buttons'][1])) + for k in ('label_top', 'label_bottom', 'value', 'orange', 'orange_light'): # the orange choices too + self.assertNotEqual(only[k], art.BASE[k], k) + for k in ('panel', 'desc', 'wood_1', 'window', 'tint_mid'): # not the panels, the title band, the backdrop + self.assertEqual(only[k], art.BASE[k], k) + self.assertEqual(round(art.hls(only['orange'])[0] * 360), 0) # the choices in the picked hue, lighter + self.assertGreater(art.hls(only['orange'])[1], art.hls(only['label_mid'])[1]) + panel = art.custom_palette({'panel': '#203040'}) + self.assertLessEqual({k for k in art.BASE if panel[k] != art.BASE[k]}, set(slots['panel'][1])) + self.assertEqual(panel['label_mid'], art.BASE['label_mid']) + for slot, (ref, keys, _) in slots.items(): + for k in keys: # lighter than the picked colour where the first scheme's is lighter if art.hls(art.BASE[k])[1] > art.hls(art.BASE[ref])[1] + 0.02: self.assertGreater(art.hls(pal[k])[1], art.hls(pal[ref])[1], k) self.assertGreater(art.luminance(pal['text']), 0.6) # light text on dark buttons light = art.custom_palette({'buttons': '#f0e0a0'}) self.assertLess(art.luminance(light['text']), 0.3) # and dark text on light ones + self.assertGreater(art.luminance(light['desc_text']), 0.6) # the description box's text stays as it was + light = art.custom_palette({'panel': '#f0e0a0'}) + self.assertLess(art.luminance(light['desc_text']), 0.3) # dark on a light panel + self.assertGreater(art.luminance(light['text']), 0.6) self.assertEqual(art.custom_palette({'buttons': 'red', 'background': None}), art.BASE) held = art.PALETTE try: self.assertEqual(art.set_theme('custom', {'buttons': '#802020', 'other': '#000000'}), 'custom') self.assertEqual(held['label_mid'], '#802020') - self.assertEqual(art.CUSTOM, {'background': art.BASE['tint_mid'], 'buttons': '#802020'}) + self.assertEqual(art.CUSTOM, {'background': art.BASE['tint_mid'], 'panel': art.BASE['panel'], + 'buttons': '#802020'}) art.set_theme('custom', {}) self.assertEqual(dict(held), art.BASE) finally: