init -10 python in draw_logic: import os import io import math import hashlib import store import enum import pygame from os import path SAVE_FOLDER = "drawings" if renpy.android and not renpy.config.developer: #todo: make sure it saves properly in android if we're porting it SAVE_FOLDER = os.environ["ANDROID_PUBLIC"] GALLERY_REL_FOLDER = "_draw" COLOR_CIRCLE = "colors.png" DRAW_SAVE_NAME = "Drawing" DRAW_EXT = ".png" ERASER_STATE = False PENCOLOR = "#0f0f0d" PAPER = "#ede6e6" SHOWING_UI = True VERSION = (1, 1, 3) last_filename = "" last_drawing = 0 seed = 0x0 class _DrawGallery(store.Gallery): BACKGROUND = PAPER def __init__(self): super(_DrawGallery, self).__init__() self.update_pictures() def update_pictures(self): for pic in self._get_pictures(): name = pic.replace('/', '_') name = name.replace(' ', '_') name = name.replace('.', '_') if name in self.buttons: continue images = [] if self.BACKGROUND: images.append(self.BACKGROUND) images.append(pic) self.button(name) self.image(*images) @staticmethod def _get_pictures(update=True): if update: renpy.loader.cleardirfiles() _fold = path.normpath(GALLERY_REL_FOLDER) for renpy_fn in renpy.list_files(): fn = path.normpath(renpy_fn) _fn, ext = path.splitext(fn) ext = ext.strip().lower() if ext not in (".jpg", ".jpeg", ".png", ".webp"): continue _dir = path.dirname(fn) while True: if _dir == _fold: yield renpy_fn break if not _dir: break _dir = path.dirname(_dir) def get_buttons(self): zoom_size = float(renpy.config.screen_width) * .15 for name, button in self.buttons.items(): disp = button.images[0].displayables[-1] w, _h = map(float, Draw._get_size(disp)) zoom = zoom_size / w disp = store.Transform(disp, zoom=zoom) yield self.make_button(name, disp) draw_gallery = _DrawGallery() class Point(object): def __init__(self, x, y, st, color, width): self.__x, self.__y = map(int, (x, y)) self.__st = abs(float(st)) self.__color = renpy.color.Color(color) self.__width = abs(int(width)) @property def x(self): return self.__x @property def y(self): return self.__y @property def st(self): return self.__st @property def color(self): return self.__color @property def width(self): return self.__width class ActionRequest(enum.Flag): NOTIFY = enum.auto() SAVE = enum.auto() ADD_TO_GALLERY = enum.auto() SKIP = enum.auto() class Draw(renpy.Displayable): DRAW_BUTTON = 1 def __init__(self, background, reference=None, **properties): super(Draw, self).__init__(**properties) self.__background = self._get_displayable(background) self.__is_pressed = False self.__curves = [] self.__active_curve = None self.__delete_log = [] self.__color = renpy.color.Color("#0f0f0d") self.__width = 10 if reference is not None: self.__reference = self._get_displayable(reference) else: self.__reference = None self.__show_reference = False self.__size = None self.__action_request = False self.__end_interact_request = False @classmethod def main( cls, background=None, reference=None, start_width=None, start_color=None, last_drawing_index=0, seed_index=0, has_drawn=False, **transform_prop ): global last_drawing, seed, bg last_drawing = last_drawing_index seed = seed_index #renpy.notify("Use Mouse left click to draw.") if background is None: background = "#ede6e6" bg = background if transform_prop: background = store.Transform(background, **transform_prop) draw_object = cls(background, reference) if (renpy.is_skipping()): #print('ooo') if has_drawn: draw_object.skip() #todo: fallback or detect if an image is already made if start_width: draw_object.set_width(start_width) if start_color: draw_object.set_color(start_color) _screen_name = "_draw_screen" renpy.mode("screen") renpy.show_screen(_screen_name, draw_object, _transient=True) roll_forward = renpy.roll_forward_info() try: rv = renpy.ui.interact( mouse="screen", type="screen", suppress_overlay=True, suppress_window=True, roll_forward=roll_forward ) except (renpy.game.JumpException, renpy.game.CallException) as ex: rv = ex renpy.checkpoint(rv) _special_ex = (renpy.game.JumpException, renpy.game.CallException) if isinstance(rv, _special_ex): raise rv return rv def _disable(self): self.__end_interact_request = True renpy.redraw(self, .0) @staticmethod def _get_displayable(data): result = renpy.displayable(data) if not isinstance(result, renpy.display.core.Displayable): raise ValueError("{0} isn't a displayable.".format(data)) return result @classmethod def _get_size(cls, data): disp = cls._get_displayable(data) rend = renpy.display.render.render_for_size( disp, renpy.config.screen_width, renpy.config.screen_height, .0, .0 ) return tuple(map(int, rend.get_size())) @staticmethod def _save_canvas(canvas): global last_filename global rawImageBytes folder = SAVE_FOLDER w, h = tuple(map(int, canvas.get_surface().get_size())) canvas_surf = pygame.Surface((w,h), pygame.SRCALPHA, 32) if bg == "#ede6e6": canvas_surf.fill(pygame.Color(237, 230, 230, 255)) canvas_drawing = canvas.get_surface() canvas_surf.blit(canvas_drawing, (0, 0)) _counter = last_drawing #time.time_ns() #timestamp fn = path.join( renpy.config.gamedir, folder, "{0}_{1}_{2}{3}".format(DRAW_SAVE_NAME, seed, _counter, DRAW_EXT) ) _fn = path.join( folder, "{0}_{1}_{2}{3}".format(DRAW_SAVE_NAME, seed, _counter, DRAW_EXT) ) #My precious, simple implementation RUINED by the fact that ren'py #uses pygame_sdl2 rather than normal pygame... #pygam_sdl2 is missing pygame.image.tobytes, and only has the .save, so we have to do #some really gross shit. #See this for proof: https://github.com/renpy/pygame_sdl2/blob/master/src/pygame_sdl2/image.pyx #If .tobytes ever appears, use the following single line: #rawImageBytes = pygame.image.tobytes(canvas_surf, png) pygame.image.save(canvas_surf, renpy.fsencode(fn)) with open(fn, "rb") as _file: _data = _file.read() rawImageBytes = _data # We save the transparent to memory and the opaque to storage if bg == "nothing": canvas_surf.fill(pygame.Color(237, 230, 230, 255)) canvas_drawing = canvas.get_surface() canvas_surf.blit(canvas_drawing, (0, 0)) pygame.image.save(canvas_surf, renpy.fsencode(fn)) last_filename = _fn return fn @classmethod def add_canvas_in_gallery(cls, canvas): fold = path.abspath( path.join(renpy.config.gamedir, GALLERY_REL_FOLDER) ) fn = cls._save_canvas(canvas, fold) draw_gallery.update_pictures() return fn @classmethod def get_canvas_as_disp(cls, canvas): #fn = cls._save_canvas(canvas) # with open(fn, "rb") as _file: #_data = _file.read() # os.remove(fn) # _hash = hashlib.sha512(_data) return False #return renpy.display.im.Data( # _data, # "{0}{1}".format(_hash.hexdigest(), DRAW_EXT) #) @property def reference(self): return self.__reference @property def reference_switcher(self): return self.__show_reference @reference_switcher.setter def reference_switcher(self, new_value): self.__show_reference = bool(new_value) renpy.redraw(self, .0) @property def width(self): return self.__width @property def eraser_state(self): return self.__eraser_state @width.setter def width(self, new_width): self.__width = int(new_width) def draw_all(self, canvas): for curve in self.__curves: if not curve: continue elif len(curve) == 1: point = curve[0] canvas.circle( point.color, (point.x, point.y), (point.width // 2) ) else: prev = None for point in curve: if prev: canvas.line( prev.color, (prev.x, prev.y), (point.x, point.y), prev.width ) prev = point def add_point(self, x, y, st): point = Point(x, y, st, self.__color, self.__width) if self.__active_curve is None: self.__active_curve = [] self.__curves.append(self.__active_curve) self.__delete_log.clear() self.__active_curve.append(point) renpy.redraw(self, .0) def add_in_gallery(self, notify=False): rq = ActionRequest.ADD_TO_GALLERY if notify: rq |= ActionRequest.NOTIFY self.__action_request = rq renpy.redraw(self, .0) def skip(self): self._disable() def save(self, notify=False): rq = ActionRequest.SAVE if notify: rq |= ActionRequest.NOTIFY renpy.store.persistent.drawn = 1 self.__action_request = rq renpy.redraw(self, .0) def back(self): self.__is_pressed = False self.__active_curve = None if len(self.__delete_log) != 0 and isinstance(self.__delete_log[-1], tuple): self.__curves = list(self.__delete_log.pop(-1)) elif self.__curves: curve = self.__curves.pop(-1) self.__delete_log.append(curve) renpy.redraw(self, .0) def forward(self): self.__is_pressed = False self.__active_curve = None if self.__delete_log: curve = self.__delete_log.pop(-1) self.__curves.append(curve) renpy.redraw(self, .0) def clear_all(self): self.__is_pressed = False self.__active_curve = None self.__delete_log.append(tuple(self.__curves.copy())) self.__curves.clear() renpy.redraw(self, .0) def set_color(self, color): self.__color = renpy.color.Color(color) def set_width(self, width): self.width = width def set_eraser_state(self, eraser_state): self.__eraser_state = eraser_state def visit(self): return [self.__background] def event(self, ev, x, y, st): if not self.__size: return w, h = self.__size in_area = False if (0 <= x < w) and (0 <= y < h): in_area = True if in_area and (ev.type == pygame.MOUSEMOTION): if self.__is_pressed: self.add_point(x, y, st) elif in_area and (ev.type == pygame.MOUSEBUTTONDOWN): if ev.button == self.DRAW_BUTTON: self.__is_pressed = True if ERASER_STATE == False: self.set_color(PENCOLOR) self.set_width(10) else: self.set_color(PAPER) self.set_width(40) self.add_point(x, y, st) raise renpy.IgnoreEvent() elif ev.type == pygame.MOUSEBUTTONUP: if ev.button == self.DRAW_BUTTON: self.__is_pressed = False self.__active_curve = None elif ev.type == pygame.KEYDOWN and ev.key == pygame.K_z and pygame.key.get_mods() & pygame.KMOD_CTRL: self.back() def per_interact(self): self.__is_pressed = False self.__active_curve = None renpy.redraw(self, .0) def render(self, *rend_args): back = renpy.render(self.__background, *rend_args) w, h = self.__size = tuple(map(int, back.get_size())) result = renpy.Render(w, h) result.blit(back, (0, 0)) if self.reference and self.reference_switcher: rend = renpy.render(self.reference, *rend_args) x = (float(result.width) * .5) - (float(rend.width) * .5) y = (float(result.height) * .5) - (float(rend.height) * .5) x, y = map(int, (x, y)) result.blit(rend, (x, y)) canvas = result.canvas() self.draw_all(canvas) if self.__end_interact_request: self.__end_interact_request = False disp = self.get_canvas_as_disp(canvas) renpy.end_interaction(False) if self.__action_request: if self.__action_request & ActionRequest.SKIP: self._disable() elif self.__action_request & ActionRequest.SAVE: fn = self._save_canvas(canvas) if self.__action_request & ActionRequest.NOTIFY: renpy.notify(_("Saved draw as \"{0}\"").format(fn)) self._disable() if self.__action_request & ActionRequest.ADD_TO_GALLERY: self.add_canvas_in_gallery(canvas) if self.__action_request & ActionRequest.NOTIFY: renpy.notify(_("A draw added in gallery.")) self.__action_request = None return result renpy.config.allow_underfull_grids = True init 150 python in draw_logic: try: _circle = Draw._get_displayable(COLOR_CIRCLE) _circle.load() except Exception: COLOR_CIRCLE = "_placeholder/girl.png" else: del(_circle) init 0 python: import pygame_sdl2 as pygame class RawSurface(renpy.Displayable): def __init__(self, surface): renpy.Displayable.__init__(self) self.surface = surface # Render the displayable def render(self, width, height, st, at): r = renpy.Render(width, height) r.blit(self.surface, (0, 0)) return r # Redraw this at each interact def per_interact(self): renpy.redraw(self, 0) transform tf_draw_moveindown(p=0.0, scale=0.8): subpixel True alpha 0.0 zoom scale pause p ypos -50 parallel: easein_back 0.5 ypos 0 parallel: linear 0.25 alpha 1 transform tf_draw_moveoutup(p=0.0, scale=0.8): subpixel True alpha 1 zoom scale pause p ypos 0 parallel: easeout_back 0.5 ypos -50 parallel: linear 0.5 alpha 0.0 transform tf_draw_moveinright(p=0.0, scale=0.8): subpixel True alpha 0.0 zoom scale pause p xpos -50 parallel: easein_back 0.5 xpos 0 parallel: linear 0.25 alpha 1 transform tf_draw_moveoutleft(p=0.0, scale=0.8): subpixel True alpha 1.0 zoom scale pause p xpos 0 parallel: easeout_back 0.5 xpos -50 parallel: linear 0.5 alpha 0 transform tf_draw_fadetext(p=0): xalign 0.5 yalign 0.1 alpha 0.0 pause 0.5+p linear 0.5 alpha 1.0 pause 3 linear 0.5 alpha 0.0 screen _draw_screen(draw_object): modal True key "h" action SetVariable("draw_logic.SHOWING_UI", not draw_logic.SHOWING_UI) fixed: xfill True yfill True add draw_object: align (.5, .5) fixed: style_prefix "draw_menu" # style "draw_tools_frame" hbox: ypos 10 xpos 10 spacing 15 use draw_tool_button("gui/button/drawing/pencil.png",False,draw_logic.SHOWING_UI,0.0) use draw_tool_button("gui/button/drawing/eraser.png",True,draw_logic.SHOWING_UI,0.2) # frame: # style "tooltip_frame" # label _("Mouse left click \n to draw.\n") vbox: yalign 1.0 xalign 0.0 use draw_menu_buttons("gui/button/menubuttons/menu_button.png", draw_logic.SHOWING_UI, [ [ _("Skip"), Function(draw_object.skip), 0.0, type(waniDemoCarryover.Chapter3Drawing) == bytes], [ _("Undo"), Function(draw_object.back), 0.1, True], [ _("Clear All"), Function(draw_object.clear_all), 0.2, True], [ _("Done"), Function(draw_object.save, False), 0.3, True] ] ) if renpy.android: use draw_menu_button("gui/button/menubuttons/menu_button.png",_("Toggle UI"), SetVariable("draw_logic.SHOWING_UI", not draw_logic.SHOWING_UI), 0.4, True) #if draw_object.reference: # textbutton (_("Hide reference") if draw_object.reference_switcher else _("Show reference")): # action ToggleField(draw_object, "reference_switcher", True, False) if persistent.drawn == 0: if renpy.android: text _("Use your finger to draw.") at tf_draw_fadetext: color "#000000" else: text _("Use left click to draw.") at tf_draw_fadetext: color "#000000" text _("Press 'H' to hide the drawing UI.") at tf_draw_fadetext(4): color "#000000" screen draw_tool_button(icon,is_eraser,showing, delay): if renpy.android: $ scale = 1.0 else: $ scale = 0.8 if showing: $ animation = tf_draw_moveindown(delay,scale) else: $ animation = tf_draw_moveoutup(delay,scale) # I can't believe there's not a less hacky way to stack images in renpy vbox: spacing -111 # This is what layers the tool icon ontop of the button. This was frankly eyeballed and I don't know why it has to be set to this value to look right. imagebutton at animation: sensitive showing idle "gui/button/drawing/drawbutton_idle.png" hover "gui/button/drawing/drawbutton_idle.png" selected_idle "gui/button/drawing/drawbutton_pressed.png" selected_hover "gui/button/drawing/drawbutton_pressed.png" activate_sound "audio/ui/snd_ui_click.wav" action SetVariable("draw_logic.ERASER_STATE", is_eraser) add icon at animation screen draw_menu_buttons(filename, showing, label_functions): for l_f in label_functions: if l_f[3]: use draw_menu_button(filename, l_f[0], l_f[1], l_f[2], showing) screen draw_menu_button(filename, label, function, delay, showing): if renpy.android: $ scale = 1.0 else: $ scale = 0.8 if showing: $ animation = tf_draw_moveinright(delay,scale) else: $ animation = tf_draw_moveoutleft(delay,scale) button at animation: sensitive showing xmaximum 250 ymaximum 100 action function if 'Done' in label: activate_sound "audio/ui/snd_ui_click.wav" else: activate_sound "audio/ui/snd_ui_back.wav" fixed: add filename zoom 0.8 xcenter 0.5 ycenter 0.5 text label xalign 0.5 yalign 0.5 size 30 #change this for better bg, for some reason renpy cannot take a defined image here, only a file path wtf #style draw_menu_frame is default: # background "gui/main_menu.png" style draw_menu_text is gui_text style draw_menu_title is draw_menu_text style draw_menu_version is draw_menu_text style draw_menu_text: properties gui.text_properties("main_menu") #, accent=True) color gui.main_menu_color hover_color gui.hover_color size gui.main_menu_text_size top_padding 170 style draw_menu_title: properties gui.text_properties("title") style draw_menu_ex is draw_menu style draw_menu_ex_vbox is draw_menu_vbox style draw_menu_ex_text is draw_menu_text style draw_menu_ex_frame is draw_menu_frame