From 822f35c08c25e74e713a3901547566dd7070cb33 Mon Sep 17 00:00:00 2001 From: LoafyLemon Date: Tue, 5 Jul 2022 21:38:03 +0100 Subject: [PATCH] Color Picker * Rewritten and re-implemented color picker in shading language * Vastly improved performance * Added live preview back * Overhauled interface * Code Cleanup * Reduced import overhead --- game/scripts/doll/clothes.rpy | 42 +- game/scripts/gui/preferences.rpy | 48 --- game/scripts/interface/color_picker.rpy | 1 + game/scripts/utility/updater.rpy | 4 +- game/scripts/wardrobe/color_picker.rpy | 528 ++++++++++++++++++++++++ 5 files changed, 564 insertions(+), 59 deletions(-) create mode 100644 game/scripts/wardrobe/color_picker.rpy diff --git a/game/scripts/doll/clothes.rpy b/game/scripts/doll/clothes.rpy index 5ae77d1a..3e392476 100644 --- a/game/scripts/doll/clothes.rpy +++ b/game/scripts/doll/clothes.rpy @@ -196,19 +196,43 @@ init python: def set_color(self, n): """Takes int layer number for manual color picking or a list to replace the cloth color in its entirety.""" if isinstance(n, int): - # Transarency slider boolean - is_cheating = config.developer or cheat_wardrobe_alpha - is_blacklisted = self.type.startswith(tuple(self.blacklist_unequip)) - is_allowed = self.type.startswith(("makeup", "tattoo")) + col = Color(tuple(self.color[n])) + dcol = Color(tuple(self.color_default[n])) - transparency = not is_blacklisted and (is_allowed or is_cheating) + cp.live_replace(col) + cp.start_replace(col) + cp.default_replace(dcol) - self.color[n] = color_picker(self.color[n], transparency, "Colour channel {}".format(n+1), pos_xy=(40, 85), color_default=self.color_default[n]) - self.char.override, self.override = False, False + renpy.show_screen("colorpickerscreen", self) + + while True: + action, value = ui.interact() + + if action == "layer": + n = value + col = Color(tuple(self.color[value])) + dcol = Color(tuple(self.color_default[n])) + + cp.live_replace(col) + cp.start_replace(col) + cp.default_replace(dcol) + elif action == "released": + self.color[n] = [int(255*x) for x in value.rgba] + self.rebuild_image() + self.char.rebuild_image() + elif action == "replace": + self.color[n] = [int(255*x) for x in value.rgba] + cp.live_replace(value) + self.rebuild_image() + self.char.rebuild_image() + elif action == "finish": + break + + renpy.hide_screen("colorpickerscreen") elif isinstance(n, list): self.color = [x[:] for x in n] - self.rebuild_image() - self.char.rebuild_image() + self.rebuild_image() + self.char.rebuild_image() self.rebuild_icon() def reset_color(self, n=None): diff --git a/game/scripts/gui/preferences.rpy b/game/scripts/gui/preferences.rpy index 9dd43fd2..4684856f 100644 --- a/game/scripts/gui/preferences.rpy +++ b/game/scripts/gui/preferences.rpy @@ -85,47 +85,6 @@ screen preferences_general(): Function(renpy.transition, trans), Function(renpy.restart_interaction) ] - - # Broken. - # - # vbox: - # style_prefix gui.theme("pref") - - # $ text_color_day = settings.get("text_color_day") - # $ text_color_night = settings.get("text_color_night") - - # label _("Text Colour") - # default color_square = "{font=[gui.glyph_font]}❖{/font}" - - # hbox: - # textbutton "Day" size_group "text_color" action [ - # Function(renpy.invoke_in_new_context, pick_color_setting, "text_color_day", "Day text colour"), - # Function(renpy.transition, trans), - # Function(gui.rebuild_styles) - # ] - # textbutton "{color=[text_color_day]}[color_square!i]{/color}" yalign 0.5 style "empty" background "#d1a261" - # textbutton "Reset" text_size 14 yalign 0.5 padding (0, 0) action [ - # settings.Reset("text_color_day"), - # Function(renpy.transition, trans), - # Function(gui.rebuild_styles) - # ] - - # hbox: - # textbutton "Night" size_group "text_color" action [ - # Function(renpy.invoke_in_new_context, pick_color_setting, "text_color_night", "Night text colour"), - # Function(renpy.transition, trans), - # Function(gui.rebuild_styles) - # ] - # textbutton "{color=[text_color_night]}[color_square!i]{/color}" yalign 0.5 style "empty" background "#5b4f4f" - # textbutton "Reset" text_size 14 yalign 0.5 padding (0, 0) action [ - # settings.Reset("text_color_night"), - # Function(renpy.transition, trans), - # Function(gui.rebuild_styles) - # ] - - # hbox: - # textbutton "Outline" size_group "text_color" action Function(print, "text_shadow") - vbox: style_prefix gui.theme("check") label _("Skipping") @@ -349,13 +308,6 @@ init python: for fn in renpy.list_saved_games(fast=True): renpy.unlink_save(fn) - def pick_color_setting(setting_name, title): - renpy.show_screen("preferences") - color = settings.get(setting_name) - r, g, b, _ = color_picker(get_rgb_list(color), False, title) - color = get_hex_string(r/255.0, g/255.0, b/255.0) - settings.set(setting_name, color) - def set_renderer(s): preferences.renderer = s if s in ("gl2", "angle2") else "auto" renpy.quit(relaunch=True) diff --git a/game/scripts/interface/color_picker.rpy b/game/scripts/interface/color_picker.rpy index 083deef1..9d707d9b 100644 --- a/game/scripts/interface/color_picker.rpy +++ b/game/scripts/interface/color_picker.rpy @@ -245,6 +245,7 @@ init -1 python: start_color = list(color) # Keep a copy renpy.show_screen("color_picker", tuple(color), alpha, title, pos_xy, color_default) + while True: _return = ui.interact() diff --git a/game/scripts/utility/updater.rpy b/game/scripts/utility/updater.rpy index 5fa8ddd7..1f1bb263 100644 --- a/game/scripts/utility/updater.rpy +++ b/game/scripts/utility/updater.rpy @@ -1,8 +1,8 @@ init python: - import binascii + from binascii import unhexlify import requests - UPDATE_URL = binascii.unhexlify("687474703a2f2f3135332e39322e3232312e3234362f757064617465732e6a736f6e") + UPDATE_URL = unhexlify("687474703a2f2f3135332e39322e3232312e3234362f757064617465732e6a736f6e") UPDATE_VER = "" @renpy.pure diff --git a/game/scripts/wardrobe/color_picker.rpy b/game/scripts/wardrobe/color_picker.rpy new file mode 100644 index 00000000..fcfc556a --- /dev/null +++ b/game/scripts/wardrobe/color_picker.rpy @@ -0,0 +1,528 @@ +init python: + + renpy.register_shader("color_picker.gradient", + variables=""" + varying vec2 v_tex_coord; + attribute vec2 a_tex_coord; + uniform float u_hue; + """, + fragment_functions=""" + vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); + } + + float lerp(float a, float b, float f){ + return a + f * (b - a); + } + """, + vertex_300=""" + // Flip gradient + v_tex_coord = vec2(a_tex_coord.s, 1.0 - a_tex_coord.t); + """, + fragment_300=""" + vec2 uv = v_tex_coord; + vec4 pixel = vec4(hsv2rgb(vec3(u_hue, 1.0, 1.0)), 1.0); + pixel = vec4(uv.y * lerp(1.0, pixel.x, uv.x), uv.y * lerp(1.0, pixel.y, uv.x), uv.y * lerp(1.0, pixel.z, uv.x), 1.0); + gl_FragColor = pixel; + // Gamma correction, but is it really needed here? + //gl_FragColor = pow(pixel, vec4(1.0/2.2)); + """ + ) + + renpy.register_shader("color_picker.hue", + variables=""" + varying vec2 v_tex_coord; + attribute vec2 a_tex_coord; + """, + fragment_functions=""" + vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); + } + """, + vertex_300=""" + v_tex_coord = a_tex_coord; + """, + fragment_300=""" + vec2 uv = v_tex_coord; + vec4 pixel = mix(vec4(hsv2rgb(vec3(uv.y, 1.0, 1.0)), 1.0), gl_FragColor, smoothstep(0.0, 1.0, gl_FragColor.x)); + // Gamma correction + gl_FragColor = pow(pixel, vec4(1.0/2.2)); + """ + ) + + renpy.register_shader("color_picker.alpha", + variables=""" + uniform vec3 u_color; + uniform vec2 u_model_size; + varying float direction; + attribute vec4 a_position; + """, + vertex_300=""" + direction = a_position.x / u_model_size.x; + """, + fragment_functions=""" + float pattern(vec2 uv, float size) { + vec2 pos = floor(uv / size); + return mod(pos.x + pos.y, 2.0); + } + """, + fragment_300=""" + const float size = 8.0; + float c = pattern(gl_FragCoord.xy, size); + vec4 checkerboard = vec4(c, c, c, 1.0); + + vec4 gradient = vec4(u_color, 1.0); + gradient = mix(gradient, vec4(0.0, 0.0, 0.0, 0.0), direction); + + gl_FragColor = mix(gradient, checkerboard, direction); + """ + ) + + class ColorPickerBase(renpy.Displayable): + canvas = Solid("#ffffff") + + def __init__(self, controller, **kwargs): + super(ColorPickerBase, self).__init__(**kwargs) + + self.controller = controller + self.size = (0, 0) + self.pos = (0, 0) + + def update(self): + raise NotImplementedError(self.__class__.__name__) + + def render(self, width, height, st, at): + raise NotImplementedError(self.__class__.__name__) + + def place_cursor(self, x, y): + w, h = self.size + x = max(min(x, w), 0) + y = max(min(y, h), 0) + + self.pos = (x, y) + renpy.redraw(self, 0) + + def unfocus(self): + self.controller.focus = None + + def focus(self): + self.controller.focus = self + + def focused(self): + return (self.controller.focus == self) + + def apply(self): + self.controller.apply() + + def per_interact(self): + return + + def visit(self): + return [] + + def event(self, ev, x, y, st): + w, h = self.size + + if not self.focused() and (not (0 < x <= w) or not (0 < y <= h)): + return + elif ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1: + # Button pressed + self.focus() + return + + if not self.focused(): + return + + if ev.type == pygame.MOUSEBUTTONUP: + # Button released + self.place_cursor(x, y) + self.update() + self.unfocus() + self.apply() + renpy.restart_interaction() + return ["released", self.controller.live_color] + + if ev.type == pygame.MOUSEMOTION and ev.buttons[0]: + # Dragged + self.place_cursor(x, y) + self.update() + return + + class ColorPickerHue(ColorPickerBase): + cursor = renpy.displayable("cph_cursor") + + def __init__(self, controller, **kwargs): + super(ColorPickerHue, self).__init__(controller, **kwargs) + + def update(self): + _, y = self.pos + _, h = self.size + + self.controller.hue = max(min(float(y) / h, 1.0), 0.0) + self.controller.update() + + def place_cursor(self, x, y): + w, h = self.size + x = 0 + y = max(min(y, h), 0) + + self.pos = (x, y) + renpy.redraw(self, 0) + + def render(self, width, height, st, at): + self.size = (width, height) + + # Hue Slider + sr = renpy.Render(width, height) + slider = renpy.render(self.canvas, width, height, st ,at) + + sr.mesh = True + sr.add_shader("color_picker.hue") + sr.blit(slider, (0, 0)) + + # Cursor + + cr = renpy.Render(width, height) + cursor = renpy.render(self.cursor, width, height, st, at) + cr.blit(cursor, self.pos) + + # Main + + rv = renpy.Render(width, height) + rv.blit(sr, (0, 0)) + rv.blit(cr, (0, -3)) + + return rv + + class ColorPickerSatVal(ColorPickerBase): + cursor = renpy.displayable("cpsv_cursor") + + def __init__(self, controller, **kwargs): + super(ColorPickerSatVal, self).__init__(controller, **kwargs) + + def update(self): + x, y = self.pos + w, h = self.size + + self.controller.saturation = max(min(float(x) / w, 1.0), 0.0) + self.controller.value = 1 - max(min(float(y) / h, 1.0), 0.0) + self.controller.update() + + def render(self, width, height, st, at): + self.size = (width, height) + + # Gradient map + + gr = renpy.Render(width, height) + gr.mesh = True + gr.add_shader("color_picker.gradient") + gr.add_uniform("u_hue", self.controller.hue) + + canvas = renpy.render(self.canvas, width, height, st ,at) + + gr.blit(canvas, (0, 0)) + + # Cursor + + cr = renpy.Render(width, height) + cursor = renpy.render(self.cursor, width, height, st, at) + cr.blit(cursor, self.pos) + + # Main + + rv = renpy.Render(width, height) + rv.blit(gr, (0, 0)) + rv.blit(cr, (-7, -8)) + + return rv + + class ColorPickerPreview(ColorPickerBase): + def __init__(self, controller, **kwargs): + super(ColorPickerPreview, self).__init__(controller, **kwargs) + + def render(self, width, height, st, at): + self.size = (width, height) + + # Live colour + + live_color = Solid(self.controller.live_color) + lcr = renpy.render(live_color, width, height, st, at) + + # # Start colour + + # start_color = Solid(self.controller.start_color) + # scr = renpy.render(start_color, one_third*2, height, st, at) + + # # # Default colour + + # default_color = Solid(self.controller.default_color) + # dcr = renpy.render(default_color, width, height, st, at) + + # Main + + rv = renpy.Render(width, height) + # Laid out from back to front + #rv.blit(dcr, (0, 0)) + #rv.blit(scr, (0, 0)) + rv.blit(lcr, (0, 0)) + + return rv + + def event(self, ev, x, y, st): + return + + class ColorPickerAlpha(ColorPickerBase): + cursor = renpy.displayable("cpa_cursor") + + def __init__(self, controller, **kwargs): + super(ColorPickerAlpha, self).__init__(controller, **kwargs) + + def update(self): + x, _ = self.pos + w, _ = self.size + + self.controller.alpha = 1.0 - max(min(float(x) / w, 1.0), 0.0) + self.controller.update() + + def place_cursor(self, x, y): + w, h = self.size + x = max(min(x, w), 0) + y = 0 + + self.pos = (x, y) + renpy.redraw(self, 0) + + def render(self, width, height, st, at): + self.size = (width, height) + + # Alpha Slider + sr = renpy.Render(width, height) + slider = renpy.render(self.canvas, width, height, st ,at) + + sr.mesh = True + sr.add_shader("color_picker.alpha") + sr.add_uniform("u_color", self.controller.live_color.rgb) + sr.blit(slider, (0, 0)) + + # Cursor + + cr = renpy.Render(width, height) + cursor = renpy.render(self.cursor, width, height, st, at) + cr.blit(cursor, self.pos) + + # Main + + rv = renpy.Render(width, height) + rv.blit(sr, (0, 0)) + rv.blit(cr, (0, 0)) + + return rv + + class ColorPicker(NoRollback): + + def __init__(self, start_color=(255, 255, 0, 255), default_color=(255, 0, 0, 255)): + col = Color(tuple(start_color)) + self.live_color = col + self.start_color = col + + default_color = Color(tuple(default_color)) + self.default_color = default_color + + self.hue, self.saturation, self.value = col.hsv + self.alpha = col.alpha + + self.cph = ColorPickerHue(self) + self.cpsv = ColorPickerSatVal(self) + self.cpp = ColorPickerPreview(self) + self.cpa = ColorPickerAlpha(self) + self.focus = None + + h, s, v = col.hsv + a = col.alpha + self.cph.pos = (0, 255 * h) + self.cpsv.pos = (255 * s, 255 * (1- v)) + self.cpa.pos = (255 * (1 - a), 0) + + def update(self): + hsv = (self.hue, self.saturation, self.value) + col = Color(hsv=hsv, alpha=self.alpha) + self.live_color = col + self.redraw() + + def redraw(self): + renpy.redraw(self.cpsv, 0) + renpy.redraw(self.cph, 0) + renpy.redraw(self.cpp, 0) + renpy.redraw(self.cpa, 0) + + def apply(self): + self.add_history(self.live_color) + + def live_replace(self, color): + h, s, v = color.hsv + a = color.alpha + self.hue, self.saturation, self.value = h, s, v + self.alpha = a + self.live_color = color + + self.cph.pos = (0, 255 * h) + self.cpsv.pos = (255 * s, 255 * (1- v)) + self.cpa.pos = (255 * (1 - a), 0) + + self.redraw() + + def start_replace(self, color): + self.start_color = color + self.redraw() + + def default_replace(self, color): + self.default_color = color + self.redraw() + + @staticmethod + def add_history(col): + colorpicker.history.append(col) + + if len(colorpicker.history) > 35: + del colorpicker.history[0] + + @staticmethod + def add_favorite(index, col): + colorpicker.favorites[index] = col + + cp = ColorPicker() + +default colorpicker.history = [] +default colorpicker.favorites = [Color((167, 77, 42)), Color((237, 179, 14)), Color((89, 116, 194)), Color((216, 163, 10)), Color((58, 115, 75)), Color((205, 205, 206)), Color((251, 198, 10)), Color((51, 43, 54))] + ([Color((255, 255, 255))] * 27) + +screen colorpickerscreen(item=None): + zorder 30 + modal True + + default is_cheating = config.developer or cheat_wardrobe_alpha + + if item: + default is_blacklisted = item.type.startswith(tuple(item.blacklist_unequip)) + default is_allowed = item.type.startswith(("makeup", "tattoo")) + + default transparency = not is_blacklisted and item and (is_allowed or is_cheating) + + if config.developer: + vbox: + ypos 100 + spacing 0 + text "Hue:[cp.hue]" + text "Sat:[cp.saturation]" + text "Val:[cp.value]" + text "Alpha:[cp.alpha]" + + frame: + style_prefix "colorpicker" + background "cp_frame" + align (0.5, 0.5) + padding (18, 18) + + has vbox + + # hbox: + # for i in colorpicker.history: + # add i xysize (12, 12) + + if item: + $ layers = [Color(tuple(i)) for i in item.color] + + hbox: + spacing 4 + for i, col in enumerate(layers): + button: + xysize (32, 32) + background col + foreground "cp_borders" + action Return(["layer", i]) + alternate Return(["replace", col]) + + hbox: + vbox: + frame: + add cp.cpsv xysize (255, 255) + + if transparency: + frame: + add cp.cpa xysize (255, 30) + + frame: + add cp.cph xysize (30, 255) + + vbox: + hbox: + spacing 4 + frame: + add cp.cpp xysize (28, 64) + frame: + button: + xysize (28, 64) + background cp.start_color + action Return(["replace", cp.start_color]) + frame: + button: + xysize (28, 64) + background cp.default_color + action Return(["replace", cp.default_color]) + + grid 5 7: + xalign 0.5 + spacing 4 + + for i, col in enumerate(colorpicker.favorites): + frame: + button: + xysize (12, 12) + background col + action Return(["replace", col]) + alternate Function(cp.add_favorite, i, cp.live_color) + + textbutton "Finish": + align (1.0, 1.0) + action Return(["finish", cp.live_color]) + +style colorpicker_button_text: + outlines [] + +style colorpicker_frame: + background "cp_borders" + padding (3, 3) + +style colorpicker_vbox: + spacing 22 + +style colorpicker_hbox: + spacing 22 + +image cp_frame = ConditionSwitch( + "not game.daytime", Frame("gui/dark_frame.png", 8, 8), + "True", Frame("gui/light_frame.png", 8, 8) + ) + +image cp_borders = ConditionSwitch( + "not game.daytime", Frame("interface/color_picker/gray/cursor_sq.webp", 3, 3), + "True", Frame("interface/color_picker/gold/cursor_sq.webp", 3, 3) + ) + +image cph_cursor = ConditionSwitch( + "not game.daytime", "interface/color_picker/gray/cursor_h.webp", + "True", "interface/color_picker/gold/cursor_h.webp" + ) + +image cpa_cursor = ConditionSwitch( + "not game.daytime", "interface/color_picker/gray/cursor_v.webp", + "True", "interface/color_picker/gold/cursor_v.webp" + ) + +image cpsv_cursor = ConditionSwitch( + "not game.daytime", "interface/color_picker/gray/cursor_sq.webp", + "True", "interface/color_picker/gold/cursor_sq.webp" + )