diff options
author | Alex Xu <alex_y_xu@yahoo.ca> | 2013-08-22 22:45:26 -0400 |
---|---|---|
committer | Alex Xu <alex_y_xu@yahoo.ca> | 2013-08-22 22:45:26 -0400 |
commit | 718936110b9511631fa1f4396be992752bf8b719 (patch) | |
tree | a871768c06adc2959f8f0d69869532d36a95ffab /unrpyc/renpy/display | |
parent | ece6cf9fbfdba9dac8d7bf98516a840c955a4853 (diff) | |
download | html5ks-718936110b9511631fa1f4396be992752bf8b719.tar.xz html5ks-718936110b9511631fa1f4396be992752bf8b719.zip |
include renpy
Diffstat (limited to 'unrpyc/renpy/display')
29 files changed, 18490 insertions, 0 deletions
diff --git a/unrpyc/renpy/display/__init__.py b/unrpyc/renpy/display/__init__.py new file mode 100644 index 0000000..055dad6 --- /dev/null +++ b/unrpyc/renpy/display/__init__.py @@ -0,0 +1,39 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import renpy.log + +# The draw object through which all drawing is routed. This object +# contains all of the distinction between the software and GL +# renderers. +draw = None + +# The interface object. +interface = None + +# Should we disable imagedissolve-type transitions? +less_imagedissolve = False + +# Logs we use. +log = renpy.log.open("log", developer=False, append=False) +ic_log = renpy.log.open("image_cache", developer=True, append=False) +to_log = renpy.log.open("text_overflow", developer=True, append=True) + diff --git a/unrpyc/renpy/display/accelerator.pyx b/unrpyc/renpy/display/accelerator.pyx new file mode 100644 index 0000000..8c4172c --- /dev/null +++ b/unrpyc/renpy/display/accelerator.pyx @@ -0,0 +1,296 @@ +#cython: profile=False +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import renpy +import math +from renpy.display.render cimport Render, Matrix2D, render + + +################################################################################ +# Surface copying +################################################################################ + +from pygame cimport * + +def nogil_copy(src, dest): + """ + Does a gil-less blit of src to dest, with minimal locking. + """ + + cdef SDL_Surface *src_surf + cdef SDL_Surface *dst_surf + + src_surf = PySurface_AsSurface(src) + dest_surf = PySurface_AsSurface(dest) + + old_alpha = src_surf.flags & SDL_SRCALPHA + + if old_alpha: + SDL_SetAlpha(src_surf, 0, 255) + + with nogil: + SDL_BlitSurface(src_surf, NULL, dest_surf, NULL) + + if old_alpha: + SDL_SetAlpha(src_surf, SDL_SRCALPHA, 255) + + + +################################################################################ +# Transform render function +################################################################################ + +cdef Matrix2D IDENTITY +IDENTITY = renpy.display.render.IDENTITY + +# This file contains implementations of methods of classes that +# are found in other files, for performance reasons. + +def transform_render(self, widtho, heighto, st, at): + + cdef double rxdx, rxdy, rydx, rydy + cdef double cosa, sina + cdef double xo, x1, x2, x3, px + cdef double yo, y1, y2, y3, py + cdef float zoom, xzoom, yzoom + cdef double cw, ch, nw, nh + cdef Render rv, cr + cdef double angle + cdef double alpha + cdef double width = widtho + cdef double height = heighto + + # Should we perform clipping? + clipping = False + + # Prevent time from ticking backwards, as can happen if we replace a + # transform but keep its state. + if st + self.st_offset <= self.st: + self.st_offset = self.st - st + if at + self.at_offset <= self.at: + self.at_offset = self.at - at + + self.st = st = st + self.st_offset + self.at = at = at + self.at_offset + + # Update the state. + self.update_state() + + # Render the child. + child = self.child + + if child is None: + raise Exception("Transform does not have a child.") + + state = self.state + + if state.size: + widtho, heighto = state.size + + cr = render(child, widtho, heighto, st - self.child_st_base, at) + + width = cr.width + height = cr.height + + self.child_size = width, height + + # The reverse matrix. + rxdx = 1 + rxdy = 0 + rydx = 0 + rydy = 1 + + xo = 0 + yo = 0 + + # Cropping. + crop = state.crop + if (state.corner1 is not None) and (crop is None) and (state.corner2 is not None): + x1, y1 = state.corner1 + x2, y2 = state.corner2 + + if x1 > x2: + x3 = x1 + x1 = x2 + x2 = x3 + if y1 > y2: + y3 = y1 + y1 = y2 + y2 = y3 + + crop = (x1, y1, x2-x1, y2-y1) + + if crop is not None: + + negative_xo, negative_yo, width, height = crop + + if state.rotate: + clipcr = Render(width, height) + clipcr.subpixel_blit(cr, (-negative_xo, -negative_yo)) + clipcr.clipping = True + cr = clipcr + else: + xo = -negative_xo + yo = -negative_yo + clipping = True + + # Size. + size = state.size + if (size is not None) and (size != (width, height)): + nw, nh = size + xzoom = 1.0 * nw / width + yzoom = 1.0 * nh / height + + rxdx = xzoom + rydy = yzoom + + xo *= xzoom + yo *= yzoom + + width, height = size + + # zoom + zoom = state.zoom + xzoom = zoom * <double> state.xzoom + yzoom = zoom * <double> state.yzoom + + if xzoom != 1: + + rxdx *= xzoom + + if xzoom < 0: + width *= -xzoom + else: + width *= xzoom + + xo *= xzoom + # origin corrections for flipping + if xzoom < 0: + xo += width + + if yzoom != 1: + + rydy *= yzoom + + if yzoom < 0: + height *= -yzoom + else: + height *= yzoom + + yo *= yzoom + # origin corrections for flipping + if yzoom < 0: + yo += height + + # Rotation. + rotate = state.rotate + if rotate is not None: + + cw = width + ch = height + + angle = rotate * 3.1415926535897931 / 180 + + cosa = math.cos(angle) + sina = math.sin(angle) + + # reverse = Matrix2D(xdx, xdy, ydx, ydy) * reverse + + # We know that at this point, rxdy and rydx are both 0, so + # we can simplify these formulae a bit. + rxdy = rydy * -sina + rydx = rxdx * sina + rxdx *= cosa + rydy *= cosa + + # first corner point (changes with flipping) + px = cw / 2.0 + if xzoom < 0: + px = -px + py = ch / 2.0 + if yzoom < 0: + py = -py + + if state.rotate_pad: + width = height = math.hypot(cw, ch) + + xo = -px * cosa + py * sina + yo = -px * sina - py * cosa + + else: + xo = -px * cosa + py * sina + yo = -px * sina - py * cosa + + x2 = -px * cosa - py * sina + y2 = -px * sina + py * cosa + + x3 = px * cosa - py * sina + y3 = px * sina + py * cosa + + x4 = px * cosa + py * sina + y4 = px * sina - py * cosa + + width = max(xo, x2, x3, x4) - min(xo, x2, x3, x4) + height = max(yo, y2, y3, y4) - min(yo, y2, y3, y4) + + xo += width / 2.0 + yo += height / 2.0 + + alpha = state.alpha + + rv = Render(width, height) + + # Default case - no transformation matrix. + if rxdx == 1 and rxdy == 0 and rydx == 0 and rydy == 1: + self.forward = IDENTITY + self.reverse = IDENTITY + + else: + + self.reverse = rv.reverse = Matrix2D(rxdx, rxdy, rydx, rydy) + + inv_det = rxdx * rydy - rxdy * rydx + + if not inv_det: + self.forward = rv.forward = Matrix2D(0, 0, 0, 0) + else: + self.forward = rv.forward = Matrix2D( + rydy / inv_det, + -rxdy / inv_det, + -rydx / inv_det, + rxdx / inv_det) + + rv.alpha = alpha + rv.clipping = clipping + + pos = (xo, yo) + + if state.subpixel: + rv.subpixel_blit(cr, pos) + else: + rv.blit(cr, pos) + + self.offsets = [ pos ] + self.render_size = (width, height) + + return rv + diff --git a/unrpyc/renpy/display/anim.py b/unrpyc/renpy/display/anim.py new file mode 100644 index 0000000..de1398a --- /dev/null +++ b/unrpyc/renpy/display/anim.py @@ -0,0 +1,634 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains support for state-machine controlled animations. + +import renpy.display +import random + +class State(object): + """ + This creates a state that can be used in a SMAnimation. + """ + + + def __init__(self, name, image, *atlist, **properties): + """ + @param name: A string giving the name of this state. + + @param image: The displayable that is shown to the user while + we are in (entering) this state. For convenience, this can + also be a string or tuple, which is interpreted with Image. + + image should be None when this State is used with motion, + to indicate that the image will be replaced with the child of + the motion. + + @param atlist: A list of functions to call on the image. (In + general, if something can be used in an at clause, it can be + used here as well.) + + If any keyword arguments are given, they are used to construct a + Position object, that modifies the position of the image. + """ + + if image and not isinstance(image, renpy.display.core.Displayable): + image = renpy.easy.displayable(image) + + self.name = name + self.image = image + self.atlist = atlist + self.properties = properties + + + def add(self, sma): + sma.states[self.name] = self + + def get_image(self): + rv = self.image + + for i in self.atlist: + rv = i(rv) + + if self.properties: + rv = renpy.display.layout.Position(rv, **self.properties) + + return rv + + def motion_copy(self, child): + + if self.image is not None: + child = self.image + + return State(self.name, child, *self.atlist) + + +class Edge(object): + """ + This creates an edge that can be used with a SMAnimation. + """ + + def __init__(self, old, delay, new, trans=None, prob=1): + """ + @param old: The name (a string) of the state that this transition is from. + + @param delay: The number of seconds that this transition takes. + + @param new: The name (a string) of the state that this transition is to. + + @param trans: The transition that will be used to show the + image found in the new state. If None, the image is show + immediately. + + When used with an SMMotion, the transition should probably be + move. + + @param prob: The number of times this edge is added. This can + be used to make a transition more probable then others. For + example, if one transition out of a state has prob=5, and the + other has prob=1, then the one with prob=5 will execute 5/6 of + the time, while the one with prob=1 will only occur 1/6 of the + time. (Don't make this too large, as memory use is proportional to + this value.) + """ + + self.old = old + self.delay = delay + self.new = new + self.trans = trans + self.prob = prob + + def add(self, sma): + for _i in range(0, self.prob): + sma.edges.setdefault(self.old, []).append(self) + + +class SMAnimation(renpy.display.core.Displayable): + """ + This creates a state-machine animation. Such an animation is + created by randomly traversing the edges between states in a + defined state machine. Each state corresponds to an image shown to + the user, with the edges corresponding to the amount of time an + image is shown, and the transition it is shown with. + + Images are shown, perhaps with a transition, when we are + transitioning into a state containing that image. + """ + + def __init__(self, initial, *args, **properties): + """ + @param initial: The name (a string) of the initial state we + start in. + + @param showold: If the keyword parameter showold is True, then + the old image is shown instead of the new image when in an + edge. + + @param anim_timebase: If True, we use the animation + timebase. If False, we use the displayable timebase. + + This accepts as additional arguments the anim.State and + anim.Edge objects that are used to make up this state + machine. + """ + + if 'delay' in properties: + self.delay = properties['delay'] + del properties['delay'] + else: + self.delay = None + + if 'showold' in properties: + self.showold = properties['showold'] + del properties['showold'] + else: + self.showold = False + + if 'anim_timebase' in properties: + self.anim_timebase = properties['anim_timebase'] + del properties['anim_timebase'] + else: + self.anim_timebase = True + + super(SMAnimation, self).__init__(**properties) + + self.properties = properties + + # The initial state. + self.initial = initial + + # A map from state name to State object. + self.states = { } + + # A map from state name to list of Edge objects. + self.edges = { } + + for i in args: + i.add(self) + + # The time at which the current edge started. If None, will be + # set to st by render. + self.edge_start = None + + # A cache for what the current edge looks like when rendered. + self.edge_cache = None + + # The current edge. + self.edge = None + + # The state we're in. + self.state = None + + def visit(self): + return [ i.image for i in self.states.values() ] + + def pick_edge(self, state): + """ + This randomly picks an edge out of the given state, if + one exists. It updates self.edge if a transition has + been selected, or returns None if none can be found. It also + updates self.image to be the new image on the selected edge. + """ + + if state not in self.edges: + self.edge = None + return + + edges = self.edges[state] + self.edge = random.choice(edges) + self.state = self.edge.new + + def update_cache(self): + """ + Places the correct Displayable into the edge cache, based on + what is contained in the given edge. This takes into account + the old and new states, and any transition that is present. + """ + + + if self.edge.trans: + im = self.edge.trans(old_widget=self.states[self.edge.old].get_image(), + new_widget=self.states[self.edge.new].get_image()) + elif self.showold: + im = self.states[self.edge.old].get_image() + else: + im = self.states[self.edge.new].get_image() + + self.edge_cache = im + + def get_placement(self): + + if self.edge_cache: + return self.edge_cache.get_placement() + + if self.state: + return self.states[self.state].get_image().get_placement() + + return super(SMAnimation, self).get_placement() + + def render(self, width, height, st, at): + + if self.anim_timebase: + t = at + else: + t = st + + if self.edge_start is None or t < self.edge_start: + self.edge_start = t + self.edge_cache = None + self.pick_edge(self.initial) + + while self.edge and t > self.edge_start + self.edge.delay: + self.edge_start += self.edge.delay + self.edge_cache = None + self.pick_edge(self.edge.new) + + # If edge is None, then we have a permanent, static picture. Deal + # with that. + + if not self.edge: + im = renpy.display.render.render(self.states[self.state].get_image(), + width, height, + st - self.edge_start, at) + + + # Otherwise, we have another edge. + + else: + if not self.edge_cache: + self.update_cache() + + im = renpy.display.render.render(self.edge_cache, width, height, t - self.edge_start, at) + + if not renpy.game.less_updates: + renpy.display.render.redraw(self.edge_cache, self.edge.delay - (t - self.edge_start)) + + + iw, ih = im.get_size() + + rv = renpy.display.render.Render(iw, ih) + rv.blit(im, (0, 0)) + + return rv + + def __call__(self, child=None, new_widget=None, old_widget=None): + """ + Used when this SMAnimation is used as a SMMotion. This creates + a duplicate of the animation, with all states containing None + as the image having that None replaced with the image that is provided here. + """ + + if child is None: + child = new_widget + + args = [ ] + + for state in self.states.values(): + args.append(state.motion_copy(child)) + + for edges in self.edges.values(): + args.extend(edges) + + return SMAnimation(self.initial, delay=self.delay, *args, **self.properties) + + +# class Animation(renpy.display.core.Displayable): +# """ +# A Displayable that draws an animation, which is a series of images +# that are displayed with time delays between them. +# """ + +# def __init__(self, *args, **properties): +# """ +# Odd (first, third, fifth, etc.) arguments to Animation are +# interpreted as image filenames, while even arguments are the +# time to delay between each image. If the number of arguments +# is odd, the animation will stop with the last image (well, +# actually delay for a year before looping). Otherwise, the +# animation will restart after the final delay time. + +# @param anim_timebase: If True, the default, use the animation +# timebase. Otherwise, use the displayable timebase. +# """ + +# properties.setdefault('style', 'animation') +# self.anim_timebase = properties.pop('anim_timebase', True) + +# super(Animation, self).__init__(**properties) + +# self.images = [ ] +# self.delays = [ ] + +# for i, arg in enumerate(args): + +# if i % 2 == 0: +# self.images.append(renpy.easy.displayable(arg)) +# else: +# self.delays.append(arg) + +# if len(self.images) > len(self.delays): +# self.delays.append(365.25 * 86400.0) # One year, give or take. + +# def render(self, width, height, st, at): + +# if self.anim_timebase: +# t = at % sum(self.delays) +# else: +# t = st % sum(self.delays) + +# for image, delay in zip(self.images, self.delays): +# if t < delay: +# renpy.display.render.redraw(self, delay - t) + +# im = renpy.display.render.render(image, width, height, t, at) +# width, height = im.get_size() +# rv = renpy.display.render.Render(width, height) +# rv.blit(im, (0, 0)) + +# return rv + +# else: +# t = t - delay + +# def visit(self): +# return self.images + +def Animation(*args, **kwargs): + newargs = [ ] + + for i, a in enumerate(args): + newargs.append(a) + if i % 2 == 1: + newargs.append(None) + + return TransitionAnimation(*newargs, **kwargs) + + +class TransitionAnimation(renpy.display.core.Displayable): + """ + A displayable that draws an animation with each frame separated + by a transition. + """ + + def __init__(self, *args, **properties): + """ + This takes arguments such that the 1st, 4th, 7th, ... + arguments are displayables, the 2nd, 5th, 8th, ... on arguments + are times, and the 3rd, 6th, 9th, ... are transitions. + + This displays the first displayable for the given time, then + transitions to the second displayable using the given + transition, and shows it for the given time (the time of the + transition is taken out of the time the frame is shown), and + so on. + + The last argument may be a displayable (in which case that + displayable is used to transition back to the first frame), or + a displayable (which is shown forever). + + There is one keyword argument, apart from the style properties: + + @param anim_timebase: If True, the default, use the animation + timebase. Otherwise, use the displayable timebase. + """ + + properties.setdefault('style', 'animation') + self.anim_timebase = properties.pop('anim_timebase', True) + + super(TransitionAnimation, self).__init__(**properties) + + images = [ ] + delays = [ ] + transitions = [ ] + + for i, arg in enumerate(args): + + if i % 3 == 0: + images.append(renpy.easy.displayable(arg)) + elif i % 3 == 1: + delays.append(arg) + else: + transitions.append(arg) + + if len(images) > len(delays): + delays.append(365.25 * 86400.0) # One year, give or take. + if len(images) > len(transitions): + transitions.append(None) + + self.images = images + self.prev_images = [ images[-1] ] + images[:-1] + self.delays = delays + self.transitions = [ transitions[-1] ] + transitions[:-1] + + + def render(self, width, height, st, at): + + if self.anim_timebase: + orig_t = at + else: + orig_t = st + + t = orig_t % sum(self.delays) + + for image, prev, delay, trans in zip(self.images, self.prev_images, self.delays, self.transitions): + if t < delay: + if not renpy.game.less_updates: + renpy.display.render.redraw(self, delay - t) + + if trans and orig_t >= self.delays[0]: + image = trans(old_widget=prev, new_widget=image) + + im = renpy.display.render.render(image, width, height, t, at) + width, height = im.get_size() + rv = renpy.display.render.Render(width, height) + rv.blit(im, (0, 0)) + + return rv + + else: + t = t - delay + + def visit(self): + return self.images + +class Blink(renpy.display.core.Displayable): + """ + """ + + def __init__(self, image, on=0.5, off=0.5, rise=0.5, set=0.5, #@ReservedAssignment + high=1.0, low=0.0, offset=0.0, anim_timebase=False, **properties): + + """ + This takes as an argument an image or widget, and blinks that image + by varying its alpha. The sequence of phases is + on - set - off - rise - on - ... All times are given in seconds, all + alphas are fractions between 0 and 1. + + @param image: The image or widget that will be blinked. + + @param on: The amount of time the widget spends on, at high alpha. + + @param off: The amount of time the widget spends off, at low alpha. + + @param rise: The amount time the widget takes to ramp from low to high alpha. + + @param set: The amount of time the widget takes to ram from high to low. + + @param high: The high alpha. + + @param low: The low alpha. + + @param offset: A time offset, in seconds. Use this to have a + blink that does not start at the start of the on phase. + + @param anim_timebase: If True, use the animation timebase, if false, the displayable timebase. + """ + + super(Blink, self).__init__(**properties) + + self.image = renpy.easy.displayable(image) + self.on = on + self.off = off + self.rise = rise + self.set = set + self.high = high + self.low = low + self.offset = offset + self.anim_timebase = anim_timebase + + self.cycle = on + set + off + rise + + + def visit(self): + return [ self.image ] + + def render(self, height, width, st, at): + + if self.anim_timebase: + t = at + else: + t = st + + time = (self.offset + t) % self.cycle + alpha = self.high + + if 0 <= time < self.on: + delay = self.on - time + alpha = self.high + + time -= self.on + + if 0 <= time < self.set: + delay = 0 + frac = time / self.set + alpha = self.low * frac + self.high * (1.0 - frac) + + time -= self.set + + if 0 <= time < self.off: + delay = self.off - time + alpha = self.low + + time -= self.off + + if 0 <= time < self.rise: + delay = 0 + frac = time / self.rise + alpha = self.high * frac + self.low * (1.0 - frac) + + + rend = renpy.display.render.render(self.image, height, width, st, at) + w, h = rend.get_size() + rv = renpy.display.render.Render(w, h) + + rv.blit(rend, (0, 0)) + rv.alpha = alpha + + if not renpy.game.less_updates: + renpy.display.render.redraw(self, delay) + + return rv + + + +def Filmstrip(image, framesize, gridsize, delay, frames=None, loop=True, **properties): + """ + This creates an animation from a single image. This image + must consist of a grid of frames, with the number of columns and + rows in the grid being taken from gridsize, and the size of each + frame in the grid being taken from framesize. This takes frames + and sticks them into an Animation, with the given delay between + each frame. The frames are taken by going from left-to-right + across the first row, left-to-right across the second row, and + so on until all frames are consumed, or a specified number of + frames are taken. + + @param image: The image that the frames must be taken from. + + @param framesize: A (width, height) tuple giving the size of + each of the frames in the animation. + + @param gridsize: A (columns, rows) tuple giving the number of + columns and rows in the grid. + + @param delay: The delay, in seconds, between frames. + + @param frames: The number of frames in this animation. If None, + then this defaults to colums * rows frames, that is, taking + every frame in the grid. + + @param loop: If True, loop at the end of the animation. If False, + this performs the animation once, and then stops. + + Other keyword arguments are as for anim.SMAnimation. + """ + + width, height = framesize + cols, rows = gridsize + + if frames is None: + frames = cols * rows + + i = 0 + + # Arguments to Animation + args = [ ] + + for r in range(0, rows): + for c in range(0, cols): + + x = c * width + y = r * height + + args.append(renpy.display.im.Crop(image, x, y, width, height)) + args.append(delay) + + i += 1 + if i == frames: + break + + if i == frames: + break + + if not loop: + args.pop() + + return Animation(*args, **properties) diff --git a/unrpyc/renpy/display/behavior.py b/unrpyc/renpy/display/behavior.py new file mode 100644 index 0000000..02eccf2 --- /dev/null +++ b/unrpyc/renpy/display/behavior.py @@ -0,0 +1,1531 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This contains various Displayables that handle events. + + +import renpy.display +import renpy.audio + +from renpy.display.render import render, Render + +import pygame + +def compile_event(key, keydown): + """ + Compiles a keymap entry into a python expression. + + keydown determines if we are dealing with keys going down (press), + or keys going up (release). + """ + + # Lists or tuples get turned into or expressions. + if isinstance(key, (list, tuple)): + if not key: + return "(False)" + + return "(" + " or ".join([compile_event(i, keydown) for i in key]) + ")" + + # If it's in config.keymap, compile what's in config.keymap. + if key in renpy.config.keymap: + return compile_event(renpy.config.keymap[key], keydown) + + if key is None: + return "(False)" + + part = key.split("_") + + # Deal with the mouse. + if part[0] == "mousedown": + if keydown: + return "(ev.type == %d and ev.button == %d)" % (pygame.MOUSEBUTTONDOWN, int(part[1])) + else: + return "(False)" + + if part[0] == "mouseup": + if keydown: + return "(ev.type == %d and ev.button == %d)" % (pygame.MOUSEBUTTONUP, int(part[1])) + else: + return "(False)" + + # Deal with the Joystick. + if part[0] == "joy": + if keydown: + return "(ev.type == %d and ev.press and ev.press == renpy.game.preferences.joymap.get(%r, None))" % (renpy.display.core.JOYEVENT, key) + else: + return "(ev.type == %d and ev.release and ev.release == renpy.game.preferences.joymap.get(%r, None))" % (renpy.display.core.JOYEVENT, key) + + # Otherwise, deal with it as a key. + if keydown: + rv = "(ev.type == %d" % pygame.KEYDOWN + else: + rv = "(ev.type == %d" % pygame.KEYUP + + if part[0] == "alt": + part.pop(0) + rv += " and (ev.mod & %d)" % pygame.KMOD_ALT + else: + rv += " and not (ev.mod & %d)" % pygame.KMOD_ALT + + if part[0] == "meta": + part.pop(0) + rv += " and (ev.mod & %d)" % pygame.KMOD_META + else: + rv += " and not (ev.mod & %d)" % pygame.KMOD_META + + if part[0] == "shift": + part.pop(0) + rv += " and (ev.mod & %d)" % pygame.KMOD_SHIFT + + if part[0] == "noshift": + part.pop(0) + rv += " and not (ev.mod & %d)" % pygame.KMOD_SHIFT + + if len(part) == 1: + if len(part[0]) != 1: + if renpy.config.developer: + raise Exception("Invalid key specifier %s" % key) + else: + return "(False)" + + rv += " and ev.unicode == %r)" % part[0] + + else: + if part[0] != "K": + if renpy.config.developer: + raise Exception("Invalid key specifier %s" % key) + else: + return "(False)" + + key = "_".join(part) + + rv += " and ev.key == %d)" % (getattr(pygame.constants, key)) + + return rv + +# These store a lambda for each compiled key in the system. +event_cache = { } +keyup_cache = { } + +def map_event(ev, name): + """Returns true if the event matches the named keycode being pressed.""" + + check_code = event_cache.get(name, None) + if check_code is None: + check_code = eval("lambda ev : " + compile_event(name, True), globals()) + event_cache[name] = check_code + + return check_code(ev) + +def map_keyup(ev, name): + """Returns true if the event matches the named keycode being released.""" + + check_code = keyup_cache.get(name, None) + if check_code is None: + check_code = eval("lambda ev : " + compile_event(name, False), globals()) + keyup_cache[name] = check_code + + return check_code(ev) + + +def skipping(ev): + """ + This handles setting skipping in response to the press of one of the + CONTROL keys. The library handles skipping in response to TAB. + """ + + if not renpy.config.allow_skipping: + return + + if map_event(ev, "skip"): + renpy.config.skipping = "slow" + renpy.exports.restart_interaction() + + if map_keyup(ev, "skip"): + renpy.config.skipping = None + renpy.exports.restart_interaction() + + return + + +def inspector(ev): + return map_event(ev, "inspector") + + +############################################################################## +# Utility functions for dealing with actions. + +def predict_action(var): + """ + Predicts some of the actions that may be caused by a variable. + """ + + if var is None: + return + + if isinstance(var, renpy.ui.Action): + var.predict() + + if isinstance(var, (list, tuple)): + for i in var: + predict_action(i) + +def run(var, *args, **kwargs): + """ + Runs a variable. This is done by calling all the functions, and + iterating over the lists and tuples. + """ + + if var is None: + return None + + if isinstance(var, (list, tuple)): + rv = None + + for i in var: + new_rv = run(i, *args, **kwargs) + + if new_rv is not None: + rv = new_rv + + return rv + + return var(*args, **kwargs) + +def run_unhovered(var): + """ + Calls the unhovered method on the variable, if it exists. + """ + + if var is None: + return None + + if isinstance(var, (list, tuple)): + for i in var: + run_unhovered(i) + + return + + f = getattr(var, "unhovered", None) + if f is not None: + f() + +def run_periodic(var, st): + + if isinstance(var, (list, tuple)): + rv = None + + for i in var: + v = run_periodic(i, st) + + if rv is None or v < rv: + rv = v + + return rv + + if isinstance(var, renpy.ui.Action): + return var.periodic(st) + + +def is_selected(clicked): + + if isinstance(clicked, (list, tuple)): + return any(is_selected(i) for i in clicked) + + elif isinstance(clicked, renpy.ui.Action): + return clicked.get_selected() + else: + return False + + +def is_sensitive(clicked): + + if isinstance(clicked, (list, tuple)): + return all(is_sensitive(i) for i in clicked) + + elif isinstance(clicked, renpy.ui.Action): + return clicked.get_sensitive() + else: + return True + + +############################################################################## +# Special-Purpose Displayables + +class Keymap(renpy.display.layout.Null): + """ + This is a behavior that maps keys to actions that are called when + the key is pressed. The keys are specified by giving the appropriate + k_constant from pygame.constants, or the unicode for the key. + """ + + def __init__(self, replaces=None, **keymap): + super(Keymap, self).__init__(style='default') + self.keymap = keymap + + def event(self, ev, x, y, st): + + for name, action in self.keymap.items(): + if map_event(ev, name): + + rv = run(action) + + if rv is not None: + return rv + + raise renpy.display.core.IgnoreEvent() + + def predict_one_action(self): + for i in self.keymap.values(): + predict_action(i) + + +class RollForward(renpy.display.layout.Null): + """ + This behavior implements rollforward. + """ + + def __init__(self, value, **properties): + super(RollForward, self).__init__(**properties) + self.value = value + + + def event(self, ev, x, y, st): + + if map_event(ev, "rollforward"): + renpy.game.interface.suppress_transition = True + renpy.game.after_rollback = True + renpy.game.log.rolled_forward = True + return self.value + + +class PauseBehavior(renpy.display.layout.Null): + """ + This is a class implementing the Pause behavior, which is to + return a value after a certain amount of time has elapsed. + """ + + def __init__(self, delay, result=False, **properties): + super(PauseBehavior, self).__init__(**properties) + + self.delay = delay + self.result = result + + def event(self, ev, x, y, st): + + if st >= self.delay: + + # If we have been drawn since the timeout, simply return + # true. Otherwise, force a redraw, and return true when + # it comes back. + if renpy.game.interface.drawn_since(st - self.delay): + return self.result + else: + renpy.game.interface.force_redraw = True + + + renpy.game.interface.timeout(max(self.delay - st, 0)) + +class SoundStopBehavior(renpy.display.layout.Null): + """ + This is a class implementing the sound stop behavior, + which is to return False when a sound is no longer playing + on the named channel. + """ + + def __init__(self, channel, result=False, **properties): + super(SoundStopBehavior, self).__init__(**properties) + + self.channel = channel + self.result = result + + + def event(self, ev, x, y, st): + + if not renpy.audio.music.get_playing(self.channel): + return self.result + + renpy.game.interface.timeout(.025) + + +class SayBehavior(renpy.display.layout.Null): + """ + This is a class that implements the say behavior, + which is to return True (ending the interaction) if + the user presses space or enter, or clicks the left + mouse button. + """ + + focusable = True + + def __init__(self, default=True, afm=None, dismiss=[ 'dismiss' ], allow_dismiss=None, **properties): + super(SayBehavior, self).__init__(default=default, **properties) + + if not isinstance(dismiss, (list, tuple)): + dismiss = [ dismiss ] + + if afm is not None: + self.afm_length = len(afm) + else: + self.afm_length = None + + # What keybindings lead to dismissal? + self.dismiss = dismiss + + self.allow_dismiss = allow_dismiss + + def set_afm_length(self, afm_length): + self.afm_length = max(afm_length, 1) + + def event(self, ev, x, y, st): + + if self.afm_length and renpy.game.preferences.afm_time and renpy.game.preferences.afm_enable: + + afm_delay = ( 1.0 * ( renpy.config.afm_bonus + self.afm_length ) / renpy.config.afm_characters ) * renpy.game.preferences.afm_time + + if renpy.game.preferences.text_cps: + afm_delay += 1.0 / renpy.game.preferences.text_cps * self.afm_length + + if st > afm_delay: + if renpy.config.afm_callback: + if renpy.config.afm_callback(): + return True + else: + renpy.game.interface.timeout(0.1) + else: + return True + else: + renpy.game.interface.timeout(afm_delay - st) + + for dismiss in self.dismiss: + + if map_event(ev, dismiss) and self.is_focused(): + + if renpy.config.skipping: + renpy.config.skipping = None + renpy.exports.restart_interaction() + raise renpy.display.core.IgnoreEvent() + + if renpy.game.preferences.using_afm_enable and renpy.game.preferences.afm_enable: + renpy.game.preferences.afm_enable = False + renpy.exports.restart_interaction() + raise renpy.display.core.IgnoreEvent() + + if self.allow_dismiss: + if not self.allow_dismiss(): + raise renpy.display.core.IgnoreEvent() + + return True + + skip_delay = renpy.config.skip_delay / 1000.0 + + if renpy.config.allow_skipping and renpy.config.skipping: + + if st >= skip_delay: + if renpy.game.preferences.skip_unseen: + return True + elif renpy.config.skipping == "fast": + return True + elif renpy.game.context().seen_current(True): + return True + else: + renpy.game.interface.timeout(skip_delay - st) + + + return None + + +############################################################################## +# Button + +class Button(renpy.display.layout.Window): + + keymap = { } + action = None + + def __init__(self, child=None, style='button', clicked=None, + hovered=None, unhovered=None, action=None, role=None, + time_policy=None, keymap={}, + **properties): + + super(Button, self).__init__(child, style=style, **properties) + + if isinstance(clicked, renpy.ui.Action): + action = clicked + + if action is not None: + clicked = action + + if not is_sensitive(action): + clicked = None + + if role is None: + if action: + if is_selected(action): + role = 'selected_' + else: + role = '' + else: + role = '' + + self.action = action + self.activated = False + self.clicked = clicked + self.hovered = hovered + self.unhovered = unhovered + self.focusable = clicked is not None + self.role = role + self.keymap = keymap + + self.time_policy_data = None + + def predict_one_action(self): + predict_action(self.clicked) + predict_action(self.hovered) + predict_action(self.unhovered) + + if self.keymap: + for v in self.keymap.values(): + predict_action(v) + + def render(self, width, height, st, at): + + if self.style.time_policy: + st, self.time_policy_data = self.style.time_policy(st, self.time_policy_data, self.style) + + rv = super(Button, self).render(width, height, st, at) + + if self.clicked: + + rect = self.style.focus_rect + if rect is not None: + fx, fy, fw, fh = rect + else: + fx = self.style.left_margin + fy = self.style.top_margin + fw = rv.width - self.style.right_margin + fh = rv.height - self.style.bottom_margin + + mask = self.style.focus_mask + + if mask is True: + mask = rv + elif mask is not None: + mask = renpy.easy.displayable(mask) + mask = renpy.display.render.render(mask, rv.width, rv.height, st, at) + + if mask is not None: + fmx = 0 + fmy = 0 + else: + fmx = None + fmy = None + + rv.add_focus(self, None, + fx, fy, fw, fh, + fmx, fmy, mask) + + return rv + + + def focus(self, default=False): + super(Button, self).focus(default) + + if self.activated: + return None + + rv = None + + if not default: + rv = run(self.hovered) + + self.set_transform_event(self.role + "hover") + self.child.set_transform_event(self.role + "hover") + + return rv + + + def unfocus(self, default=False): + super(Button, self).unfocus(default) + + if self.activated: + return None + + if not default: + run_unhovered(self.hovered) + run(self.unhovered) + + self.set_transform_event(self.role + "idle") + self.child.set_transform_event(self.role + "idle") + + + def per_interact(self): + if not self.clicked: + self.set_style_prefix(self.role + "insensitive_", True) + else: + self.set_style_prefix(self.role + "idle_", True) + + super(Button, self).per_interact() + + def event(self, ev, x, y, st): + + # Call self.action.periodic() + timeout = run_periodic(self.action, st) + + if timeout is not None: + renpy.game.interface.timeout(timeout) + + # If we have a child, try passing the event to it. (For keyboard + # events, this only happens if we're focused.) + if self.is_focused() or not (ev.type == pygame.KEYDOWN or ev.type == pygame.KEYUP): + rv = super(Button, self).event(ev, x, y, st) + if rv is not None: + return rv + + # If not focused, ignore all events. + if not self.is_focused(): + return None + + # Check the keymap. + for name, action in self.keymap.items(): + if map_event(ev, name): + return run(action) + + # Ignore as appropriate: + if map_event(ev, "button_ignore") and self.clicked: + raise renpy.display.core.IgnoreEvent() + + # If clicked, + if map_event(ev, "button_select") and self.clicked: + + self.activated = True + self.style.set_prefix(self.role + 'activate_') + + if self.style.sound: + renpy.audio.music.play(self.style.sound, channel="sound") + + rv = run(self.clicked) + + if rv is not None: + return rv + else: + self.activated = False + + if self.is_focused(): + self.set_style_prefix(self.role + "hover_", True) + else: + self.set_style_prefix(self.role + "idle_", True) + + raise renpy.display.core.IgnoreEvent() + + return None + + + def set_style_prefix(self, prefix, root): + if root: + super(Button, self).set_style_prefix(prefix, root) + + +# Reimplementation of the TextButton widget as a Button and a Text +# widget. +def TextButton(text, style='button', text_style='button_text', + clicked=None, **properties): + + text = renpy.text.text.Text(text, style=text_style) #@UndefinedVariable + return Button(text, style=style, clicked=clicked, **properties) + +class ImageButton(Button): + """ + Used to implement the guts of an image button. + """ + + def __init__(self, + idle_image, + hover_image, + insensitive_image = None, + activate_image = None, + selected_idle_image = None, + selected_hover_image = None, + selected_insensitive_image = None, + selected_activate_image = None, + style='image_button', + clicked=None, + hovered=None, + **properties): + + insensitive_image = insensitive_image or idle_image + activate_image = activate_image or hover_image + + selected_idle_image = selected_idle_image or idle_image + selected_hover_image = selected_hover_image or hover_image + selected_insensitive_image = selected_insensitive_image or insensitive_image + selected_activate_image = selected_activate_image or activate_image + + self.state_children = dict( + idle_ = renpy.easy.displayable(idle_image), + hover_ = renpy.easy.displayable(hover_image), + insensitive_ = renpy.easy.displayable(insensitive_image), + activate_ = renpy.easy.displayable(activate_image), + + selected_idle_ = renpy.easy.displayable(selected_idle_image), + selected_hover_ = renpy.easy.displayable(selected_hover_image), + selected_insensitive_ = renpy.easy.displayable(selected_insensitive_image), + selected_activate_ = renpy.easy.displayable(selected_activate_image), + ) + + super(ImageButton, self).__init__(renpy.display.layout.Null(), + style=style, + clicked=clicked, + hovered=hovered, + **properties) + + def visit(self): + return list(self.state_children.values()) + + def get_child(self): + return self.style.child or self.state_children[self.style.prefix] + + +# This is used for an input that takes its focus from a button. +class HoveredProxy(object): + def __init__(self, a, b): + self.a = a + self.b = b + + def __call__(self): + self.a() + if self.b: + return self.b() + + +class Input(renpy.text.text.Text): #@UndefinedVariable + """ + This is a Displayable that takes text as input. + """ + + changed = None + prefix = "" + suffix = "" + caret_pos = 0 + + def __init__(self, + default="", + length=None, + style='input', + allow=None, + exclude=None, + prefix="", + suffix="", + changed=None, + button=None, + replaces=None, + editable=True, + **properties): + + super(Input, self).__init__("", style=style, replaces=replaces, substitute=False, **properties) + + self.content = str(default) + self.length = length + + self.allow = allow + self.exclude = exclude + self.prefix = prefix + self.suffix = suffix + + self.changed = changed + + self.editable = editable + + caretprops = { 'color' : None } + + for i in properties: + if i.endswith("color"): + caretprops[i] = properties[i] + + self.caret = renpy.display.image.Solid(xmaximum=1, style=style, **caretprops) + self.caret_pos = len(self.content) + + if button: + self.editable = False + button.hovered = HoveredProxy(self.enable, button.hovered) + button.unhovered = HoveredProxy(self.disable, button.unhovered) + + if isinstance(replaces, Input): + self.content = replaces.content + self.editable = replaces.editable + self.caret_pos = replaces.caret_pos + + self.update_text(self.content, self.editable) + + + def update_text(self, content, editable): + + if content != self.content or editable != self.editable: + renpy.display.render.redraw(self, 0) + + if content != self.content: + self.content = content + + if self.changed: + self.changed(content) + + if content == "": + content = "\u200b" + + self.editable = editable + + # Choose the caret. + caret = self.style.caret + if caret is None: + caret = self.caret + + if editable: + l = len(content) + self.set_text([self.prefix, content[0:self.caret_pos].replace("{", "{{"), caret, + content[self.caret_pos:l].replace("{", "{{"), self.suffix]) + else: + self.set_text([self.prefix, content.replace("{", "{{"), self.suffix ]) + + # This is needed to ensure the caret updates properly. + def set_style_prefix(self, prefix, root): + if prefix != self.style.prefix: + self.update_text(self.content, self.editable) + + super(Input, self).set_style_prefix(prefix, root) + + def enable(self): + self.update_text(self.content, True) + + def disable(self): + self.update_text(self.content, False) + + def event(self, ev, x, y, st): + + if not self.editable: + return None + + l = len(self.content) + + if map_event(ev, "input_backspace"): + + if self.content and self.caret_pos > 0: + content = self.content[0:self.caret_pos-1] + self.content[self.caret_pos:l] + self.caret_pos -= 1 + self.update_text(content, self.editable) + + renpy.display.render.redraw(self, 0) + raise renpy.display.core.IgnoreEvent() + + elif map_event(ev, "input_enter"): + if not self.changed: + return self.content + + elif map_event(ev, "input_left"): + if self.caret_pos > 0: + self.caret_pos -= 1 + self.update_text(self.content, self.editable) + + renpy.display.render.redraw(self, 0) + raise renpy.display.core.IgnoreEvent() + + elif map_event(ev, "input_right"): + if self.caret_pos < l: + self.caret_pos += 1 + self.update_text(self.content, self.editable) + + renpy.display.render.redraw(self, 0) + raise renpy.display.core.IgnoreEvent() + + elif map_event(ev, "input_delete"): + if self.caret_pos < l: + content = self.content[0:self.caret_pos] + self.content[self.caret_pos+1:l] + self.update_text(content, self.editable) + + renpy.display.render.redraw(self, 0) + raise renpy.display.core.IgnoreEvent() + + elif ev.type == pygame.KEYDOWN and ev.str: + if ord(ev.str[0]) < 32: + return None + + if self.length and len(self.content) >= self.length: + raise renpy.display.core.IgnoreEvent() + + if self.allow and ev.str not in self.allow: + raise renpy.display.core.IgnoreEvent() + + if self.exclude and ev.str in self.exclude: + raise renpy.display.core.IgnoreEvent() + + content = self.content[0:self.caret_pos] + ev.str + self.content[self.caret_pos:l] + self.caret_pos += 1 + + self.update_text(content, self.editable) + + raise renpy.display.core.IgnoreEvent() + +# A map from adjustment to lists of displayables that want to be redrawn +# if the adjustment changes. +adj_registered = { } + +# This class contains information about an adjustment that can change the +# position of content. +class Adjustment(renpy.object.Object): + """ + :doc: ui + :name: ui.adjustment class + + Adjustment objects represent a value that can be adjusted by a bar + or viewport. They contain information about the value, the range + of the value, and how to adjust the value in small steps and large + pages. + + + """ + + def __init__(self, range=1, value=0, step=None, page=0, changed=None, adjustable=None, ranged=None): #@ReservedAssignment + """ + The following parameters correspond to fields or properties on + the adjustment object: + + `range` + The range of the adjustment, a number. + + `value` + The value of the adjustment, a number. + + `step` + The step size of the adjustment, a number. If None, then + defaults to 1/10th of a page, if set. Otherwise, defaults + to the 1/20th of the range. + + This is used when scrolling a viewport with the mouse wheel. + + `page` + The page size of the adjustment. If None, this is set + automatically by a viewport. If never set, defaults to 1/10th + of the range. + + It's can be used when clicking on a scrollbar. + + The following parameters control the behavior of the adjustment. + + `adjustable` + If True, this adjustment can be changed by a bar. If False, + it can't. + + It defaults to being adjustable if a `changed` function + is given or if the adjustment is associated with a viewport, + and not adjustable otherwise. + + `changed` + This function is called with the new value when the value of + the adjustment changes. + + `ranged` + This function is called with the adjustment object when + the range of the adjustment is set by a viewport. + + .. method:: change(value) + + Changes the value of the adjustment to `value`, updating + any bars and viewports that use the adjustment. + """ + + + super(Adjustment, self).__init__() + + if adjustable is None: + if changed: + adjustable = True + + self._value = value + self._range = range + self._page = page + self._step = step + self.changed = changed + self.adjustable = adjustable + self.ranged = ranged + + def get_value(self): + if self._value > self._range: + return self._range + + return self._value + + def set_value(self, v): + self._value = v + + value = property(get_value, set_value) + + def get_range(self): + return self._range + + def set_range(self, v): + self._range = v + if self.ranged: + self.ranged(self) + + range = property(get_range, set_range) #@ReservedAssignment + + def get_page(self): + if self._page is not None: + return self._page + + return self._range / 10 + + def set_page(self, v): + self._page = v + + page = property(get_page, set_page) + + def get_step(self): + if self._step is not None: + return self._step + + if self._page is not None and self.page > 0: + return self._page / 10 + + if isinstance(self._range, float): + return self._range / 10 + else: + return 1 + + def set_step(self, v): + self._step = v + + step = property(get_step, set_step) + + # Register a displayable to be redrawn when this adjustment changes. + def register(self, d): + adj_registered.setdefault(self, [ ]).append(d) + + def change(self, value): + + if value < 0: + value = 0 + if value > self._range: + value = self._range + + if value != self._value: + self._value = value + for d in adj_registered.setdefault(self, [ ]): + renpy.display.render.redraw(d, 0) + if self.changed: + return self.changed(value) + + return None + +class Bar(renpy.display.core.Displayable): + """ + Implements a bar that can display an integer value, and respond + to clicks on that value. + """ + + __version__ = 2 + + def after_upgrade(self, version): + + if version < 1: + self.adjustment = Adjustment(self.range, self.value, changed=self.changed) # E1101 + self.adjustment.register(self) + del self.range # E1101 + del self.value # E1101 + del self.changed # E1101 + + if version < 2: + self.value = None + + def __init__(self, + range=None, #@ReservedAssignment + value=None, + width=None, + height=None, + changed=None, + adjustment=None, + step=None, + page=None, + bar=None, + style=None, + vertical=False, + replaces=None, + hovered=None, + unhovered=None, + **properties): + + self.value = None + + if adjustment is None: + if isinstance(value, renpy.ui.BarValue): + + if isinstance(replaces, Bar): + value.replaces(replaces.value) + + self.value = value + adjustment = value.get_adjustment() + renpy.game.interface.timeout(0) + else: + adjustment = Adjustment(range, value, step=step, page=page, changed=changed) + + if style is None: + if self.value is not None: + if vertical: + style = self.value.get_style()[1] + else: + style = self.value.get_style()[0] + else: + if vertical: + style = 'vbar' + else: + style = 'bar' + + if width is not None: + properties['xmaximum'] = width + + if height is not None: + properties['ymaximum'] = height + + super(Bar, self).__init__(style=style, **properties) + + self.adjustment = adjustment + self.focusable = True + + # These are set when we are first rendered. + self.thumb_dim = 0 + self.height = 0 + self.width = 0 + self.hidden = False + + self.hovered = hovered + self.unhovered = unhovered + + def per_interact(self): + self.focusable = self.adjustment.adjustable + self.adjustment.register(self) + + def predict_one(self): + pd = renpy.display.predict.displayable + style = self.style + + pd(style.insensitive_fore_bar) + pd(style.idle_fore_bar) + pd(style.hover_fore_bar) + pd(style.selected_idle_fore_bar) + pd(style.selected_hover_fore_bar) + + pd(style.insensitive_aft_bar) + pd(style.idle_aft_bar) + pd(style.hover_aft_bar) + pd(style.selected_idle_aft_bar) + pd(style.selected_hover_aft_bar) + + pd(style.insensitive_thumb) + pd(style.idle_thumb) + pd(style.hover_thumb) + pd(style.selected_idle_thumb) + pd(style.selected_hover_thumb) + + pd(style.insensitive_thumb_shadow) + pd(style.idle_thumb_shadow) + pd(style.hover_thumb_shadow) + pd(style.selected_idle_thumb_shadow) + pd(style.selected_hover_thumb_shadow) + + def render(self, width, height, st, at): + + # Handle redrawing. + if self.value is not None: + redraw = self.value.periodic(st) + + if redraw is not None: + renpy.display.render.redraw(self, redraw) + + # Store the width and height for the event function to use. + self.width = width + self.height = height + range = self.adjustment.range #@ReservedAssignment + value = self.adjustment.value + page = self.adjustment.page + + if range <= 0: + if self.style.unscrollable == "hide": + self.hidden = True + return renpy.display.render.Render(width, height) + elif self.style.unscrollable == "insensitive": + self.set_style_prefix("insensitive_", True) + + self.hidden = False + + if self.style.bar_invert ^ self.style.bar_vertical: + value = range - value + + bar_vertical = self.style.bar_vertical + + if bar_vertical: + dimension = height + else: + dimension = width + + fore_gutter = self.style.fore_gutter + aft_gutter = self.style.aft_gutter + + active = dimension - fore_gutter - aft_gutter + if range: + thumb_dim = active * page / (range + page) + else: + thumb_dim = active + + thumb_offset = abs(self.style.thumb_offset) + + if bar_vertical: + thumb = render(self.style.thumb, width, thumb_dim, st, at) + thumb_shadow = render(self.style.thumb_shadow, width, thumb_dim, st, at) + thumb_dim = thumb.height + else: + thumb = render(self.style.thumb, thumb_dim, height, st, at) + thumb_shadow = render(self.style.thumb_shadow, thumb_dim, height, st, at) + thumb_dim = thumb.width + + # Remove the offset from the thumb. + thumb_dim -= thumb_offset * 2 + self.thumb_dim = thumb_dim + + active -= thumb_dim + + if range: + fore_size = active * value / range + else: + fore_size = active + + fore_size = int(fore_size) + + aft_size = active - fore_size + + fore_size += fore_gutter + aft_size += aft_gutter + + rv = renpy.display.render.Render(width, height) + + if bar_vertical: + + if self.style.bar_resizing: + foresurf = render(self.style.fore_bar, width, fore_size, st, at) + aftsurf = render(self.style.aft_bar, width, aft_size, st, at) + rv.blit(thumb_shadow, (0, fore_size - thumb_offset)) + rv.blit(foresurf, (0, 0), main=False) + rv.blit(aftsurf, (0, height-aft_size), main=False) + rv.blit(thumb, (0, fore_size - thumb_offset)) + + else: + foresurf = render(self.style.fore_bar, width, height, st, at) + aftsurf = render(self.style.aft_bar, width, height, st, at) + + rv.blit(thumb_shadow, (0, fore_size - thumb_offset)) + rv.blit(foresurf.subsurface((0, 0, width, fore_size)), (0, 0), main=False) + rv.blit(aftsurf.subsurface((0, height - aft_size, width, aft_size)), (0, height - aft_size), main=False) + rv.blit(thumb, (0, fore_size - thumb_offset)) + + else: + if self.style.bar_resizing: + foresurf = render(self.style.fore_bar, fore_size, height, st, at) + aftsurf = render(self.style.aft_bar, aft_size, height, st, at) + rv.blit(thumb_shadow, (fore_size - thumb_offset, 0)) + rv.blit(foresurf, (0, 0), main=False) + rv.blit(aftsurf, (width-aft_size, 0), main=False) + rv.blit(thumb, (fore_size - thumb_offset, 0)) + + else: + foresurf = render(self.style.fore_bar, width, height, st, at) + aftsurf = render(self.style.aft_bar, width, height, st, at) + + rv.blit(thumb_shadow, (fore_size - thumb_offset, 0)) + rv.blit(foresurf.subsurface((0, 0, fore_size, height)), (0, 0), main=False) + rv.blit(aftsurf.subsurface((width - aft_size, 0, aft_size, height)), (width-aft_size, 0), main=False) + rv.blit(thumb, (fore_size - thumb_offset, 0)) + + if self.focusable: + rv.add_focus(self, None, 0, 0, width, height) + + return rv + + + def focus(self, default=False): + super(Bar, self).focus(default) + self.set_transform_event("hover") + + if not default: + run(self.hovered) + + + def unfocus(self, default=False): + super(Bar, self).unfocus() + self.set_transform_event("idle") + + if not default: + run_unhovered(self.hovered) + run(self.unhovered) + + def event(self, ev, x, y, st): + + if not self.focusable: + return None + + if not self.is_focused(): + return None + + if self.hidden: + return None + + range = self.adjustment.range #@ReservedAssignment + old_value = self.adjustment.value + value = old_value + + vertical = self.style.bar_vertical + invert = self.style.bar_invert ^ vertical + if invert: + value = range - value + + grabbed = (renpy.display.focus.get_grab() is self) + just_grabbed = False + + if not grabbed and map_event(ev, "bar_activate"): + renpy.display.focus.set_grab(self) + just_grabbed = True + grabbed = True + + if grabbed: + + if vertical: + increase = "bar_down" + decrease = "bar_up" + else: + increase = "bar_right" + decrease = "bar_left" + + if map_event(ev, decrease): + value -= self.adjustment.step + + if map_event(ev, increase): + value += self.adjustment.step + + if ev.type in (pygame.MOUSEMOTION, pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN): + + if vertical: + + tgutter = self.style.fore_gutter + bgutter = self.style.aft_gutter + zone_height = self.height - tgutter - bgutter - self.thumb_dim + if zone_height: + value = (y - tgutter - self.thumb_dim / 2) * range / zone_height + else: + value = 0 + + else: + lgutter = self.style.fore_gutter + rgutter = self.style.aft_gutter + zone_width = self.width - lgutter - rgutter - self.thumb_dim + if zone_width: + value = (x - lgutter - self.thumb_dim / 2) * range / zone_width + else: + value = 0 + + if isinstance(range, int): + value = int(value) + + if value < 0: + value = 0 + + if value > range: + value = range + + if invert: + value = range - value + + if grabbed and not just_grabbed and map_event(ev, "bar_deactivate"): + renpy.display.focus.set_grab(None) + + if value != old_value: + return self.adjustment.change(value) + + return None + + +class Conditional(renpy.display.layout.Container): + """ + This class renders its child if and only if the condition is + true. Otherwise, it renders nothing. (Well, a Null). + + Warning: the condition MUST NOT update the game state in any + way, as that would break rollback. + """ + + def __init__(self, condition, *args, **properties): + super(Conditional, self).__init__(*args, **properties) + + self.condition = condition + self.null = renpy.display.layout.Null() + + self.state = eval(self.condition, vars(renpy.store)) + + def render(self, width, height, st, at): + if self.state: + return render(self.child, width, height, st, at) + else: + return render(self.null, width, height, st, at) + + def event(self, ev, x, y, st): + + state = eval(self.condition, vars(renpy.store)) + + if state != self.state: + renpy.display.render.redraw(self, 0) + + self.state = state + + if state: + return self.child.event(ev, x, y, st) + + +class TimerState(renpy.python.RevertableObject): + """ + Stores the state of the timer, which may need to be rolled back. + """ + + # Prevents us from having to worry about our initialization being + # rolled back. + started = False + next_event = None + +class Timer(renpy.display.layout.Null): + + __version__ = 1 + + started = False + + def after_upgrade(self, version): + if version < 1: + self.state = TimerState() + self.state.started = self.started + self.state.next_event = self.next_event + + def __init__(self, delay, action=None, repeat=False, args=(), kwargs={}, replaces=None, **properties): + super(Timer, self).__init__(**properties) + + if action is None: + raise Exception("A timer must have an action supplied.") + + if delay <= 0: + raise Exception("A timer's delay must be > 0.") + + # The delay. + self.delay = delay + + # Should we repeat the event? + self.repeat = repeat + + # The time the next event should occur. + self.next_event = None + + # The function and its arguments. + self.function = action + self.args = args + self.kwargs = kwargs + + # Did we start the timer? + self.started = False + + if replaces is not None: + self.state = replaces.state + else: + self.state = TimerState() + + + def event(self, ev, x, y, st): + + state = self.state + + if not state.started: + state.started = True + state.next_event = st + self.delay + + if state.next_event is None: + return + + if st < state.next_event: + renpy.game.interface.timeout(state.next_event - st) + return + + if not self.repeat: + state.next_event = None + else: + state.next_event = state.next_event + self.delay + if state.next_event < st: + state.next_event = st + self.delay + + renpy.game.interface.timeout(state.next_event - st) + + return run(self.function, *self.args, **self.kwargs) + + +class MouseArea(renpy.display.core.Displayable): + + def __init__(self, hovered=None, unhovered=None, replaces=None, **properties): + super(MouseArea, self).__init__(**properties) + + self.hovered = hovered + self.unhovered = unhovered + + # Are we hovered right now? + self.is_hovered = False + + if replaces is not None: + self.is_hovered = replaces.is_hovered + + # Taken from the render. + self.width = 0 + self.height = 0 + + + def render(self, width, height, st, at): + self.width = width + self.height = height + + return Render(width, height) + + def event(self, ev, x, y, st): + + if 0 <= x < self.width and 0 <= y < self.height: + is_hovered = True + else: + is_hovered = False + + if is_hovered and not self.is_hovered: + self.is_hovered = True + + return run(self.hovered) + + elif not is_hovered and self.is_hovered: + self.is_hovered = False + + run_unhovered(self.hovered) + run(self.unhovered) + + diff --git a/unrpyc/renpy/display/core.py b/unrpyc/renpy/display/core.py new file mode 100644 index 0000000..4731df0 --- /dev/null +++ b/unrpyc/renpy/display/core.py @@ -0,0 +1,2463 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains code for initializing and managing the display +# window. + +import renpy.display +import renpy.audio +import renpy.text + +import pygame #@UnusedImport + +import sys +import os +import time +import io +import threading + +try: + import android #@UnresolvedImport @UnusedImport + import android.sound #@UnresolvedImport +except: + android = None + +# Is the cpu idle enough to do other things? +cpu_idle = threading.Event() +cpu_idle.clear() + +# Need to be +4, so we don't interfere with FFMPEG's events. +TIMEEVENT = pygame.USEREVENT + 4 +PERIODIC = pygame.USEREVENT + 5 +JOYEVENT = pygame.USEREVENT + 6 +REDRAW = pygame.USEREVENT + 7 + +# All events except for TIMEEVENT and REDRAW +ALL_EVENTS = [ i for i in range(0, REDRAW + 1) if i != TIMEEVENT and i != REDRAW ] + +# The number of msec between periodic events. +PERIODIC_INTERVAL = 50 + +# Time management. +time_base = None + +def init_time(): + global time_base + time_base = time.time() - pygame.time.get_ticks() / 1000.0 + +def get_time(): + return time_base + pygame.time.get_ticks() / 1000.0 + + +def displayable_by_tag(layer, tag): + """ + Get the displayable on the given layer with the given tag. + """ + + return renpy.game.context().scene_lists.get_displayable_by_tag(layer, tag) + +class IgnoreEvent(Exception): + """ + Exception that is raised when we want to ignore an event, but + also don't want to return anything. + """ + + pass + +class EndInteraction(Exception): + """ + Exception that can be raised (for example, during the render method of + a displayable) to end the current interaction immediately. + """ + + def __init__(self, value): + self.value = value + +class absolute(float): + """ + This represents an absolute float coordinate. + """ + __slots__ = [ ] + + +class Displayable(renpy.object.Object): + """ + The base class for every object in Ren'Py that can be + displayed to the screen. + + Drawables will be serialized to a savegame file. Therefore, they + shouldn't store non-serializable things (like pygame surfaces) in + their fields. + """ + + # Some invariants about method call order: + # + # per_interact is called before render. + # render is called before event. + # + # get_placement can be called at any time, so can't + # assume anything. + + activated = False + focusable = False + full_focus_name = None + role = '' + + # The event we'll pass on to our parent transform. + transform_event = None + + # Can we change our look in response to transform_events? + transform_event_responder = False + + def __init__(self, focus=None, default=False, style='default', **properties): # W0231 + self.style = renpy.style.Style(style, properties, heavy=True) + self.focus_name = focus + self.default = default + + def find_focusable(self, callback, focus_name): + + focus_name = self.focus_name or focus_name + + if self.focusable: + callback(self, focus_name) + + for i in self.visit(): + if i is None: + continue + + i.find_focusable(callback, focus_name) + + + def focus(self, default=False): + """ + Called to indicate that this widget has the focus. + """ + + if not self.activated: + self.set_style_prefix(self.role + "hover_", True) + + if not default and not self.activated: + if self.style.sound: + renpy.audio.music.play(self.style.sound, channel="sound") + + def unfocus(self, default=False): + """ + Called to indicate that this widget has become unfocused. + """ + + if not self.activated: + self.set_style_prefix(self.role + "idle_", True) + + def is_focused(self): + + if renpy.display.focus.grab and renpy.display.focus.grab is not self: + return + + return renpy.game.context().scene_lists.focused is self + + def set_style_prefix(self, prefix, root): + """ + Called to set the style prefix of this widget and its child + widgets, if any. + + `root` - True if this is the root of a style tree, False if this + has been passed on to a child. + """ + + if prefix == self.style.prefix: + return + + self.style.set_prefix(prefix) + renpy.display.render.redraw(self, 0) + + def parameterize(self, name, parameters): + """ + Called to parameterize this. By default, we don't take any + parameters. + """ + + if parameters: + raise Exception("Image '%s' can't take parameters '%s'. (Perhaps you got the name wrong?)" % + (' '.join(name), ' '.join(parameters))) + + return self + + def render(self, width, height, st, at): + """ + Called to display this displayable. This is called with width + and height parameters, which give the largest width and height + that this drawable can be drawn to without overflowing some + bounding box. It's also given two times. It returns a Surface + that is the current image of this drawable. + + @param st: The time since this widget was first shown, in seconds. + @param at: The time since a similarly named widget was first shown, + in seconds. + """ + + assert False, "Draw not implemented." + + def event(self, ev, x, y, st): + """ + Called to report than an event has occured. Ev is the raw + pygame event object representing that event. If the event + involves the mouse, x and y are the translation of the event + into the coordinates of this displayable. st is the time this + widget has been shown for. + + @returns A value that should be returned from Interact, or None if + no value is appropriate. + """ + + return None + + def get_placement(self): + """ + Returns a style object containing placement information for + this Displayable. Children are expected to overload this + to return something more sensible. + """ + + return self.style.get_placement() + + def visit_all(self, callback): + """ + Calls the callback on this displayable and all children of this + displayable. + """ + + for d in self.visit(): + if not d: + continue + d.visit_all(callback) + + callback(self) + + def visit(self): + """ + Called to ask the displayable to return a list of its children + (including children taken from styles). For convenience, this + list may also include None values. + """ + + return [ ] + + def per_interact(self): + """ + Called once per widget per interaction. + """ + + return None + + def predict_one(self): + """ + Called to ask this displayable to call the callback with all + the images it may want to load. + """ + + return + + def predict_one_action(self): + """ + Called to ask this displayable to cause image prediction + to occur for images that may be loaded by its actions. + """ + + return + + def place(self, dest, x, y, width, height, surf, main=True): + """ + This draws this Displayable onto a destination surface, using + the placement style information returned by this object's + get_placement() method. + + @param dest: The surface that this displayable will be drawn + on. + + @param x: The minimum x coordinate on this surface that this + Displayable will be drawn to. + + @param y: The minimum y coordinate on this surface that this + displayable will be drawn to. + + @param width: The width of the area allocated to this + Displayable. + + @param height: The height of the area allocated to this + Displayable. + + @param surf: The surface returned by a previous call to + self.render(). + """ + + xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = self.get_placement() + + if xpos is None: + xpos = 0 + if ypos is None: + ypos = 0 + if xanchor is None: + xanchor = 0 + if yanchor is None: + yanchor = 0 + if xoffset is None: + xoffset = 0 + if yoffset is None: + yoffset = 0 + + # We need to use type, since isinstance(absolute(0), float). + if xpos.__class__ is float: + xpos *= width + + if xanchor.__class__ is float: + xanchor *= surf.width + + xpos += x + xoffset - xanchor + + # y + + if ypos.__class__ is float: + ypos *= height + + if yanchor.__class__ is float: + yanchor *= surf.height + + ypos += y + yoffset - yanchor + + if dest is not None: + if subpixel: + dest.subpixel_blit(surf, (xpos, ypos), main, main, None) + else: + dest.blit(surf, (xpos, ypos), main, main, None) + + return xpos, ypos + + def set_transform_event(self, event): + """ + Sets the transform event of this displayable to event. + """ + + if event == self.transform_event: + return + + self.transform_event = event + if self.transform_event_responder: + renpy.display.render.redraw(self, 0) + + def _hide(self, st, at, kind): + """ + Returns None if this displayable is ready to be hidden, or + a replacement displayable if it doesn't want to be hidden + quite yet. Kind is either "hide" or "replaced". + """ + + return None + + def _show(self): + """ + Called when the displayable is added to a scene list. + """ + + def _get_parameterized(self): + """ + If this is a ImageReference to a parameterized image, return + the get_parameterized() of the parameterized image. Otherwise, + return this displayable. + """ + + return self + + def _change_transform_child(self, child): + """ + If this is a transform, makes a copy of the transform and sets + the child of the innermost transform to this. Otherwise, + simply returns child. + """ + + return child + + + +class SceneListEntry(renpy.object.Object): + """ + Represents a scene list entry. Since this was replacing a tuple, + it should be treated as immutable after its initial creation. + """ + + def __init__(self, tag, zorder, show_time, animation_time, displayable, name): + self.tag = tag + self.zorder = zorder + self.show_time = show_time + self.animation_time = animation_time + self.displayable = displayable + self.name = name + + def __iter__(self): + return iter((self.tag, self.zorder, self.show_time, self.animation_time, self.displayable)) + + def __getitem__(self, index): + return (self.tag, self.zorder, self.show_time, self.animation_time, self.displayable)[index] + + def __repr__(self): + return "<SLE: %r %r %r>" % (self.tag, self.name, self.displayable) + + def copy(self): + return SceneListEntry( + self.tag, + self.zorder, + self.show_time, + self.animation_time, + self.displayable, + self.name) + + def update_time(self, time): + + rv = self + + if self.show_time is None or self.animation_time is None: + rv = self.copy() + rv.show_time = rv.show_time or time + rv.animation_time = rv.animation_time or time + + return rv + + +class SceneLists(renpy.object.Object): + """ + This stores the current scene lists that are being used to display + things to the user. + """ + + __version__ = 6 + + def after_setstate(self): + for i in renpy.config.layers + renpy.config.top_layers: + if i not in self.layers: + self.layers[i] = [ ] + self.at_list[i] = { } + self.layer_at_list[i] = (None, [ ]) + + def after_upgrade(self, version): + + if version < 1: + + self.at_list = { } + self.layer_at_list = { } + + for i in renpy.config.layers + renpy.config.top_layers: + self.at_list[i] = { } + self.layer_at_list[i] = (None, [ ]) + + if version < 3: + self.shown_window = False + + if version < 4: + for k in self.layers: + self.layers[k] = [ SceneListEntry(*(i + (None,)) ) for i in self.layers[k] ] + + self.additional_transient = [ ] + + if version < 5: + self.drag_group = None + + if version < 6: + self.shown = self.image_predict_info + + def __init__(self, oldsl, shown): + + super(SceneLists, self).__init__() + + # Has a window been shown as part of these scene lists? + self.shown_window = False + + # A map from layer name -> list(SceneListEntry) + self.layers = { } + + # A map from layer name -> tag -> at_list associated with that tag. + self.at_list = { } + + # A map from layer to (star time, at_list), where the at list has + # been applied to the layer as a whole. + self.layer_at_list = { } + + # The current shown images, + self.shown = shown + + # A list of (layer, tag) pairs that are considered to be + # transient. + self.additional_transient = [ ] + + # Either None, or a DragGroup that's used as the default for + # drags with names. + self.drag_group = None + + if oldsl: + + for i in renpy.config.layers + renpy.config.top_layers: + + try: + self.layers[i] = oldsl.layers[i][:] + except KeyError: + self.layers[i] = [ ] + + if i in oldsl.at_list: + self.at_list[i] = oldsl.at_list[i].copy() + self.layer_at_list[i] = oldsl.layer_at_list[i] + else: + self.at_list[i] = { } + self.layer_at_list[i] = (None, [ ]) + + for i in renpy.config.overlay_layers: + self.clear(i) + + self.replace_transient() + + self.focused = None + + self.drag_group = oldsl.drag_group + + else: + for i in renpy.config.layers + renpy.config.top_layers: + self.layers[i] = [ ] + self.at_list[i] = { } + self.layer_at_list[i] = (None, [ ]) + + self.music = None + self.focused = None + + def replace_transient(self): + """ + Replaces the contents of the transient display list with + a copy of the master display list. This is used after a + scene is displayed to get rid of transitions and interface + elements. + """ + + for i in renpy.config.transient_layers: + self.clear(i, True) + + for layer, tag in self.additional_transient: + self.remove(layer, tag) + + self.additional_transient = [ ] + + def transient_is_empty(self): + """ + This returns True if all transient layers are empty. This is + used by the rollback code, as we can't start a new rollback + if there is something in a transient layer (as things in the + transient layer may contain objects that cannot be pickled, + like lambdas.) + """ + + for i in renpy.config.transient_layers: + if self.layers[i]: + return False + + return True + + def transform_state(self, old_thing, new_thing): + """ + If the old thing is a transform, then move the state of that transform + to the new thing. + """ + + if old_thing is None: + return new_thing + + # Don't bother wrapping screens, as they can't be transformed. + if isinstance(new_thing, renpy.display.screen.ScreenDisplayable): + return new_thing + + old_transform = old_thing._get_parameterized() + if not isinstance(old_transform, renpy.display.motion.Transform): + return new_thing + + new_transform = new_thing._get_parameterized() + if not isinstance(new_transform, renpy.display.motion.Transform): + new_thing = new_transform = renpy.display.motion.Transform(child=new_thing) + + new_transform.take_state(old_transform) + return new_thing + + + def find_index(self, layer, tag, zorder, behind): + """ + This finds the spot in the named layer where we should insert the + displayable. It returns two things: an index at which the new thing + should be added, and an index at which the old thing should be hidden. + (Note that the indexes are relative to the current state of the list, + which may change on an add or remove.) + """ + + add_index = None + remove_index = None + + + for i, sle in enumerate(self.layers[layer]): + + if add_index is None: + + if sle.zorder == zorder: + if sle.tag and (sle.tag == tag or sle.tag in behind): + add_index = i + + elif sle.zorder > zorder: + add_index = i + + + if remove_index is None: + if (sle.tag and sle.tag == tag) or sle.displayable == tag: + remove_index = i + + + if add_index is None: + add_index = len(self.layers[layer]) + + return add_index, remove_index + + + def add(self, + layer, + thing, + key=None, + zorder=0, + behind=[ ], + at_list=[ ], + name=None, + atl=None, + default_transform=None, + transient=False): + """ + Adds something to this scene list. Some of these names are quite a bit + out of date. + + `thing` - The displayable to add. + + `key` - A string giving the tag associated with this thing. + + `zorder` - Where to place this thing in the zorder, an integer + A greater value means closer to the user. + + `behind` - A list of tags to place the thing behind. + + `at_list` - The at_list associated with this + displayable. Counterintunitively, this is not actually + applied, but merely stored for future use. + + `name` - The full name of the image being displayed. This is used for + image lookup. + + `atl` - If not None, an atl block applied to the thing. (This actually is + applied here.) + + `default_transform` - The default transform that is used to initialized + the values in the other transforms. + """ + + if not isinstance(thing, Displayable): + raise Exception("Attempting to show something that isn't a displayable:" + repr(thing)) + + if layer not in self.layers: + raise Exception("Trying to add something to non-existent layer '%s'." % layer) + + if key: + self.remove_hide_replaced(layer, key) + self.at_list[layer][key] = at_list + + if key and name: + self.shown.predict_show(layer, name) + + if transient: + self.additional_transient.append((layer, key)) + + l = self.layers[layer] + + if atl: + thing = renpy.display.motion.ATLTransform(atl, child=thing) + + add_index, remove_index = self.find_index(layer, key, zorder, behind) + + at = None + st = None + + if remove_index is not None: + sle = l[remove_index] + at = sle.animation_time + old = sle.displayable + + if (not atl and + not at_list and + renpy.config.keep_running_transform and + isinstance(old, renpy.display.motion.Transform)): + + thing = sle.displayable._change_transform_child(thing) + else: + thing = self.transform_state(l[remove_index].displayable, thing) + + thing.set_transform_event("replace") + thing._show() + + else: + + if not isinstance(thing, renpy.display.motion.Transform): + thing = self.transform_state(default_transform, thing) + + thing.set_transform_event("show") + thing._show() + + sle = SceneListEntry(key, zorder, st, at, thing, name) + l.insert(add_index, sle) + + if remove_index is not None: + if add_index <= remove_index: + remove_index += 1 + + self.hide_or_replace(layer, remove_index, "replaced") + + def hide_or_replace(self, layer, index, prefix): + """ + Hides or replaces the scene list entry at the given + index. `prefix` is a prefix that is used if the entry + decides it doesn't want to be hidden quite yet. + """ + + if index is None: + return + + l = self.layers[layer] + oldsle = l[index] + + now = get_time() + + st = oldsle.show_time or now + at = oldsle.animation_time or now + + if oldsle.tag: + + d = oldsle.displayable._hide(now - st, now - at, prefix) + + # _hide can mutate the layers, so we need to recompute + # index. + index = l.index(oldsle) + + if d is not None: + + sle = SceneListEntry( + prefix + "$" + oldsle.tag, + oldsle.zorder, + st, + at, + d, + None) + + l[index] = sle + + return + + l.pop(index) + + def get_all_displayables(self): + """ + Gets all displayables reachable from this scene list. + """ + + rv = [ ] + for l in self.layers.values(): + for sle in l: + rv.append(sle.displayable) + + return rv + + def remove_above(self, layer, thing): + """ + Removes everything on the layer that is closer to the user + than thing, which may be either a tag or a displayable. Thing must + be displayed, or everything will be removed. + """ + + for i in reversed(range(len(self.layers[layer]))): + + sle = self.layers[layer][i] + + if thing: + if sle.tag == thing or sle.displayable == thing: + break + + if sle.tag and "$" in sle.tag: + continue + + self.hide_or_replace(layer, i, "hide") + + def remove(self, layer, thing): + """ + Thing is either a key or a displayable. This iterates through the + named layer, searching for entries matching the thing. + When they are found, they are removed from the displaylist. + + It's not an error to remove something that isn't in the layer in + the first place. + """ + + if layer not in self.layers: + raise Exception("Trying to remove something from non-existent layer '%s'." % layer) + + _add_index, remove_index = self.find_index(layer, thing, 0, [ ]) + + if remove_index is not None: + tag = self.layers[layer][remove_index].tag + + if tag: + self.shown.predict_hide(layer, (tag,)) + self.at_list[layer].pop(tag, None) + + self.hide_or_replace(layer, remove_index, "hide") + + def clear(self, layer, hide=False): + """ + Clears the named layer, making it empty. + + If hide is True, then objects are hidden. Otherwise, they are + totally wiped out. + """ + + if not hide: + self.layers[layer] = [ ] + + else: + + # Have to iterate in reverse order, since otherwise + # the indexes might change. + for i in reversed(range(len(self.layers[layer]))): + self.hide_or_replace(layer, i, hide) + + self.at_list[layer].clear() + self.shown.predict_scene(layer) + self.layer_at_list[layer] = (None, [ ]) + + def set_layer_at_list(self, layer, at_list): + self.layer_at_list[layer] = (None, list(at_list)) + + def set_times(self, time): + """ + This finds entries with a time of None, and replaces that + time with the given time. + """ + + for l, (t, list) in list(self.layer_at_list.items()): #@ReservedAssignment + self.layer_at_list[l] = (t or time, list) + + for l, ll in self.layers.items(): + self.layers[l] = [ i.update_time(time) for i in ll ] + + def showing(self, layer, name): + """ + Returns true if something with the prefix of the given name + is found in the scene list. + """ + + return self.shown.showing(layer, name) + + def make_layer(self, layer, properties): + """ + Creates a Fixed with the given layer name and scene_list. + """ + + rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **properties) + rv.append_scene_list(self.layers[layer]) + + time, at_list = self.layer_at_list[layer] + + if at_list: + for a in at_list: + + if isinstance(a, renpy.display.motion.Transform): + rv = a(child=rv) + else: + rv = a(rv) + + f = renpy.display.layout.MultiBox(layout='fixed') + f.add(rv, time, time) + rv = f + + rv.layer_name = layer + return rv + + def remove_hide_replaced(self, layer, tag): + """ + Removes things that are hiding or replaced, that have the given + tag. + """ + + hide_tag = "hide$" + tag + replaced_tag = "replaced$" + tag + + l = self.layers[layer] + self.layers[layer] = [ i for i in l if i.tag != hide_tag and i.tag != replaced_tag ] + + def remove_hidden(self): + """ + Goes through all of the layers, and removes things that are + hidden and are no longer being kept alive by their hide + methods. + """ + + now = get_time() + + for l in self.layers: + newl = [ ] + + for sle in self.layers[l]: + + if sle.tag: + + if sle.tag.startswith("hide$"): + d = sle.displayable._hide(now - sle.show_time, now - sle.animation_time, "hide") + if not d: + continue + + elif sle.tag.startswith("replaced$"): + d = sle.displayable._hide(now - sle.show_time, now - sle.animation_time, "replaced") + if not d: + continue + + newl.append(sle) + + self.layers[l] = newl + + def get_displayable_by_tag(self, layer, tag): + """ + Returns the displayable on the layer with the given tag, or None + if no such displayable exists. Note that this will usually return + a Transform. + """ + + if layer not in self.layers: + raise Exception("Unknown layer %r." % layer) + + for sle in self.layers[layer]: + if sle.tag == tag: + return sle.displayable + + return None + + def get_displayable_by_name(self, layer, name): + """ + Returns the displayable on the layer with the given tag, or None + if no such displayable exists. Note that this will usually return + a Transform. + """ + + if layer not in self.layers: + raise Exception("Unknown layer %r." % layer) + + for sle in self.layers[layer]: + + if sle.name == name: + return sle.displayable + + return None + + +def scene_lists(index=-1): + """ + Returns either the current scenelists object, or the one for the + context at the given index. + """ + + return renpy.game.context(index).scene_lists + + +class Interface(object): + """ + This represents the user interface that interacts with the user. + It manages the Display objects that display things to the user, and + also handles accepting and responding to user input. + + @ivar display: The display that we used to display the screen. + + @ivar profile_time: The time of the last profiling. + + @ivar screenshot: A screenshot, or None if no screenshot has been + taken. + + @ivar old_scene: The last thing that was displayed to the screen. + + @ivar transition: A map from layer name to the transition that will + be applied the next time interact restarts. + + @ivar transition_time: A map from layer name to the time the transition + involving that layer started. + + @ivar transition_from: A map from layer name to the scene that we're + transitioning from on that layer. + + @ivar suppress_transition: If True, then the next transition will not + happen. + + @ivar force_redraw: If True, a redraw is forced. + + @ivar restart_interaction: If True, the current interaction will + be restarted. + + @ivar pushed_event: If not None, an event that was pushed back + onto the stack. + + @ivar mouse: The name of the mouse cursor to use during the current + interaction. + + @ivar ticks: The number of 20hz ticks. + + @ivar frame_time: The time at which we began drawing this frame. + + @ivar interact_time: The time of the start of the first frame of the current interact_core. + + @ivar time_event: A singleton ignored event. + + @ivar event_time: The time of the current event. + + @ivar timeout_time: The time at which the timeout will occur. + """ + + def __init__(self): + self.screenshot = None + self.old_scene = { } + self.transition = { } + self.ongoing_transition = { } + self.transition_time = { } + self.transition_from = { } + self.suppress_transition = False + self.quick_quit = False + self.force_redraw = False + self.restart_interaction = False + self.pushed_event = None + self.ticks = 0 + self.mouse = 'default' + self.timeout_time = None + self.last_event = None + self.current_context = None + self.roll_forward = None + + # Things to be preloaded. + self.preloads = [ ] + + # The time at which this draw occurs. + self.frame_time = 0 + + # The time when this interaction occured. + self.interact_time = None + + # The time we last tried to quit. + self.quit_time = 0 + + self.time_event = pygame.event.Event(TIMEEVENT) + self.redraw_event = pygame.event.Event(REDRAW) + + # Are we focused? + self.focused = True + + # Properties for each layer. + self.layer_properties = { } + + # Have we shown the window this interaction? + self.shown_window = False + + # Are we in fullscren mode? + self.fullscreen = False + + for layer in renpy.config.layers + renpy.config.top_layers: + if layer in renpy.config.layer_clipping: + x, y, w, h = renpy.config.layer_clipping[layer] + self.layer_properties[layer] = dict( + xpos = x, + xanchor = 0, + ypos = y, + yanchor = 0, + xmaximum = w, + ymaximum = h, + xminimum = w, + yminimum = h, + clipping = True, + ) + + else: + self.layer_properties[layer] = dict() + + + # A stack giving the values of self.transition and self.transition_time + # for contexts outside the current one. This is used to restore those + # in the case where nothing has changed in the new context. + self.transition_info_stack = [ ] + + # The time when the event was dispatched. + self.event_time = 0 + + # The time we saw the last mouse event. + self.mouse_event_time = None + + # Should we show the mouse? + self.show_mouse = True + + # Should we reset the display? + self.display_reset = False + + # The last size we were resized to. This lets us debounce the + # VIDEORESIZE event. + self.last_resize = None + + # Ensure that we kill off the presplash. + renpy.display.presplash.end() + + # Initialize pygame. + if pygame.version.vernum < (1, 8, 1): + raise Exception("Ren'Py requires pygame 1.8.1 to run.") + + try: + import pygame.macosx as macosx + macosx.init() #@UndefinedVariable + except: + pass + + try: + macosx.Video_AutoInit() #@UndefinedVariable + except: + pass + + pygame.font.init() + renpy.audio.audio.init() + renpy.display.joystick.init() + pygame.display.init() + + # Init timing. + init_time() + self.profile_time = get_time() + self.mouse_event_time = get_time() + + # The current window caption. + self.window_caption = None + + renpy.game.interface = self + renpy.display.interface = self + + # Are we in safe mode, from holding down shift at start? + self.safe_mode = False + if renpy.first_utter_start and (pygame.key.get_mods() & pygame.KMOD_SHIFT): + self.safe_mode = True + + # Setup the video mode. + self.set_mode() + + # Load the image fonts. + renpy.text.font.load_image_fonts() + + # Setup the android keymap. + if android is not None: + android.map_key(android.KEYCODE_BACK, pygame.K_PAGEUP) + android.map_key(android.KEYCODE_MENU, pygame.K_ESCAPE) + + # Double check, since at least on Linux, we can't set safe_mode until + # the window maps. + if renpy.first_utter_start and (pygame.key.get_mods() & pygame.KMOD_SHIFT): + self.safe_mode = True + + # Setup periodic event. + pygame.time.set_timer(PERIODIC, PERIODIC_INTERVAL) + + # Don't grab the screen. + pygame.event.set_grab(False) + + # Do we need a background screenshot? + self.bgscreenshot_needed = False + + # Event used to signal background screenshot taken. + self.bgscreenshot_event = threading.Event() + + # The background screenshot surface. + self.bgscreenshot_surface = None + + + def post_init(self): + # Setup. + + # Needed for Unity. + wmclass = renpy.config.save_directory or os.path.basename(sys.argv[0]) + os.environ['SDL_VIDEO_X11_WMCLASS'] = wmclass + + self.set_window_caption(force=True) + self.set_icon() + + if renpy.config.key_repeat is not None: + delay, repeat_delay = renpy.config.key_repeat + pygame.key.set_repeat(int(1000 * delay), int(1000 * repeat_delay)) + + def set_icon(self): + """ + This is called to set up the window icon. + """ + + # Window icon. + icon = renpy.config.window_icon + + if renpy.windows and renpy.config.windows_icon: + icon = renpy.config.windows_icon + + if icon: + + im = renpy.display.scale.image_load_unscaled( + renpy.loader.load(icon), + icon, + convert=False, + ) + + # Convert the aspect ratio to be square. + iw, ih = im.get_size() + imax = max(iw, ih) + square_im = renpy.display.pgrender.surface_unscaled((imax, imax), True) + square_im.blit(im, ( (imax-iw)/2, (imax-ih)/2 )) + im = square_im + + if renpy.windows and im.get_size() != (32, 32): + im = renpy.display.scale.real_smoothscale(im, (32, 32)) + + pygame.display.set_icon(im) + + + def set_window_caption(self, force=False): + caption = renpy.config.window_title + renpy.store._window_subtitle + if not force and caption == self.window_caption: + return + + self.window_caption = caption + pygame.display.set_caption(caption.encode("utf-8")) + + def iconify(self): + pygame.display.iconify() + + def get_draw_constructors(self): + """ + Figures out the list of draw constructors to try. + """ + + renderer = renpy.game.preferences.renderer + renderer = os.environ.get("RENPY_RENDERER", renderer) + + if self.safe_mode: + renderer = "sw" + + renpy.config.renderer = renderer + + if renderer == "auto": + if renpy.windows: + renderers = [ "gl", "angle", "sw" ] + else: + renderers = [ "gl", "sw" ] + else: + renderers = [ renderer, "sw" ] + + draw_objects = { } + + def make_draw(name, mod, cls, *args): + if name not in renderers: + return False + + try: + __import__(mod) + module = sys.modules[mod] + draw_class = getattr(module, cls) + draw_objects[name] = draw_class(*args) + return True + + except: + renpy.display.log.write("Couldn't import {0} renderer:".format(name)) + renpy.display.log.exception() + + return False + + if renpy.windows: + has_angle = make_draw("angle", "renpy.angle.gldraw", "GLDraw") + else: + has_angle = False + + make_draw("gl", "renpy.gl.gldraw", "GLDraw", not has_angle) + make_draw("sw", "renpy.display.swdraw", "SWDraw") + + rv = [ ] + + def append_draw(name): + if name in draw_objects: + rv.append(draw_objects[name]) + else: + renpy.display.log.write("Unknown renderer: {0}".format(name)) + + for i in renderers: + append_draw(i) + + return rv + + + def kill_textures(self): + renpy.display.render.free_memory() + renpy.text.text.layout_cache_clear() + + def kill_textures_and_surfaces(self): + """ + Kill all textures and surfaces that are loaded. + """ + + self.kill_textures() + + renpy.display.im.cache.clear() + renpy.display.module.bo_cache = None + + def set_mode(self, physical_size=None): + """ + This sets the video mode. It also picks the draw object. + """ + + # Ensure that we kill off the movie when changing screen res. + if renpy.display.draw and renpy.display.draw.info["renderer"] == "sw": + renpy.display.video.movie_stop(clear=False) + + if self.display_reset: + renpy.display.draw.deinit() + + if renpy.display.draw.info["renderer"] == "angle": + renpy.display.draw.quit() + + renpy.display.render.free_memory() + renpy.display.im.cache.clear() + renpy.text.text.layout_cache_clear() + + renpy.display.module.bo_cache = None + + self.kill_textures_and_surfaces() + + self.display_reset = False + + virtual_size = (renpy.config.screen_width, renpy.config.screen_height) + + if physical_size is None: + if renpy.android or renpy.game.preferences.physical_size is None: #@UndefinedVariable + physical_size = (renpy.config.screen_width, renpy.config.screen_height) + else: + physical_size = renpy.game.preferences.physical_size + + # Setup screen. + fullscreen = renpy.game.preferences.fullscreen + + # If we're in fullscreen mode, and changing to another mode, go to + # windowed mode first. + s = pygame.display.get_surface() + if s and (s.get_flags() & pygame.FULLSCREEN): + fullscreen = False + + old_fullscreen = self.fullscreen + self.fullscreen = fullscreen + + if os.environ.get('RENPY_DISABLE_FULLSCREEN', False): + fullscreen = False + self.fullscreen = renpy.game.preferences.fullscreen + + if renpy.display.draw: + draws = [ renpy.display.draw ] + else: + draws = self.get_draw_constructors() + + for draw in draws: + if draw.set_mode(virtual_size, physical_size, fullscreen): + renpy.display.draw = draw + break + else: + pygame.display.quit() + else: + # Ensure we don't get stuck in fullscreen. + renpy.game.preferences.fullscreen = False + raise Exception("Could not set video mode.") + + # Save the video size. + if renpy.config.save_physical_size and not fullscreen and not old_fullscreen: + renpy.game.preferences.physical_size = renpy.display.draw.get_physical_size() + + if android: + android.init() + + # We need to redraw the (now blank) screen. + self.force_redraw = True + + # Assume we have focus until told otherwise. + self.focused = True + + # Assume we're not minimized. + self.minimized = False + + # Force an interaction restart. + self.restart_interaction = True + + + def draw_screen(self, root_widget, fullscreen_video, draw): + + surftree = renpy.display.render.render_screen( + root_widget, + renpy.config.screen_width, + renpy.config.screen_height, + ) + + if draw: + renpy.display.draw.draw_screen(surftree, fullscreen_video) + + renpy.display.render.mark_sweep() + renpy.display.focus.take_focuses() + + self.surftree = surftree + self.fullscreen_video = fullscreen_video + + + def take_screenshot(self, scale, background=False): + """ + This takes a screenshot of the current screen, and stores it so + that it can gotten using get_screenshot() + + `background` + If true, we're in a background thread. So queue the request + until it can be handled by the main thread. + """ + + if background: + self.bgscreenshot_event.clear() + self.bgscreenshot_needed = True + self.bgscreenshot_event.wait() + + window = self.bgscreenshot_surface + self.bgscreenshot_surface = None + + else: + + window = renpy.display.draw.screenshot(self.surftree, self.fullscreen_video) + + surf = renpy.display.pgrender.copy_surface(window, True) + surf = renpy.display.scale.smoothscale(surf, scale) + surf = surf.convert() + + sio = io.StringIO() + renpy.display.module.save_png(surf, sio, 0) + self.screenshot = sio.getvalue() + sio.close() + + + def save_screenshot(self, filename): + """ + Saves a full-size screenshot in the given filename. + """ + + window = renpy.display.draw.screenshot(self.surftree, self.fullscreen_video) + + if renpy.config.screenshot_crop: + window = window.subsurface(renpy.config.screenshot_crop) + + try: + renpy.display.scale.image_save_unscaled(window, filename) + except: + if renpy.config.debug: + raise + pass + + + def get_screenshot(self): + """ + Gets the current screenshot, as a string. Returns None if there isn't + a current screenshot. + """ + + rv = self.screenshot + + if not rv: + self.take_screenshot((renpy.config.thumbnail_width, renpy.config.thumbnail_height)) + rv = self.screenshot + self.lose_screenshot() + + return rv + + + def lose_screenshot(self): + """ + This deallocates the saved screenshot. + """ + + self.screenshot = None + + + def show_window(self): + + if not renpy.store._window: + return + + if renpy.game.context().scene_lists.shown_window: + return + + if renpy.config.empty_window: + renpy.config.empty_window() + + def do_with(self, trans, paired, clear=False): + + if renpy.config.with_callback: + trans = renpy.config.with_callback(trans, paired) + + if (not trans) or self.suppress_transition: + self.with_none() + return False + else: + self.set_transition(trans) + return self.interact(trans_pause=True, + suppress_overlay=not renpy.config.overlay_during_with, + mouse='with', + clear=clear) + + def with_none(self): + """ + Implements the with None command, which sets the scene we will + be transitioning from. + """ + + renpy.exports.say_attributes = None + + # Show the window, if that's necessary. + self.show_window() + + # Compute the overlay. + self.compute_overlay() + + scene_lists = renpy.game.context().scene_lists + + # Compute the scene. + self.old_scene = self.compute_scene(scene_lists) + + # Get rid of transient things. + + for i in renpy.config.overlay_layers: + scene_lists.clear(i) + + scene_lists.replace_transient() + scene_lists.shown_window = False + + + def set_transition(self, transition, layer=None, force=False): + """ + Sets the transition that will be performed as part of the next + interaction. + """ + + if self.suppress_transition and not force: + return + + if transition is None: + self.transition.pop(layer, None) + else: + self.transition[layer] = transition + + + def event_peek(self): + """ + This peeks the next event. It returns None if no event exists. + """ + + if self.pushed_event: + return self.pushed_event + + ev = pygame.event.poll() + + if ev.type == pygame.NOEVENT: + # Seems to prevent the CPU from speeding up. + renpy.display.draw.event_peek_sleep() + return None + + self.pushed_event = ev + + return ev + + def event_poll(self): + """ + Called to busy-wait for an event while we're waiting to + redraw a frame. + """ + + if self.pushed_event: + rv = self.pushed_event + self.pushed_event = None + else: + rv = pygame.event.poll() + + self.last_event = rv + + return rv + + + def event_wait(self): + """ + This is in its own function so that we can track in the + profiler how much time is spent in interact. + """ + + if self.pushed_event: + rv = self.pushed_event + self.pushed_event = None + self.last_event = rv + return rv + + # Handle a request for a background screenshot. + if self.bgscreenshot_needed: + self.bgscreenshot_needed = False + self.bgscreenshot_surface = renpy.display.draw.screenshot(self.surftree, self.fullscreen_video) + self.bgscreenshot_event.set() + + try: + cpu_idle.set() + ev = pygame.event.wait() + finally: + cpu_idle.clear() + + self.last_event = ev + + return ev + + + def compute_overlay(self): + + if renpy.store.suppress_overlay: + return + + # Figure out what the overlay layer should look like. + renpy.ui.layer("overlay") + + for i in renpy.config.overlay_functions: + i() + + if renpy.game.context().scene_lists.shown_window: + for i in renpy.config.window_overlay_functions: + i() + + renpy.ui.close() + + + def compute_scene(self, scene_lists): + """ + This converts scene lists into a dictionary mapping layer + name to a Fixed containing that layer. + """ + + rv = { } + + for layer in renpy.config.layers + renpy.config.top_layers: + rv[layer] = scene_lists.make_layer(layer, self.layer_properties[layer]) + + root = renpy.display.layout.MultiBox(layout='fixed') + root.layers = { } + + for layer in renpy.config.layers: + root.layers[layer] = rv[layer] + root.add(rv[layer]) + rv[None] = root + + return rv + + + def quit_event(self): + """ + This is called to handle the user invoking a quit. + """ + + if self.quit_time > (time.time() - .75): + raise renpy.game.QuitException() + + if renpy.config.quit_action is not None: + self.quit_time = time.time() + + # Make the screen more suitable for interactions. + renpy.exports.movie_stop(only_fullscreen=True) + renpy.store.mouse_visible = True + + renpy.display.behavior.run(renpy.config.quit_action) + else: + raise renpy.game.QuitException() + + + def get_mouse_info(self): + # Figure out if the mouse visibility algorithm is hiding the mouse. + if self.mouse_event_time + renpy.config.mouse_hide_time < renpy.display.core.get_time(): + visible = False + else: + visible = renpy.store.mouse_visible and (not renpy.game.less_mouse) + + visible = visible and self.show_mouse + + # If not visible, hide the mouse. + if not visible: + return False, 0, 0, None + + # Deal with a hardware mouse, the easy way. + if not renpy.config.mouse: + return True, 0, 0, None + + # Deal with the mouse going offscreen. + if not self.focused: + return False, 0, 0, None + + mouse_kind = renpy.display.focus.get_mouse() or self.mouse + + # Figure out the mouse animation. + if mouse_kind in renpy.config.mouse: + anim = renpy.config.mouse[mouse_kind] + else: + anim = renpy.config.mouse[getattr(renpy.store, 'default_mouse', 'default')] + + img, x, y = anim[self.ticks % len(anim)] + tex = renpy.display.im.load_image(img) + + return False, x, y, tex + + def drawn_since(self, seconds_ago): + """ + Returns true if the screen has been drawn in the last `seconds_ago`, + and false otherwise. + """ + + return (get_time() - self.frame_time) <= seconds_ago + + def android_check_suspend(self): + + if android.check_pause(): + + android.sound.pause_all() + + pygame.time.set_timer(PERIODIC, 0) + pygame.time.set_timer(REDRAW, 0) + pygame.time.set_timer(TIMEEVENT, 0) + + # The game has to be saved. + renpy.loadsave.save("_reload-1") + + android.wait_for_resume() + + # Since we came back to life, we can get rid of the + # auto-reload. + renpy.loadsave.unlink_save("_reload-1") + + pygame.time.set_timer(PERIODIC, PERIODIC_INTERVAL) + + android.sound.unpause_all() + + def iconified(self): + """ + Called when we become an icon. + """ + + if self.minimized: + return + + self.minimized = True + + renpy.display.log.write("The window was minimized.") + + + def restored(self): + """ + Called when we are restored from being an icon. + """ + + # This is necessary on Windows/DirectX/Angle, as otherwise we get + # a blank screen. + + if not self.minimized: + return + + self.minimized = False + + renpy.display.log.write("The window was restored.") + + if renpy.windows: + self.display_reset = True + self.set_mode(self.last_resize) + + def enter_context(self): + """ + Called when we enter a new context. + """ + + # Stop ongoing transitions. + self.ongoing_transition.clear() + self.transition_from.clear() + self.transition_time.clear() + + def post_time_event(self): + """ + Posts a time_event object to the queue. + """ + + try: + pygame.event.post(self.time_event) + except: + pass + + def interact(self, clear=True, suppress_window=False, **kwargs): + """ + This handles an interaction, restarting it if necessary. All of the + keyword arguments are passed off to interact_core. + """ + + # Cancel magic error reporting. + renpy.bootstrap.report_error = None + + context = renpy.game.context() + + if context.interacting: + raise Exception("Cannot start an interaction in the middle of an interaction, without creating a new context.") + + context.interacting = True + + + # Show a missing window. + if not suppress_window: + self.show_window() + + # These things can be done once per interaction. + + preloads = self.preloads + self.preloads = [ ] + + try: + renpy.game.after_rollback = False + + for i in renpy.config.start_interact_callbacks: + i() + + repeat = True + + while repeat: + repeat, rv = self.interact_core(preloads=preloads, **kwargs) + + return rv + + finally: + + context.interacting = False + + # Clean out transient stuff at the end of an interaction. + if clear: + scene_lists = renpy.game.context().scene_lists + scene_lists.replace_transient() + + self.ongoing_transition = { } + self.transition_time = { } + self.transition_from = { } + + self.restart_interaction = True + + renpy.game.context().scene_lists.shown_window = False + + def interact_core(self, + show_mouse=True, + trans_pause=False, + suppress_overlay=False, + suppress_underlay=False, + mouse='default', + preloads=[], + roll_forward=None, + ): + + """ + This handles one cycle of displaying an image to the user, + and then responding to user input. + + @param show_mouse: Should the mouse be shown during this + interaction? Only advisory, and usually doesn't work. + + @param trans_pause: If given, we must have a transition. Should we + add a pause behavior during the transition? + + @param suppress_overlay: This suppresses the display of the overlay. + @param suppress_underlay: This suppresses the display of the underlay. + """ + + self.roll_forward = roll_forward + self.show_mouse = show_mouse + + suppress_transition = renpy.config.skipping or renpy.game.less_updates + + # The global one. + self.suppress_transition = False + + # Figure out transitions. + for k in self.transition: + if k not in self.old_scene: + continue + + self.ongoing_transition[k] = self.transition[k] + self.transition_from[k] = self.old_scene[k] + self.transition_time[k] = None + + self.transition.clear() + + if suppress_transition: + self.ongoing_transition.clear() + self.transition_from.clear() + self.transition_time.clear() + + ## Safety condition, prevents deadlocks. + if trans_pause: + if not self.ongoing_transition: + return False, None + if None not in self.ongoing_transition: + return False, None + if suppress_transition: + return False, None + if not self.old_scene: + return False, None + + # Check to see if the language has changed. + renpy.translation.check_language() + + # We just restarted. + self.restart_interaction = False + + # Setup the mouse. + self.mouse = mouse + + # The start and end times of this interaction. + start_time = get_time() + end_time = start_time + + # frames = 0 + + for i in renpy.config.interact_callbacks: + i() + + # Set the window caption. + self.set_window_caption() + + # Tick time forward. + renpy.display.im.cache.tick() + renpy.text.text.layout_cache_tick() + renpy.display.predict.reset() + + # Cleare the size groups. + renpy.display.layout.size_groups.clear() + + # Clear some events. + pygame.event.clear((pygame.MOUSEMOTION, + PERIODIC, + TIMEEVENT, + REDRAW)) + + # Add a single TIMEEVENT to the queue. + self.post_time_event() + + # Figure out the scene list we want to show. + scene_lists = renpy.game.context().scene_lists + + # Remove the now-hidden things. + scene_lists.remove_hidden() + + # Compute the overlay. + if not suppress_overlay: + self.compute_overlay() + + # The root widget of everything that is displayed on the screen. + root_widget = renpy.display.layout.MultiBox(layout='fixed') + root_widget.layers = { } + + # A list of widgets that are roots of trees of widgets that are + # considered for focusing. + focus_roots = [ ] + + # Add the underlay to the root widget. + if not suppress_underlay: + for i in renpy.config.underlay: + root_widget.add(i) + focus_roots.append(i) + + if roll_forward is not None: + rfw = renpy.display.behavior.RollForward(roll_forward) + root_widget.add(rfw) + focus_roots.append(rfw) + + # Figure out the scene. (All of the layers, and the root.) + scene = self.compute_scene(scene_lists) + + # If necessary, load all images here. + for w in scene.values(): + try: + renpy.display.predict.displayable(w) + except: + pass + + # The root widget of all of the layers. + layers_root = renpy.display.layout.MultiBox(layout='fixed') + layers_root.layers = { } + + def add_layer(where, layer): + + scene_layer = scene[layer] + focus_roots.append(scene_layer) + + if (self.ongoing_transition.get(layer, None) and + not suppress_transition): + + trans = self.ongoing_transition[layer]( + old_widget=self.transition_from[layer], + new_widget=scene_layer) + + if not isinstance(trans, Displayable): + raise Exception("Expected transition to be a displayable, not a %r" % trans) + + transition_time = self.transition_time.get(layer, None) + + where.add(trans, transition_time, transition_time) + where.layers[layer] = trans + + else: + where.layers[layer] = scene_layer + where.add(scene_layer) + + # Add layers (perhaps with transitions) to the layers root. + for layer in renpy.config.layers: + add_layer(layers_root, layer) + + # Add layers_root to root_widget, perhaps through a transition. + if (self.ongoing_transition.get(None, None) and + not suppress_transition): + + old_root = renpy.display.layout.MultiBox(layout='fixed') + old_root.layers = { } + + for layer in renpy.config.layers: + d = self.transition_from[None].layers[layer] + old_root.layers[layer] = d + old_root.add(d) + + trans = self.ongoing_transition[None]( + old_widget=old_root, + new_widget=layers_root) + + if not isinstance(trans, Displayable): + raise Exception("Expected transition to be a displayable, not a %r" % trans) + + trans._show() + + transition_time = self.transition_time.get(None, None) + root_widget.add(trans, transition_time, transition_time) + + if trans_pause: + sb = renpy.display.behavior.SayBehavior() + root_widget.add(sb) + focus_roots.append(sb) + + pb = renpy.display.behavior.PauseBehavior(trans.delay) + root_widget.add(pb, transition_time, transition_time) + focus_roots.append(pb) + + else: + root_widget.add(layers_root) + + # Add top_layers to the root_widget. + for layer in renpy.config.top_layers: + add_layer(root_widget, layer) + + prediction_coroutine = renpy.display.predict.prediction_coroutine(root_widget) + prediction_coroutine.send(None) + + # Clean out the registered adjustments. + renpy.display.behavior.adj_registered.clear() + + # Clean up some movie-related things. + renpy.display.video.early_interact() + + # Call per-interaction code for all widgets. + root_widget.visit_all(lambda i : i.per_interact()) + + # Now, update various things regarding scenes and transitions, + # so we are ready for a new interaction or a restart. + self.old_scene = scene + + # Okay, from here on we now have a single root widget (root_widget), + # which we will try to show to the user. + + # Figure out what should be focused. + renpy.display.focus.before_interact(focus_roots) + + # Redraw the screen. + renpy.display.render.process_redraws() + needs_redraw = True + + # First pass through the while loop? + first_pass = True + + # We don't yet know when the interaction began. + self.interact_time = None + + # We only want to do autosave once. + did_autosave = False + + old_timeout_time = None + old_redraw_time = None + + rv = None + + # Start sound. + renpy.audio.audio.interact() + + # How long until we redraw. + redraw_in = 3600 + + # Have we drawn a frame yet? + video_frame_drawn = False + + # This try block is used to force cleanup even on termination + # caused by an exception propagating through this function. + try: + + while rv is None: + + # Check for a change in fullscreen preference. + if self.fullscreen != renpy.game.preferences.fullscreen or self.display_reset: + self.set_mode() + needs_redraw = True + + # Check for suspend. + if android: + self.android_check_suspend() + + # Redraw the screen. + if (self.force_redraw or + ((first_pass or not pygame.event.peek(ALL_EVENTS)) and + renpy.display.draw.should_redraw(needs_redraw, first_pass))): + + self.force_redraw = False + + # If we have a movie, start showing it. + fullscreen_video = renpy.display.video.interact() + + # Clean out the redraws, if we have to. + # renpy.display.render.kill_redraws() + + # Draw the screen. + self.frame_time = get_time() + + if not self.interact_time: + self.interact_time = self.frame_time + + self.draw_screen(root_widget, fullscreen_video, (not fullscreen_video) or video_frame_drawn) + + if first_pass: + scene_lists.set_times(self.interact_time) + for k, v in self.transition_time.items(): + if v is None: + self.transition_time[k] = self.interact_time + + renpy.config.frames += 1 + + # If profiling is enabled, report the profile time. + if renpy.config.profile : + new_time = get_time() + + if new_time - self.profile_time > .015: + print("Profile: Redraw took %f seconds." % (new_time - self.frame_time)) + print("Profile: %f seconds to complete event." % (new_time - self.profile_time)) + + if first_pass and self.last_event: + x, y = renpy.display.draw.get_mouse_pos() + renpy.display.focus.mouse_handler(self.last_event, x, y, default=False) + + needs_redraw = False + first_pass = False + + pygame.time.set_timer(REDRAW, 0) + pygame.event.clear([REDRAW]) + old_redraw_time = None + + # Draw the mouse, if it needs drawing. + renpy.display.draw.update_mouse() + + # See if we want to restart the interaction entirely. + if self.restart_interaction: + return True, None + + # Determine if we need a redraw. (We want to run these + # functions, so we put them first to prevent short-circuiting.) + + if renpy.display.video.frequent(): + needs_redraw = True + video_frame_drawn = True + + needs_redraw = renpy.display.video.frequent() or needs_redraw + needs_redraw = renpy.display.render.process_redraws() or needs_redraw + + # How many seconds until we timeout. + timeout_in = 3600 + + # Handle the redraw timer. + redraw_time = renpy.display.render.redraw_time() + + if (redraw_time is not None) and not needs_redraw: + if redraw_time != old_redraw_time: + time_left = redraw_time - get_time() + time_left = min(time_left, 3600) + redraw_in = time_left + + if time_left <= 0: + try: + pygame.event.post(self.redraw_event) + except: + pass + pygame.time.set_timer(REDRAW, 0) + else: + pygame.time.set_timer(REDRAW, max(int(time_left * 1000), 1)) + + old_redraw_time = redraw_time + else: + redraw_in = 3600 + pygame.time.set_timer(REDRAW, 0) + + # Handle the timeout timer. + if not self.timeout_time: + pygame.time.set_timer(TIMEEVENT, 0) + else: + time_left = self.timeout_time - get_time() + time_left = min(time_left, 3600) + timeout_in = time_left + + if time_left <= 0: + self.timeout_time = None + pygame.time.set_timer(TIMEEVENT, 0) + self.post_time_event() + elif self.timeout_time != old_timeout_time: + # Always set to at least 1ms. + pygame.time.set_timer(TIMEEVENT, int(time_left * 1000 + 1)) + old_timeout_time = self.timeout_time + + # Predict images, if we haven't done so already. + while prediction_coroutine is not None: + + # Can we do expensive prediction? + expensive_predict = not (needs_redraw or self.event_peek() or renpy.audio.music.is_playing("movie")) + + result = prediction_coroutine.send(expensive_predict) + + if not result: + prediction_coroutine = None + break + + if not expensive_predict: + break + + # If we need to redraw again, do it if we don't have an + # event going on. + if needs_redraw and not self.event_peek(): + if renpy.config.profile: + self.profile_time = get_time() + continue + + # Handle autosaving, as necessary. + if not did_autosave and not needs_redraw and not self.event_peek() and redraw_in > .25 and timeout_in > .25: + renpy.loadsave.autosave() + did_autosave = True + + if needs_redraw or renpy.display.video.playing(): + ev = self.event_poll() + else: + ev = self.event_wait() + + if ev.type == pygame.NOEVENT: + continue + + if renpy.config.profile: + self.profile_time = get_time() + + # Try to merge an TIMEEVENT with other timeevents. + if ev.type == TIMEEVENT: + old_timeout_time = None + pygame.event.clear([TIMEEVENT]) + + # On Android, where we have multiple mouse buttons, we can + # merge a mouse down and mouse up event with its successor. This + # prevents us from getting overwhelmed with too many events on + # a multitouch screen. + if android and (ev.type == pygame.MOUSEBUTTONDOWN or ev.type == pygame.MOUSEBUTTONUP): + pygame.event.clear(ev.type) + + # Handle redraw timeouts. + if ev.type == REDRAW: + pygame.event.clear([REDRAW]) + old_redraw_time = None + continue + + # Handle periodic events. This includes updating the mouse timers (and through the loop, + # the mouse itself), and the audio system periodic calls. + if ev.type == PERIODIC: + events = 1 + len(pygame.event.get([PERIODIC])) + self.ticks += events + + if renpy.config.periodic_callback: + renpy.config.periodic_callback() + + renpy.audio.audio.periodic() + continue + + # This can set the event to None, to ignore it. + ev = renpy.display.joystick.event(ev) + if not ev: + continue + + # Handle skipping. + renpy.display.behavior.skipping(ev) + + # Handle quit specially for now. + if ev.type == pygame.QUIT: + self.quit_event() + continue + + # Handle videoresize. + if ev.type == pygame.VIDEORESIZE: + evs = pygame.event.get([pygame.VIDEORESIZE]) + if len(evs): + ev = evs[-1] + + if self.last_resize != ev.size: + self.last_resize = ev.size + self.set_mode((ev.w, ev.h)) + + continue + + if ev.type == pygame.MOUSEMOTION or \ + ev.type == pygame.MOUSEBUTTONDOWN or \ + ev.type == pygame.MOUSEBUTTONUP: + + self.mouse_event_time = renpy.display.core.get_time() + + # Merge mousemotion events. + if ev.type == pygame.MOUSEMOTION: + evs = pygame.event.get([pygame.MOUSEMOTION]) + if len(evs): + ev = evs[-1] + + if renpy.windows: + self.focused = True + + # Handle focus notifications. + if ev.type == pygame.ACTIVEEVENT: + if ev.state & 1: + self.focused = ev.gain + + if ev.state & 4: + if ev.gain: + self.restored() + else: + self.iconified() + + pygame.key.set_mods(0) + + # This returns the event location. It also updates the + # mouse state as necessary. + x, y = renpy.display.draw.mouse_event(ev) + + if not self.focused: + x = -1 + y = -1 + + self.event_time = end_time = get_time() + + try: + + # Handle the event normally. + rv = renpy.display.focus.mouse_handler(ev, x, y) + + + if rv is None: + rv = root_widget.event(ev, x, y, 0) + + if rv is None: + rv = renpy.display.focus.key_handler(ev) + + if rv is not None: + break + + # Handle displayable inspector. + if renpy.config.inspector and renpy.display.behavior.inspector(ev): + l = self.surftree.main_displayables_at_point(x, y, renpy.config.transient_layers + renpy.config.context_clear_layers + renpy.config.overlay_layers) + renpy.game.invoke_in_new_context(renpy.config.inspector, l) + + except IgnoreEvent: + # An ignored event can change the timeout. So we want to + # process an TIMEEVENT to ensure that the timeout is + # set correctly. + self.post_time_event() + + + # Check again after handling the event. + needs_redraw |= renpy.display.render.process_redraws() + + if self.restart_interaction: + return True, None + + # If we were trans-paused and rv is true, suppress + # transitions up to the next interaction. + if trans_pause and rv: + self.suppress_transition = True + + # But wait, there's more! The finally block runs some cleanup + # after this. + return False, rv + + except EndInteraction as e: + return False, e.value + + finally: + + renpy.exports.say_attributes = None + + # Clean out the overlay layers. + for i in renpy.config.overlay_layers: + scene_lists.clear(i) + + # Stop ongoing preloading. + renpy.display.im.cache.end_tick() + + # We no longer disable periodic between interactions. + # pygame.time.set_timer(PERIODIC, 0) + + pygame.time.set_timer(TIMEEVENT, 0) + pygame.time.set_timer(REDRAW, 0) + + renpy.game.context().runtime += end_time - start_time + + # Restart the old interaction, which also causes a + # redraw if needed. + self.restart_interaction = True + + # print "It took", frames, "frames." + + def timeout(self, offset): + if offset < 0: + return + + if self.timeout_time: + self.timeout_time = min(self.event_time + offset, self.timeout_time) + else: + self.timeout_time = self.event_time + offset + diff --git a/unrpyc/renpy/display/dragdrop.py b/unrpyc/renpy/display/dragdrop.py new file mode 100644 index 0000000..0b99e63 --- /dev/null +++ b/unrpyc/renpy/display/dragdrop.py @@ -0,0 +1,731 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# TODO: Use overlap (rather than simple pointer location) to determine +# drag and drop. + +import renpy.display +from renpy.display.render import render, Render, redraw +from renpy.display.core import absolute +from renpy.display.behavior import map_event, run + +import pygame + +def default_drag_group(): + """ + Gets the default drag group. If it doesn't exist yet, creates it. + """ + + sls = renpy.game.context().scene_lists + + rv = sls.drag_group + + if rv is None: + rv = DragGroup() + sls.drag_group = rv + + return rv + +def default_drag_joined(drag): + return [ (drag, 0, 0) ] + +class Drag(renpy.display.core.Displayable, renpy.python.RevertableObject): + """ + :doc: drag_drop class + :args: (d=None, drag_name=None, draggable=True, droppable=True, drag_raise=True, dragged=None, dropped=None, drag_handle=(0.0, 0.0, 1.0, 1.0), drag_joined=..., clicked=None, hovered=None, unhovered=None, **properties) + + A displayable that represents an object that can be dragged around + its enclosing area. A Drag can also represent an area that + other Drags can be dropped on. + + A Drag can be moved around inside is parent. Generally, its parent + should be either a :func:`Fixed` or :class:`DragGroup`. + + A Drag has one child. The child's state reflects the status + of the drag and drop operation: + + * ``selected_hover`` - when it is being dragged. + * ``selected_idle`` - when it can be dropped on. + * ``hover`` - when the draggable will be dragged when the mouse is + clicked. + * ``idle`` - otherwise. + + The drag handle is a rectangle inside the child. The mouse must be over + a non-transparent pixel inside the drag handle for dragging or clicking + to occur. + + A newly-created draggable is added to the default DragGroup. A draggable + can only be in a single DragGroup - if it's added to a second group, + it's removed from the first. + + When a Drag is first rendered, if it's position cannot be determined + from the DragGroup it is in, the position of its upper-left corner + is computed using the standard layout algorithm. Once that position + + + `d` + If present, the child of this Drag. Drags use the child style + in preference to this, if it's not None. + + `drag_name` + If not None, the name of this draggable. This is available + as the `name` property of draggable objects. If a Drag + with the same name is or was in the DragGroup, the starting + position of this Drag is taken from that Draggable. + + `draggable` + If true, the Drag can be dragged around the screen with + the mouse. + + `droppable` + If true, other Drags can be dropped on this Drag. + + `drag_raise` + If true, this Drag is raised to the top when it is dragged. If + it is joined to other Drags, all joined drags are raised. + + `dragged` + A callback (or list of callbacks) that is called when the Drag + has been dragged. It is called with two arguments. The first is + a list of Drags that are being dragged. The second is either + a Drag that is being dropped onto, or None of a drop did not + occur. If the callback returns a value other than None, that + value is returned as the result of the interaction. + + `dropped` + A callback (or list of callbacks) that is called when this Drag + is dropped onto. It is called with two arguments. The first + is the Drag being dropped onto. The second is a list of Drags that + are being dragged. If the callback returns a value other than None, + that value is returned as the result of the interaction. + + When a dragged and dropped callback are triggered for the same + event, the dropped callback is only called if dragged returns + None. + + `clicked` + A callback this is called, with no arguments, when the Drag is + clicked without being moved. A droppable can also be focused + and clicked. If the callback returns a value othe than None, + that value is returned as the result of the interaction. + + `drag_handle` + A (x, y, width, height) tuple, giving the position of the drag + handle within the child. In this tuple, integers are considered + to be a literal number of pixels, while floats are relative to + the size of the child. + + `drag_joined` + This is called with the current Drag as an argument. It's + expected to return a list of [ (drag, x, y) ] tuples, giving + the draggables to drag as a unit. `x` and `y` are the offsets + of the drags relative to each other, they are not relative + to the corner of this drag. + + Except for `d`, all of the parameters are available as fields (with + the same name) on the Drag object. In addition, after the drag has + been rendered, the following fields become available: + + `x`, `y` + The position of the Drag relative to its parent, in pixels. + + `w`, `h` + The width and height of the Drag's child, in pixels. + """ + + def __init__(self, + d=None, + drag_name=None, + draggable=True, + droppable=True, + drag_raise=True, + dragged=None, + dropped=None, + drag_handle=(0.0, 0.0, 1.0, 1.0), + drag_joined=default_drag_joined, + clicked=None, + hovered=None, + unhovered=None, + replaces=None, + **properties): + + super(Drag, self).__init__(self, **properties) + + self.drag_name = drag_name + self.draggable = draggable + self.droppable = droppable + self.drag_raise = drag_raise + self.dragged = dragged + self.dropped = dropped + self.drag_handle = drag_handle + self.drag_joined = drag_joined + self.clicked = clicked + self.hovered = hovered + self.unhovered = unhovered + + self.child = None + + # Add us to a drag group on creation. + if drag_name: + self.drag_group = default_drag_group() + + # The current x and y coordinates of this displayable. + self.x = None + self.y = None + + # The width and height of the child. + self.w = None + self.h = None + + # The width and height of our parent. + self.parent_width = None + self.parent_height = None + + # The target x and y coordinates of this displayable. (The + # coordinates that we're snapping to.) + self.target_x = None + self.target_y = None + + # The offset from the location of the mouse to the "grab point", + # which is where the things that are being moved are offset from. + self.grab_x = None + self.grab_y = None + + # x and y from the last time we rendered. + self.last_x = None + self.last_y = None + + # The abs_x and abs_y from when we started the grab. + self.start_x = 0 + self.start_y = 0 + + # The last time we were shown, using the animation timebases. + self.at = 0 + + # The (animation timebase) time at which we should reach + # the target coordinates. + self.target_at = 0 + + # The displayable we were last dropping on. + self.last_drop = None + + # Did we move over the course of this drag? + self.drag_moved = False + + if replaces is not None: + self.x = replaces.x + self.y = replaces.y + self.at = replaces.at + self.target_x = replaces.target_x + self.target_y = replaces.target_y + self.target_at = replaces.target_at + + if d is not None: + self.add(d) + + + def snap(self, x, y, delay=0): + """ + :doc: drag_drop method + + Changes the position of the drag. If the drag is not showing, + then the position change is instantaneous. Otherwise, the + position change takes `delay` seconds, and is animated as a + linear move. + """ + + self.target_x = x + self.target_y = y + + if self.x is not None: + self.target_at = self.at + delay + else: + self.target_at = self.at + self.x = x + self.y = y + + redraw(self, 0) + + def set_style_prefix(self, prefix, root): + super(Drag, self).set_style_prefix(prefix, root) + + if self.child is not None: + self.child.set_style_prefix(prefix, False) + + def add(self, d): + if self.child is not None: + raise Exception("Drag expects either zero or one children.") + + self.child = renpy.easy.displayable(d) + + def set_child(self, d): + """ + :doc: drag_drop method + + Changes the child of this drag to `d`. + """ + + d.per_interact() + self.child = renpy.easy.displayable(d) + + def top(self): + """ + :doc: drag_drop method + + Raises this displayable to the top of its drag_group. + """ + + if self.drag_group is not None: + self.drag_group.raise_children([ self ]) + + def visit(self): + return [ self.child ] + + def focus(self, default=False): + super(Drag, self).focus(default) + + rv = None + + if not default: + rv = run(self.hovered) + + return rv + + def unfocus(self, default=False): + super(Drag, self).unfocus(default) + + if not default: + run(self.unhovered) + + def render(self, width, height, st, at): + + child = self.style.child + if child is None: + child = self.child + + self.parent_width = width + self.parent_height = height + + cr = render(child, width, height, st, at) + cw, ch = cr.get_size() + + rv = Render(cw, ch) + rv.blit(cr, (0, 0)) + + self.w = cw + self.h = ch + + # If we don't have a position, then look for it in a drag group. + if (self.x is None) and (self.drag_group is not None) and (self.drag_name is not None): + if self.drag_name in self.drag_group.positions: + self.x, self.y = self.drag_group.positions[self.drag_name] + + # If we don't have a position, run the placement code and use + # that to compute our placement. + if self.x is None: + self.x, self.y = self.place(None, 0, 0, width, height, rv) + self.x = int(self.x) + self.y = int(self.y) + + if self.target_x is None: + self.target_x = self.x + self.target_y = self.y + self.target_at = at + + # Determine if we need to do the snap animation. + if at >= self.target_at: + self.x = self.target_x + self.y = self.target_y + else: + done = (at - self.at) / (self.target_at - self.at) + self.x = absolute(self.x + done * (self.target_x - self.x)) + self.y = absolute(self.y + done * (self.target_y - self.y)) + redraw(self, 0) + + if self.draggable or self.clicked is not None: + + fx, fy, fw, fh = self.drag_handle + + if isinstance(fx, float): + fx = int(fx * cw) + + if isinstance(fy, float): + fy = int(fy * ch) + + if isinstance(fw, float): + fw = int(fw * cw) + + if isinstance(fh, float): + fh = int(fh * ch) + + rv.add_focus(self, None, fx, fy, fw, fh, fx, fy, cr.subsurface((fx, fy, fw, fh))) + + self.last_x = self.x + self.last_y = self.y + self.at = at + + return rv + + def event(self, ev, x, y, st): + + if not self.is_focused(): + return self.child.event(ev, x, y, st) + + # if not self.draggable: + # return self.child.event(ev, x, y, st) + + # Mouse, in parent-relative coordinates. + par_x = self.last_x + x + par_y = self.last_y + y + + grabbed = (renpy.display.focus.get_grab() is self) + + if grabbed: + joined_offsets = self.drag_joined(self) + joined = [ i[0] for i in joined_offsets ] + + elif self.draggable and map_event(ev, "drag_activate"): + + joined_offsets = self.drag_joined(self) + joined = [ i[0] for i in joined_offsets ] + + if not joined: + raise renpy.display.core.IgnoreEvent() + + renpy.display.focus.set_grab(self) + + self.grab_x = x + self.grab_y = y + + # If we're not the only thing we're joined with, we + # might need to adjust our grab point. + for i, xo, yo in joined_offsets: + if i is self: + self.grab_x += xo + self.grab_y += yo + break + + self.drag_moved = False + self.start_x = par_x + self.start_y = par_y + + grabbed = True + + # Handle clicking on droppables. + if not grabbed: + if self.clicked is not None and map_event(ev, "drag_deactivate"): + rv = run(self.clicked) + if rv is not None: + return rv + + raise renpy.display.core.IgnoreEvent() + + return self.child.event(ev, x, y, st) + + # Handle moves by moving things relative to the grab point. + if ev.type in (pygame.MOUSEMOTION, pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN): + + if not self.drag_moved and (self.start_x != par_x or self.start_y != par_y): + self.drag_moved = True + + # We may not be in the drag_joined group. + self.set_style_prefix("idle_", True) + + # Set the style. + for i in joined: + i.set_style_prefix("selected_hover_", True) + + # Raise the joined items. + if self.drag_raise and self.drag_group is not None: + self.drag_group.raise_children(joined) + + if self.drag_moved: + for i, xo, yo in joined_offsets: + + new_x = par_x - self.grab_x + xo + new_y = par_y - self.grab_y + yo + new_x = max(new_x, 0) + new_x = min(new_x, i.parent_width - i.w) + new_y = max(new_y, 0) + new_y = min(new_y, i.parent_height - i.h) + + if i.drag_group is not None and i.drag_name is not None: + i.drag_group.positions[i.drag_name] = (new_x, new_y) + + i.x = new_x + i.y = new_y + i.target_x = new_x + i.target_y = new_y + i.target_at = self.at + redraw(i, 0) + + if (self.drag_group is not None) and self.drag_moved: + drop = self.drag_group.get_best_drop(joined) + else: + drop = None + + if drop is not self.last_drop: + + if self.last_drop is not None: + self.last_drop.set_style_prefix("idle_", True) + + if drop is not None: + drop.set_style_prefix("selected_idle_", True) + + self.last_drop = drop + + if map_event(ev, 'drag_deactivate'): + renpy.display.focus.set_grab(None) + + if drop is not None: + drop.set_style_prefix("idle_", True) + + for i in joined: + i.set_style_prefix("idle_", True) + + self.set_style_prefix("hover_", True) + + self.grab_x = None + self.grab_y = None + self.last_drop = None + + if self.drag_moved: + + # Call the drag callback. + drag = joined[0] + if drag.dragged is not None: + rv = run(drag.dragged, joined, drop) + if rv is not None: + return rv + + # Call the drop callback. + if drop is not None and drop.dropped is not None: + rv = run(drop.dropped, drop, joined) + if rv is not None: + return rv + + else: + + # Call the clicked callback. + if self.clicked: + rv = run(self.clicked) + if rv is not None: + return rv + + raise renpy.display.core.IgnoreEvent() + + + def get_placement(self): + + if self.x is not None: + return self.x, self.y, 0, 0, 0, 0, True + else: + return super(Drag, self).get_placement() + + def per_interact(self): + self.set_style_prefix("idle_", True) + super(Drag, self).per_interact() + + +class DragGroup(renpy.display.layout.MultiBox): + """ + :doc: drag_drop class + + Represents a group of Drags. A Drag is limited to the boundary of + its DragGroup. Dropping only works between Drags that are in the + same DragGroup. Drags may only be raised when they are inside a + DragGroup. + + A DragGroup is laid out like a :func:`Fixed`. + + All positional parameters to the DragGroup constructor should be + Drags, that are added to the DragGroup. + """ + + _list_type = renpy.python.RevertableList + + def __init__(self, *children, **properties): + properties.setdefault("style", "fixed") + properties.setdefault("layout", "fixed") + + replaces = properties.pop("replaces", None) + + super(DragGroup, self).__init__(**properties) + + if replaces is not None: + self.positions = renpy.python.RevertableDict(replaces.positions) + self.sensitive = replaces.sensitive + else: + self.positions = renpy.python.RevertableDict() + self.sensitive = True + + for i in children: + self.add(i) + + + def add(self, child): + """ + :doc: drag_drop method + + Adds `child`, which must be a Drag, to this DragGroup. + """ + + if not isinstance(child, Drag): + raise Exception("Only drags can be added to a drag group.") + + child.drag_group = self + super(DragGroup, self).add(child) + + def remove(self, child): + """ + :doc: drag_drop method + + Removes `child` from this DragGroup. + """ + + + if not isinstance(child, Drag): + raise Exception("Only drags can be removed from a drag group.") + + child.x = None + super(DragGroup, self).remove(child) + + + def event(self, ev, x, y, st): + + if not self.sensitive: + return None + + return super(DragGroup, self).event(ev, x, y, st) + + def raise_children(self, l): + """ + Raises the children in `l` to the top of this drag_group, using the + order given in l for those children. + """ + + s = set(l) + + offset_map = { } + + children = [ ] + offsets = [ ] + + for i, c in enumerate(self.children): + if i < len(self.offsets): + o = self.offsets[i] + else: + o = (0, 0) + + if c not in s: + children.append(c) + offsets.append(o) + else: + offset_map[c] = o + + for c in l: + if c in offset_map: + children.append(c) + offsets.append(offset_map[c]) + + self.children = self._list_type(children) + self.offsets = self._list_type(offsets) + + + def get_best_drop(self, joined): + """ + Returns the droppable that the members of joined overlap the most. + """ + + max_overlap = 0 + rv = 0 + + joined_set = set(joined) + + for d in joined: + + r1 = (d.x, d.y, d.w, d.h) + + for c in self.children: + if c in joined_set: + continue + + if not c.droppable: + continue + + r2 = (c.x, c.y, c.w, c.h) + + overlap = rect_overlap_area(r1, r2) + + if overlap >= max_overlap: + rv = c + max_overlap = overlap + + if max_overlap <= 0: + return None + else: + return rv + + def get_children(self): + """ + Returns a list of Drags that are the children of + this DragGroup. + """ + + return renpy.python.RevertableList(self.children) + + def get_child_by_name(self, name): + """ + :doc: drag_drop method + + Returns the first child of this DragGroup that has a drag_name + of name. + """ + + for i in self.children: + if i.drag_name == name: + return i + + return None + + +def rect_overlap_area(r1, r2): + """ + Returns the number of pixels by which rectangles r1 and r2 overlap. + """ + + x1, y1, w1, h1 = r1 + x2, y2, w2, h2 = r2 + + maxleft = max(x1, x2) + minright = min(x1 + w1, x2 + w2) + maxtop = max(y1, y2) + minbottom = min(y1 + h1, y2 + h2) + + if minright < maxleft: + return 0 + + if minbottom < maxtop: + return 0 + + return (minright - maxleft) * (minbottom - maxtop) + + diff --git a/unrpyc/renpy/display/error.py b/unrpyc/renpy/display/error.py new file mode 100644 index 0000000..a8f7e1b --- /dev/null +++ b/unrpyc/renpy/display/error.py @@ -0,0 +1,159 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains code to handle GUI-based error reporting. + +import renpy.display +import os + +error_handled = False + +############################################################################## +# Initialized approach. + +def call_exception_screen(screen_name, **kwargs): + try: + + old_quit = renpy.config.quit_action + renpy.config.quit_action = renpy.exports.quit + + for i in renpy.config.layers: + renpy.game.context().scene_lists.clear(i) + + renpy.exports.show_screen(screen_name, _transient=True, **kwargs) + return renpy.ui.interact(mouse="screen", type="screen", suppress_overlay=True, suppress_underlay=True) + + finally: + renpy.config.quit_action = old_quit + +def rollback_action(): + renpy.exports.rollback(force=True) + +def init_display(): + """ + The minimum amount of code required to init the display. + """ + + if not renpy.game.interface: + renpy.display.core.Interface() + renpy.loader.index_archives() + renpy.display.im.cache.init() + renpy.style.styles_built = True # The styles we use were built in renpy.main. + + renpy.ui.reset() + +def error_dump(): + """ + Handles dumps in the case where an error occurs. + """ + + renpy.dump.dump(True) + +def report_exception(short, full, traceback_fn): + """ + Reports an exception to the user. Returns True if the exception should + be raised by the normal reporting mechanisms. Otherwise, should raise + the appropriate exception to cause a reload or quit or rollback. + """ + + global error_handled + error_handled = True + + error_dump() + + if renpy.game.args.command != "run": #@UndefinedVariable + return True + + if "RENPY_SIMPLE_EXCEPTIONS" in os.environ: + return True + + if not renpy.exports.has_screen("_exception"): + return True + + try: + init_display() + except: + return True + + if renpy.display.draw is None: + return True + + ignore_action = None + rollback_action = None + reload_action = None + + try: + if not renpy.game.context().init_phase: + + if renpy.config.rollback_enabled: + rollback_action = renpy.display.error.rollback_action + + reload_action = renpy.exports.curried_call_in_new_context("_save_reload_game") + + if renpy.game.context(-1).next_node is not None: + ignore_action = renpy.ui.returns(False) + except: + pass + + renpy.game.invoke_in_new_context( + call_exception_screen, + "_exception", + short=short, full=full, + rollback_action=rollback_action, + reload_action=reload_action, + ignore_action=ignore_action, + traceback_fn=traceback_fn, + ) + + +def report_parse_errors(errors, error_fn): + """ + Reports an exception to the user. Returns True if the exception should + be raised by the normal reporting mechanisms. Otherwise, should raise + the appropriate exception. + """ + + global error_handled + error_handled = True + + error_dump() + + if renpy.game.args.command != "run": #@UndefinedVariable + return True + + if "RENPY_SIMPLE_EXCEPTIONS" in os.environ: + return True + + if not renpy.exports.has_screen("_parse_errors"): + return True + + init_display() + + reload_action = renpy.exports.utter_restart + + renpy.game.invoke_in_new_context( + call_exception_screen, + "_parse_errors", + reload_action=reload_action, + errors=errors, + error_fn = error_fn, + ) + diff --git a/unrpyc/renpy/display/focus.py b/unrpyc/renpy/display/focus.py new file mode 100644 index 0000000..521fe22 --- /dev/null +++ b/unrpyc/renpy/display/focus.py @@ -0,0 +1,449 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains code to manage focus on the display. + +import pygame +import renpy.display + +class Focus(object): + + def __init__(self, widget, arg, x, y, w, h): + + self.widget = widget + self.arg = arg + self.x = x + self.y = y + self.w = w + self.h = h + + def copy(self): + return Focus( + self.widget, + self.arg, + self.x, + self.y, + self.w, + self.h) + + def __repr__(self): + return "<Focus: %r %r (%r, %r, %r, %r)>" % ( + self.widget, + self.arg, + self.x, + self.y, + self.w, + self.h) + + +# The current focus argument. +argument = None + +# The widget currently grabbing the input, if any. +grab = None + +# The default focus for the current screen. +default_focus = None + +# Sets the currently focused widget. +def set_focused(widget, arg): + global argument + argument = arg + renpy.game.context().scene_lists.focused = widget + +# Gets the currently focused widget. +def get_focused(): + return renpy.game.context().scene_lists.focused + +# Get the mouse cursor for the focused widget. +def get_mouse(): + focused = get_focused() + if focused is None: + return None + else: + return focused.style.mouse + +def set_grab(widget): + global grab + grab = widget + +def get_grab(): + return grab + +# The current list of focuses that we know about. +focus_list = [ ] + +# This takes in a focus list from the rendering system. +def take_focuses(): + global focus_list + focus_list = [ ] + + renpy.display.render.take_focuses(focus_list) + + global default_focus + default_focus = None + + for f in focus_list: + if f.x is None: + default_focus = f + +def focus_coordinates(): + """ + :doc: other + + This attempts to find the coordinates of the currently-focused + displayable. If it can, it will return them as a (x, y, w, h) + tuple. If not, it will return a (None, None, None, None) tuple. + """ + + current = get_focused() + + for i in focus_list: + if i.widget == current and i.arg == argument: + return i.x, i.y, i.w, i.h + + return None, None, None, None + + +# This is called before each interaction. It's purpose is to choose +# the widget that is focused, and to mark it as focused and all of +# the other widgets as unfocused. + +# The new grab widget. (The one that replaced the old grab widget at the start +# of the interaction.) +new_grab = None + +def before_interact(roots): + + global new_grab + global grab + + # a list of focusable, name tuples. + fwn = [ ] + + def callback(f, n): + fwn.append((f, n)) + + for root in roots: + root.find_focusable(callback, None) + + # Assign a full name to each focusable. + + namecount = { } + + for f, n in fwn: + serial = namecount.get(n, 0) + namecount[n] = serial + 1 + + f.full_focus_name = n, serial + + # If there's something with the same full name as the current widget, + # it becomes the new current widget. + + current = get_focused() + + if current is not None: + current_name = current.full_focus_name + + for f, n in fwn: + if f.full_focus_name == current_name: + current = f + set_focused(f, None) + break + else: + current = None + + # Otherwise, focus the default widget, or nothing. + if current is None: + + for f, n in fwn: + if f.default: + current = f + set_focused(f, None) + break + else: + set_focused(None, None) + + # Finally, mark the current widget as the focused widget, and + # all other widgets as unfocused. + for f, n in fwn: + if f is not current: + f.unfocus(default=True) + + if current: + current.focus(default=True) + + grab = new_grab + new_grab = None + +# This changes the focus to be the widget contained inside the new +# focus object. +def change_focus(newfocus, default=False): + rv = None + + if grab: + return + + if newfocus is None: + widget = None + else: + widget = newfocus.widget + + current = get_focused() + + # Nothing to do. + if current is widget and (newfocus is None or newfocus.arg == argument): + return rv + + if current is not None: + current.unfocus(default=default) + + current = widget + + if newfocus is not None: + arg = newfocus.arg + else: + arg = None + + set_focused(current, arg) + + if widget is not None: + rv = widget.focus(default=default) + + return rv + +# This handles mouse events, to see if they change the focus. +def mouse_handler(ev, x, y, default=False): + + if ev.type not in (pygame.MOUSEMOTION, pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN): + return + + new_focus = renpy.display.render.focus_at_point(x, y) + + if new_focus is None: + new_focus = default_focus + + return change_focus(new_focus, default=default) + + +# This focuses an extreme widget, which is one of the widgets that's +# at an edge. To do this, we multiply the x, y, width, and height by +# the supplied multiplers, add them all up, and take the focus with +# the largest value. +def focus_extreme(xmul, ymul, wmul, hmul): + + max_focus = None + max_score = -(65536**2) + + for f in focus_list: + + if not f.x: + continue + + score = (f.x * xmul + + f.y * ymul + + f.w * wmul + + f.h * hmul) + + if score > max_score: + max_score = score + max_focus = f + + if max_focus: + return change_focus(max_focus) + + +# This calculates the distance between two points, applying +# the given fudge factors. The distance is left squared. +def points_dist(x0, y0, x1, y1, xfudge, yfudge): + return (( x0 - x1 ) * xfudge ) ** 2 + \ + (( y0 - y1 ) * yfudge ) ** 2 + + +# This computes the distance between two horizontal lines. (So the +# distance is either vertical, or has a vertical component to it.) +# +# The distance is left squared. +def horiz_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1): + + # The lines overlap in x. + if bx0 <= ax0 <= ax1 <= bx1 or \ + ax0 <= bx0 <= bx1 <= ax1 or \ + ax0 <= bx0 <= ax1 <= bx1 or \ + bx0 <= ax0 <= bx1 <= ax1: + return (ay0 - by0) ** 2 + + # The right end of a is to the left of the left end of b. + if ax0 <= ax1 <= bx0 <= bx1: + return points_dist(ax1, ay1, bx0, by0, renpy.config.focus_crossrange_penalty, 1.0) + + if bx0 <= bx1 <= ax0 <= ax1: + return points_dist(ax0, ay0, bx1, by1, renpy.config.focus_crossrange_penalty, 1.0) + + assert False + +# This computes the distance between two vertical lines. (So the +# distance is either hortizontal, or has a horizontal component to it.) +# +# The distance is left squared. +def verti_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1): + + # The lines overlap in x. + if by0 <= ay0 <= ay1 <= by1 or \ + ay0 <= by0 <= by1 <= ay1 or \ + ay0 <= by0 <= ay1 <= by1 or \ + by0 <= ay0 <= by1 <= ay1: + return (ax0 - bx0) ** 2 + + # The right end of a is to the left of the left end of b. + if ay0 <= ay1 <= by0 <= by1: + return points_dist(ax1, ay1, bx0, by0, 1.0, renpy.config.focus_crossrange_penalty) + + if by0 <= by1 <= ay0 <= ay1: + return points_dist(ax0, ay0, bx1, by1, 1.0, renpy.config.focus_crossrange_penalty) + + assert False + + + +# This focuses the widget that is nearest to the current widget. To +# determine nearest, we compute points on the widgets using the +# {from,to}_{x,y}off values. We pick the nearest, applying a fudge +# multiplier to the distances in each direction, that satisfies +# the condition (which is given a Focus object to evaluate). +# +# If no focus can be found matching the above, we look for one +# with an x of None, and make that the focus. Otherwise, we do +# nothing. +# +# If no widget is focused, we pick one and focus it. +# +# If the current widget has an x of None, we pass things off to +# focus_extreme to deal with. +def focus_nearest(from_x0, from_y0, from_x1, from_y1, + to_x0, to_y0, to_x1, to_y1, + line_dist, + condition, + xmul, ymul, wmul, hmul): + + if not focus_list: + return + + # No widget focused. + current = get_focused() + + if not current: + change_focus(focus_list[0]) + return + + # Find the current focus. + for f in focus_list: + if f.widget is current and f.arg == argument: + from_focus = f + break + else: + # If we can't pick something. + change_focus(focus_list[0]) + return + + # If placeless, focus_extreme. + if from_focus.x is None: + focus_extreme(xmul, ymul, wmul, hmul) + return + + fx0 = from_focus.x + from_focus.w * from_x0 + fy0 = from_focus.y + from_focus.h * from_y0 + fx1 = from_focus.x + from_focus.w * from_x1 + fy1 = from_focus.y + from_focus.h * from_y1 + + placeless = None + new_focus = None + + # a really big number. + new_focus_dist = (65536.0 * renpy.config.focus_crossrange_penalty) ** 2 + + for f in focus_list: + + if f is from_focus: + continue + + if f.x is None: + placeless = f + continue + + if not condition(from_focus, f): + continue + + tx0 = f.x + f.w * to_x0 + ty0 = f.y + f.h * to_y0 + tx1 = f.x + f.w * to_x1 + ty1 = f.y + f.h * to_y1 + + dist = line_dist(fx0, fy0, fx1, fy1, + tx0, ty0, tx1, ty1) + + if dist < new_focus_dist: + new_focus = f + new_focus_dist = dist + + # If we couldn't find anything, try the placeless focus. + new_focus = new_focus or placeless + + # If we have something, switch to it. + if new_focus: + return change_focus(new_focus) + + # And, we're done. + + + +def key_handler(ev): + + if renpy.display.behavior.map_event(ev, 'focus_right'): + return focus_nearest(0.9, 0.1, 0.9, 0.9, + 0.1, 0.1, 0.1, 0.9, + verti_line_dist, + lambda old, new : old.x + old.w <= new.x, + -1, 0, 0, 0) + + if renpy.display.behavior.map_event(ev, 'focus_left'): + return focus_nearest(0.1, 0.1, 0.1, 0.9, + 0.9, 0.1, 0.9, 0.9, + verti_line_dist, + lambda old, new : new.x + new.w <= old.x, + 1, 0, 1, 0) + + if renpy.display.behavior.map_event(ev, 'focus_up'): + return focus_nearest(0.1, 0.1, 0.9, 0.1, + 0.1, 0.9, 0.9, 0.9, + horiz_line_dist, + lambda old, new : new.y + new.h <= old.y, + 0, 1, 0, 1) + + if renpy.display.behavior.map_event(ev, 'focus_down'): + return focus_nearest(0.1, 0.9, 0.9, 0.9, + 0.1, 0.1, 0.9, 0.1, + horiz_line_dist, + lambda old, new : old.y + old.h <= new.y, + 0, -1, 0, 0) + + + diff --git a/unrpyc/renpy/display/im.py b/unrpyc/renpy/display/im.py new file mode 100644 index 0000000..d90fb51 --- /dev/null +++ b/unrpyc/renpy/display/im.py @@ -0,0 +1,1562 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains the new image code, which includes provisions for +# size-based caching and constructing images from operations (like +# cropping and scaling). + +import renpy.display + +import math +import zipfile +import io +import threading +import time + + +# This is an entry in the image cache. +class CacheEntry(object): + + def __init__(self, what, surf): + + # The object that is being cached (which needs to be + # hashable and comparable). + self.what = what + + # The pygame surface corresponding to the cached object. + self.surf = surf + + # The size of this image. + w, h = surf.get_size() + self.size = w * h + + # The time when this cache entry was last used. + self.time = 0 + +# This is the singleton image cache. +class Cache(object): + + def __init__(self): + + # The current arbitrary time. (Increments by one for each + # interaction.) + self.time = 0 + + # A map from Image object to CacheEntry. + self.cache = { } + + # A list of Image objects that we want to preload. + self.preloads = [ ] + + # False if this is not the first preload in this tick. + self.first_preload_in_tick = True + + # The total size of the current generation of images. + self.size_of_current_generation = 0 + + # The total size of everything in the cache. + self.total_cache_size = 0 + + # A lock that must be held when updating the cache. + self.lock = threading.Condition() + + # A lock that mist be held to notify the preload thread. + self.preload_lock = threading.Condition() + + # Is the preload_thread alive? + self.keep_preloading = True + + # A map from image object to surface, only for objects that have + # been pinned into memory. + self.pin_cache = { } + + # Images that we tried, and failed, to preload. + self.preload_blacklist = set() + + # The size of the cache, in pixels. + self.cache_limit = 0 + + # The preload thread. + self.preload_thread = threading.Thread(target=self.preload_thread_main, name="preloader") + self.preload_thread.setDaemon(True) + self.preload_thread.start() + + # Have we been added this tick? + self.added = set() + + # A list of (time, filename, preload) tuples. This is updated when + # config.developer is True and an image is loaded. Preload is a + # flag that is true if the image was loaded from the preload + # thread. The log is limited to 100 entries, and the newest entry + # is first. + # + # This is only updated when config.developer is True. + self.load_log = [ ] + + + def init(self): + """ + Updates the cache object to make use of settings that might be provided + by the game-maker. + """ + + self.cache_limit = renpy.config.image_cache_size * renpy.config.screen_width * renpy.config.screen_height + + def quit(self): #@ReservedAssignment + if not self.preload_thread.isAlive(): + return + + with self.preload_lock: + self.keep_preloading = False + self.preload_lock.notify() + + self.preload_thread.join() + + + # Clears out the cache. + def clear(self): + + self.lock.acquire() + + self.preloads = [ ] + self.pin_cache = { } + self.cache = { } + self.first_preload_in_tick = True + self.size_of_current_generation = 0 + self.total_cache_size = 0 + + self.added.clear() + + self.lock.release() + + # Increments time, and clears the list of images to be + # preloaded. + def tick(self): + + with self.lock: + self.time += 1 + self.preloads = [ ] + self.first_preload_in_tick = True + self.size_of_current_generation = 0 + self.added.clear() + + if renpy.config.debug_image_cache: + renpy.display.ic_log.write("----") + filename, line = renpy.exports.get_filename_line() + renpy.display.ic_log.write("%s %d", filename, line) + + # The preload thread can deal with this update, so we don't need + # to lock things. + def end_tick(self): + self.preloads = [ ] + + + # This returns the pygame surface corresponding to the provided + # image. It also takes care of updating the age of images in the + # cache to be current, and maintaining the size of the current + # generation of images. + def get(self, image, predict=False): + + if not isinstance(image, ImageBase): + raise Exception("Expected an image of some sort, but got" + str(image) + ".") + + if not image.cache: + surf = image.load() + renpy.display.render.mutated_surface(surf) + return surf + + # First try to grab the image out of the cache without locking it. + ce = self.cache.get(image, None) + + # Otherwise, we load the image ourselves. + if ce is None: + + try: + if image in self.pin_cache: + surf = self.pin_cache[image] + else: + surf = image.load() + + except: + raise + + with self.lock: + + ce = CacheEntry(image, surf) + + if image not in self.cache: + self.total_cache_size += ce.size + + self.cache[image] = ce + + # Indicate that this surface had changed. + renpy.display.render.mutated_surface(ce.surf) + + if renpy.config.debug_image_cache: + if predict: + renpy.display.ic_log.write("Added %r (%.02f%%)", ce.what, 100.0 * self.total_cache_size / self.cache_limit) + else: + renpy.display.ic_log.write("Total Miss %r", ce.what) + + renpy.display.draw.load_texture(ce.surf) + + + # Move it into the current generation. This isn't protected by + # a lock, so in certain circumstances we could have an + # inaccurate size - but that will be cured at the end of the + # current generation. + + if ce.time != self.time: + ce.time = self.time + self.size_of_current_generation += ce.size + + # Done... return the surface. + return ce.surf + + + # This kills off a given cache entry. + def kill(self, ce): + + # Should never happen... but... + if ce.time == self.time: + self.size_of_current_generation -= ce.size + + self.total_cache_size -= ce.size + del self.cache[ce.what] + + if renpy.config.debug_image_cache: + renpy.display.ic_log.write("Removed %r", ce.what) + + def cleanout(self): + """ + Cleans out the cache, if it's gotten too large. Returns True + if the cache is smaller than the size limit, or False if it's + bigger and we don't want to continue preloading. + """ + + # If we're within the limit, return. + if self.total_cache_size <= self.cache_limit: + return True + + # If we're outside the cache limit, we need to go and start + # killing off some of the entries until we're back inside it. + + for ce in sorted(iter(self.cache.values()), key=lambda a : a.time): + + if ce.time == self.time: + # If we're bigger than the limit, and there's nothing + # to remove, we should stop the preloading right away. + return False + + # Otherwise, kill off the given cache entry. + self.kill(ce) + + # If we're in the limit, we're done. + if self.total_cache_size <= self.cache_limit: + break + + return True + + + # Called to report that a given image would like to be preloaded. + def preload_image(self, im): + + if not isinstance(im, ImageBase): + return + + with self.lock: + + if im in self.added: + return + + self.added.add(im) + + if im in self.cache: + self.get(im) + in_cache = True + else: + self.preloads.append(im) + in_cache = False + + if not in_cache: + + with self.preload_lock: + self.preload_lock.notify() + + if in_cache and renpy.config.debug_image_cache: + renpy.display.ic_log.write("Kept %r", im) + + + def start_prediction(self): + """ + Called at the start of prediction, to ensure the thread runs + at least once to clean out the cache. + """ + + with self.preload_lock: + self.preload_lock.notify() + + def preload_thread_main(self): + + while self.keep_preloading: + + self.preload_lock.acquire() + self.preload_lock.wait() + self.preload_lock.release() + + while self.preloads and self.keep_preloading: + + start = time.time() + + # If the size of the current generation is bigger than the + # total cache size, stop preloading. + with self.lock: + + # If the cache is overfull, clean it out. + if not self.cleanout(): + + if renpy.config.debug_image_cache: + for i in self.preloads: + renpy.display.ic_log.write("Overfull %r", i) + + self.preloads = [ ] + + break + + try: + image = self.preloads.pop(0) + + if image not in self.preload_blacklist: + try: + self.get(image, True) + except: + self.preload_blacklist.add(image) + except: + pass + + with self.lock: + self.cleanout() + + # If we have time, preload pinned images. + if self.keep_preloading and not renpy.game.less_memory: + + workset = set(renpy.store._cache_pin_set) + + # Remove things that are not in the workset from the pin cache, + # and remove things that are in the workset from pin cache. + for i in list(self.pin_cache.keys()): + + if i in workset: + workset.remove(i) + else: + surf = self.pin_cache[i] + + del self.pin_cache[i] + + + # For each image in the worklist... + for image in workset: + + if image in self.preload_blacklist: + continue + + # If we have normal preloads, break out. + if self.preloads: + break + + try: + surf = image.load() + self.pin_cache[image] = surf + renpy.display.draw.load_texture(surf) + except: + self.preload_blacklist.add(image) + + def add_load_log(self, filename): + + if not renpy.config.developer: + return + + preload = (threading.current_thread() is self.preload_thread) + + self.load_log.insert(0, (time.time(), filename, preload)) + + while len(self.load_log) > 100: + self.load_log.pop() + + + +# The cache object. +cache = Cache() + +def free_memory(): + """ + Frees some memory. + """ + + renpy.display.draw.free_memory() + cache.clear() + + +class ImageBase(renpy.display.core.Displayable): + """ + This is the base class for all of the various kinds of images that + we can possibly have. + """ + + __version__ = 1 + + def after_upgrade(self, version): + if version < 1: + self.cache = True + + def __init__(self, *args, **properties): + + self.rle = properties.pop('rle', None) + self.cache = properties.pop('cache', True) + + properties.setdefault('style', 'image') + + super(ImageBase, self).__init__(**properties) + self.identity = (type(self).__name__, ) + args + + + def __hash__(self): + return hash(self.identity) + + def __eq__(self, other): + + if not isinstance(other, ImageBase): + return False + + return self.identity == other.identity + + def __repr__(self): + return "<" + " ".join([repr(i) for i in self.identity]) + ">" + + def load(self): + """ + This function is called by the image cache code to cause this + image to be loaded. It's expected that children of this class + would override this. + """ + + assert False + + def render(self, w, h, st, at): + + im = cache.get(self) + texture = renpy.display.draw.load_texture(im) + + w, h = im.get_size() + rv = renpy.display.render.Render(w, h) + rv.blit(texture, (0, 0)) + return rv + + def predict_one(self): + renpy.display.predict.image(self) + + def predict_files(self): + """ + Returns a list of files that will be accessed when this image + operation is performed. + """ + + return [ ] + +class Image(ImageBase): + """ + This image manipulator loads an image from a file. + """ + + def __init__(self, filename, **properties): + """ + @param filename: The filename that the image will be loaded from. + """ + + super(Image, self).__init__(filename, **properties) + self.filename = filename + + def get_mtime(self): + return renpy.loader.get_mtime(self.filename) + + def load(self, unscaled=False): + + cache.add_load_log(self.filename) + + try: + + if unscaled: + surf = renpy.display.pgrender.load_image_unscaled(renpy.loader.load(self.filename), self.filename) + else: + surf = renpy.display.pgrender.load_image(renpy.loader.load(self.filename), self.filename) + + return surf + + except Exception as e: + + if renpy.config.missing_image_callback: + im = renpy.config.missing_image_callback(self.filename) + if im is None: + raise e + + return im.load() + + raise + + def predict_files(self): + + if renpy.loader.loadable(self.filename): + return [ self.filename ] + else: + if renpy.config.missing_image_callback: + im = renpy.config.missing_image_callback(self.filename) + if im is not None: + return im.predict_files() + + return [ self.filename ] + +class ZipFileImage(ImageBase): + + def __init__(self, zipfilename, filename, mtime=0, **properties): + super(ZipFileImage, self).__init__(zipfilename, filename, mtime, **properties) + + self.zipfilename = zipfilename + self.filename = filename + + def load(self): + try: + zf = zipfile.ZipFile(self.zipfilename, 'r') + data = zf.read(self.filename) + sio = io.StringIO(data) + rv = renpy.display.pgrender.load_image(sio, self.filename) + zf.close() + return rv + except: + return renpy.display.pgrender.surface((2, 2), True) + + + + def predict_files(self): + return [ ] + + + +class Composite(ImageBase): + """ + :doc: im_im + + This image manipulator composites multiple images together to + form a single image. + + The `size` should be a (width, height) tuple giving the size + of the composed image. + + The remaining positional arguments are interpreted as groups of + two. The first argument in a group should be an (x, y) tuple, + while the second should be an image manipulator. The image + produced by the image manipulator is composited at the location + given by the tuple. + + :: + + image girl clothed happy = im.Composite( + (300, 600), + (0, 0), "girl_body.png", + (0, 0), "girl_clothes.png", + (100, 100), "girl_happy.png" + ) + + """ + + def __init__(self, size, *args, **properties): + + super(Composite, self).__init__(size, *args, **properties) + + if len(args) % 2 != 0: + raise Exception("Composite requires an odd number of arguments.") + + self.size = size + self.positions = args[0::2] + self.images = [ image(i) for i in args[1::2] ] + + def get_mtime(self): + return min(i.get_mtime() for i in self.images) + + def load(self): + + if self.size: + size = self.size + else: + size = cache.get(self.images[0]).get_size() + + rv = renpy.display.pgrender.surface(size, True) + + for pos, im in zip(self.positions, self.images): + rv.blit(cache.get(im), pos) + + return rv + + def predict_files(self): + + rv = [ ] + + for i in self.images: + rv.extend(i.predict_files()) + + return rv + +class Scale(ImageBase): + """ + :doc: im_im + + An image manipulator that scales `im` (an image manipulator) to + `width` and `height`. + + If `bilinear` is true, then bilinear interpolation is used for + the scaling. Otherwise, nearest neighbor interpolation is used. + + :: + + image logo scale = im.Scale("logo.png", 100, 150) + """ + + def __init__(self, im, width, height, bilinear=True, **properties): + + im = image(im) + super(Scale, self).__init__(im, width, height, bilinear, **properties) + + self.image = im + self.width = int(width) + self.height = int(height) + self.bilinear = bilinear + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + child = cache.get(self.image) + + if self.bilinear: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.scale.smoothscale(child, (self.width, self.height)) + finally: + renpy.display.render.blit_lock.release() + else: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.transform_scale(child, (self.width, self.height)) + finally: + renpy.display.render.blit_lock.release() + + return rv + + def predict_files(self): + return self.image.predict_files() + +class FactorScale(ImageBase): + """ + :doc: im_im + + An image manipulator that scales `im` (a second image manipulator) + to `width` times its original `width`, and `height` times its + original height. If `height` is ommitted, it defaults to `width`. + + If `bilinear` is true, then bilinear interpolation is used for + the scaling. Otherwise, nearest neighbor interpolation is used. + + :: + + image logo doubled = im.FactorScale("logo.png", 1.5) + """ + + + def __init__(self, im, width, height=None, bilinear=True, **properties): + + if height is None: + height = width + + im = image(im) + super(FactorScale, self).__init__(im, width, height, bilinear, **properties) + + self.image = im + self.width = width + self.height = height + self.bilinear = bilinear + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + width, height = surf.get_size() + + width = int(width * self.width) + height = int(height * self.height) + + if self.bilinear: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.scale.smoothscale(surf, (width, height)) + finally: + renpy.display.render.blit_lock.release() + + else: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.transform_scale(surf, (width, height)) + finally: + renpy.display.render.blit_lock.release() + + return rv + + def predict_files(self): + return self.image.predict_files() + + +class Flip(ImageBase): + """ + :doc: im_im + + An image manipulator that flips `im` (an image manipulator) + vertically or horizontally. `vertical` and `horizontal` control + the directions in which the image is flipped. + + :: + + image eileen flip = im.Flip("eileen_happy.png", vertical=True) + """ + + def __init__(self, im, horizontal=False, vertical=False, **properties): + + if not (horizontal or vertical): + raise Exception("im.Flip must be called with a true value for horizontal or vertical.") + + im = image(im) + super(Flip, self).__init__(im, horizontal, vertical, **properties) + + self.image = im + self.horizontal = horizontal + self.vertical = vertical + + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + child = cache.get(self.image) + + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.flip(child, self.horizontal, self.vertical) + finally: + renpy.display.render.blit_lock.release() + + return rv + + + def predict_files(self): + return self.image.predict_files() + + + +class Rotozoom(ImageBase): + """ + This is an image manipulator that is a smooth rotation and zoom of another image manipulator. + """ + + def __init__(self, im, angle, zoom, **properties): + """ + @param im: The image to be rotozoomed. + + @param angle: The number of degrees counterclockwise the image is + to be rotated. + + @param zoom: The zoom factor. Numbers that are greater than 1.0 + lead to the image becoming larger. + """ + + im = image(im) + super(Rotozoom, self).__init__(im, angle, zoom, **properties) + + self.image = im + self.angle = angle + self.zoom = zoom + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + child = cache.get(self.image) + + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.rotozoom(child, self.angle, self.zoom) + finally: + renpy.display.render.blit_lock.release() + + return rv + + def predict_files(self): + return self.image.predict_files() + + + +class Crop(ImageBase): + """ + :doc: im_im + :args: (im, rect) + + An image manipulator that crops `rect`, a (x, y, width, height) tuple, + out of `im`, an image manipulator. + + :: + + image logo crop = im.Crop("logo.png", (0, 0, 100, 307)) + """ + + def __init__(self, im, x, y=None, w=None, h=None, **properties): + + im = image(im) + + if y is None: + (x, y, w, h) = x + + super(Crop, self).__init__(im, x, y, w, h, **properties) + + self.image = im + self.x = x + self.y = y + self.w = w + self.h = h + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + return cache.get(self.image).subsurface((self.x, self.y, + self.w, self.h)) + + def predict_files(self): + return self.image.predict_files() + + +ramp_cache = { } + + +def ramp(start, end): + """ + Returns a 256 character linear ramp, where the first character has + the value start and the last character has the value end. Such a + ramp can be used as a map argument of im.Map. + """ + + rv = ramp_cache.get((start, end), None) + if rv is None: + + chars = [ ] + + for i in range(0, 256): + i = i / 255.0 + chars.append(chr(int( end * i + start * (1.0 - i) ) ) ) + + rv = "".join(chars) + ramp_cache[start, end] = rv + + return rv + +identity = ramp(0, 255) + +class Map(ImageBase): + """ + This adjusts the colors of the image that is its child. It takes + as arguments 4 256 character strings. If a pixel channel has a + value of 192, then the value of the 192nd character in the string + is used for the mapped pixel component. + """ + + def __init__(self, im, rmap=identity, gmap=identity, bmap=identity, + amap=identity, force_alpha=False, **properties): + + im = image(im) + + super(Map, self).__init__(im, rmap, gmap, bmap, amap, force_alpha, **properties) + + self.image = im + self.rmap = rmap + self.gmap = gmap + self.bmap = bmap + self.amap = amap + + self.force_alpha = force_alpha + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.map(surf, rv, + self.rmap, self.gmap, self.bmap, self.amap) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class Twocolor(ImageBase): + """ + This takes as arguments two colors, white and black. The image is + mapped such that pixels in white have the white color, pixels in + black have the black color, and shades of gray are linearly + interpolated inbetween. The alpha channel is mapped linearly + between 0 and the alpha found in the white color, the black + color's alpha is ignored. + """ + + def __init__(self, im, white, black, force_alpha=False, **properties): + + white = renpy.easy.color(white) + black = renpy.easy.color(black) + + im = image(im) + + super(Twocolor, self).__init__(im, white, black, force_alpha, **properties) + + self.image = im + self.white = white + self.black = black + + self.force_alpha = force_alpha + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.twomap(surf, rv, + self.white, self.black) + + return rv + + def predict_files(self): + return self.image.predict_files() + + +class Recolor(ImageBase): + """ + This adjusts the colors of the image that is its child. It takes as an + argument 4 numbers between 0 and 255, and maps each channel of the image + linearly between 0 and the supplied color. + """ + + def __init__(self, im, rmul=255, gmul=255, bmul=255, + amul=255, force_alpha=False, **properties): + + im = image(im) + + super(Recolor, self).__init__(im, rmul, gmul, bmul, amul, force_alpha, **properties) + + self.image = im + self.rmul = rmul + 1 + self.gmul = gmul + 1 + self.bmul = bmul + 1 + self.amul = amul + 1 + + self.force_alpha = force_alpha + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.linmap(surf, rv, + self.rmul, self.gmul, self.bmul, self.amul) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class MatrixColor(ImageBase): + """ + :doc: im_matrixcolor + + An image operator that uses `matrix` to linearly transform the + image manipulator `im`. + + `Matrix` should be a list, tuple, or :func:`im.matrix` that is 20 + or 25 elements long. If the object has 25 elements, then elements + past the 20th are ignored. + + When the four components of the source color are R, G, B, and A, + which range from 0.0 to 1.0; the four components of the transformed + color are R', G', B', and A', with the same range; and the elements + of the matrix are named:: + + [ a, b, c, d, e, + f, g, h, i, j, + k, l, m, n, o, + p, q, r, s, t ] + + the transformed colors can be computed with the formula:: + + R' = (a * R) + (b * G) + (c * B) + (d * A) + e + G' = (f * R) + (g * G) + (h * B) + (i * A) + j + B' = (k * R) + (l * G) + (m * B) + (n * A) + o + A' = (p * R) + (q * G) + (r * B) + (s * A) + t + + The components of the transformed color are clamped to the + range [0.0, 1.0]. + """ + + def __init__(self, im, matrix, **properties): + + im = image(im) + + if len(matrix) != 20 and len(matrix) != 25: + raise Exception("ColorMatrix expects a 20 or 25 element matrix, got %d elements." % len(matrix)) + + matrix = tuple(matrix) + super(MatrixColor, self).__init__(im, matrix, **properties) + + self.image = im + self.matrix = matrix + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.colormatrix(surf, rv, self.matrix) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class matrix(tuple): + """ + :doc: im_matrixcolor + + Constructs an im.matrix object from `matrix`. im.matrix objects + support The operations supported are matrix multiplication, scalar + multiplication, element-wise addition, and element-wise + subtraction. These operations are invoked using the standard + mathematical operators (\\*, \\*, +, and -, respectively). If two + im.matrix objects are multiplied, matrix multiplication is + performed, otherwise scalar multiplication is used. + + `matrix` is a 20 or 25 element list or tuple. If it is 20 elements + long, it is padded with (0, 0, 0, 0, 1) to make a 5x5 matrix, + suitable for multiplication. + """ + + def __new__(cls, *args): + + if len(args) == 1: + args = tuple(args[0]) + + if len(args) == 20: + args = args + (0, 0, 0, 0, 1) + + if len(args) != 25: + raise Exception("Matrix expects to be given 20 or 25 entries, not %d." % len(args)) + + return tuple.__new__(cls, args) + + def mul(self, a, b): + + if not isinstance(a, matrix): + a = matrix(a) + + if not isinstance(b, matrix): + b = matrix(b) + + result = [ 0 ] * 25 + for y in range(0, 5): + for x in range(0, 5): + for i in range(0, 5): + result[x + y * 5] += a[x + i * 5] * b[i + y * 5] + + return matrix(result) + + def scalar_mul(self, other): + other = float(other) + return matrix([ i * other for i in self ]) + + def vector_mul(self, o): + + return (o[0]*self[0] + o[1]*self[1] + o[2]*self[2] + o[3]*self[3] + self[4], + o[0]*self[5] + o[1]*self[6] + o[2]*self[7] + o[3]*self[8] + self[9], + o[0]*self[10] + o[1]*self[11] + o[2]*self[12] + o[3]*self[13] + self[14], + o[0]*self[15] + o[1]*self[16] + o[2]*self[17] + o[3]*self[18] + self[19], + 1) + + + def __add__(self, other): + if isinstance(other, (int, float)): + other = float(other) + return matrix([ i + other for i in self ]) + + other = matrix(other) + return matrix([ i + j for i, j in zip(self, other)]) + + __radd__ = __add__ + + def __sub__(self, other): + return self + other * -1 + + def __rsub__(self, other): + return self * -1 + other + + def __mul__(self, other): + if isinstance(other, (int, float)): + return self.scalar_mul(other) + + return self.mul(self, other) + + def __rmul__(self, other): + if isinstance(other, (int, float)): + return self.scalar_mul(other) + + return self.mul(other, self) + + def __repr__(self): + return """\ +im.matrix(%f, %f, %f, %f, %f. + %f, %f, %f, %f, %f, + %f, %f, %f, %f, %f, + %f, %f, %f, %f, %f, + %f, %f, %f, %f, %f)""" % self + + + @staticmethod + def identity(): + """ + :doc: im_matrixcolor + :name: im.matrix.identity + + Returns an identity matrix, one that does not change color or + alpha. + """ + + return matrix(1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0) + @staticmethod + def saturation(level, desat=(0.2126, 0.7152, 0.0722)): + """ + :doc: im_matrixcolor + :name: im.matrix.saturation + + Returns an im.matrix that alters the saturation of an + image. The alpha channel is untouched. + + `level` + The amount of saturation in the resulting image. 1.0 is + the unaltered image, while 0.0 is grayscale. + + `desat` + This is a 3-element tuple that controls how much of the + red, green, and blue channels will be placed into all + three channels of a fully desaturated image. The default + is based on the constants used for the luminance channel + of an NTSC television signal. Since the human eye is + mostly sensitive to green, more of the green channel is + kept then the other two channels. + """ + + r, g, b = desat + + def I(a, b): + return a + (b - a) * level + + return matrix(I(r, 1), I(g, 0), I(b, 0), 0, 0, + I(r, 0), I(g, 1), I(b, 0), 0, 0, + I(r, 0), I(g, 0), I(b, 1), 0, 0, + 0, 0, 0, 1, 0) + + @staticmethod + def desaturate(): + """ + :doc: im_matrixcolor + :name: im.matrix.desaturate + + Returns an im.matrix that desaturates the image (makes it + grayscale). This is equivalent to calling + im.matrix.saturation(0). + """ + + return matrix.saturation(0.0) + + @staticmethod + def tint(r, g, b): + """ + :doc: im_matrixcolor + :name: im.matrix.tint + + Returns an im.matrix that tints an image, without changing + the alpha channel. `r`, `g`, and `b` should be numbers between + 0 and 1, and control what fraction of the given channel is + placed into the final image. (For example, if `r` is .5, and + the value of the red channel is 100, the transformed color + will have a red value of 50.) + """ + + return matrix(r, 0, 0, 0, 0, + 0, g, 0, 0, 0, + 0, 0, b, 0, 0, + 0, 0, 0, 1, 0) + + @staticmethod + def invert(): + """ + :doc: im_matrixcolor + :name: im.matrix.invert + + Returns an im.matrix that inverts the red, green, and blue + channels of the image without changing the alpha channel. + """ + + return matrix(-1, 0, 0, 0, 1, + 0, -1, 0, 0, 1, + 0, 0, -1, 0, 1, + 0, 0, 0, 1, 0) + + @staticmethod + def brightness(b): + """ + :doc: im_matrixcolor + :name: im.matrix.brightness + + Returns an im.matrix that alters the brightness of an image. + + `b` + The amount of change in image brightness. This should be + a number between -1 and 1, with -1 the darkest possible + image and 1 the brightest. + """ + + return matrix(1, 0, 0, 0, b, + 0, 1, 0, 0, b, + 0, 0, 1, 0, b, + 0, 0, 0, 1, 0) + + @staticmethod + def opacity(o): + """ + :doc: im_matrixcolor + :name: im.matrix.opacity + + Returns an im.matrix that alters the opacity of an image. An + `o` of 0.0 is fully transparent, while 1.0 is fully opaque. + """ + + return matrix(1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, o, 0) + + @staticmethod + def contrast(c): + """ + :doc: im_matrixcolor + :name: im.matrix.contrast + + Returns an im.matrix that alters the contrast of an image. `c` should + be greater than 0.0, with values between 0.0 and 1.0 decreasing contrast, and + values greater than 1.0 increasing contrast. + """ + + return matrix.brightness(-.5) * matrix.tint(c, c, c) * matrix.brightness(.5) + + # from http://www.gskinner.com/blog/archives/2005/09/flash_8_source.html + @staticmethod + def hue(h): + """ + :doc: im_matrixcolor + :name: im.matrix.hue + + Returns an im.matrix that rotates the hue by `h` degrees, while + preserving luminosity. + """ + + h = h * math.pi / 180 + cosVal = math.cos(h) + sinVal = math.sin(h) + lumR = 0.213 + lumG = 0.715 + lumB = 0.072 + return matrix( + lumR+cosVal*(1-lumR)+sinVal*(-lumR),lumG+cosVal*(-lumG)+sinVal*(-lumG),lumB+cosVal*(-lumB)+sinVal*(1-lumB),0,0, + lumR+cosVal*(-lumR)+sinVal*(0.143),lumG+cosVal*(1-lumG)+sinVal*(0.140),lumB+cosVal*(-lumB)+sinVal*(-0.283),0,0, + lumR+cosVal*(-lumR)+sinVal*(-(1-lumR)),lumG+cosVal*(-lumG)+sinVal*(lumG),lumB+cosVal*(1-lumB)+sinVal*(lumB),0,0, + 0,0,0,1,0, + 0,0,0,0,1 + ) + + @staticmethod + def colorize(black_color, white_color): + """ + :doc: im_matrixcolor + :name: im.matrix.colorize + + Returns an im.matrix that colorizes a black and white image. + `black_color` and `white_color` are Ren'Py style colors, so + they may be specfied as strings or tuples of (0-255) color + values. :: + + # This makes black colors red, and white colors blue. + image logo colored = im.MatrixColor( + "bwlogo.png", + im.matrix.colorize("#f00", "#00f")) + + """ + + (r0, g0, b0, _a0) = renpy.easy.color(black_color) + (r1, g1, b1, _a1) = renpy.easy.color(white_color) + + r0 /= 255.0 + g0 /= 255.0 + b0 /= 255.0 + r1 /= 255.0 + g1 /= 255.0 + b1 /= 255.0 + + return matrix((r1-r0), 0, 0, 0, r0, + 0, (g1-g0), 0, 0, g0, + 0, 0, (b1-b0), 0, b0, + 0, 0, 0, 1, 0) + + + +def Grayscale(im, desat=(0.2126, 0.7152, 0.0722), **properties): + """ + :doc: im_im + :args: (im, **properties) + + An image manipulator that creats a desaturated version of the image + manipulator `im`. + """ + + return MatrixColor(im, matrix.saturation(0.0, desat), **properties) + + +def Sepia(im, tint=(1.0, .94, .76), desat=(0.2126, 0.7152, 0.0722), **properties): + """ + :doc: im_im + :args: (im, **properties) + + An image manipulator that creates a sepia-toned version of the image + manipulator `im`. + """ + + return MatrixColor(im, matrix.saturation(0.0, desat) * matrix.tint(tint[0], tint[1], tint[2]), **properties) + + +def Color(im, color): + """ + This recolors the supplied image, mapping colors such that black is + black and white is the supplied color. + """ + + r, g, b, a = renpy.easy.color(color) + + return Recolor(im, r, g, b, a) + + +def Alpha(image, alpha, **properties): + """ + Returns an alpha-mapped version of the image. Alpha is the maximum + alpha that this image can have, a number between 0.0 (fully + transparent) and 1.0 (opaque). + + If an image already has an alpha channel, values in that alpha + channel are reduced as appropriate. + """ + + return Recolor(image, 255, 255, 255, int(255 * alpha), force_alpha=True, **properties) + +class Tile(ImageBase): + """ + :doc: im_im + + An image manipulator that tiles the image manipulator `im`, until + it is `size`. + + `size` + If not None, a (width, height) tuple. If None, this defaults to + (:var:`config.screen_width`, :var:`config.screen_height`). + """ + + def __init__(self, im, size=None, **properties): + + im = image(im) + + super(Tile, self).__init__(im, size, **properties) + self.image = im + self.size = size + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + size = self.size + + if size is None: + size = (renpy.config.screen_width, renpy.config.screen_height) + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(size, True) + + width, height = size + sw, sh = surf.get_size() + + for y in range(0, height, sh): + for x in range(0, width, sw): + rv.blit(surf, (x, y)) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class AlphaMask(ImageBase): + """ + :doc: im_im + + An image manipulator that takes two image manipulators, `base` and + `mask`, as arguments. It replaces the alpha channel of `base` with + the red channel of `mask`. + + This is used to provide an image's alpha channel in a second + image, like having one jpeg for color data, and a second one + for alpha. In some cases, two jpegs can be smaller than a + single png file. + """ + + def __init__(self, base, mask, **properties): + super(AlphaMask, self).__init__(base, mask, **properties) + + self.base = image(base) + self.mask = image(mask) + + def get_mtime(self): + return max(self.base.get_mtime(), self.image.get_mtime()) + + def load(self): + + basesurf = cache.get(self.base) + masksurf = cache.get(self.mask) + + if basesurf.get_size() != masksurf.get_size(): + raise Exception("AlphaMask surfaces must be the same size.") + + # Used to copy the surface. + rv = renpy.display.pgrender.copy_surface(basesurf) + renpy.display.module.alpha_munge(masksurf, rv, identity) + + return rv + + def predict_files(self): + return self.base.predict_files() + self.mask.predict_files() + +def image(arg, loose=False, **properties): + """ + :doc: im_image + :name: Image + :args: (filename, **properties) + + Loads an image from a file. `filename` is a + string giving the name of the file. + + `filename` should be a JPEG or PNG file with an appropriate + extension. + """ + + """ + (Actually, the user documentation is a bit misleading, as + this tries for compatibility with several older forms of + image specification.) + + If the loose argument is False, then this will report an error if an + arbitrary argument is given. If it's True, then the argument is passed + through unchanged. + """ + + if isinstance(arg, ImageBase): + return arg + + elif isinstance(arg, str): + return Image(arg, **properties) + + elif isinstance(arg, renpy.display.image.ImageReference): + arg.find_target() + return image(arg.target, loose=loose, **properties) + + elif isinstance(arg, tuple): + params = [ ] + + for i in arg: + params.append((0, 0)) + params.append(i) + + return Composite(None, *params) + + elif loose: + return arg + + if isinstance(arg, renpy.display.core.Displayable): + raise Exception("Expected an image, but got a general displayable.") + else: + raise Exception("Could not construct image from argument.") + + +def load_image(fn): + """ + This loads an image from the given filename, using the cache. + """ + + surf = cache.get(image(fn)) + return renpy.display.draw.load_texture(surf) diff --git a/unrpyc/renpy/display/image.py b/unrpyc/renpy/display/image.py new file mode 100644 index 0000000..362f87a --- /dev/null +++ b/unrpyc/renpy/display/image.py @@ -0,0 +1,397 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains some miscellaneous displayables that involve images. +# Most of the guts of this file have been moved into im.py, with only some +# of the stuff thar uses images remaining. + +import renpy.display +import renpy.text +from renpy.display.render import render, Render + +import collections + +# A map from image name to the displayable object corresponding to that +# image. +images = { } + +# A map from image tag to lists of possible attributes for images with that +# tag. +image_attributes = collections.defaultdict(list) + +def register_image(name, d): + """ + Registers the existence of an image with `name`, and that the image + used displayable d. + """ + + tag = name[0] + rest = name[1:] + + images[name] = d + image_attributes[tag].append(rest) + + +def wrap_render(child, w, h, st, at): + rend = render(child, w, h, st, at) + rv = Render(rend.width, rend.height) + rv.blit(rend, (0, 0)) + return rv + +class ImageReference(renpy.display.core.Displayable): + """ + ImageReference objects are used to reference images by their name, + which is a tuple of strings corresponding to the name used to define + the image in an image statment. + """ + + nosave = [ 'target' ] + target = None + param_target = None + + def __init__(self, name, **properties): + """ + @param name: A tuple of strings, the name of the image. Or else + a displayable, containing the image directly. + """ + + super(ImageReference, self).__init__(**properties) + + self.name = name + + def _get_parameterized(self): + if self.param_target: + return self.param_target._get_parameterized() + + return self + + def find_target(self): + + if self.param_target: + self.target = self.param_target + return None + + name = self.name + + if isinstance(name, renpy.display.core.Displayable): + self.target = name + return True + + if not isinstance(name, tuple): + name = tuple(name.split()) + + parameters = [ ] + + def error(msg): + self.target = renpy.text.text.Text(msg, color=(255, 0, 0, 255), xanchor=0, xpos=0, yanchor=0, ypos=0) + + if renpy.config.debug: + raise Exception(msg) + + + # Scan through, searching for an image (defined with an + # input statement) that is a prefix of the given name. + while name: + if name in images: + target = images[name] + + try: + self.target = target.parameterize(name, parameters) + if self.target is not target: + self.param_target = self.target + + except Exception as e: + if renpy.config.debug: + raise + + error(str(e)) + + return True + + else: + parameters.insert(0, name[-1]) + name = name[:-1] + + error("Image '%s' not found." % ' '.join(self.name)) + return False + + def parameterize(self, name, parameters): + if not self.target: + self.find_target() + + return self.target.parameterize(name, parameters) + + def _hide(self, st, at, kind): + if not self.target: + self.find_target() + + return self.target._hide(st, at, kind) + + def set_transform_event(self, event): + if not self.target: + self.find_target() + + return self.target.set_transform_event(event) + + def event(self, ev, x, y, st): + if not self.target: + self.find_target() + + return self.target.event(ev, x, y, st) + + def render(self, width, height, st, at): + if not self.target: + self.find_target() + + return wrap_render(self.target, width, height, st, at) + + def get_placement(self): + if not self.target: + self.find_target() + + if not renpy.config.imagereference_respects_position: + return self.target.get_placement() + + xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = self.target.get_placement() + + if xpos is None: + xpos = self.style.xpos + + if ypos is None: + ypos = self.style.ypos + + if xanchor is None: + xanchor = self.style.xanchor + + if yanchor is None: + yanchor = self.style.yanchor + + return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel + + def visit(self): + if not self.target: + self.find_target() + + return [ self.target ] + + + +class ShownImageInfo(renpy.object.Object): + """ + This class keeps track of which images are being shown right now, + and what the attributes of those images are. (It's used for a similar + purpose during prediction, regarding the state in the future.) + """ + + __version__ = 2 + + def __init__(self, old=None): + """ + Creates a new object. If `old` is given, copies the default state + from old, otherwise initializes the object to a default state. + """ + + if old is None: + + # A map from (layer, tag) -> tuple of attributes + # This doesn't necessarily correspond to something that is + # currently showing, as we can remember the state of a tag + # for use in SideImage. + self.attributes = { } + + # A set of (layer, tag) pairs that are being shown on the + # screen right now. + self.shown = set() + + else: + self.attributes = old.attributes.copy() + self.shown = old.shown.copy() + + + def after_upgrade(self, version): + if version < 2: + + self.attributes = { } + self.shown = set() + + for layer in self.images: + for tag in self.images[layer]: + self.attributes[layer, tag] = self.images[layer][tag][1:] + self.shown.add((layer, tag)) + + def get_attributes(self, layer, tag): + """ + Get the attributes associated the image with tag on the given + layer. + """ + + return self.attributes.get((layer, tag), ()) + + def showing(self, layer, name): + """ + Returns true if name is the prefix of an image that is showing + on layer, or false otherwise. + """ + + tag = name[0] + rest = name[1:] + + if (layer, tag) not in self.shown: + return None + + shown = self.attributes[layer, tag] + + if len(shown) < len(rest): + return False + + for a, b in zip(shown, rest): + if a != b: + return False + + return True + + def predict_scene(self, layer): + """ + Predicts the scene statement being called on layer. + """ + + for l, t in list(self.attributes.keys()): + if l == layer: + del self.attributes[l, t] + + self.shown = set((l, t) for l, t in self.shown if l != layer) + + def predict_show(self, layer, name, show=True): + """ + Predicts name being shown on layer. + + `show` + If True, the image will be flagged as being shown to the user. If + False, only the attributes will be updated. + """ + + tag = name[0] + rest = name[1:] + + self.attributes[layer, tag] = rest + + if show: + self.shown.add((layer, tag)) + + def predict_hide(self, layer, name): + tag = name[0] + + if (layer, tag) in self.attributes: + del self.attributes[layer, tag] + + self.shown.discard((layer, tag)) + + + def apply_attributes(self, layer, tag, name): + """ + Given a layer, tag, and an image name (with attributes), + returns the canonical name of an image, if one exists. Raises + an exception if it's ambiguious, and returns None if an image + with that name couldn't be found. + """ + + # If the name matches one that exactly exists, return it. + if name in images: + return name + + nametag = name[0] + + # The set of attributes a matching image must have. + required = set(name[1:]) + + # The set of attributes a matching image may have. + optional = set(self.attributes.get((layer, tag), [ ])) + + # Deal with banned attributes.. + for i in name[1:]: + if i[0] == "-": + optional.discard(i[1:]) + required.discard(i) + + return self.choose_image(nametag, required, optional, name) + + def choose_image(self, tag, required, optional, exception_name): + """ + """ + + # The longest length of an image that matches. + max_len = 0 + + # The list of matching images. + matches = None + + for attrs in image_attributes[tag]: + + num_required = 0 + + for i in attrs: + if i in required: + num_required += 1 + continue + + elif i not in optional: + break + + else: + + # We don't have any not-found attributes. But we might not + # have all of the attributes. + + if num_required != len(required): + continue + + len_attrs = len(attrs) + + if len_attrs < max_len: + continue + + if len_attrs > max_len: + max_len = len_attrs + matches = [ ] + + matches.append((tag, ) + attrs) + + if matches is None: + return None + + if len(matches) == 1: + return matches[0] + + if exception_name: + raise Exception("Showing '" + " ".join(exception_name) + "' is ambiguous, possible images include: " + ", ".join(" ".join(i) for i in matches)) + else: + return None + +renpy.display.core.ImagePredictInfo = ShownImageInfo + + +# Functions that have moved from this module to other modules, +# that live here for the purpose of backward-compatibility. +Image = renpy.display.im.image +Solid = renpy.display.imagelike.Solid +Frame = renpy.display.imagelike.Frame +ImageButton = renpy.display.behavior.ImageButton + diff --git a/unrpyc/renpy/display/imagelike.py b/unrpyc/renpy/display/imagelike.py new file mode 100644 index 0000000..aea1d15 --- /dev/null +++ b/unrpyc/renpy/display/imagelike.py @@ -0,0 +1,382 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import renpy.display +from renpy.display.render import render, Render, Matrix2D + +# This file contains displayables that are image-like, because they take +# up a rectangular area of the screen, and do not respond to input. + +class Solid(renpy.display.core.Displayable): + """ + :doc: disp_imagelike + + A displayable that fills the area its assigned with `color`. + + :: + + image white = Solid("#fff") + + """ + + def __init__(self, color, **properties): + + super(Solid, self).__init__(**properties) + + if color is not None: + self.color = renpy.easy.color(color) + else: + self.color = None + + def visit(self): + return [ ] + + def render(self, width, height, st, at): + + color = self.color or self.style.color + + rv = Render(width, height) + + if color is None or width <= 0 or height <= 0: + return rv + + SIZE = 10 + + if width < SIZE or height < SIZE: + tex = renpy.display.draw.solid_texture(width, height, color) + else: + tex = renpy.display.draw.solid_texture(SIZE, SIZE, color) + rv.forward = Matrix2D(1.0 * SIZE / width, 0, 0, 1.0 * SIZE / height) + rv.reverse = Matrix2D(1.0 * width / SIZE, 0, 0, 1.0 * height / SIZE) + + rv.blit(tex, (0, 0)) + + return rv + +class Frame(renpy.display.core.Displayable): + """ + :doc: disp_imagelike + :args: (image, xborder, yborder, tile=False, **properties) + + A displayable that resizes an image to fill the available area, + while preserving the width and height of its borders. is often + used as the background of a window or button. + + .. figure:: frame_example.png + + Using a frame to resize an image to double its size. + + `image` + An image manipulator that will be resized by this frame. + + `left` + The size of the border on the left side. + + `top` + The size of the border on the top. + + `right` + The size of the border on the right side. If None, defaults + to `left`. + + `bottom` + The side of the border on the bottom. If None, defaults to `top`. + + `tile` + If true, tiling is used to resize sections of the image, + rather than scaling. + + :: + + # Resize the background of the text window if it's too small. + init python: + style.window.background = Frame("frame.png", 10, 10) + """ + + __version__ = 1 + + def after_upgrade(self, version): + if version < 2: + self.left = self.xborder + self.right = self.xborder + self.top = self.yborder + self.bottom = self.yborder + + def __init__(self, image, left, top, right=None, bottom=None, bilinear=True, tile=False, **properties): + super(Frame, self).__init__(**properties) + + self.image = renpy.easy.displayable(image) + self.tile = tile + + if right is None: + right = left + if bottom is None: + bottom = top + + self.left = left + self.top = top + self.right = right + self.bottom = bottom + + def render(self, width, height, st, at): + + crend = render(self.image, width, height, st, at) + + sw, sh = crend.get_size() + sw = int(sw) + sh = int(sh) + + dw = int(width) + dh = int(height) + + bw = self.left + self.right + bh = self.top + self.bottom + + xborder = min(bw, sw - 2, dw) + if xborder: + left = self.left * xborder / bw + right = self.right * xborder / bw + else: + left = 0 + right = 0 + + yborder = min(bh, sh - 2, dh) + if yborder: + top = self.top * yborder / bh + bottom = self.bottom * yborder / bh + else: + top = 0 + bottom = 0 + + if renpy.display.draw.info["renderer"] == "sw": + return self.sw_render(crend, dw, dh, left, top, right, bottom) + + def draw(x0, x1, y0, y1): + + # Compute the coordinates of the left, right, top, and + # bottom sides of the region, for both the source and + # destination surfaces. + + # left side. + if x0 >= 0: + dx0 = x0 + sx0 = x0 + else: + dx0 = dw + x0 + sx0 = sw + x0 + + # right side. + if x1 > 0: + dx1 = x1 + sx1 = x1 + else: + dx1 = dw + x1 + sx1 = sw + x1 + + # top side. + if y0 >= 0: + dy0 = y0 + sy0 = y0 + else: + dy0 = dh + y0 + sy0 = sh + y0 + + # bottom side + if y1 > 0: + dy1 = y1 + sy1 = y1 + else: + dy1 = dh + y1 + sy1 = sh + y1 + + # Quick exit. + if sx0 == sx1 or sy0 == sy1: + return + + # Compute sizes. + csw = sx1 - sx0 + csh = sy1 - sy0 + cdw = dx1 - dx0 + cdh = dy1 - dy0 + + if csw <= 0 or csh <= 0 or cdh <= 0 or cdw <= 0: + return + + # Get a subsurface. + cr = crend.subsurface((sx0, sy0, csw, csh)) + + # Scale or tile if we have to. + if csw != cdw or csh != cdh: + + if self.tile: + newcr = Render(cdw, cdh) + newcr.clipping = True + + for x in range(0, cdw, csw): + for y in range(0, cdh, csh): + newcr.blit(cr, (x, y)) + + cr = newcr + + else: + + newcr = Render(cdw, cdh) + newcr.forward = Matrix2D(1.0 * csw / cdw, 0, 0, 1.0 * csh / cdh) + newcr.reverse = Matrix2D(1.0 * cdw / csw, 0, 0, 1.0 * cdh / csh) + newcr.blit(cr, (0, 0)) + + cr = newcr + + # Blit. + rv.blit(cr, (dx0, dy0)) + return + + rv = Render(dw, dh) + + self.draw_pattern(draw, left, top, right, bottom) + + return rv + + def draw_pattern(self, draw, left, top, right, bottom): + # Top row. + if top: + + if left: + draw(0, left, 0, top) + + draw(left, -right, 0, top) + + if right: + draw(-right, 0, 0, top) + + # Middle row. + if left: + draw(0, left, top, -bottom) + + draw(left, -right, top, -bottom) + + if right: + draw(-right, 0, top, -bottom) + + # Bottom row. + if bottom: + if left: + draw(0, left, -bottom, 0) + + draw(left, -right, -bottom, 0) + + if right: + draw(-right, 0, -bottom, 0) + + + + def sw_render(self, crend, dw, dh, left, top, right, bottom): + + source = crend.render_to_texture(True) + sw, sh = source.get_size() + + dest = renpy.display.swdraw.surface(dw, dh, True) + rv = dest + + def draw(x0, x1, y0, y1): + + # Compute the coordinates of the left, right, top, and + # bottom sides of the region, for both the source and + # destination surfaces. + + # left side. + if x0 >= 0: + dx0 = x0 + sx0 = x0 + else: + dx0 = dw + x0 + sx0 = sw + x0 + + # right side. + if x1 > 0: + dx1 = x1 + sx1 = x1 + else: + dx1 = dw + x1 + sx1 = sw + x1 + + # top side. + if y0 >= 0: + dy0 = y0 + sy0 = y0 + else: + dy0 = dh + y0 + sy0 = sh + y0 + + # bottom side + if y1 > 0: + dy1 = y1 + sy1 = y1 + else: + dy1 = dh + y1 + + sy1 = sh + y1 + + # Quick exit. + if sx0 == sx1 or sy0 == sy1 or dx1 <= dx0 or dy1 <= dy0: + return + + # Compute sizes. + srcsize = (sx1 - sx0, sy1 - sy0) + dstsize = (int(dx1 - dx0), int(dy1 - dy0)) + + # Get a subsurface. + surf = source.subsurface((sx0, sy0, srcsize[0], srcsize[1])) + + # Scale or tile if we have to. + if dstsize != srcsize: + if self.tile: + tilew, tileh = srcsize + dstw, dsth = dstsize + + surf2 = renpy.display.pgrender.surface_unscaled(dstsize, surf) + + for y in range(0, dsth, tileh): + for x in range(0, dstw, tilew): + surf2.blit(surf, (x, y)) + + surf = surf2 + + else: + surf2 = renpy.display.scale.real_transform_scale(surf, dstsize) + surf = surf2 + + # Blit. + dest.blit(surf, (dx0, dy0)) + + self.draw_pattern(draw, left, top, right, bottom) + + rrv = renpy.display.render.Render(dw, dh) + rrv.blit(rv, (0, 0)) + rrv.depends_on(crend) + + # And, finish up. + return rrv + + def visit(self): + return [ self.image ] + + diff --git a/unrpyc/renpy/display/imagemap.py b/unrpyc/renpy/display/imagemap.py new file mode 100644 index 0000000..380bf80 --- /dev/null +++ b/unrpyc/renpy/display/imagemap.py @@ -0,0 +1,233 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file handles imagemap caching. + +import pygame +import renpy.display + +from renpy.display.render import render + +import hashlib +import os + +# A list of cache images we've already written. +cached = set() + +class ImageMapCrop(renpy.display.core.Displayable): + """ + This handles the cropping of uncached imagemap components. + """ + + def __init__(self, child, rect): + super(ImageMapCrop, self).__init__() + + self.child = child + self.rect = rect + + def visit(self): + return [ self.child ] + + def render(self, width, height, st, at): + cr = render(self.child, width, height, st, at) + return cr.subsurface(self.rect) + + +class ImageCacheCrop(renpy.display.core.Displayable): + """ + This handles the cropping of an imagemap component. + """ + + def __init__(self, cache, index): + super(ImageCacheCrop, self).__init__() + + # The cache object we're associated with. + self.cache = cache + + # The index of + self.index = index + + def visit(self): + return self.cache.visit(self.index) + + def render(self, width, height, st, at): + return self.cache.render(self.index, width, height, st, at) + +class ImageMapCache(renpy.object.Object): + + def __init__(self, enable): + self.md5 = hashlib.md5() + + # A list of (image, rect) tuples. The index in this list is used + # as a unique identifier for an ImageCacheCrop object. + self.imagerect = [ ] + + # A map from (image, rect) to ImageCacheCrop object. + self.hotspots = { } + + # A list of (width, height, index) tuples. + self.areas = [ ] + + # The image containing our children. + self.cache = None + + # A list that, for each hotspot, gives the rectangle in the cache + # image corresponding to that hotspot. + self.cache_rect = None + + # The size of the cache. + self.cache_width = None + self.cache_height = None + + self.enable = enable + + def visit(self, index): + if self.cache is not None: + return [ self.cache ] + else: + return [ self.imagerect[index][0] ] + + def crop(self, d, rect): + if not isinstance(d, renpy.display.im.ImageBase) or \ + not renpy.config.imagemap_cache or \ + not self.enable: + return ImageMapCrop(d, rect) + + key = (d, rect) + rv = self.hotspots.get(key, None) + if rv is not None: + return rv + + self.md5.update(repr(d.identity)) + self.md5.update(repr(d.identity)) + + index = len(self.imagerect) + rv = ImageCacheCrop(self, index) + + self.imagerect.append(key) + self.hotspots[key] = rv + self.areas.append((rect[2] + 2, rect[3] + 2, index)) + + return rv + + def layout(self): + self.areas.sort() + self.areas.reverse() + self.cache_rect = [ None ] * len(self.areas) + + # The width of the cache image. + width = self.areas[0][0] + + x = 0 + y = 0 + line_height = 0 + + for w, h, i in self.areas: + + if x + w > width: + y += line_height + line_height = 0 + x = 0 + + self.cache_rect[i] = (x+1, y+1, w-2, h-2) + + x += w + if line_height < h: + line_height = h + + self.cache_width = width + self.cache_height = y + line_height + + def write_cache(self, filename): + + if filename in cached: + return + + cached.add(filename) + + # If all of our dependencies are of the same age or less, + # we don't need to rebuild the cache. + + if renpy.loader.loadable(filename): + d_set = set() + mtime = 0 + + for i, rect in self.imagerect: + if i in d_set: + continue + + d_set.add(i) + mtime = max(i.get_mtime(), mtime) + + if renpy.loader.get_mtime(filename) >= mtime: + return + + fn = os.path.join(renpy.config.gamedir, filename) + dir = os.path.dirname(fn) #@ReservedAssignment + + if not os.path.exists(dir): + os.makedirs(dir) + + cache = pygame.Surface((self.cache_width, self.cache_height), pygame.SRCALPHA, 32) + + for i, (d, rect) in enumerate(self.imagerect): + x, y, _w, _h = self.cache_rect[i] + + surf = renpy.display.im.cache.get(d).subsurface(rect) + cache.blit(surf, (x, y)) + + pygame.image.save(cache, renpy.exports.fsencode(fn)) + + def finish(self): + if not self.areas: + return + + filename = "im-%s.png" % (self.md5.hexdigest()) + + if renpy.game.preferences.language: + filename = renpy.game.preferences.language + "-" + filename + + filename = "cache/" + filename + + self.md5 = None + + self.layout() + + if renpy.config.developer: + try: + self.write_cache(filename) + except: + pass + + if renpy.loader.loadable(filename): + self.cache = renpy.display.im.Image(filename) + + + def render(self, index, width, height, st, at): + if self.cache is None: + d, rect = self.imagerect[index] + return render(d, width, height, st, at).subsurface(rect) + + return render(self.cache, width, height, st, at).subsurface(self.cache_rect[index]) + + + + diff --git a/unrpyc/renpy/display/joystick.py b/unrpyc/renpy/display/joystick.py new file mode 100644 index 0000000..c606dd9 --- /dev/null +++ b/unrpyc/renpy/display/joystick.py @@ -0,0 +1,126 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file is responsible for joystick support in Ren'Py. + +import os +import pygame + +import renpy.display + +# Do we have a joystick enabled? +enabled = False + +# The old states for each axis. +old_axis_states = { } + +def init(): + """ + Initialize the joystick system. + """ + + global enabled + + if not renpy.config.joystick: + return + + if 'RENPY_DISABLE_JOYSTICK' in os.environ: + return + + try: + pygame.joystick.init() + + for i in range(0, pygame.joystick.get_count()): + pygame.joystick.Joystick(i).init() + enabled = True + except: + if renpy.config.debug: + raise + +def event(ev): + + if not enabled: + return ev + + if ev.type == pygame.JOYAXISMOTION: + + if not renpy.display.interface.focused: + return None + + if ev.value >= 0.5: + state = "Positive" + elif ev.value <= -0.5: + state = "Negative" + else: + state = None + + oldstate = old_axis_states.get((ev.joy, ev.axis), None) + + if state == oldstate: + return None + + if oldstate: + release = "Axis %d.%d %s" % (ev.joy, ev.axis, oldstate) + else: + release = None + + old_axis_states[ev.joy, ev.axis] = state + + if state: + press = "Axis %d.%d %s" % (ev.joy, ev.axis, state) + else: + press = None + + if not press and not release: + return None + + return pygame.event.Event(renpy.display.core.JOYEVENT, + press=press, release=release) + + if ev.type == pygame.JOYBUTTONDOWN: + + if not renpy.display.interface.focused: + return None + + return pygame.event.Event(renpy.display.core.JOYEVENT, + press="Button %d.%d" % (ev.joy, ev.button), + release=None) + if ev.type == pygame.JOYBUTTONUP: + + if not renpy.display.interface.focused: + return None + + return pygame.event.Event(renpy.display.core.JOYEVENT, + press=None, + release="Button %d.%d" % (ev.joy, ev.button)) + + return ev + +class JoyBehavior(renpy.display.layout.Null): + """ + This is a behavior intended for joystick calibration. If a joystick + event occurs, this returns it as a string. + """ + + def event(self, ev, x, y, st): + if ev.type == renpy.display.core.JOYEVENT: + return ev.press + diff --git a/unrpyc/renpy/display/layout.py b/unrpyc/renpy/display/layout.py new file mode 100644 index 0000000..e07e28d --- /dev/null +++ b/unrpyc/renpy/display/layout.py @@ -0,0 +1,1744 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains classes that handle layout of displayables on +# the screen. + +from renpy.display.render import render, Render +import renpy.display + + +def scale(num, base): + """ + If num is a float, multiplies it by base and returns that. Otherwise, + returns num unchanged. + """ + + if isinstance(num, float): + return num * base + else: + return num + +class Null(renpy.display.core.Displayable): + """ + :doc: disp_imagelike + + A displayable that creates an empty box on the screen. The size + of the box is controlled by `width` and `height`. This can be used + when a displayable requires a child, but no child is suitable, or + as a spacer inside a box. + + :: + + image logo spaced = HBox("logo.png", Null(width=100), "logo.png") + + """ + + def __init__(self, width=0, height=0, **properties): + super(Null, self).__init__(**properties) + self.width = width + self.height = height + + def render(self, width, height, st, at): + rv = renpy.display.render.Render(self.width, self.height) + + if self.focusable: + rv.add_focus(self, None, None, None, None, None) + + return rv + + +class Container(renpy.display.core.Displayable): + """ + This is the base class for containers that can have one or more + children. + + @ivar children: A list giving the children that have been added to + this container, in the order that they were added in. + + @ivar child: The last child added to this container. This is also + used to access the sole child in containers that can only hold + one child. + + @ivar offsets: A list giving offsets for each of our children. + It's expected that render will set this up each time it is called. + + @ivar sizes: A list giving sizes for each of our children. It's + also expected that render will set this each time it is called. + + """ + + # We indirect all list creation through this, so that we can + # use RevertableLists if we want. + _list_type = list + + def __init__(self, *args, **properties): + + self.children = self._list_type() + self.child = None + self.offsets = self._list_type() + + for i in args: + self.add(i) + + super(Container, self).__init__(**properties) + + def set_style_prefix(self, prefix, root): + super(Container, self).set_style_prefix(prefix, root) + + for i in self.children: + i.set_style_prefix(prefix, False) + + def add(self, d): + """ + Adds a child to this container. + """ + + child = renpy.easy.displayable(d) + + self.children.append(child) + + self.child = child + self.offsets = self._list_type() + + def remove(self, d): + """ + Removes the first instance of child from this container. May + not work with all containers. + """ + + for i, c in enumerate(self.children): + if c is d: + break + else: + return + + self.children.pop(i) # W0631 + self.offsets = self._list_type() + + if self.children: + self.child = self.children[-1] + else: + self.child = None + + + def update(self): + """ + This should be called if a child is added to this + displayable outside of the render function. + """ + + renpy.display.render.invalidate(self) + + + def render(self, width, height, st, at): + + rv = Render(width, height) + self.offsets = self._list_type() + + for c in self.children: + cr = render(c, width, height, st, at) + offset = c.place(rv, 0, 0, width, height, cr) + self.offsets.append(offset) + + return rv + + + def event(self, ev, x, y, st): + + children = self.children + offsets = self.offsets + + for i in range(len(offsets) - 1, -1, -1): + + d = children[i] + xo, yo = offsets[i] + + rv = d.event(ev, x - xo, y - yo, st) + if rv is not None: + return rv + + return None + + def visit(self): + return self.children + + # These interact with the ui functions to allow use as a context + # manager. + + def __enter__(self): + + renpy.ui.context_enter(self) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + renpy.ui.context_exit(self) + return False + + + + +def LiveComposite(size, *args, **properties): + """ + :args: disp_imagelike + + This creates a new displayable of `size`, by compositing other + displayables. `size` is a (width, height) tuple. + + The remaining positional arguments are used to place images inside + the LiveComposite. The remaining positional arguments should come + in groups of two, with the first member of each group an (x, y) + tuple, and the second member of a group is a displayable that + is composited at that position. + + Displayables are composited from back to front. + + :: + + image eileen composite = LiveComposite( + (300, 600), + (0, 0), "body.png", + (0, 0), "clothes.png", + (50, 50), "expression.png") + """ + + properties.setdefault('style', 'image_placement') + + width, height = size + + rv = Fixed(xmaximum=width, ymaximum=height, xminimum=width, yminimum=height, **properties) + + if len(args) % 2 != 0: + raise Exception("LiveComposite requires an odd number of arguments.") + + for pos, widget in zip(args[0::2], args[1::2]): + xpos, ypos = pos + rv.add(Position(renpy.easy.displayable(widget), + xpos=xpos, xanchor=0, ypos=ypos, yanchor=0)) + + return rv + +class Position(Container): + """ + Controls the placement of a displayable on the screen, using + supplied position properties. This is the non-curried form of + Position, which should be used when the user has directly created + the displayable that will be shown on the screen. + """ + + def __init__(self, child, style='image_placement', **properties): + """ + @param child: The child that is being laid out. + + @param style: The base style of this position. + + @param properties: Position properties that control where the + child of this widget is placed. + """ + + super(Position, self).__init__(style=style, **properties) + self.add(child) + + def render(self, width, height, st, at): + + surf = render(self.child, width, height, st, at) + + self.offsets = [ (0, 0) ] + + rv = renpy.display.render.Render(surf.width, surf.height) + rv.blit(surf, (0, 0)) + + return rv + + def get_placement(self): + + xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = self.child.get_placement() + + v = self.style.xpos + if v is not None: + xpos = v + + v = self.style.ypos + if v is not None: + ypos = v + + v = self.style.xanchor + if v is not None: + xanchor = v + + v = self.style.yanchor + if v is not None: + yanchor = v + + v = self.style.xoffset + if v is not None: + xoffset = v + + v = self.style.yoffset + if v is not None: + yoffset = v + + v = self.style.subpixel + if v is not None: + subpixel = v + + return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel + + +class Grid(Container): + """ + A grid is a widget that evenly allocates space to its children. + The child widgets should not be greedy, but should instead be + widgets that only use part of the space available to them. + """ + + def __init__(self, cols, rows, padding=None, + transpose=False, + style='grid', **properties): + """ + @param cols: The number of columns in this widget. + + @params rows: The number of rows in this widget. + + @params transpose: True if the grid should be transposed. + """ + + if padding is not None: + properties.setdefault('spacing', padding) + + super(Grid, self).__init__(style=style, **properties) + + cols = int(cols) + rows = int(rows) + + self.cols = cols + self.rows = rows + + self.transpose = transpose + + def render(self, width, height, st, at): + + # For convenience and speed. + padding = self.style.spacing + cols = self.cols + rows = self.rows + + if len(self.children) != cols * rows: + if len(self.children) < cols * rows: + raise Exception("Grid not completely full.") + else: + raise Exception("Grid overfull.") + + # If necessary, transpose the grid (kinda hacky, but it works here.) + if self.transpose: + self.transpose = False + + old_children = self.children[:] + + for y in range(0, rows): + for x in range(0, cols): + self.children[x + y * cols] = old_children[ y + x * rows ] + + + # Now, start the actual rendering. + + renwidth = width + renheight = height + + if self.style.xfill: + renwidth = (width - (cols - 1) * padding) / cols + if self.style.yfill: + renheight = (height - (rows - 1) * padding) / rows + + renders = [ render(i, renwidth, renheight, st, at) for i in self.children ] + sizes = [ i.get_size() for i in renders ] + + cwidth = 0 + cheight = 0 + + for w, h in sizes: + cwidth = max(cwidth, w) + cheight = max(cheight, h) + + if self.style.xfill: + cwidth = renwidth + + if self.style.yfill: + cheight = renheight + + width = cwidth * cols + padding * (cols - 1) + height = cheight * rows + padding * (rows - 1) + + rv = renpy.display.render.Render(width, height) + + self.offsets = [ ] + + for y in range(0, rows): + for x in range(0, cols): + + child = self.children[ x + y * cols ] + surf = renders[x + y * cols] + + xpos = x * (cwidth + padding) + ypos = y * (cheight + padding) + + offset = child.place(rv, xpos, ypos, cwidth, cheight, surf) + self.offsets.append(offset) + + return rv + +class IgnoreLayers(Exception): + """ + Raise this to have the event ignored by layers, but reach the + underlay. + """ + + pass + +class MultiBox(Container): + + layer_name = None + first = True + order_reverse = False + + def __init__(self, spacing=None, layout=None, style='default', **properties): + + if spacing is not None: + properties['spacing'] = spacing + + super(MultiBox, self).__init__(style=style, **properties) + + self.default_layout = layout + + # The start and animation times for children of this + # box. + self.start_times = [ ] + self.anim_times = [ ] + + # A map from layer name to the widget corresponding to + # that layer. + self.layers = None + + # The scene list for this widget. + self.scene_list = None + + def add(self, widget, start_time=None, anim_time=None): # W0221 + super(MultiBox, self).add(widget) + self.start_times.append(start_time) + self.anim_times.append(anim_time) + + def append_scene_list(self, l): + + for sle in l: + self.add(sle.displayable, sle.show_time, sle.animation_time) + + if self.scene_list is None: + self.scene_list = [ ] + + self.scene_list.extend(l) + + def render(self, width, height, st, at): + + # Do we need to adjust the child times due to our being a layer? + if self.layer_name or (self.layers is not None): + adjust_times = True + else: + adjust_times = False + + xminimum = self.style.xminimum + if xminimum is not None: + width = max(width, scale(xminimum, width)) + + yminimum = self.style.yminimum + if yminimum is not None: + height = max(height, scale(yminimum, height)) + + if self.first: + + self.first = False + + if adjust_times: + + it = renpy.game.interface.interact_time + + self.start_times = [ i or it for i in self.start_times ] + self.anim_times = [ i or it for i in self.anim_times ] + + layout = self.style.box_layout + + if layout is None: + layout = self.default_layout + + self.layout = layout # W0201 + + else: + layout = self.layout + + + # Handle time adjustment, store the results in csts and cats. + if adjust_times: + t = renpy.game.interface.frame_time + + csts = [ t - start for start in self.start_times ] + cats = [ t - anim for anim in self.anim_times ] + + else: + csts = [ st ] * len(self.children) + cats = [ at ] * len(self.children) + + offsets = [ ] + + if layout == "fixed": + + rv = None + + if self.style.order_reverse: + iterator = list(zip(reversed(self.children), reversed(csts), reversed(cats))) + else: + iterator = list(zip(self.children, csts, cats)) + + for child, cst, cat in iterator: + + surf = render(child, width, height, cst, cat) + + if rv is None: + + if self.style.fit_first: + sw, sh = surf.get_size() + width = min(width, sw) + height = min(height, sh) + + + rv = renpy.display.render.Render(width, height, layer_name=self.layer_name) + + if surf: + offset = child.place(rv, 0, 0, width, height, surf) + offsets.append(offset) + else: + offsets.append((0, 0)) + + if rv is None: + rv = renpy.display.render.Render(width, height, layer_name=self.layer_name) + + if self.style.order_reverse: + offsets.reverse() + + self.offsets = offsets + + return rv + + # If we're here, we have a box, either horizontal or vertical. Which is good, + # as we can share some code between boxes. + + + spacing = self.style.spacing + first_spacing = self.style.first_spacing + + if first_spacing is None: + first_spacing = spacing + + spacings = [ first_spacing ] + [ spacing ] * (len(self.children) - 1) + + box_wrap = self.style.box_wrap + + xfill = self.style.xfill + yfill = self.style.yfill + + # The shared height and width of the current line. The line_height must + # be 0 for a vertical box, and the line_width must be 0 for a horizontal + # box. + line_width = 0 + line_height = 0 + + # The children to layout. + children = list(self.children) + if self.style.box_reverse: + children.reverse() + spacings.reverse() + + # a list of (child, x, y, w, h, surf) tuples that are turned into + # calls to child.place(). + placements = [ ] + + # The maximum x and y. + maxx = 0 + maxy = 0 + + def layout_line(line, xfill, yfill): + """ + Lays out a single line. + + `line` a list of (child, x, y, surf) tuples. + `xfill` the amount of space to add in the x direction. + `yfill` the amount of space to add in the y direction. + """ + + xfill = max(0, xfill) + yfill = max(0, yfill) + + if line: + xperchild = xfill / len(line) + yperchild = yfill / len(line) + else: + xperchild = 0 + yperchild = 0 + + maxxout = maxx + maxyout = maxy + + for i, (child, x, y, surf) in enumerate(line): + sw, sh = surf.get_size() + sw = max(line_width, sw) + sh = max(line_height, sh) + + x += i * xperchild + y += i * yperchild + + sw += xperchild + sh += yperchild + + placements.append((child, x, y, sw, sh, surf)) + + maxxout = max(maxxout, x + sw) + maxyout = max(maxyout, y + sh) + + return maxxout, maxyout + + x = 0 + y = 0 + + full_width = False + full_height = False + + if layout == "horizontal": + + full_height = yfill + + line_height = 0 + line = [ ] + remwidth = width + + for d, padding, cst, cat in zip(children, spacings, csts, cats): + + if box_wrap: + rw = width + else: + rw = remwidth + + surf = render(d, rw, height - y, cst, cat) + sw, sh = surf.get_size() + + if box_wrap and remwidth - sw - padding <= 0 and line: + maxx, maxy = layout_line(line, remwidth if xfill else 0, 0) + + y += line_height + x = 0 + line_height = 0 + remwidth = width + line = [ ] + + + line.append((d, x, y, surf)) + line_height = max(line_height, sh) + x += sw + padding + remwidth -= (sw + padding) + + maxx, maxy = layout_line(line, remwidth if xfill else 0, 0) + + + elif layout == "vertical": + + full_width = xfill + + line_width = 0 + line = [ ] + remheight = height + + for d, padding, cst, cat in zip(children, spacings, csts, cats): + + if box_wrap: + rh = height + else: + rh = remheight + + surf = render(d, width - x, rh, cst, cat) + sw, sh = surf.get_size() + + if box_wrap and remheight - sh - padding <= 0: + maxx, maxy = layout_line(line, 0, remheight if yfill else 0) + + x += line_width + y = 0 + line_width = 0 + remheight = height + line = [ ] + + line.append((d, x, y, surf)) + line_width = max(line_width, sw) + y += sh + padding + remheight -= (sh + padding) + + maxx, maxy = layout_line(line, 0, remheight if yfill else 0) + + # Back to the common for vertical and horizontal. + + if not xfill: + width = maxx + + if not yfill: + height = maxy + + rv = renpy.display.render.Render(width, height) + + if self.style.box_reverse ^ self.style.order_reverse: + placements.reverse() + + for child, x, y, w, h, surf in placements: + if full_width: + w = width + if full_height: + h = height + + offset = child.place(rv, x, y, w, h, surf) + offsets.append(offset) + + if self.style.order_reverse: + offsets.reverse() + + self.offsets = offsets + + return rv + + + def event(self, ev, x, y, st): + + + children_offsets = list(zip(self.children, self.offsets, self.start_times)) + + if not self.style.order_reverse: + children_offsets.reverse() + + try: + + for i, (xo, yo), t in children_offsets: + + if t is None: + cst = st + else: + cst = renpy.game.interface.event_time - t + + rv = i.event(ev, x - xo, y - yo, cst) + if rv is not None: + return rv + + except IgnoreLayers: + if self.layers: + return None + else: + raise + + return None + +def Fixed(**properties): + return MultiBox(layout='fixed', **properties) + +class SizeGroup(renpy.object.Object): + + def __init__(self): + + super(SizeGroup, self).__init__() + + self.members = [ ] + self._width = None + self.computing_width = False + + def width(self, width, height, st, at): + if self._width is not None: + return self._width + + if self.computing_width: + return 0 + + self.computing_width = True + + maxwidth = 0 + + for i in self.members: + rend = renpy.display.render.render(i, width, height, st, at) + maxwidth = max(rend.width, maxwidth) + renpy.display.render.invalidate(i) + + self._width = maxwidth + self.computing_width = False + + return maxwidth + + +size_groups = dict() + +class Window(Container): + """ + A window that has padding and margins, and can place a background + behind its child. `child` is the child added to this + displayable. All other properties are as for the :ref:`Window` + screen language statement. + """ + + def __init__(self, child, style='window', **properties): + + super(Window, self).__init__(style=style, **properties) + if child is not None: + self.add(child) + + def visit(self): + return [ self.style.background ] + self.children + + def get_child(self): + return self.style.child or self.child + + def per_interact(self): + size_group = self.style.size_group + + if size_group: + group = size_groups.get(size_group, None) + if group is None: + group = size_groups[size_group] = SizeGroup() + + group.members.append(self) + + def predict_one(self): + # Child will be predicted by visiting. + + pd = renpy.display.predict.displayable + style = self.style + + pd(style.insensitive_background) + pd(style.idle_background) + pd(style.hover_background) + pd(style.selected_idle_background) + pd(style.selected_hover_background) + + pd(style.insensitive_child) + pd(style.idle_child) + pd(style.hover_child) + pd(style.selected_idle_child) + pd(style.selected_hover_child) + + pd(style.insensitive_foreground) + pd(style.idle_foreground) + pd(style.hover_foreground) + pd(style.selected_idle_foreground) + pd(style.selected_hover_foreground) + + def render(self, width, height, st, at): + + # save some typing. + style = self.style + + xminimum = scale(style.xminimum, width) + yminimum = scale(style.yminimum, height) + + size_group = self.style.size_group + if size_group and size_group in size_groups: + xminimum = max(xminimum, size_groups[size_group].width(width, height, st, at)) + + left_margin = scale(style.left_margin, width) + left_padding = scale(style.left_padding, width) + + right_margin = scale(style.right_margin, width) + right_padding = scale(style.right_padding, width) + + top_margin = scale(style.top_margin, height) + top_padding = scale(style.top_padding, height) + + bottom_margin = scale(style.bottom_margin, height) + bottom_padding = scale(style.bottom_padding, height) + + # c for combined. + cxmargin = left_margin + right_margin + cymargin = top_margin + bottom_margin + + cxpadding = left_padding + right_padding + cypadding = top_padding + bottom_padding + + child = self.get_child() + + # Render the child. + surf = render(child, + width - cxmargin - cxpadding, + height - cymargin - cypadding, + st, at) + + sw, sh = surf.get_size() + + # If we don't fill, shrink our size to fit. + + if not style.xfill: + width = max(cxmargin + cxpadding + sw, xminimum) + + if not style.yfill: + height = max(cymargin + cypadding + sh, yminimum) + + rv = renpy.display.render.Render(width, height) + + # Draw the background. The background should render at exactly the + # requested size. (That is, be a Frame or a Solid). + if style.background: + bw = width - cxmargin + bh = height - cymargin + + back = render(style.background, bw, bh, st, at) + + style.background.place(rv, left_margin, top_margin, bw, bh, back, main=False) + + offsets = child.place(rv, + left_margin + left_padding, + top_margin + top_padding, + width - cxmargin - cxpadding, + height - cymargin - cypadding, + surf) + + # Draw the foreground. The background should render at exactly the + # requested size. (That is, be a Frame or a Solid). + if style.foreground: + bw = width - cxmargin + bh = height - cymargin + + back = render(style.foreground, bw, bh, st, at) + + style.foreground.place(rv, left_margin, top_margin, bw, bh, back, main=False) + + self.offsets = [ offsets ] + + self.window_size = width, height # W0201 + + return rv + + +def dynamic_displayable_compat(st, at, expr): + child = renpy.python.py_eval(expr) + return child, None + +class DynamicDisplayable(renpy.display.core.Displayable): + """ + :doc: disp_dynamic + + A displayable that can change its child based on a Python + function, over the course of an interaction. + + `function` + A function that is called with the arguments: + + * The amount of time the displayable has been shown for. + * The amount of time any displayable with the same tag has been shown for. + * Any positional or keyword arguments supplied to DynamicDisplayable. + + and should return a (d, redraw) tuple, where: + + * `d` is a displayable to show. + * `redraw` is the amount of time to wait before calling the + function again, or None to not call the function again + before the start of the next interaction. + + `function` is called at the start of every interaction. + + As a special case, `function` may also be a python string that evaluates + to a displayable. In that case, function is run once per interaction. + + :: + + # If tooltip is not empty, shows it in a text. Otherwise, + # show Null. Checks every tenth of a second to see if the + # tooltip has been updated. + init python: + def show_tooltip(st, at): + if tooltip: + return tooltip, .1 + else: + return Null() + + image tooltipper = DynamicDisplayable(show_tooltip) + + """ + + nosave = [ 'child' ] + + def after_setstate(self): + self.child = None + + def __init__(self, function, *args, **kwargs): + + super(DynamicDisplayable, self).__init__() + self.child = None + + if isinstance(function, str): + args = ( function, ) + kwargs = { } + function = dynamic_displayable_compat + + self.predict_function = kwargs.pop("_predict_function", None) + self.function = function + self.args = args + self.kwargs = kwargs + self.st = 0 + self.at = 0 + + def visit(self): + return [ ] + + def per_interact(self): + child, _ = self.function(self.st, self.at, *self.args, **self.kwargs) + child = renpy.easy.displayable(child) + child.visit_all(lambda a : a.per_interact()) + + if child is not self.child: + renpy.display.render.redraw(self, 0) + self.child = child + + def render(self, w, h, st, at): + + self.st = st + self.at = at + + child, redraw = self.function(st, at, *self.args, **self.kwargs) + child = renpy.easy.displayable(child) + child.visit_all(lambda c : c.per_interact()) + + self.child = child + + if redraw is not None: + renpy.display.render.redraw(self, redraw) + + return renpy.display.render.render(self.child, w, h, st, at) + + def predict_one(self): + if not self.predict_function: + return + + for i in self.predict_function(*self.args, **self.kwargs): + if i is not None: + renpy.display.predict.displayable(i) + + def get_placement(self): + if not self.child: + self.per_interact() + + return self.child.get_placement() + + + def event(self, ev, x, y, st): + if self.child: + return self.child.event(ev, x, y, st) + +# This chooses the first member of switch that's being shown on the +# given layer. +def condition_switch_pick(switch): + for cond, d in switch: + if cond is None or renpy.python.py_eval(cond): + return d + + raise Exception("Switch could not choose a displayable.") + +def condition_switch_show(st, at, switch): + return condition_switch_pick(switch), None + +def condition_switch_predict(switch): + + if renpy.game.lint: + return [ d for _cond, d in switch ] + + return [ condition_switch_pick(switch) ] + +def ConditionSwitch(*args, **kwargs): + """ + :doc: disp_dynamic + + This is a displayable that changes what it is showing based on + python conditions. The positional argument should be given in + groups of two, where each group consists of: + + * A string containing a python condition. + * A displayable to use if the condition is true. + + The first true condition has its displayable shown, at least + one condition should always be true. + + :: + + image jill = ConditionSwitch( + "jill_beers > 4", "jill_drunk.png", + "True", "jill_sober.png") + """ + + kwargs.setdefault('style', 'default') + + switch = [ ] + + if len(args) % 2 != 0: + raise Exception('ConditionSwitch takes an even number of arguments') + + for cond, d in zip(args[0::2], args[1::2]): + + d = renpy.easy.displayable(d) + switch.append((cond, d)) + + rv = DynamicDisplayable(condition_switch_show, + switch, + _predict_function=condition_switch_predict) + + return Position(rv, **kwargs) + + +def ShowingSwitch(*args, **kwargs): + """ + :doc: disp_dynamic + + This is a displayable that changes what it is showing based on the + images are showing on the screen. The positional argument should + be given in groups of two, where each group consists of: + + * A string giving an image name, or None to indicate the default. + * A displayable to use if the condition is true. + + A default image should be specified. + + One use of ShowingSwitch is to have side images change depending on + the current emotion of a character. For example:: + + define e = Character("Eileen", + show_side_image=ShowingSwitch( + "eileen happy", Image("eileen_happy_side.png", xalign=1.0, yalign=1.0), + "eileen vhappy", Image("eileen_vhappy_side.png", xalign=1.0, yalign=1.0), + None, Image("eileen_happy_default.png", xalign=1.0, yalign=1.0), + ) + ) + """ + + layer = kwargs.pop('layer', 'master') + + if len(args) % 2 != 0: + raise Exception('ShowingSwitch takes an even number of positional arguments') + + condargs = [ ] + + + for name, d in zip(args[0::2], args[1::2]): + if name is not None: + if not isinstance(name, tuple): + name = tuple(name.split()) + cond = "renpy.showing(%r, layer=%r)" % (name, layer) + else: + cond = None + + + condargs.append(cond) + condargs.append(d) + + return ConditionSwitch(*condargs, **kwargs) + + +class IgnoresEvents(Container): + + def __init__(self, child, **properties): + super(IgnoresEvents, self).__init__(**properties) + self.add(child) + + def render(self, w, h, st, at): + cr = renpy.display.render.render(self.child, w, h, st, at) + cw, ch = cr.get_size() + rv = renpy.display.render.Render(cw, ch) + rv.blit(cr, (0, 0), focus=False) + + return rv + + def get_placement(self): + return self.child.get_placement() + + # Ignores events. + def event(self, ev, x, y, st): + return None + +def edgescroll_proportional(n): + """ + An edgescroll function that causes the move speed to be proportional + from the edge distance. + """ + return n + +class Viewport(Container): + + __version__ = 3 + + def after_upgrade(self, version): + if version < 1: + self.xadjustment = renpy.display.behavior.Adjustment(1, 0) + self.yadjustment = renpy.display.behavior.Adjustment(1, 0) + self.set_adjustments = False + self.mousewheel = False + self.draggable = False + self.width = 0 + self.height = 0 + + if version < 2: + self.drag_position = None + + if version < 3: + self.edge_size = False + self.edge_speed = False + self.edge_function = None + self.edge_xspeed = 0 + self.edge_yspeed = 0 + self.edge_last_st = None + + def __init__(self, + child=None, + child_size=(None, None), + offsets=(None, None), + xadjustment=None, + yadjustment=None, + set_adjustments=True, + mousewheel=False, + draggable=False, + edgescroll=None, + style='viewport', + xinitial=None, + yinitial=None, + replaces=None, + **properties): + + super(Viewport, self).__init__(style=style, **properties) + if child is not None: + self.add(child) + + if xadjustment is None: + self.xadjustment = renpy.display.behavior.Adjustment(1, 0) + else: + self.xadjustment = xadjustment + + if yadjustment is None: + self.yadjustment = renpy.display.behavior.Adjustment(1, 0) + else: + self.yadjustment = yadjustment + + + if isinstance(replaces, Viewport): + self.xadjustment.range = replaces.xadjustment.range + self.yadjustment.range = replaces.yadjustment.range + self.xadjustment.value = replaces.xadjustment.value + self.yadjustment.value = replaces.yadjustment.value + self.xoffset = replaces.xoffset + self.yoffset = replaces.yoffset + self.drag_position = replaces.drag_position + else: + self.xoffset = offsets[0] if (offsets[0] is not None) else xinitial + self.yoffset = offsets[1] if (offsets[1] is not None) else yinitial + self.drag_position = None + + if self.xadjustment.adjustable is None: + self.xadjustment.adjustable = True + + if self.yadjustment.adjustable is None: + self.yadjustment.adjustable = True + + self.set_adjustments = set_adjustments + + self.child_width, self.child_height = child_size + + self.mousewheel = mousewheel + self.draggable = draggable + + self.width = 0 + self.height = 0 + + # The speed at which we scroll in the x and y directions, in pixels + # per second. + self.edge_xspeed = 0 + self.edge_yspeed = 0 + + # The last time we edgescrolled. + self.edge_last_st = None + + if edgescroll is not None: + + # The size of the edges that trigger scrolling. + self.edge_size = edgescroll[0] + + # How far from the edge we can scroll. + self.edge_speed = edgescroll[1] + + if len(edgescroll) >= 3: + self.edge_function = edgescroll[2] + else: + self.edge_function = edgescroll_proportional + + else: + self.edge_size = 0 + self.edge_speed = 0 + self.edge_function = edgescroll_proportional + + + def per_interact(self): + self.xadjustment.register(self) + self.yadjustment.register(self) + + def render(self, width, height, st, at): + + self.width = width + self.height = height + + child_width = self.child_width or width + child_height = self.child_height or height + + surf = render(self.child, child_width, child_height, st, at) + + cw, ch = surf.get_size() + + # width = min(cw, width) + # height = min(ch, height) + + if self.set_adjustments: + self.xadjustment.range = max(cw - width, 0) + self.xadjustment.page = width + self.yadjustment.range = max(ch - height, 0) + self.yadjustment.page = height + + if self.xoffset is not None: + if isinstance(self.xoffset, int): + value = self.xoffset + else: + value = max(cw - width, 0) * self.xoffset + + self.xadjustment.value = value + + if self.yoffset is not None: + if isinstance(self.yoffset, int): + value = self.yoffset + else: + value = max(ch - height, 0) * self.yoffset + + self.yadjustment.value = value + + if self.edge_size and self.edge_last_st and (self.edge_xspeed or self.edge_yspeed): + + duration = max(st - self.edge_last_st, 0) + self.xadjustment.change(self.xadjustment.value + duration * self.edge_xspeed) + self.yadjustment.change(self.yadjustment.value + duration * self.edge_yspeed) + + self.check_edge_redraw() + + self.edge_last_st = st + + cxo = -int(self.xadjustment.value) + cyo = -int(self.yadjustment.value) + + self.offsets = [ (cxo, cyo) ] + + rv = renpy.display.render.Render(width, height) + rv.blit(surf, (cxo, cyo)) + + return rv + + def check_edge_redraw(self): + redraw = False + + if (self.edge_xspeed > 0) and (self.xadjustment.value < self.xadjustment.range): + redraw = True + if (self.edge_xspeed < 0) and (self.xadjustment.value > 0): + redraw = True + + if (self.edge_yspeed > 0) and (self.yadjustment.value < self.yadjustment.range): + redraw = True + if (self.edge_yspeed < 0) and (self.yadjustment.value > 0): + redraw = True + + if redraw: + renpy.display.render.redraw(self, 0) + + + def event(self, ev, x, y, st): + + self.xoffset = None + self.yoffset = None + + rv = super(Viewport, self).event(ev, x, y, st) + if rv is not None: + return rv + + if self.draggable and renpy.display.focus.get_grab() == self: + + oldx, oldy = self.drag_position + dx = x - oldx + dy = y - oldy + + self.xadjustment.change(self.xadjustment.value - dx) + self.yadjustment.change(self.yadjustment.value - dy) + + self.drag_position = (x, y) # W0201 + + if renpy.display.behavior.map_event(ev, 'viewport_drag_end'): + renpy.display.focus.set_grab(None) + raise renpy.display.core.IgnoreEvent() + + if not ((0 <= x < self.width) and (0 <= y <= self.height)): + return + + if self.mousewheel: + + if renpy.display.behavior.map_event(ev, 'viewport_up'): + rv = self.yadjustment.change(self.yadjustment.value - self.yadjustment.step) + if rv is not None: + return rv + else: + raise renpy.display.core.IgnoreEvent() + + if renpy.display.behavior.map_event(ev, 'viewport_down'): + rv = self.yadjustment.change(self.yadjustment.value + self.yadjustment.step) + if rv is not None: + return rv + else: + raise renpy.display.core.IgnoreEvent() + + if self.draggable: + + if renpy.display.behavior.map_event(ev, 'viewport_drag_start'): + self.drag_position = (x, y) + renpy.display.focus.set_grab(self) + raise renpy.display.core.IgnoreEvent() + + if self.edge_size: + + def speed(n, zero, one): + """ + Given a position `n`, computes the speed. The speed is 0.0 + when `n` == `zero`, 1.0 when `n` == `one`, and linearly + interpolated when between. + + Returns 0.0 when outside the bounds - in either direction. + """ + + n = 1.0 * (n - zero) / (one - zero) + if n < 0.0: + return 0.0 + if n > 1.0: + return 0.0 + + return n + + xspeed = speed(x, self.width - self.edge_size, self.width) + xspeed -= speed(x, self.edge_size, 0) + self.edge_xspeed = self.edge_speed * self.edge_function(xspeed) + + yspeed = speed(y, self.height - self.edge_size, self.height) + yspeed -= speed(y, self.edge_size, 0) + self.edge_yspeed = self.edge_speed * self.edge_function(yspeed) + + if xspeed or yspeed: + self.check_edge_redraw() + if self.edge_last_st is None: + self.edge_last_st = st + else: + self.edge_last_st = None + + return None + + def set_xoffset(self, offset): + self.xoffset = offset + renpy.display.render.redraw(self, 0) + + def set_yoffset(self, offset): + self.yoffset = offset + renpy.display.render.redraw(self, 0) + +def LiveCrop(rect, child, **properties): + """ + :doc: disp_imagelike + + This created a displayable by cropping `child` to `rect`, where + `rect` is an (x, y, width, height) tuple. :: + + image eileen cropped = LiveCrop((0, 0, 300, 300), "eileen happy") + """ + + x, y, w, h = rect + + return Viewport(child, offsets=(x, y), xmaximum=w, ymaximum=h, **properties) + +class Side(Container): + + possible_positions = set([ 'tl', 't', 'tr', 'r', 'br', 'b', 'bl', 'l', 'c']) + + def after_setstate(self): + self.sized = False + + def __init__(self, positions, style='side', **properties): + + super(Side, self).__init__(style=style, **properties) + + if isinstance(positions, str): + positions = positions.split() + + for i in positions: + if not i in Side.possible_positions: + raise Exception("Side used with impossible position '%s'." % (i,)) + + self.positions = tuple(positions) + self.sized = False + + def render(self, width, height, st, at): + + pos_d = { } + pos_i = { } + + for i, (pos, d) in enumerate(zip(self.positions, self.children)): + pos_d[pos] = d + pos_i[pos] = i + + # Figure out the size of each widget (and hence where the + # widget needs to be placed). + + if not self.sized: + self.sized = True + + # Deal with various spacings. + spacing = self.style.spacing + + def spacer(a, b, c, axis): + if (a in pos_d) or (b in pos_d) or (c in pos_d): + return spacing, axis - spacing + else: + return 0, axis + + self.left_space, width = spacer('tl', 'l', 'bl', width) # W0201 + self.right_space, width = spacer('tr', 'r', 'br', width) # W0201 + self.top_space, height = spacer('tl', 't', 'tr', height) # W0201 + self.bottom_space, height = spacer('bl', 'b', 'br', height) # W0201 + + # The sizes of the various borders. + left = 0 + right = 0 + top = 0 + bottom = 0 + cwidth = 0 + cheight = 0 + + def sizeit(pos, width, height, owidth, oheight): + if pos not in pos_d: + return owidth, oheight + + rend = render(pos_d[pos], width, height, st, at) + rv = max(owidth, rend.width), max(oheight, rend.height) + rend.kill() + return rv + + cwidth, cheight = sizeit('c', width, height, 0, 0) + cwidth, top = sizeit('t', cwidth, height, cwidth, top) + cwidth, bottom = sizeit('b', cwidth, height, cwidth, bottom) + left, cheight = sizeit('l', width, cheight, left, cheight) + right, cheight = sizeit('r', width, cheight, right, cheight) + + left, top = sizeit('tl', left, top, left, top) + left, bottom = sizeit('bl', left, bottom, left, bottom) + right, top = sizeit('tr', right, top, right, top) + right, bottom = sizeit('br', right, bottom, right, bottom) + + self.cwidth = cwidth # W0201 + self.cheight = cheight # W0201 + + self.top = top # W0201 + self.bottom = bottom # W0201 + self.left = left # W0201 + self.right = right # W0201 + + else: + cwidth = self.cwidth + cheight = self.cheight + top = self.top + bottom = self.bottom + left = self.left + right = self.right + + # Now, place everything onto the render. + + self.offsets = [ None ] * len(self.children) + + lefts = self.left_space + rights = self.right_space + tops = self.top_space + bottoms = self.bottom_space + + + cwidth = min(cwidth, width - left - lefts - right - rights) + cheight = min(cheight, height - top - tops - bottom - bottoms) + + rv = renpy.display.render.Render(left + lefts + cwidth + rights + right, + top + tops + cheight + bottoms + bottom) + + def place(pos, x, y, w, h): + + if pos not in pos_d: + return + + d = pos_d[pos] + i = pos_i[pos] + rend = render(d, w, h, st, at) + self.offsets[i] = pos_d[pos].place(rv, x, y, w, h, rend) + + col1 = 0 + col2 = left + lefts + col3 = left + lefts + cwidth + rights + + row1 = 0 + row2 = top + tops + row3 = top + tops + cheight + bottoms + + place('c', col2, row2, cwidth, cheight) + + place('t', col2, row1, cwidth, top) + place('r', col3, row2, right, cheight) + place('b', col2, row3, cwidth, bottom) + place('l', col1, row2, left, cheight) + + place('tl', col1, row1, left, top) + place('tr', col3, row1, right, top) + place('br', col3, row3, right, bottom) + place('bl', col1, row3, left, bottom) + + return rv + +class Alpha(renpy.display.core.Displayable): + def __init__(self, start, end, time, child=None, repeat=False, bounce=False, + anim_timebase=False, time_warp=None, **properties): + + super(Alpha, self).__init__(**properties) + + self.start = start + self.end = end + self.time = time + self.child = renpy.easy.displayable(child) + self.repeat = repeat + self.anim_timebase = anim_timebase + self.time_warp = time_warp + + def visit(self): + return [ self.child ] + + def render(self, height, width, st, at): + if self.anim_timebase: + t = at + else: + t = st + + if self.time: + done = min(t / self.time, 1.0) + else: + done = 1.0 + + if renpy.game.less_updates: + done = 1.0 + elif self.repeat: + done = done % 1.0 + renpy.display.render.redraw(self, 0) + elif done != 1.0: + renpy.display.render.redraw(self, 0) + + if self.time_warp: + done = self.time_warp(done) + + alpha = self.start + done * (self.end - self.start) + + rend = renpy.display.render.render(self.child, height, width, st, at) + + w, h = rend.get_size() + rv = renpy.display.render.Render(w, h) + rv.blit(rend, (0, 0)) + rv.alpha = alpha + + return rv + + +class AdjustTimes(Container): + + def __init__(self, child, start_time, anim_time, **properties): + super(AdjustTimes, self).__init__(**properties) + + self.start_time = start_time + self.anim_time = anim_time + + self.add(child) + + def render(self, w, h, st, at): + + if self.start_time is None: + self.start_time = renpy.game.interface.frame_time + + if self.anim_time is None: + self.anim_time = renpy.game.interface.frame_time + + st = renpy.game.interface.frame_time - self.start_time + at = renpy.game.interface.frame_time - self.anim_time + + cr = renpy.display.render.render(self.child, w, h, st, at) + cw, ch = cr.get_size() + rv = renpy.display.render.Render(cw, ch) + rv.blit(cr, (0, 0)) + + self.offsets = [ (0, 0) ] + + return rv + + def get_placement(self): + return self.child.get_placement() + + +class LiveTile(Container): + """ + :doc: disp_imagelike + + Tiles `child` until it fills the area allocated to this displayable. + + :: + + image bg tile = LiveTile("bg.png") + + """ + + def __init__(self, child, style='tile', **properties): + super(LiveTile, self).__init__(style=style, **properties) + + self.add(child) + + def render(self, width, height, st, at): + + cr = renpy.display.render.render(self.child, width, height, st, at) + cw, ch = cr.get_size() + rv = renpy.display.render.Render(width, height) + + width = int(width) + height = int(height) + cw = int(cw) + ch = int(ch) + + for y in range(0, height, ch): + for x in range(0, width, cw): + rv.blit(cr, (x, y), focus=False) + + return rv diff --git a/unrpyc/renpy/display/minigame.py b/unrpyc/renpy/display/minigame.py new file mode 100644 index 0000000..7469922 --- /dev/null +++ b/unrpyc/renpy/display/minigame.py @@ -0,0 +1,25 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +def Minigame(*args, **kwargs): + raise Exception("Minigame is no longer implemented.") + diff --git a/unrpyc/renpy/display/module.py b/unrpyc/renpy/display/module.py new file mode 100644 index 0000000..1bf66e0 --- /dev/null +++ b/unrpyc/renpy/display/module.py @@ -0,0 +1,275 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file mediates access to the _renpy module, which is a C module that +# allows us to enhance the feature set of pygame in a renpy specific way. + +VERSION = (6, 12, 0) + +import renpy.display +import pygame; pygame # prevents pyflakes warning. + +import sys + +try: + import _renpy + version = _renpy.version() + + if version != VERSION: + print("Found Ren'Py module version %s, while expecting %s." % ( + ".".join(str(i) for i in version), + ".".join(str(i) for i in VERSION), + )) + + print("Trying to run anyway, but you should expect errors.", file=sys.stderr) + +except: + print("The _renpy module was not found. Please read module/README.txt for", file=sys.stderr) + print("more information.", file=sys.stderr) + + sys.exit(-1) + +def convert_and_call(function, src, dst, *args): + """ + This calls the function with the source and destination + surface. The surfaces must have the same alpha. + + If the surfaces are not 24 or 32 bits per pixel, or don't have the + same format, they are converted and then converted back. + """ + + # Now that all surfaces are 32bpp, this function doesn't do much + # of anything anymore. + + if (dst.get_masks()[3] != 0) != (src.get_masks()[3] != 0): + raise Exception("Surface alphas do not match.") + + function(src, dst, *args) + + +def pixellate(src, dst, avgwidth, avgheight, outwidth, outheight): + """ + This pixellates the source surface. First, every pixel in the + source surface is projected onto a virtual surface, such that + the average value of every avgwidth x avgheight pixels becomes + one virtual pixel. It then gets projected back onto the + destination surface at a ratio of one virtual pixel to every + outwidth x outheight destination pixels. + + If either src or dst is not a 24 or 32 bit surface, they are + converted... but that may be a significant performance hit. + + The two surfaces must either have the same alpha or no alpha. + """ + + convert_and_call(_renpy.pixellate, + src, dst, + avgwidth, avgheight, + outwidth, outheight) + + +def scale(s, size): + """ + Scales down the supplied pygame surface by the given X and Y + factors. + + Always works, but may not be high quality. + """ + + d = renpy.display.pgrender.surface(size, True) + + bilinear_scale(s, d) + + return d + + +# What we have here are a pair of tables mapping masks to byte offsets +# for 24 and 32 bpp modes. We represent 0xff000000 as positive and negative +# numbers so that it doesn't yield a warning, and so that it works on +# 32 and 64 bit platforms. +if sys.byteorder == 'big': + bo32 = { 255 : 3, 65280 : 2, 16711680 : 1, 4278190080 : 0, -16777216 : 0, } +else: + bo32 = { 255 : 0, 65280 : 1, 16711680 : 2, 4278190080 : 3, -16777216 : 3, } + +bo_cache = None + +def byte_offset(src): + """ + Given the surface src, returns a 4-tuple giving the byte offsets + for the red, green, blue, and alpha components of the pixels in + the surface. If a component doesn't exist, None is returned. + """ + + global bo_cache + + if bo_cache is None: + bo_cache = [ bo32[i] for i in src.get_masks() ] + + return bo_cache + +def endian_order(src, r, g, b, a): + + if bo_cache is None: + byte_offset(src) + + rv = [ a, a, a, a ] + + for i, index_i in zip((r, g, b, a), bo_cache): + rv[index_i] = i + + return rv + + + +def linmap(src, dst, rmap, gmap, bmap, amap): + """ + This maps the colors between two surfaces. The various map + parameters should be fixed-point integers, with 1.0 == 256. + """ + + convert_and_call(_renpy.linmap, + src, dst, + *endian_order(dst, rmap, gmap, bmap, amap)) + + +save_png = _renpy.save_png + +def map(src, dst, rmap, gmap, bmap, amap): #@ReservedAssignment + """ + This maps the colors between two surfaces. The various map + parameters must be 256 character long strings, with the value + of a character at a given offset being what a particular pixel + component value is mapped to. + """ + + convert_and_call(_renpy.map, + src, dst, + *endian_order(dst, rmap, gmap, bmap, amap)) + + + +def twomap(src, dst, white, black): + """ + Given colors for white and black, linearly maps things + appropriately, taking the alpha channel from white. + """ + + wr = white[0] + wg = white[1] + wb = white[2] + wa = white[3] + + br = black[0] + bg = black[1] + bb = black[2] + + ramp = renpy.display.im.ramp + + if br == 0 and bg == 0 and bb == 0: + linmap(src, dst, + wr + 1, + wg + 1, + wb + 1, + wa + 1) + else: + list(map(src, dst, + ramp(br, wr), + ramp(bg, wg), + ramp(bb, wb), + ramp(0, wa))) + + +def alpha_munge(src, dst, amap): + """ + This samples the red channel from src, maps it through amap, and + place it into the alpha channel of amap. + """ + + if src.get_size() != dst.get_size(): + return + + red = byte_offset(src)[0] + alpha = byte_offset(dst)[3] + + if red is not None and alpha is not None: + _renpy.alpha_munge(src, dst, red, alpha, amap) + + +def bilinear_scale(src, dst, sx=0, sy=0, sw=None, sh=None, dx=0, dy=0, dw=None, dh=None, precise=0): + + if sw is None: + sw, sh = src.get_size() + if dw is None: + dw, dh = dst.get_size() + + while True: + + if sw <= dw * 2 and sh <= dh * 2: + break + + nsw = max(sw / 2, dw) + nsh = max(sh / 2, dh) + + nsrc = renpy.display.pgrender.surface((nsw, nsh), src.get_masks()[3]) + + _renpy.bilinear(src, nsrc, sx, sy, sw, sh, precise=precise) + + sx = 0 + sy = 0 + sw = nsw + sh = nsh + src = nsrc + + _renpy.bilinear(src, dst, sx, sy, sw, sh, dx, dy, dw, dh, precise=precise) + + +transform = _renpy.transform + +# Note: Blend requires all surfaces to be the same size. +blend = _renpy.blend + +def imageblend(a, b, dst, img, amap): + alpha = byte_offset(img)[3] + _renpy.imageblend(a, b, dst, img, alpha, amap) + + +def colormatrix(src, dst, matrix): + c = [ matrix[0:5], matrix[5:10], matrix[10:15], matrix[15:20] ] + offs = byte_offset(src) + + o = [ None ] * 4 + for i in range(0, 4): + o[offs[i]] = i + + _renpy.colormatrix(src, dst, + c[o[0]][o[0]], c[o[0]][o[1]], c[o[0]][o[2]], c[o[0]][o[3]], c[o[0]][4], + c[o[1]][o[0]], c[o[1]][o[1]], c[o[1]][o[2]], c[o[1]][o[3]], c[o[1]][4], + c[o[2]][o[0]], c[o[2]][o[1]], c[o[2]][o[2]], c[o[2]][o[3]], c[o[2]][4], + c[o[3]][o[0]], c[o[3]][o[1]], c[o[3]][o[2]], c[o[3]][o[3]], c[o[3]][4]) + + +def subpixel(src, dst, x, y): + + shift = src.get_shifts()[3] + _renpy.subpixel(src, dst, x, y, shift) + + diff --git a/unrpyc/renpy/display/motion.py b/unrpyc/renpy/display/motion.py new file mode 100644 index 0000000..1c9d667 --- /dev/null +++ b/unrpyc/renpy/display/motion.py @@ -0,0 +1,1526 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains displayables that move, zoom, rotate, or otherwise +# transform displayables. (As well as displayables that support them.) +import math +import types #@UnresolvedImport + +import renpy.display #@UnusedImport +from renpy.display.render import render +from renpy.display.layout import Container + +import renpy.display.accelerator + +# The null object that's used if we don't have a defined child. +null = None + +def get_null(): + global null + + if null is None: + null = renpy.display.layout.Null() + + return null + +# Convert a position from cartesian to polar coordinates. +def cartesian_to_polar(x, y, xaround, yaround): + """ + Converts cartesian coordinates to polar coordinates. + """ + + dx = x - xaround + dy = y - yaround + + radius = math.hypot(dx, dy) + angle = math.atan2(dx, -dy) / math.pi * 180 + + if angle < 0: + angle += 360 + + return angle, radius + +def polar_to_cartesian(angle, radius, xaround, yaround): + """ + Converts polart coordinates to cartesian coordinates. + """ + + angle = angle * math.pi / 180 + + dx = radius * math.sin(angle) + dy = -radius * math.cos(angle) + + x = type(xaround)(xaround + dx) + y = type(yaround)(yaround + dy) + + return x, y + +def first_not_none(*args): + """ + Returns the first argument that is not None. + """ + + for i in args: + if i is not None: + return i + return i + + +class TransformState(renpy.object.Object): + + xoffset = None + yoffset = None + default_xpos = None + default_ypos = None + default_xanchor = None + default_yanchor = None + default_xoffset = None + default_yoffset = None + transform_anchor = False + + def __init__(self): # W0231 + self.alpha = 1 + self.rotate = None + self.rotate_pad = True + self.transform_anchor = False + self.zoom = 1 + self.xzoom = 1 + self.yzoom = 1 + + self.xpos = None + self.ypos = None + self.xanchor = None + self.yanchor = None + self.xoffset = 0 + self.yoffset = 0 + + self.xaround = 0.0 + self.yaround = 0.0 + self.xanchoraround = 0.0 + self.yanchoraround = 0.0 + + self.subpixel = False + + self.crop = None + self.corner1 = None + self.corner2 = None + self.size = None + + self.delay = 0 + + # Note: When adding a new property, we need to add it to: + # - take_state + # - diff + # - renpy.atl.PROPERTIES + # - Proxies in Transform + + # Default values for various properties, taken from our + # parent. + self.default_xpos = None + self.default_ypos = None + self.default_xanchor = None + self.default_yanchor = None + + def take_state(self, ts): + + self.alpha = ts.alpha + self.rotate = ts.rotate + self.rotate_pad = ts.rotate_pad + self.transform_anchor = ts.transform_anchor + self.zoom = ts.zoom + self.xzoom = ts.xzoom + self.yzoom = ts.yzoom + + self.xaround = ts.xaround + self.yaround = ts.yaround + self.xanchoraround = ts.xanchoraround + self.yanchoraround = ts.yanchoraround + + self.subpixel = ts.subpixel + + self.crop = ts.crop + self.corner1 = ts.corner1 + self.corner2 = ts.corner2 + self.size = ts.size + + # Take the computed position properties, not the + # raw ones. + (self.default_xpos, + self.default_ypos, + self.default_xanchor, + self.default_yanchor, + self.xoffset, + self.yoffset, + self.subpixel) = ts.get_placement() + + # Returns a dict, with p -> (old, new) where p is a property that + # has changed between this object and the new object. + def diff(self, newts): + + rv = { } + + def diff2(prop, new, old): + if new != old: + rv[prop] = (old, new) + + def diff4(prop, new, default_new, old, default_old): + if new is None: + new_value = default_new + else: + new_value = new + + if old is None: + old_value = default_old + else: + old_value = old + + if new_value != old_value: + rv[prop] = (old_value, new_value) + + diff2("alpha", newts.alpha, self.alpha) + diff2("rotate", newts.rotate, self.rotate) + diff2("rotate_pad", newts.rotate_pad, self.rotate_pad) + diff2("transform_anchor", newts.transform_anchor, self.transform_anchor) + diff2("zoom", newts.zoom, self.zoom) + diff2("xzoom", newts.xzoom, self.xzoom) + diff2("yzoom", newts.yzoom, self.yzoom) + + diff2("xaround", newts.xaround, self.xaround) + diff2("yaround", newts.yaround, self.yaround) + diff2("xanchoraround", newts.xanchoraround, self.xanchoraround) + diff2("yanchoraround", newts.yanchoraround, self.yanchoraround) + + diff2("subpixel", newts.subpixel, self.subpixel) + + diff2("crop", newts.crop, self.crop) + diff2("corner1", newts.corner1, self.corner1) + diff2("corner2", newts.corner2, self.corner2) + diff2("size", newts.size, self.size) + + diff4("xpos", newts.xpos, newts.default_xpos, self.xpos, self.default_xpos) + + diff4("xanchor", newts.xanchor, newts.default_xanchor, self.xanchor, self.default_xanchor) + diff2("xoffset", newts.xoffset, self.xoffset) + + diff4("ypos", newts.ypos, newts.default_ypos, self.ypos, self.default_ypos) + diff4("yanchor", newts.yanchor, newts.default_yanchor, self.yanchor, self.default_yanchor) + diff2("yoffset", newts.yoffset, self.yoffset) + + return rv + + def get_placement(self, cxoffset=0, cyoffset=0): + + return ( + first_not_none(self.xpos, self.default_xpos), + first_not_none(self.ypos, self.default_ypos), + first_not_none(self.xanchor, self.default_xanchor), + first_not_none(self.yanchor, self.default_yanchor), + self.xoffset + cxoffset, + self.yoffset + cyoffset, + self.subpixel, + ) + + # These update various properties. + def get_xalign(self): + return self.xpos + + def set_xalign(self, v): + self.xpos = v + self.xanchor = v + + xalign = property(get_xalign, set_xalign) + + def get_yalign(self): + return self.ypos + + def set_yalign(self, v): + self.ypos = v + self.yanchor = v + + yalign = property(get_yalign, set_yalign) + + def get_around(self): + return (self.xaround, self.yaround) + + def set_around(self, value): + self.xaround, self.yaround = value + self.xanchoraround, self.yanchoraround = None, None + + def set_alignaround(self, value): + self.xaround, self.yaround = value + self.xanchoraround, self.yanchoraround = value + + around = property(get_around, set_around) + alignaround = property(get_around, set_alignaround) + + def get_angle(self): + xpos = first_not_none(self.xpos, self.default_xpos, 0) + ypos = first_not_none(self.ypos, self.default_ypos, 0) + angle, _radius = cartesian_to_polar(xpos, ypos, self.xaround, self.yaround) + return angle + + def get_radius(self): + xpos = first_not_none(self.xpos, self.default_xpos, 0) + ypos = first_not_none(self.ypos, self.default_ypos, 0) + _angle, radius = cartesian_to_polar(xpos, ypos, self.xaround, self.yaround) + return radius + + def set_angle(self, value): + xpos = first_not_none(self.xpos, self.default_xpos, 0) + ypos = first_not_none(self.ypos, self.default_ypos, 0) + _angle, radius = cartesian_to_polar(xpos, ypos, self.xaround, self.yaround) + angle = value + self.xpos, self.ypos = polar_to_cartesian(angle, radius, self.xaround, self.yaround) + + if self.xanchoraround: + self.xanchor, self.yanchor = polar_to_cartesian(angle, radius, self.xaround, self.yaround) + + def set_radius(self, value): + xpos = first_not_none(self.xpos, self.default_xpos, 0) + ypos = first_not_none(self.ypos, self.default_ypos, 0) + angle, _radius = cartesian_to_polar(xpos, ypos, self.xaround, self.yaround) + radius = value + self.xpos, self.ypos = polar_to_cartesian(angle, radius, self.xaround, self.yaround) + + if self.xanchoraround: + self.xanchor, self.yanchor = polar_to_cartesian(angle, radius, self.xaround, self.yaround) + + angle = property(get_angle, set_angle) + radius = property(get_radius, set_radius) + + def get_pos(self): + return self.xpos, self.ypos + + def set_pos(self, value): + self.xpos, self.ypos = value + + pos = property(get_pos, set_pos) + + def get_anchor(self): + return self.xanchor, self.yanchor + + def set_anchor(self, value): + self.xanchor, self.yanchor = value + + anchor = property(get_anchor, set_anchor) + + def get_align(self): + return self.xpos, self.ypos + + def set_align(self, value): + self.xanchor, self.yanchor = value + self.xpos, self.ypos = value + + align = property(get_align, set_align) + + def get_offset(self): + return self.xoffset, self.yoffset + + def set_offset(self, value): + self.xoffset, self.yoffset = value + + offset = property(get_offset, set_offset) + + def set_xcenter(self, value): + self.xpos = value + self.xanchor = 0.5 + + def get_xcenter(self): + return self.xpos + + def set_ycenter(self, value): + self.ypos = value + self.yanchor = 0.5 + + def get_ycenter(self): + return self.ypos + + xcenter = property(get_xcenter, set_xcenter) + ycenter = property(get_ycenter, set_ycenter) + +class Proxy(object): + """ + This class proxies a field from the transform to its state. + """ + + def __init__(self, name): + self.name = name + + def __get__(self, instance, owner): + return getattr(instance.state, self.name) + + def __set__(self, instance, value): + return setattr(instance.state, self.name, value) + +class Transform(Container): + """ + Documented in sphinx, because we can't scan this object. + """ + + __version__ = 5 + transform_event_responder = True + + # Proxying things over to our state. + alpha = Proxy("alpha") + rotate = Proxy("rotate") + rotate_pad = Proxy("rotate_pad") + transform_anchor = Proxy("rotate_pad") + zoom = Proxy("zoom") + xzoom = Proxy("xzoom") + yzoom = Proxy("yzoom") + + xpos = Proxy("xpos") + ypos = Proxy("ypos") + xanchor = Proxy("xanchor") + yanchor = Proxy("yanchor") + + xalign = Proxy("xalign") + yalign = Proxy("yalign") + + around = Proxy("around") + alignaround = Proxy("alignaround") + angle = Proxy("angle") + radius = Proxy("radius") + + xaround = Proxy("xaround") + yaround = Proxy("yaround") + xanchoraround = Proxy("xanchoraround") + yanchoraround = Proxy("yanchoraround") + + pos = Proxy("pos") + anchor = Proxy("anchor") + align = Proxy("align") + + crop = Proxy("crop") + corner1 = Proxy("corner1") + corner2 = Proxy("corner2") + size = Proxy("size") + + delay = Proxy("delay") + + xoffset = Proxy("xoffset") + yoffset = Proxy("yoffset") + offset = Proxy("offset") + + subpixel = Proxy("subpixel") + + xcenter = Proxy("xcenter") + ycenter = Proxy("ycenter") + + def after_upgrade(self, version): + + if version < 1: + self.active = False + self.state = TransformState() + + self.state.xpos = self.xpos or 0 + self.state.ypos = self.ypos or 0 + self.state.xanchor = self.xanchor or 0 + self.state.yanchor = self.yanchor or 0 + self.state.alpha = self.alpha + self.state.rotate = self.rotate + self.state.zoom = self.zoom + self.state.xzoom = self.xzoom + self.state.yzoom = self.yzoom + + self.hide_request = False + self.hide_response = True + + if version < 2: + self.st = 0 + self.at = 0 + + if version < 3: + self.st_offset = 0 + self.at_offset = 0 + self.child_st_base = 0 + + if version < 4: + self.style_arg = 'transform' + + if version < 5: + self.replaced_request = False + self.replaced_response = True + + DEFAULT_ARGUMENTS = { + "selected_activate" : { }, + "selected_hover" : { }, + "selected_idle" : { }, + "selected_insensitive" : { }, + "activate" : { }, + "hover" : { }, + "idle" : { }, + "insensitive" : { }, + "" : { }, + } + + # Compatibility with old versions of the class. + active = False + children = False + arguments = DEFAULT_ARGUMENTS + + def __init__(self, + child=None, + function=None, + + style='transform', + focus=None, + default=False, + + **kwargs): + + self.kwargs = kwargs + self.style_arg = style + + super(Transform, self).__init__(style=style, focus=focus, default=default) + + self.function = function + + child = renpy.easy.displayable_or_none(child) + if child is not None: + self.add(child) + + self.state = TransformState() + + self.arguments = dict((k, {}) for k in self.DEFAULT_ARGUMENTS) + + # Split up the keyword arguments. + for k, v in kwargs.items(): + if "_" in k: + prefix, prop = k.rsplit("_", 1) + else: + prefix = "" + prop = k + + if prefix not in self.arguments: + raise Exception("Unknown transform property prefix: %r" % prefix) + + if prop not in renpy.atl.PROPERTIES: + raise Exception("Unknown transform property: %r") + + self.arguments[prefix][prop] = v + + + # Apply the keyword arguments. + for k, v in kwargs.items(): + setattr(self.state, k, v) + + # This is the matrix transforming our coordinates into child coordinates. + self.forward = None + + # Have we called the function at least once? + self.active = False + + # Have we been requested to hide? + self.hide_request = False + + # True if it's okay for us to hide. + self.hide_response = True + + # Have we been requested to replaced? + self.replaced_request = False + + # True if it's okay for us to replaced. + self.replaced_response = True + + self.st = 0 + self.at = 0 + self.st_offset = 0 + self.at_offset = 0 + + self.child_st_base = 0 + + def visit(self): + if self.child is None: + return [ ] + else: + return [ self.child ] + + # The default function chooses entries from self.arguments that match + # the style prefix, and applies them to the state. + def default_function(self, state, st, at): + + prefix = self.style.prefix.strip("_") + prefixes = [ ] + + while prefix: + prefixes.insert(0, prefix) + _, _, prefix = prefix.partition("_") + + prefixes.insert(0, "") + + for i in prefixes: + for k, v in self.arguments[i].items(): + setattr(state, k, v) + + return None + + def set_transform_event(self, event): + if self.child is not None: + self.child.set_transform_event(event) + + super(Transform, self).set_transform_event(event) + + + def take_state(self, t): + """ + Takes the transformation state from object t into this object. + """ + + self.state.take_state(t.state) + + # The arguments will be applied when the default function is + # called. + + + def take_execution_state(self, t): + """ + Takes the execution state from object t into this object. This is + overridden by renpy.atl.TransformBase. + """ + + self.hide_request = t.hide_request + self.replaced_request = t.replaced_request + + self.state.xpos = t.state.xpos + self.state.ypos = t.state.ypos + self.state.xanchor = t.state.xanchor + self.state.yanchor = t.state.yanchor + + if isinstance(self.child, Transform) and isinstance(t.child, Transform): + self.child.take_execution_state(t.child) + + + def copy(self): + """ + Makes a copy of this transform. + """ + + d = self() + d.kwargs = { } + d.take_state(self) + d.take_execution_state(self) + d.st = self.st + d.at = self.at + + return d + + def _change_transform_child(self, child): + rv = self.copy() + + if self.child is not None: + rv.set_child(self.child._change_transform_child(child)) + + return rv + + def _hide(self, st, at, kind): + + if not self.child: + return None + + if not (self.hide_request or self.replaced_request): + d = self.copy() + else: + d = self + + d.st_offset = self.st_offset + d.at_offset = self.at_offset + + if kind == "hide": + d.hide_request = True + else: + d.replaced_request = True + + d.hide_response = True + d.replaced_response = True + + if d.function is not None: + d.function(d, st + d.st_offset, at + d.at_offset) + + new_child = d.child._hide(st, at, kind) + + if new_child is not None: + d.child = new_child + d.hide_response = False + d.replaced_response = False + + if (not d.hide_response) or (not d.replaced_response): + renpy.display.render.redraw(d, 0) + return d + + return None + + def set_child(self, child): + + child = renpy.easy.displayable(child) + + self.child = child + self.child_st_base = self.st + + child.per_interact() + + renpy.display.render.redraw(self, 0) + + def update_state(self): + """ + This updates the state to that at self.st, self.at. + """ + + # If we have to, call the function that updates this transform. + if self.function is not None: + fr = self.function(self, self.st, self.at) + else: + fr = self.default_function(self, self.st, self.at) + + # Order a redraw, if necessary. + if fr is not None: + renpy.display.render.redraw(self, fr) + + state = self.state + + self.active = True + + # Use non-None elements of the child placement as defaults. + child = self.child + if child is not None and renpy.config.transform_uses_child_position: + + pos = child.get_placement() + + if pos[0] is not None: + state.default_xpos = pos[0] + if pos[2] is not None: + state.default_xanchor = pos[2] + if pos[1] is not None: + state.default_ypos = pos[1] + if pos[3] is not None: + state.default_yanchor = pos[3] + + state.subpixel |= pos[6] + + # The render method is now defined in accelerator.pyx. + + def event(self, ev, x, y, st): + + if self.hide_request: + return None + + children = self.children + offsets = self.offsets + + if not offsets: + return None + + for i in range(len(self.children)-1, -1, -1): + + d = children[i] + xo, yo = offsets[i] + + cx = x - xo + cy = y - yo + + # Transform screen coordinates to child coordinates. + cx, cy = self.forward.transform(cx, cy) + + rv = d.event(ev, cx, cy, st) + if rv is not None: + return rv + + return None + + def __call__(self, child=None, take_state=True): + + if child is None: + child = self.child + + # If we don't have a child for some reason, set it to null. + if child is None: + child = get_null() + + rv = Transform( + child=child, + function=self.function, + style=self.style_arg, + **self.kwargs) + + rv.take_state(self) + + return rv + + def get_placement(self): + + if not self.active: + self.update_state() + + if self.child is not None: + _cxpos, _cypos, _cxanchor, _cyanchor, cxoffset, cyoffset, _csubpixel = self.child.get_placement() + else: + cxoffset = 0 + cyoffset = 0 + + cxoffset = cxoffset or 0 + cyoffset = cyoffset or 0 + + rv = self.state.get_placement(cxoffset, cyoffset) + + if self.state.transform_anchor: + + xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = rv + if (xanchor is not None) and (yanchor is not None): + + cw, ch = self.child_size + rw, rh = self.render_size + + if isinstance(xanchor, float): + xanchor *= cw + if isinstance(yanchor, float): + yanchor *= ch + + xanchor -= cw / 2.0 + yanchor -= ch / 2.0 + + xanchor, yanchor = self.reverse.transform(xanchor, yanchor) + + xanchor += rw / 2.0 + yanchor += rh / 2.0 + + xanchor = renpy.display.core.absolute(xanchor) + yanchor = renpy.display.core.absolute(yanchor) + + rv = (xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel) + + return rv + + def update(self): + """ + This should be called when a transform property field is updated outside + of the callback method, to ensure that the change takes effect. + """ + + renpy.display.render.invalidate(self) + + def parameterize(self, name, parameters): + if parameters: + raise Exception("Image '%s' can't take parameters '%s'. (Perhaps you got the name wrong?)" % + (' '.join(name), ' '.join(parameters))) + + # Note the call here. + return self() + + def _show(self): + self.update_state() + +Transform.render = types.MethodType(renpy.display.accelerator.transform_render, None, Transform) + +class ATLTransform(renpy.atl.ATLTransformBase, Transform): + + def __init__(self, atl, child=None, context={}, parameters=None, **properties): + renpy.atl.ATLTransformBase.__init__(self, atl, context, parameters) + Transform.__init__(self, child=child, function=self.execute, **properties) + + self.raw_child = self.child + + def _show(self): + super(ATLTransform, self)._show() + self.execute(self, self.st, self.at) + + +class Motion(Container): + """ + This is used to move a child displayable around the screen. It + works by supplying a time value to a user-supplied function, + which is in turn expected to return a pair giving the x and y + location of the upper-left-hand corner of the child, or a + 4-tuple giving that and the xanchor and yanchor of the child. + + The time value is a floating point number that ranges from 0 to + 1. If repeat is True, then the motion repeats every period + sections. (Otherwise, it stops.) If bounce is true, the + time value varies from 0 to 1 to 0 again. + + The function supplied needs to be pickleable, which means it needs + to be defined as a name in an init block. It cannot be a lambda or + anonymous inner function. If you can get away with using Pan or + Move, use them instead. + + Please note that floats and ints are interpreted as for xpos and + ypos, with floats being considered fractions of the screen. + """ + + def __init__(self, function, period, child=None, new_widget=None, old_widget=None, repeat=False, bounce=False, delay=None, anim_timebase=False, tag_start=None, time_warp=None, add_sizes=False, style='motion', **properties): + """ + @param child: The child displayable. + + @param new_widget: If child is None, it is set to new_widget, + so that we can speak the transition protocol. + + @param old_widget: Ignored, for compatibility with the transition protocol. + + @param function: A function that takes a floating point value and returns + an xpos, ypos tuple. + + @param period: The amount of time it takes to go through one cycle, in seconds. + + @param repeat: Should we repeat after a period is up? + + @param bounce: Should we bounce? + + @param delay: How long this motion should take. If repeat is None, defaults to period. + + @param anim_timebase: If True, use the animation timebase rather than the shown timebase. + + @param time_warp: If not None, this is a function that takes a + fraction of the period (between 0.0 and 1.0), and returns a + new fraction of the period. Use this to warp time, applying + acceleration and deceleration to motions. + + This can also be used as a transition. When used as a + transition, the motion is applied to the new_widget for delay + seconds. + """ + + if child is None: + child = new_widget + + if delay is None and not repeat: + delay = period + + super(Motion, self).__init__(style=style, **properties) + + if child is not None: + self.add(child) + + self.function = function + self.period = period + self.repeat = repeat + self.bounce = bounce + self.delay = delay + self.anim_timebase = anim_timebase + self.time_warp = time_warp + self.add_sizes = add_sizes + + self.position = None + + + def get_placement(self): + + if self.position is None: + return super(Motion, self).get_placement() + else: + return self.position + (self.style.xoffset, self.style.yoffset, self.style.subpixel) + + def render(self, width, height, st, at): + + if self.anim_timebase: + t = at + else: + t = st + + if renpy.game.less_updates: + if self.delay: + t = self.delay + if self.repeat: + t = t % self.period + else: + t = self.period + elif self.delay and t >= self.delay: + t = self.delay + if self.repeat: + t = t % self.period + elif self.repeat: + t = t % self.period + renpy.display.render.redraw(self, 0) + else: + if t > self.period: + t = self.period + else: + renpy.display.render.redraw(self, 0) + + if self.period > 0: + t /= self.period + else: + t = 1 + + if self.time_warp: + t = self.time_warp(t) + + if self.bounce: + t = t * 2 + if t > 1.0: + t = 2.0 - t + + child = render(self.child, width, height, st, at) + cw, ch = child.get_size() + + if self.add_sizes: + res = self.function(t, (width, height, cw, ch)) + else: + res = self.function(t) + + res = tuple(res) + + if len(res) == 2: + self.position = res + (self.style.xanchor, self.style.yanchor) + else: + self.position = res + + rv = renpy.display.render.Render(cw, ch) + rv.blit(child, (0, 0)) + + self.offsets = [ (0, 0) ] + + return rv + + +class Interpolate(object): + + anchors = { + 'top' : 0.0, + 'center' : 0.5, + 'bottom' : 1.0, + 'left' : 0.0, + 'right' : 1.0, + } + + def __init__(self, start, end): + + if len(start) != len(end): + raise Exception("The start and end must have the same number of arguments.") + + self.start = [ self.anchors.get(i, i) for i in start ] + self.end = [ self.anchors.get(i, i) for i in end ] + + def __call__(self, t, sizes=(None, None, None, None)): + + def interp(a, b, c): + + if c is not None: + if type(a) is float: + a = a * c + if type(b) is float: + b = b * c + + rv = a + t * (b - a) + + return renpy.display.core.absolute(rv) + + return [ interp(a, b, c) for a, b, c in zip(self.start, self.end, sizes) ] + + +def Pan(startpos, endpos, time, child=None, repeat=False, bounce=False, + anim_timebase=False, style='motion', time_warp=None, **properties): + """ + This is used to pan over a child displayable, which is almost + always an image. It works by interpolating the placement of the + upper-left corner of the screen, over time. It's only really + suitable for use with images that are larger than the screen, + and we don't do any cropping on the image. + + @param startpos: The initial coordinates of the upper-left + corner of the screen, relative to the image. + + @param endpos: The coordinates of the upper-left corner of the + screen, relative to the image, after time has elapsed. + + @param time: The time it takes to pan from startpos to endpos. + + @param child: The child displayable. + + @param repeat: True if we should repeat this forever. + + @param bounce: True if we should bounce from the start to the end + to the start. + + @param anim_timebase: True if we use the animation timebase, False to use the + displayable timebase. + + @param time_warp: If not None, this is a function that takes a + fraction of the period (between 0.0 and 1.0), and returns a + new fraction of the period. Use this to warp time, applying + acceleration and deceleration to motions. + + This can be used as a transition. See Motion for details. + """ + + x0, y0 = startpos + x1, y1 = endpos + + return Motion(Interpolate((-x0, -y0), (-x1, -y1)), + time, + child, + repeat=repeat, + bounce=bounce, + style=style, + anim_timebase=anim_timebase, + time_warp=time_warp, + add_sizes=True, + **properties) + +def Move(startpos, endpos, time, child=None, repeat=False, bounce=False, + anim_timebase=False, style='motion', time_warp=None, **properties): + """ + This is used to pan over a child displayable relative to + the containing area. It works by interpolating the placement of the + the child, over time. + + @param startpos: The initial coordinates of the child + relative to the containing area. + + @param endpos: The coordinates of the child at the end of the + move. + + @param time: The time it takes to move from startpos to endpos. + + @param child: The child displayable. + + @param repeat: True if we should repeat this forever. + + @param bounce: True if we should bounce from the start to the end + to the start. + + @param anim_timebase: True if we use the animation timebase, False to use the + displayable timebase. + + @param time_warp: If not None, this is a function that takes a + fraction of the period (between 0.0 and 1.0), and returns a + new fraction of the period. Use this to warp time, applying + acceleration and deceleration to motions. + + This can be used as a transition. See Motion for details. + """ + + return Motion(Interpolate(startpos, endpos), + time, + child, + repeat=repeat, + bounce=bounce, + anim_timebase=anim_timebase, + style=style, + time_warp=time_warp, + add_sizes=True, + **properties) + + +class Revolver(object): + + def __init__(self, start, end, child, around=(0.5, 0.5), cor=(0.5, 0.5), pos=None): + self.start = start + self.end = end + self.around = around + self.cor = cor + self.pos = pos + self.child = child + + def __call__(self, t, xxx_todo_changeme): + + # Converts a float to an integer in the given range, passes + # integers through unchanged. + (w, h, cw, ch) = xxx_todo_changeme + def fti(x, r): + if x is None: + x = 0 + + if isinstance(x, float): + return int(x * r) + else: + return x + + if self.pos is None: + pos = self.child.get_placement() + else: + pos = self.pos + + xpos, ypos, xanchor, yanchor, _xoffset, _yoffset, _subpixel = pos + + xpos = fti(xpos, w) + ypos = fti(ypos, h) + xanchor = fti(xanchor, cw) + yanchor = fti(yanchor, ch) + + xaround, yaround = self.around + + xaround = fti(xaround, w) + yaround = fti(yaround, h) + + xcor, ycor = self.cor + + xcor = fti(xcor, cw) + ycor = fti(ycor, ch) + + angle = self.start + (self.end - self.start) * t + angle *= math.pi / 180 + + # The center of rotation, relative to the xaround. + x = xpos - xanchor + xcor - xaround + y = ypos - yanchor + ycor - yaround + + # Rotate it. + nx = x * math.cos(angle) - y * math.sin(angle) + ny = x * math.sin(angle) + y * math.cos(angle) + + # Project it back. + nx = nx - xcor + xaround + ny = ny - ycor + yaround + + return (renpy.display.core.absolute(nx), renpy.display.core.absolute(ny), 0, 0) + + +def Revolve(start, end, time, child, around=(0.5, 0.5), cor=(0.5, 0.5), pos=None, **properties): + + return Motion(Revolver(start, end, child, around=around, cor=cor, pos=pos), + time, + child, + add_sizes=True, + **properties) + + + +def zoom_render(crend, x, y, w, h, zw, zh, bilinear): + """ + This creates a render that zooms its child. + + `crend` - The render of the child. + `x`, `y`, `w`, `h` - A rectangle inside the child. + `zw`, `zh` - The size the rectangle is rendered to. + `bilinear` - Should we be rendering in bilinear mode? + """ + + rv = renpy.display.render.Render(zw, zh) + + if zw == 0 or zh == 0 or w == 0 or h == 0: + return rv + + + rv.forward = renpy.display.render.Matrix2D(w / zw, 0, 0, h / zh) + rv.reverse = renpy.display.render.Matrix2D(zw / w, 0, 0, zh / h) + + rv.clipping = True + + rv.blit(crend, rv.reverse.transform(-x, -y)) + + return rv + + +class ZoomCommon(renpy.display.core.Displayable): + def __init__(self, + time, child, + end_identity=False, + after_child=None, + time_warp=None, + bilinear=True, + opaque=True, + anim_timebase=False, + repeat=False, + style='motion', + **properties): + """ + @param time: The amount of time it will take to + interpolate from the start to the end rectange. + + @param child: The child displayable. + + @param after_child: If present, a second child + widget. This displayable will be rendered after the zoom + completes. Use this to snap to a sharp displayable after + the zoom is done. + + @param time_warp: If not None, this is a function that takes a + fraction of the period (between 0.0 and 1.0), and returns a + new fraction of the period. Use this to warp time, applying + acceleration and deceleration to motions. + """ + + super(ZoomCommon, self).__init__(style=style, **properties) + + child = renpy.easy.displayable(child) + + self.time = time + self.child = child + self.repeat = repeat + + if after_child: + self.after_child = renpy.easy.displayable(after_child) + else: + if end_identity: + self.after_child = child + else: + self.after_child = None + + self.time_warp = time_warp + self.bilinear = bilinear + self.opaque = opaque + self.anim_timebase = anim_timebase + + + def visit(self): + return [ self.child, self.after_child ] + + def render(self, width, height, st, at): + + if self.anim_timebase: + t = at + else: + t = st + + if self.time: + done = min(t / self.time, 1.0) + else: + done = 1.0 + + if self.repeat: + done = done % 1.0 + + if renpy.game.less_updates: + done = 1.0 + + self.done = done + + if self.after_child and done == 1.0: + return renpy.display.render.render(self.after_child, width, height, st, at) + + if self.time_warp: + done = self.time_warp(done) + + rend = renpy.display.render.render(self.child, width, height, st, at) + + rx, ry, rw, rh, zw, zh = self.zoom_rectangle(done, rend.width, rend.height) + + if rx < 0 or ry < 0 or rx + rw > rend.width or ry + rh > rend.height: + raise Exception("Zoom rectangle %r falls outside of %dx%d parent surface." % ((rx, ry, rw, rh), rend.width, rend.height)) + + rv = zoom_render(rend, rx, ry, rw, rh, zw, zh, self.bilinear) + + if self.done < 1.0: + renpy.display.render.redraw(self, 0) + + return rv + + def event(self, ev, x, y, st): + + if not self.time: + done = 1.0 + else: + done = min(st / self.time, 1.0) + + if done == 1.0 and self.after_child: + return self.after_child.event(ev, x, y, st) + else: + return None + + +class Zoom(ZoomCommon): + + def __init__(self, size, start, end, time, child, **properties): + + end_identity = (end == (0.0, 0.0) + size) + + super(Zoom, self).__init__(time, child, end_identity=end_identity, **properties) + + self.size = size + self.start = start + self.end = end + + def zoom_rectangle(self, done, width, height): + + rx, ry, rw, rh = [ (a + (b - a) * done) for a, b in zip(self.start, self.end) ] + + return rx, ry, rw, rh, self.size[0], self.size[1] + + +class FactorZoom(ZoomCommon): + + def __init__(self, start, end, time, child, **properties): + + end_identity = (end == 1.0) + + super(FactorZoom, self).__init__(time, child, end_identity=end_identity, **properties) + + self.start = start + self.end = end + + def zoom_rectangle(self, done, width, height): + + factor = self.start + (self.end - self.start) * done + + return 0, 0, width, height, factor * width, factor * height + + + +class SizeZoom(ZoomCommon): + + def __init__(self, start, end, time, child, **properties): + + end_identity = False + + super(SizeZoom, self).__init__(time, child, end_identity=end_identity, **properties) + + self.start = start + self.end = end + + def zoom_rectangle(self, done, width, height): + + sw, sh = self.start + ew, eh = self.end + + zw = sw + (ew - sw) * done + zh = sh + (eh - sh) * done + + return 0, 0, width, height, zw, zh + + +class RotoZoom(renpy.display.core.Displayable): + + transform = None + + def __init__(self, + rot_start, + rot_end, + rot_delay, + zoom_start, + zoom_end, + zoom_delay, + child, + rot_repeat=False, + zoom_repeat=False, + rot_bounce=False, + zoom_bounce=False, + rot_anim_timebase=False, + zoom_anim_timebase=False, + rot_time_warp=None, + zoom_time_warp=None, + opaque=False, + style='motion', + **properties): + + super(RotoZoom, self).__init__(style=style, **properties) + + self.rot_start = rot_start + self.rot_end = rot_end + self.rot_delay = rot_delay + + self.zoom_start = zoom_start + self.zoom_end = zoom_end + self.zoom_delay = zoom_delay + + self.child = renpy.easy.displayable(child) + + self.rot_repeat = rot_repeat + self.zoom_repeat = zoom_repeat + + self.rot_bounce = rot_bounce + self.zoom_bounce = zoom_bounce + + self.rot_anim_timebase = rot_anim_timebase + self.zoom_anim_timebase = zoom_anim_timebase + + self.rot_time_warp = rot_time_warp + self.zoom_time_warp = zoom_time_warp + + self.opaque = opaque + + + def visit(self): + return [ self.child ] + + + def render(self, width, height, st, at): + + if self.rot_anim_timebase: + rot_time = at + else: + rot_time = st + + if self.zoom_anim_timebase: + zoom_time = at + else: + zoom_time = st + + if self.rot_delay == 0: + rot_time = 1.0 + else: + rot_time /= self.rot_delay + + if self.zoom_delay == 0: + zoom_time = 1.0 + else: + zoom_time /= self.zoom_delay + + if self.rot_repeat: + rot_time %= 1.0 + + if self.zoom_repeat: + zoom_time %= 1.0 + + if self.rot_bounce: + rot_time *= 2 + rot_time = min(rot_time, 2.0 - rot_time) + + if self.zoom_bounce: + zoom_time *= 2 + zoom_time = min(zoom_time, 2.0 - zoom_time) + + if renpy.game.less_updates: + rot_time = 1.0 + zoom_time = 1.0 + + rot_time = min(rot_time, 1.0) + zoom_time = min(zoom_time, 1.0) + + if self.rot_time_warp: + rot_time = self.rot_time_warp(rot_time) + + if self.zoom_time_warp: + zoom_time = self.zoom_time_warp(zoom_time) + + + angle = self.rot_start + (1.0 * self.rot_end - self.rot_start) * rot_time + zoom = self.zoom_start + (1.0 * self.zoom_end - self.zoom_start) * zoom_time + # angle = -angle * math.pi / 180 + + zoom = max(zoom, 0.001) + + if self.transform is None: + self.transform = Transform(self.child) + + self.transform.rotate = angle + self.transform.zoom = zoom + + rv = renpy.display.render.render(self.transform, width, height, st, at) + + if rot_time <= 1.0 or zoom_time <= 1.0: + renpy.display.render.redraw(self.transform, 0) + + return rv + + +# For compatibility with old games. +renpy.display.layout.Transform = Transform +renpy.display.layout.RotoZoom = RotoZoom +renpy.display.layout.SizeZoom = SizeZoom +renpy.display.layout.FactorZoom = FactorZoom +renpy.display.layout.Zoom = Zoom +renpy.display.layout.Revolver = Revolver +renpy.display.layout.Motion = Motion +renpy.display.layout.Interpolate = Interpolate + +# Leave these functions around - they might have been pickled somewhere. +renpy.display.layout.Revolve = Revolve # function +renpy.display.layout.Move = Move # function +renpy.display.layout.Pan = Pan # function diff --git a/unrpyc/renpy/display/movetransition.py b/unrpyc/renpy/display/movetransition.py new file mode 100644 index 0000000..1b3bdd7 --- /dev/null +++ b/unrpyc/renpy/display/movetransition.py @@ -0,0 +1,640 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# NOTE: +# Transitions need to be able to work even when old_widget and new_widget +# are None, at least to the point of making it through __init__. This is +# so that prediction of images works. + +import renpy.display + +# Utility function used by MoveTransition et al. +def position(d): + + xpos, ypos, xanchor, yanchor, _xoffset, _yoffset, _subpixel = d.get_placement() + + if xpos is None: + xpos = 0 + if ypos is None: + ypos = 0 + if xanchor is None: + xanchor = 0 + if yanchor is None: + yanchor = 0 + + return xpos, ypos, xanchor, yanchor + +def offsets(d): + + _xpos, _ypos, _xanchor, _yanchor, xoffset, yoffset, _subpixel = d.get_placement() + + if renpy.config.movetransition_respects_offsets: + return { 'xoffset' : xoffset, 'yoffset' : yoffset } + else: + return { } + + +# These are used by MoveTransition. +def MoveFactory(pos1, pos2, delay, d, **kwargs): + if pos1 == pos2: + return d + + return renpy.display.motion.Move(pos1, pos2, delay, d, **kwargs) + +def default_enter_factory(pos, delay, d, **kwargs): + return d + +def default_leave_factory(pos, delay, d, **kwargs): + return None + +# These can be used to move things in and out of the screen. +def MoveIn(pos, pos1, delay, d, **kwargs): + + def aorb(a, b): + if a is None: + return b + return a + + pos = tuple([aorb(a, b) for a, b in zip(pos, pos1)]) + return renpy.display.motion.Move(pos, pos1, delay, d, **kwargs) + +def MoveOut(pos, pos1, delay, d, **kwargs): + + def aorb(a, b): + if a is None: + return b + return a + + pos = tuple([aorb(a, b) for a, b in zip(pos, pos1)]) + return renpy.display.motion.Move(pos1, pos, delay, d, **kwargs) + +def ZoomInOut(start, end, pos, delay, d, **kwargs): + + xpos, ypos, xanchor, yanchor = pos + + FactorZoom = renpy.display.motion.FactorZoom + + if end == 1.0: + return FactorZoom(start, end, delay, d, after_child=d, opaque=False, + xpos=xpos, ypos=ypos, xanchor=xanchor, yanchor=yanchor, **kwargs) + else: + return FactorZoom(start, end, delay, d, opaque=False, + xpos=xpos, ypos=ypos, xanchor=xanchor, yanchor=yanchor, **kwargs) + +def RevolveInOut(start, end, pos, delay, d, **kwargs): + return renpy.display.motion.Revolve(start, end, delay, d, pos=pos, **kwargs) + + +def OldMoveTransition(delay, old_widget=None, new_widget=None, factory=None, enter_factory=None, leave_factory=None, old=False, layers=[ 'master' ]): + """ + Returns a transition that attempts to find images that have changed + position, and moves them from the old position to the new transition, taking + delay seconds to complete the move. + + If `factory` is given, it is expected to be a function that takes as + arguments: an old position, a new position, the delay, and a + displayable, and to return a displayable as an argument. If not + given, the default behavior is to move the displayable from the + starting to the ending positions. Positions are always given as + (xpos, ypos, xanchor, yanchor) tuples. + + If `enter_factory` or `leave_factory` are given, they are expected + to be functions that take as arguments a position, a delay, and a + displayable, and return a displayable. They are applied to + displayables that are entering or leaving the scene, + respectively. The default is to show in place displayables that + are entering, and not to show those that are leaving. + + If `old` is True, then factory moves the old displayable with the + given tag. Otherwise, it moves the new displayable with that + tag. + + `layers` is a list of layers that the transition will be applied + to. + + Images are considered to be the same if they have the same tag, in + the same way that the tag is used to determine which image to + replace or to hide. They are also considered to be the same if + they have no tag, but use the same displayable. + + Computing the order in which images are displayed is a three-step + process. The first step is to create a list of images that + preserves the relative ordering of entering and moving images. The + second step is to insert the leaving images such that each leaving + image is at the lowest position that is still above all images + that were below it in the original scene. Finally, the list + is sorted by zorder, to ensure no zorder violations occur. + + If you use this transition to slide an image off the side of the + screen, remember to hide it when you are done. (Or just use + a leave_factory.) + """ + + if factory is None: + factory = MoveFactory + + if enter_factory is None: + enter_factory = default_enter_factory + + if leave_factory is None: + leave_factory = default_leave_factory + + use_old = old + + def merge_slide(old, new): + + # If new does not have .layers or .scene_list, then we simply + # insert a move from the old position to the new position, if + # a move occured. + + if (not isinstance(new, renpy.display.layout.MultiBox) + or (new.layers is None and new.layer_name is None)): + + if use_old: + child = old + else: + child = new + + old_pos = position(old) + new_pos = position(new) + + if old_pos != new_pos: + return factory(old_pos, + new_pos, + delay, + child, + **offsets(child) + ) + + else: + return child + + # If we're in the layers_root widget, merge the child widgets + # for each layer. + if new.layers: + + assert old.layers + + rv = renpy.display.layout.MultiBox(layout='fixed') + rv.layers = { } + + for layer in renpy.config.layers: + + f = new.layers[layer] + + if (isinstance(f, renpy.display.layout.MultiBox) + and layer in layers + and f.scene_list is not None): + + f = merge_slide(old.layers[layer], new.layers[layer]) + + rv.layers[layer] = f + rv.add(f) + + return rv + + # Otherwise, we recompute the scene list for the two widgets, merging + # as appropriate. + + # Wraps the displayable found in SLE so that the various timebases + # are maintained. + def wrap(sle): + return renpy.display.layout.AdjustTimes(sle.displayable, sle.show_time, sle.animation_time) + + def tag(sle): + return sle.tag or sle.displayable + + def merge(sle, d): + rv = sle.copy() + rv.show_time = 0 + rv.displayable = d + return rv + + def entering(sle): + new_d = wrap(new_sle) + move = enter_factory(position(new_d), delay, new_d, **offsets(new_d)) + + if move is None: + return + + rv_sl.append(merge(new_sle, move)) + + def leaving(sle): + old_d = wrap(sle) + move = leave_factory(position(old_d), delay, old_d, **offsets(old_d)) + + if move is None: + return + + move = renpy.display.layout.IgnoresEvents(move) + rv_sl.append(merge(old_sle, move)) + + + def moving(old_sle, new_sle): + old_d = wrap(old_sle) + new_d = wrap(new_sle) + + if use_old: + child = old_d + else: + child = new_d + + move = factory(position(old_d), position(new_d), delay, child, **offsets(child)) + if move is None: + return + + rv_sl.append(merge(new_sle, move)) + + + # The old, new, and merged scene_lists. + old_sl = old.scene_list[:] + new_sl = new.scene_list[:] + rv_sl = [ ] + + + # A list of tags in old_sl, new_sl, and rv_sl. + old_map = dict((tag(i), i) for i in old_sl if i is not None) + new_tags = set(tag(i) for i in new_sl if i is not None) + rv_tags = set() + + while old_sl or new_sl: + + # If we have something in old_sl, then + if old_sl: + + old_sle = old_sl[0] + old_tag = tag(old_sle) + + # If the old thing has already moved, then remove it. + if old_tag in rv_tags: + old_sl.pop(0) + continue + + # If the old thing does not match anything in new_tags, + # have it enter. + if old_tag not in new_tags: + leaving(old_sle) + rv_tags.add(old_tag) + old_sl.pop(0) + continue + + + # Otherwise, we must have something in new_sl. We want to + # either move it or have it enter. + + new_sle = new_sl.pop(0) + new_tag = tag(new_sle) + + # If it exists in both, move. + if new_tag in old_map: + old_sle = old_map[new_tag] + + moving(old_sle, new_sle) + rv_tags.add(new_tag) + continue + + else: + entering(new_sle) + rv_tags.add(new_tag) + continue + + # Sort everything by zorder, to ensure that there are no zorder + # violations in the result. + rv_sl.sort(key=lambda a : a.zorder) + + layer = new.layer_name + rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **renpy.game.interface.layer_properties[layer]) + rv.append_scene_list(rv_sl) + rv.layer_name = layer + + return rv + + + # This calls merge_slide to actually do the merging. + + rv = merge_slide(old_widget, new_widget) + rv.delay = delay # W0201 + + return rv + +############################################################################## +# New Move Transition (since 6.14) + + +class MoveInterpolate(renpy.display.core.Displayable): + """ + This displayable has two children. It interpolates between the positions + of its two children to place them on the screen. + """ + + def __init__(self, delay, old, new, use_old, time_warp): + super(MoveInterpolate, self).__init__() + + # The old and new displayables. + self.old = old + self.new = new + + # Should we display the old displayable? + self.use_old = False + + # Time warp function or None. + self.time_warp = time_warp + + # The width of the screen. + self.screen_width = 0 + self.screen_height = 0 + + # The width of the selected child. + self.child_width = 0 + self.child_height = 0 + + # The delay and st. + self.delay = delay + self.st = 0 + + def render(self, width, height, st, at): + self.screen_width = width + self.screen_height = height + + old_r = renpy.display.render.render(self.old, width, height, st, at) + new_r = renpy.display.render.render(self.new, width, height, st, at) + + if self.use_old: + cr = old_r + else: + cr = new_r + + self.child_width, self.child_height = cr.get_size() + self.st = st + + if self.st < self.delay: + renpy.display.render.redraw(self, 0) + + return cr + + def child_placement(self, child): + + def based(v, base): + if v is None: + return 0 + elif isinstance(v, int): + return v + elif isinstance(v, renpy.display.core.absolute): + return v + else: + return v * base + + xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = child.get_placement() + + xpos = based(xpos, self.screen_width) + ypos = based(ypos, self.screen_height) + xanchor = based(xanchor, self.child_width) + yanchor = based(yanchor, self.child_height) + + return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel + + def get_placement(self): + + if self.st > self.delay: + done = 1.0 + else: + done = self.st / self.delay + + if self.time_warp is not None: + done = self.time_warp(done) + + absolute = renpy.display.core.absolute + + def I(a, b): + return absolute(a + done * (b - a)) + + old_xpos, old_ypos, old_xanchor, old_yanchor, old_xoffset, old_yoffset, old_subpixel = self.child_placement(self.old) + new_xpos, new_ypos, new_xanchor, new_yanchor, new_xoffset, new_yoffset, new_subpixel = self.child_placement(self.new) + + xpos = I(old_xpos, new_xpos) + ypos = I(old_ypos, new_ypos) + xanchor = I(old_xanchor, new_xanchor) + yanchor = I(old_yanchor, new_yanchor) + xoffset = I(old_xoffset, new_xoffset) + yoffset = I(old_yoffset, new_yoffset) + subpixel = old_subpixel or new_subpixel + + return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel + + +def MoveTransition(delay, old_widget=None, new_widget=None, enter=None, leave=None, old=False, layers=[ 'master' ], time_warp=None, enter_time_warp=None, leave_time_warp=None): + """ + :doc: transition function + :args: (delay, enter=None, leave=None, old=False, layers=['master'], time_warp=None, enter_time_warp=None, leave_time_warp=None) + :name: MoveTransition + + Returns a transition that interpolates the position of images (with the + same tag) in the old and new scenes. + + `delay` + The time it takes for the interpolation to finish. + + `enter` + If not None, images entering the scene will also be moved. The value + of `enter` should be a transform that is applied to the image to + get its starting position. + + `leave` + If not None, images leaving the scene will also be move. The value + of `leave` should be a transform that is applied to the image to + get its ending position. + + `old` + If true, the old image will be used in preference to the new one. + + `layers` + A list of layers that moves are applied to. + + `time_warp` + A time warp function that's applied to the interpolation. This + takes a number between 0.0 and 1.0, and should return a number in + the same range. + + `enter_time_warp` + A time warp function that's applied to images entering the scene. + + `enter_time_warp` + A time warp function that's applied to images leaving the scene. + + """ + + use_old = old + + def merge_slide(old, new): + + # If new does not have .layers or .scene_list, then we simply + # insert a move from the old position to the new position, if + # a move occured. + + if (not isinstance(new, renpy.display.layout.MultiBox) + or (new.layers is None and new.layer_name is None)): + + if old is new: + return new + else: + return MoveInterpolate(delay, old, new, use_old, time_warp) + + + # If we're in the layers_root widget, merge the child widgets + # for each layer. + if new.layers: + + assert old.layers + + rv = renpy.display.layout.MultiBox(layout='fixed') + + for layer in renpy.config.layers: + + f = new.layers[layer] + + if (isinstance(f, renpy.display.layout.MultiBox) + and layer in layers + and f.scene_list is not None): + + f = merge_slide(old.layers[layer], new.layers[layer]) + + rv.add(f) + + return rv + + # Otherwise, we recompute the scene list for the two widgets, merging + # as appropriate. + + # Wraps the displayable found in SLE so that the various timebases + # are maintained. + def wrap(sle): + return renpy.display.layout.AdjustTimes(sle.displayable, sle.show_time, sle.animation_time) + + def tag(sle): + return sle.tag or sle.displayable + + def merge(sle, d): + rv = sle.copy() + rv.show_time = 0 + rv.displayable = d + return rv + + def entering(sle): + + if not enter: + return + + new_d = wrap(new_sle) + move = MoveInterpolate(delay, enter(new_d), new_d, False, enter_time_warp) + rv_sl.append(merge(new_sle, move)) + + def leaving(sle): + + if not leave: + return + + old_d = wrap(sle) + move = MoveInterpolate(delay, old_d, leave(old_d), True, leave_time_warp) + move = renpy.display.layout.IgnoresEvents(move) + rv_sl.append(merge(old_sle, move)) + + + def moving(old_sle, new_sle): + + if old_sle.displayable is new_sle.displayable: + rv_sl.append(new_sle) + return + + old_d = wrap(old_sle) + new_d = wrap(new_sle) + + move = MoveInterpolate(delay, old_d, new_d, use_old, time_warp) + + rv_sl.append(merge(new_sle, move)) + + + # The old, new, and merged scene_lists. + old_sl = old.scene_list[:] + new_sl = new.scene_list[:] + rv_sl = [ ] + + # A list of tags in old_sl, new_sl, and rv_sl. + old_map = dict((tag(i), i) for i in old_sl if i is not None) + new_tags = set(tag(i) for i in new_sl if i is not None) + rv_tags = set() + + while old_sl or new_sl: + + # If we have something in old_sl, then + if old_sl: + + old_sle = old_sl[0] + old_tag = tag(old_sle) + + # If the old thing has already moved, then remove it. + if old_tag in rv_tags: + old_sl.pop(0) + continue + + # If the old thing does not match anything in new_tags, + # have it enter. + if old_tag not in new_tags: + leaving(old_sle) + rv_tags.add(old_tag) + old_sl.pop(0) + continue + + + # Otherwise, we must have something in new_sl. We want to + # either move it or have it enter. + + new_sle = new_sl.pop(0) + new_tag = tag(new_sle) + + # If it exists in both, move. + if new_tag in old_map: + old_sle = old_map[new_tag] + + moving(old_sle, new_sle) + rv_tags.add(new_tag) + continue + + else: + entering(new_sle) + rv_tags.add(new_tag) + continue + + # Sort everything by zorder, to ensure that there are no zorder + # violations in the result. + rv_sl.sort(key=lambda a : a.zorder) + + layer = new.layer_name + rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **renpy.game.interface.layer_properties[layer]) + rv.append_scene_list(rv_sl) + + return rv + + # Call merge_slide to actually do the merging. + rv = merge_slide(old_widget, new_widget) + rv.delay = delay + + return rv + diff --git a/unrpyc/renpy/display/particle.py b/unrpyc/renpy/display/particle.py new file mode 100644 index 0000000..cf52417 --- /dev/null +++ b/unrpyc/renpy/display/particle.py @@ -0,0 +1,615 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This code supports sprite and particle animation. + +from renpy.display.render import render, BLIT + +import renpy.display +import random + + +class SpriteCache(renpy.object.Object): + """ + This stores information about a displayble, including the identity + of the displayable, and when it was first displayed. It is also + responsible for caching the displayable surface, so it doesn't + need to be re-rendered. + """ + + # Private Fields: + # + # child - The child displayable. + # + # st - The shown time when this was first displayed, or None if it hasn't + # been rendered. + # + # render - The render of child. + # + # If true, then the render is simple enough it can just be appended to + # the manager's render's children list. + +class Sprite(renpy.object.Object): + """ + :doc: sprites class + + This represents a sprite that is managed by the SpriteManager. It contains + fields that control the placement of the sprite on the screen. Sprites + should not be created directly. Instead, they should be created by + calling :meth:`SpriteManager.create`. + + The fields of a sprite object are: + + `x`, `y` + The x and y coordinates of the upper-left corner of the sprite, + relative to the SpriteManager. + + `zorder` + An integer that's used to control the order of this sprite in the + relative to the other sprites in the SpriteManager. The larger the + number is, the closer to the viewer the sprite is. + + `events` + If True, then events are passed to child. If False, the default, + the children igore events (and hence don't spend time processing + them). + + The methods of a Sprite object are: + """ + + # Fields: + # + # child - the displayable that is the child of this sprite. + # cache - the SpriteCache of child. + # live - True if this sprite is still alive. + # manager - A reference to the SpriteManager. + + def set_child(self, d): + """ + :doc: sprites method + + Changes the Displayable associated with this sprite to `d`. + """ + + id_d = id(d) + + sc = self.manager.displayable_map.get(id_d, None) + if sc is None: + d = renpy.easy.displayable(d) + + sc = SpriteCache() + sc.render = None + sc.child = d + sc.st = None + + self.manager.displayable_map[id_d] = sc + + self.cache = sc + + def destroy(self): + """ + :doc: sprites method + + Destroys this sprite, preventing it from being displayed and + removing it from the SpriteManager. + """ + + self.manager.dead_child = True + self.live = False + self.events = False + + + +class SpriteManager(renpy.display.core.Displayable): + """ + :doc: sprites class + + This displayable manages a collection of sprites, and displays + them at the fastest speed possible. + """ + + def __init__(self, update=None, event=None, predict=None, ignore_time=False, **properties): + """ + `update` + If not None, a function that is called each time a sprite + is rendered by this sprite manager. It is called with one + argument, the time in seconds since this sprite manager + was first displayed. It is expected to return the number + of seconds until the function is called again, and the + SpriteManager is rendered again. + + `event` + If not None, a function that is called when an event occurs. + It takes as arguments: + * A pygame event object. + * The x coordinate of the event. + * The y coordinate of the event. + * The time since the sprite manager was first shown. + If it returns a non-None value, the interaction ends, and + that value is returned. + + `predict` + If not None, a function that returns a list of + displayables. These displayables are predicted when the + sprite manager is. + + `ignore_time` + If True, then time is ignored when rendering displayables. This + should be used when the sprite manager is used with a relatively + small pool of images, and those images do not change over time. + This should only be used with a small number of displayables, as + it will keep all displayables used in memory for the life of the + SpriteManager. + + After being rendered once (before the `update` function is called), + SpriteManagers have the following fields: + + `width`, `height` + + The width and height of this SpriteManager, in pixels. + + + SpriteManagers have the following methods: + """ + + super(SpriteManager, self).__init__(self, **properties) + + self.update_function = update + self.event_function = event + self.predict_function = predict + self.ignore_time = ignore_time + + # A map from a displayable to the SpriteDisplayable object + # representing that displayable. + self.displayable_map = { } + + # A list of children of this displayable, in zorder. (When sorted.) + # This is a list of Sprites. + self.children = [ ] + + # True if at least one child has been killed. + self.dead_child = False + + # True if at least one child responds to events. + self.events = False + + # The width and height. + self.width = None + self.height = None + + def create(self, d): + """ + :doc: sprites method + + Creates a new Sprite for the displayable `d`, and adds it to this + SpriteManager. + """ + + id_d = id(d) + + sc = self.displayable_map.get(id_d, None) + if sc is None: + d = renpy.easy.displayable(d) + + sc = SpriteCache() + sc.render = None + sc.child = d + sc.st = None + self.displayable_map[id_d] = sc + + s = Sprite() + s.x = 0 + s.y = 0 + s.zorder = 0 + s.cache = sc + s.live = True + s.manager = self + s.events = False + + self.children.append(s) + + return s + + def predict_one(self): + if self.predict_function is not None: + for i in self.predict_function(): + renpy.display.predict.displayable(i) + + + def redraw(self, delay=0): + """ + :doc: sprite method + + Causes this SpriteManager to be redrawn in `delay` seconds. + """ + + renpy.display.render.redraw(self, delay) + + def render(self, width, height, st, at): + + self.width = width + self.height = height + + if self.update_function is not None: + + redraw = self.update_function(st) + + if redraw is not None: + renpy.display.render.redraw(self, redraw) + + if not self.ignore_time: + self.displayable_map.clear() + + if self.dead_child: + self.children = [ i for i in self.children if i.live ] + + self.children.sort(key=lambda sc:sc.zorder) + + caches = [ ] + + rv = renpy.display.render.Render(width, height) + + events = False + + for i in self.children: + + events |= i.events + + cache = i.cache + r = i.cache.render + if cache.render is None: + if cache.st is None: + cache.st = st + + cst = st - cache.st + + cache.render = r = render(cache.child, width, height, cst, cst) + cache.fast = (r.operation == BLIT) and (r.forward is None) and (r.alpha == 1.0) + rv.depends_on(r) + + caches.append(cache) + + + if cache.fast: + for child, xo, yo, _focus, _main in r.children: + rv.children.append((child, + xo + i.x, + yo + i.y, + False, + False)) + + else: + rv.subpixel_blit(r, (i.x, i.y)) + + for i in caches: + i.render = None + + return rv + + def event(self, ev, x, y, st): + for i in range(len(self.children) -1, -1, -1): + s = self.children[i] + + if s.events: + rv = s.cache.child.event(ev, x - s.x, y - s.y, st - s.cache.st) + if rv is not None: + return rv + + if self.event_function is not None: + return self.event_function(ev, x, y, st) + else: + return None + + def visit(self): + rv = [ ] + + try: + if self.predict_function: + pl = self.predict_function() + for i in pl: + i = renpy.easy.displayable(i) + rv.append(i) + except: + pass + + return rv + + def destroy_all(self): + self.children = [ ] + + +class Particles(renpy.display.core.Displayable, renpy.python.NoRollback): + """ + Supports particle motion, using the old API. + """ + + __version__ = 1 + + nosave = [ 'particles' ] + + def after_upgrade(self, version): + if version < 1: + self.sm = SpriteManager(update=self.update_callback, predict=self.predict_callback) + + def after_setstate(self): + self.particles = None + + def __init__(self, factory, **properties): + """ + @param factory: A factory object. + """ + + super(Particles, self).__init__(**properties) + + self.sm = SpriteManager(update=self.update_callback, predict=self.predict_callback) + + self.factory = factory + self.particles = None + + def update_callback(self, st): + + particles = self.particles + + if st == 0 or particles is None: + self.sm.destroy_all() + particles = [ ] + + add_parts = self.factory.create(particles, st) + + new_particles = [ ] + + for sprite, p in particles: + update = p.update(st) + + if update is None: + sprite.destroy() + continue + + x, y, _t, d = update + + if d is not sprite.cache.child: + sprite.set_child(d) + + sprite.x = x + sprite.y = y + + new_particles.append((sprite, p)) + + if add_parts: + for p in add_parts: + update = p.update(st) + + if update is None: + continue + + x, y, _t, d = update + + if d is None: + continue + + sprite = self.sm.create(d) + sprite.x = x + sprite.y = y + + new_particles.append((sprite, p)) + + self.particles = new_particles + + return 0 + + def predict_callback(self): + return self.factory.predict() + + def render(self, w, h, st, at): + return renpy.display.render.render(self.sm, w, h, st, at) + +class SnowBlossomFactory(renpy.python.NoRollback): + + rotate = False + + def __setstate__(self, state): + self.start = 0 + vars(self).update(state) + self.init() + + def __init__(self, image, count, xspeed, yspeed, border, start, fast, rotate=False): + self.image = renpy.easy.displayable(image) + self.count = count + self.xspeed = xspeed + self.yspeed = yspeed + self.border = border + self.start = start + self.fast = fast + self.rotate = rotate + self.init() + + def init(self): + self.starts = [ random.uniform(0, self.start) for _i in range(0, self.count) ] # W0201 + self.starts.append(self.start) + self.starts.sort() + + def create(self, particles, st): + + def ranged(n): + if isinstance(n, tuple): + return random.uniform(n[0], n[1]) + else: + return n + + if not particles and self.fast: + rv = [ ] + + for _i in range(0, self.count): + rv.append(SnowBlossomParticle(self.image, + ranged(self.xspeed), + ranged(self.yspeed), + self.border, + st, + random.uniform(0, 100), + fast=True, + rotate=self.rotate)) + return rv + + + if particles is None or len(particles) < self.count: + + # Check to see if we have a particle ready to start. If not, + # don't start it. + if particles and st < self.starts[len(particles)]: + return None + + return [ SnowBlossomParticle(self.image, + ranged(self.xspeed), + ranged(self.yspeed), + self.border, + st, + random.uniform(0, 100), + fast=False, + rotate=self.rotate) ] + + def predict(self): + return [ self.image ] + + +class SnowBlossomParticle(renpy.python.NoRollback): + + def __init__(self, image, xspeed, yspeed, border, start, offset, fast, rotate): + + # safety. + if yspeed == 0: + yspeed = 1 + + self.image = image + self.xspeed = xspeed + self.yspeed = yspeed + self.border = border + self.start = start + self.offset = offset + self.rotate = rotate + + + if not rotate: + sh = renpy.config.screen_height + sw = renpy.config.screen_width + else: + sw = renpy.config.screen_height + sh = renpy.config.screen_width + + + if self.yspeed > 0: + self.ystart = -border + else: + self.ystart = sh + border + + + travel_time = (2.0 * border + sh) / abs(yspeed) + + xdist = xspeed * travel_time + + x0 = min(-xdist, 0) + x1 = max(sw + xdist, sw) + + self.xstart = random.uniform(x0, x1) + + if fast: + self.ystart = random.uniform(-border, sh + border) + self.xstart = random.uniform(0, sw) + + def update(self, st): + to = st - self.start + + xpos = self.xstart + to * self.xspeed + ypos = self.ystart + to * self.yspeed + + if not self.rotate: + sh = renpy.config.screen_height + else: + sh = renpy.config.screen_width + + if ypos > sh + self.border: + return None + + if ypos < -self.border: + return None + + if not self.rotate: + return int(xpos), int(ypos), to + self.offset, self.image + else: + return int(ypos), int(xpos), to + self.offset, self.image + +def SnowBlossom(d, + count=10, + border=50, + xspeed=(20, 50), + yspeed=(100, 200), + start=0, + fast=False, + horizontal=False): + + """ + :doc: sprites_extra + + The snowblossom effect moves multiple instances of a sprite up, + down, left or right on the screen. When a sprite leaves the screen, it + is returned to the start. + + `d` + The displayable to use for the sprites. + + `border` + The size of the border of the screen. The sprite is considered to be + on the screen until it clears the border, ensuring that sprites do + not disappear abruptly. + + `xspeed`, `yspeed` + The speed at which the sprites move, in the horizontal and vertical + directions, respectively. These can be a single number or a tuple of + two numbers. In the latter case, each particle is assigned a random + speed between the two numbers. The speeds can be positive or negative, + as long as the second number in a tuple is larger than the first. + + `start` + The delay, in seconds, before each particle is added. This can be + allows the particles to start at the top of the screen, while not + looking like a "wave" effect. + + `fast` + If true, particles start in the center of the screen, rather than + only at the edges. + + `horizontal` + If true, particles appear on the left or right side of the screen, + rather than the top or bottom. + """ + + # If going horizontal, swap the xspeed and the yspeed. + if horizontal: + xspeed, yspeed = yspeed, xspeed + + return Particles(SnowBlossomFactory(image=d, + count=count, + border=border, + xspeed=xspeed, + yspeed=yspeed, + start=start, + fast=fast, + rotate=horizontal)) + diff --git a/unrpyc/renpy/display/pgrender.py b/unrpyc/renpy/display/pgrender.py new file mode 100644 index 0000000..32d1df8 --- /dev/null +++ b/unrpyc/renpy/display/pgrender.py @@ -0,0 +1,167 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This module wraps the pygame surface class (and associated functions). It +# ensures that returned surfaces have a 2px border around them. + +import sys +import pygame +import renpy.display + +# Sample surfaces, with and without alpha. +sample_alpha = None +sample_noalpha = None + +def set_rgba_masks(): + """ + This rebuilds the sample surfaces, to ones that use the given + masks. + """ + + # Annoyingly, the value for the big mask seems to vary from + # platform to platform. So we read it out of a surface. + + global sample_alpha + global sample_noalpha + + # Create a sample surface. + s = pygame.Surface((10, 10), 0, 32) + sample_alpha = s.convert_alpha() + + # Sort the components by absolute value. + masks = list(sample_alpha.get_masks()) + masks.sort(key=lambda a : abs(a)) + + # Choose the masks. + if sys.byteorder == 'big': + masks = ( masks[3], masks[2], masks[1], masks[0] ) + else: + masks = ( masks[0], masks[1], masks[2], masks[3] ) + + # Create the sample surface. + sample_alpha = pygame.Surface((10, 10), 0, 32, masks) + sample_noalpha = pygame.Surface((10, 10), 0, 32, masks[:3] + (0,)) + + +class Surface(pygame.Surface): + """ + This allows us to wrap around pygame's surface, to change + its mode, as necessary. + """ + + opaque = False + + def is_opaque(self): + return self.opaque + + def convert_alpha(self, surface=None): + return copy_surface_unscaled(self, True) + + def convert(self, surface=None): + return copy_surface(self, False) + + def copy(self): + return copy_surface(self, self) + + def subsurface(self, rect): + rv = pygame.Surface.subsurface(self, rect) + return rv + +def surface(xxx_todo_changeme, alpha): + """ + Constructs a new surface. The allocated surface is actually a subsurface + of a surface that has a 2 pixel border in all directions. + + `alpha` - True if the new surface should have an alpha channel. + """ + (width, height) = xxx_todo_changeme + if isinstance(alpha, pygame.Surface): + alpha = alpha.get_masks()[3] + + if alpha: + sample = sample_alpha + else: + sample = sample_noalpha + + # We might not have initialized properly yet. This is enough + # to get us underway. + if sample is None: + sample = pygame.Surface((4, 4), pygame.SRCALPHA, 32) + + surf = Surface((width + 4, height + 4), 0, sample) + return surf.subsurface((2, 2, width, height)) # E1101 + +surface_unscaled = surface + +def copy_surface(surf, alpha=True): + """ + Creates a copy of the surface. + """ + + rv = surface_unscaled(surf.get_size(), alpha) + renpy.display.accelerator.nogil_copy(surf, rv) # @UndefinedVariable + return rv + +copy_surface_unscaled = copy_surface + + +# Wrapper around image loading. + +def load_image(f, filename): + surf = pygame.image.load(f, renpy.exports.fsencode(filename)) + rv = copy_surface_unscaled(surf) + return rv + +load_image_unscaled = load_image + + +# Wrapper around functions we use from pygame.surface. + +def flip(surf, horizontal, vertical): + surf = pygame.transform.flip(surf, horizontal, vertical) + return copy_surface_unscaled(surf) + +flip_unscaled = flip + + +def rotozoom(surf, angle, zoom): + + surf = pygame.transform.rotozoom(surf, angle, zoom) + return copy_surface_unscaled(surf) + +rotozoom_unscaled = rotozoom + + +def transform_scale(surf, size): + surf = pygame.transform.scale(surf, size) + return copy_surface_unscaled(surf, surf) + +transform_scale_unscaled = transform_scale + + +def transform_rotate(surf, angle): + surf = pygame.transform.rotate(surf, angle) + return copy_surface(surf) + +transform_rotate_unscaled = transform_rotate + + + diff --git a/unrpyc/renpy/display/predict.py b/unrpyc/renpy/display/predict.py new file mode 100644 index 0000000..2bdc16f --- /dev/null +++ b/unrpyc/renpy/display/predict.py @@ -0,0 +1,156 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains the routines that manage image prediction. + +import renpy.display + +# Called to indicate an image should be loaded or preloaded. This is +# a function that takes an image manipulator, set by reset and predict, +# and winds up bound to either im.cache.get or im.cache.preload_image +image = None + +# The set of displayables we've predicted since reset was last called. +predicted = set() + +# A flag that indicates if we're currently predicting. +predicting = False + +# A list of (screen name, argument dict) tuples, giving the screens we'd +# like to predict. +screens = [ ] + +def displayable(d): + """ + Called to predict that the displayable `d` will be shown. + """ + + if d is None: + return + + if d not in predicted: + predicted.add(d) + d.visit_all(lambda i : i.predict_one()) + +def screen(_screen_name, *args, **kwargs): + """ + Called to predict that the named screen is about to be shown + with the given arguments. + """ + + screens.append((_screen_name, args, kwargs)) + + +def reset(): + global image + image = renpy.display.im.cache.get + predicted.clear() + del screens[:] + + +def prediction_coroutine(root_widget): + """ + The image prediction co-routine. This predicts the images that can + be loaded in the near future, and passes them to the image cache's + preload_image method to be queued up for loading. + + The .send should be called with True to do a expensive prediction, + and with False to either do an inexpensive prediction or no + prediction at all. + + Returns True if there's more predicting to be done, or False + if there's no more predicting worth doing. + """ + + global predicting + + # Wait to be told to start. + yield True + + # Start the prediction thread (to clean out the cache). + renpy.display.im.cache.start_prediction() + + # Set up the image prediction method. + global image + image = renpy.display.im.cache.preload_image + + # Predict images that are going to be reached in the next few + # clicks. + predicting = True + + for _i in renpy.game.context().predict(): + + predicting = False + yield True + predicting = True + + # If there's a parent context, predict we'll be returning to it + # shortly. Otherwise, call the functions in + # config.predict_callbacks. + + if len(renpy.game.contexts) >= 2: + sls = renpy.game.contexts[-2].scene_lists + + for l in sls.layers.values(): + for sle in l: + try: + displayable(sle.displayable) + except: + pass + + else: + for i in renpy.config.predict_callbacks: + i() + + predicting = False + + while not (yield True): + continue + + # Predict things (especially screens) that are reachable through + # an action. + predicting = True + + try: + root_widget.visit_all(lambda i : i.predict_one_action()) + except: + pass + + predicting = False + + # Predict the screens themselves. + for name, args, kwargs in screens: + while not (yield True): + continue + + predicting = True + + try: + renpy.display.screen.predict_screen(name, *args, **kwargs) + except: + if renpy.config.debug_image_cache: + renpy.display.ic_log.write("While predicting screen %s %r", name, kwargs) + renpy.display.ic_log.exception() + + predicting = False + + yield False + diff --git a/unrpyc/renpy/display/presplash.py b/unrpyc/renpy/display/presplash.py new file mode 100644 index 0000000..54717fd --- /dev/null +++ b/unrpyc/renpy/display/presplash.py @@ -0,0 +1,113 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Pre-splash code. The goal of this code is to try to get a pre-splash +# screen up as soon as possible, to let the user know something is +# going on. + +# The presplash process, if any. +proc = None + +# Called from the main process. This determines if +# we're even doing presplash, and if so what will be shown to the +# user. If it decides to show something to the user, uses subprocess +# to actually handle the showing. +def start(basedir, gamedir): + import os.path + + if "RENPY_LESS_UPDATES" in os.environ: + return + + global proc + + filenames = [ "/presplash.png", "/presplash.jpg" ] + for fn in filenames: + fn = gamedir + fn + if os.path.exists(fn): + break + else: + return + + try: + import subprocess + import sys + + cmd = [sys.executable, "-EOO", sys.argv[0], "show", "presplash", fn] + + def fsencode(s): + if isinstance(s, str): + return s + + return s.encode(sys.getfilesystemencoding() or "utf-8", "replace") + + proc = subprocess.Popen([ fsencode(i) for i in cmd ], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + except: + pass + + +# Called just before we initialize the display for real, to +# hide the splash, and terminate window centering. +def end(): + + global proc + + if not proc: + return + + proc.stdin.close() + proc.wait() + + proc = None + +# Called in the presplash process, to actually display the presplash. +def show(fn): + + import pygame.display + import pygame.constants + import sys + import os + + os.environ['SDL_VIDEO_CENTERED'] = "1" + + try: + import pygame.macosx + pygame.macosx.init() #@UndefinedVariable + except: + pass + + try: + import pygame.macosx #@Reimport + pygame.macosx.Video_AutoInit() + except: + pass + + pygame.display.init() + + img = pygame.image.load(fn, fn) + screen = pygame.display.set_mode(img.get_size(), pygame.constants.NOFRAME) + screen.blit(img, (0, 0)) + pygame.display.update() + + sys.stdout.write("READY\r\n") + sys.stdout.flush() + sys.stdin.read() + + sys.exit(0) diff --git a/unrpyc/renpy/display/render.pxd b/unrpyc/renpy/display/render.pxd new file mode 100644 index 0000000..a4d8d68 --- /dev/null +++ b/unrpyc/renpy/display/render.pxd @@ -0,0 +1,47 @@ +cdef class Matrix2D: + cdef public double xdx + cdef public double xdy + cdef public double ydx + cdef public double ydy + + cpdef tuple transform(Matrix2D self, double x, double y) + +cdef class Render: + + cdef public bint mark, cache_killed + + cdef public float width, height + cdef public object layer_name + + cdef public list children + cdef public set parents + cdef public list depends_on_list + + cdef public int operation + cdef public double operation_complete + cdef public bint operation_alpha + cdef public object operation_parameter + + cdef public Matrix2D forward, reverse + cdef public double alpha + + cdef public list focuses + cdef public list pass_focuses + cdef public object draw_func + cdef public object render_of + + cdef public bint opaque + cdef public list visible_children + + cdef public bint clipping + + cdef public object surface, alpha_surface, half_cache + + cdef public bint modal + + cpdef int blit(Render self, source, tuple pos, object focus=*, object main=*, object index=*) + cpdef int subpixel_blit(Render self, source, tuple pos, object focus=*, object main=*, object index=*) + + +cpdef render(object d, object widtho, object heighto, double st, double at) + diff --git a/unrpyc/renpy/display/render.pyx b/unrpyc/renpy/display/render.pyx new file mode 100644 index 0000000..5aa45a0 --- /dev/null +++ b/unrpyc/renpy/display/render.pyx @@ -0,0 +1,1174 @@ +#cython: profile=False +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import collections +import pygame +import threading +import renpy +import gc + +# We grab the blit lock each time it is necessary to blit +# something. This allows call to the pygame.transform functions to +# disable blitting, should it prove necessary. +blit_lock = threading.Condition() + +# This is a dictionary containing all the renders that we know of. It's a +# map from displayable to dictionaries containing the render of that +# displayable. +render_cache = collections.defaultdict(dict) + +# The queue of redraws. A list of (time, displayable) pairs. +redraw_queue = [ ] + +# The render returned from render_screen. +screen_render = None + +# A list of renders the system knows about, and thinks are still alive. +cdef list live_renders +live_renders = [ ] + +# A copy of renpy.display.interface.frame_time, for speed reasons. +cdef double frame_time +frame_time = 0 + +def free_memory(): + """ + Frees memory used by the render system. + """ + + global screen_render + screen_render = None + + mark_sweep() + + render_cache.clear() + + # This can hang onto a render. + renpy.display.interface.surftree = None + + +def check_at_shutdown(): + """ + This is called at shutdown time to check that everything went okay. + The big thing it checks for is memory leaks. + """ + + if not renpy.config.developer: + return + + free_memory() + + gc.collect() + l = gc.get_objects() + + count = 0 + objects = gc.get_objects() + + for i in objects: + if isinstance(i, Render): + count += 1 + + if count: + raise Exception("%d Renders are alive at shutdown. This is probably a memory leak bug in Ren'Py." % count) + + + +cpdef render(d, object widtho, object heighto, double st, double at): + """ + :doc: udd_utility + :args: (d, width, height, st, at) + + Causes a displayable to be rendered, and a renpy.Render object to + be returned. + + `d` + The displayable to render. + + `width`, `height` + The width and height available for the displayable to render into. + + `st`, `at` + The shown and animation timebases. + + Renders returned by this object may be cached, and should not be modified + once they have been retrieved. + """ + + cdef float width, height + cdef float orig_width, orig_height + cdef tuple orig_wh, wh + cdef dict render_cache_d + cdef Render rv + + orig_wh = (widtho, heighto, frame_time-st, frame_time-at) + + render_cache_d = render_cache[d] + rv = render_cache_d.get(orig_wh, None) + + if rv is not None: + return rv + + orig_width = width = widtho + orig_height = height = heighto + + style = d.style + xmaximum = style.xmaximum + ymaximum = style.ymaximum + + if xmaximum is not None: + if isinstance(xmaximum, float): + width = width * xmaximum + else: + width = min(xmaximum, width) + + if ymaximum is not None: + if isinstance(ymaximum, float): + height = height * ymaximum + else: + height = min(ymaximum, height) + + if width < 0: + width = 0 + if height < 0: + height = 0 + + if orig_width != width or orig_height != height: + widtho = width + heighto = height + wh = (widtho, heighto, frame_time-st, frame_time-at) + rv = render_cache_d.get(wh, None) + + if rv is not None: + return rv + + else: + wh = orig_wh + + rv = d.render(widtho, heighto, st, at) + + rv.render_of.append(d) + + if style.clipping: + rv = rv.subsurface((0, 0, rv.width, rv.height), focus=True) + rv.render_of.append(d) + + render_cache_d[wh] = rv + + if wh is not orig_wh: + render_cache_d[orig_wh] = rv + + return rv + + +# This is true if something has been invalidated, and a redraw needs +# to occur. It's automatically cleared to False at the end of each +# redraw. +invalidated = False + +def invalidate(d): + """ + Removes d from the render cache. If we're not in a redraw, triggers + a redraw to start. + """ + + global invalidated + + if d in render_cache: + for v in render_cache[d].values(): + v.kill_cache() + + invalidated = True + + +def process_redraws(): + """ + Called to determine if any redraws are pending. Returns true if we + need to redraw the screen now, false otherwise. + """ + + global redraw_queue + + redraw_queue.sort() + + now = renpy.display.core.get_time() + rv = invalidated + + new_redraw_queue = [ ] + seen = set() + + for t in redraw_queue: + when, d = t + + if d in seen: + continue + + seen.add(d) + + if d not in render_cache: + continue + + if when <= now: + # Remove this displayable and all its parents from the + # render cache. But don't kill them yet, as that will kill the + # children that we want to reuse. + + for v in render_cache[d].values(): + v.kill_cache() + + rv = True + + else: + new_redraw_queue.append(t) + + redraw_queue = new_redraw_queue + + return rv + + +def redraw_time(): + """ + Returns the time at which the next redraw is scheduled. + """ + + if redraw_queue: + return redraw_queue[0][0] + + return None + + +def redraw(d, when): + """ + :doc: udd_utility + + Causes the displayable `d` to be redrawn after `when` seconds have + elapsed. + """ + + if not renpy.game.interface: + return + + redraw_queue.append((when + renpy.game.interface.frame_time, d)) + + +cdef class Matrix2D: + """ + This represents a 2d matrix that can be used to transform + points and things like that. + """ + + def __getstate__(self): + return dict( + xdx = self.xdx, + xdy = self.xdy, + ydx = self.ydx, + ydy = self.ydy) + + def __setstate__(self, state): + self.xdx = state['xdx'] + self.xdy = state['xdy'] + self.ydx = state['ydx'] + self.ydy = state['ydy'] + + def __init__(Matrix2D self, double xdx, double xdy, double ydx, double ydy): + self.xdx = xdx + self.xdy = xdy + self.ydx = ydx + self.ydy = ydy + + cpdef tuple transform(Matrix2D self, double x, double y): + return (x * self.xdx + y * self.xdy), (x * self.ydx + y * self.ydy) + + def __mul__(Matrix2D self, Matrix2D other): + return Matrix2D( + other.xdx * self.xdx + other.xdy * self.ydx, + other.xdx * self.xdy + other.xdy * self.ydy, + other.ydx * self.xdx + other.ydy * self.ydx, + other.ydx * self.xdy + other.ydy * self.ydy) + + def __repr__(self): + return "Matrix2D(xdx=%f, xdy=%f, ydx=%f, ydy=%f)" % (self.xdx, self.xdy, self.ydx, self.ydy) + +IDENTITY = Matrix2D(1, 0, 0, 1) + +def take_focuses(focuses): + """ + Adds a list of rectangular focus regions to the focuses list. + """ + + screen_render.take_focuses( + 0, 0, screen_render.width, screen_render.height, + IDENTITY, 0, 0, focuses) + +# The result of focus_at_point for a modal render. This overrides any +# specific focus from below us. +Modal = object() + +def focus_at_point(x, y): + """ + Returns a focus object corresponding to the uppermost displayable + at point, or None if nothing focusable is at point. + """ + + if screen_render is None: + return None + + cf = screen_render.focus_at_point(x, y) + if cf is None or cf is Modal: + return None + else: + d, arg = cf + return renpy.display.focus.Focus(d, arg, None, None, None, None) + + +def mutated_surface(surf): + """ + Called to indicate that the given surface has changed. + """ + + renpy.display.draw.mutated_surface(surf) + + +def render_screen(root, width, height): + """ + Renders `root` (a displayable) as the root of a screen with the given + `width` and `height`. + """ + + + + global old_screen_render + global screen_render + global invalidated + global frame_time + + frame_time = renpy.display.interface.frame_time + + rv = render(root, width, height, 0, 0) + screen_render = rv + + invalidated = False + + rv.is_opaque() + + return rv + +def mark_sweep(): + """ + This performs mark-and-sweep garbage collection on the live_renders + list. + """ + + global live_renders + + cdef list worklist + cdef int i + cdef Render r, j + + worklist = [ ] + + if screen_render is not None: + worklist.append(screen_render) + + i = 0 + + while i < len(worklist): + r = worklist[i] + + for j in r.depends_on_list: + if not j.mark: + j.mark = True + worklist.append(j) + + i += 1 + + for r in live_renders: + if not r.mark: + r.kill_cache() + else: + r.mark = False + + live_renders = worklist + +def compute_subline(sx0, sw, cx0, cw): + """ + Given a source line (start sx0, width sw) and a crop line (cx0, cw), + return three things: + + * The offset of the portion of the source line that overlaps with + the crop line, relative to the crop line. + * The offset of the portion of the source line that overlaps with the + the crop line, relative to the source line. + * The length of the overlap in pixels. (can be <= 0) + """ + + sx1 = sx0 + sw + cx1 = cx0 + cw + + if sx0 > cx0: + start = sx0 + else: + start = cx0 + + offset = start - cx0 + crop = start - sx0 + + if sx1 < cx1: + width = sx1 - start + else: + width = cx1 - start + + + return offset, crop, width + + + + +# Possible operations that can be done as part of a render. +BLIT = 0 +DISSOLVE = 1 +IMAGEDISSOLVE = 2 +PIXELLATE = 3 + +cdef class Render: + + def __init__(Render self, float width, float height, draw_func=None, layer_name=None, bint opaque=None): #@DuplicatedSignature + """ + Creates a new render corresponding to the given widget with + the specified width and height. + + If `layer_name` is given, then this render corresponds to a + layer. + """ + + # The mark bit, used for mark/sweep-style garbage collection of + # renders. + self.mark = False + + # Is has this render been removed from the cache? + self.cache_killed = False + + + self.width = width + self.height = height + + self.layer_name = layer_name + + # A list of (surface/render, xoffset, yoffset, focus, main) tuples, ordered from + # back to front. + self.children = [ ] + + # The set of renders that either have us as children, or depend on + # us. + self.parents = set() + + # The renders we depend on, including our children. + self.depends_on_list = [ ] + + # The operation we're performing. (BLIT, DISSOLVE, OR IMAGE_DISSOLVE) + self.operation = BLIT + + # The fraction of the operation that is complete. + self.operation_complete = 0.0 + + # Should the dissolve operations preserve alpha? + self.operation_alpha = False + + # The parameter to the operation. + self.operation_parameter = 0 + + # Forward is used to transform from screen coordinates to child + # coordinates. + # Reverse is used to transform from child coordinates to screen + # coordinates. + # + # For performance reasons, these aren't used to transform the + # x and y offsets found in self.children. Those offsets should + # be of the (0, 0) point in the child coordinate space. + self.forward = None + self.reverse = None + + # This is used to adjust the alpha of children of this render. + self.alpha = 1 + + # A list of focus regions in this displayable. + self.focuses = None + + # Other renders that we should pass focus onto. + self.pass_focuses = None + + # The displayable(s) that this is a render of. (Set by render) + self.render_of = [ ] + + # If set, this is a function that's called to draw this render + # instead of the default. + self.draw_func = draw_func + + # Is this displayable opaque? (May be set on init, or later on + # if we have opaque children.) This may be True, False, or None + # to indicate we don't know yet. + self.opaque = opaque + + # A list of our visible children. (That is, children above and + # including our uppermost opaque child.) If nothing is opaque, + # includes all children. + self.visible_children = self.children + + # Should children be clipped to a rectangle? + self.clipping = False + + # Caches of the texture created by rendering this surface. + self.surface = None + self.alpha_surface = None + + # Cache of the texture created by rendering this surface at half size. + # (This is set in gldraw.) + self.half_cache = None + + # Are we modal? + self.modal = False + + live_renders.append(self) + + def __repr__(self): #@DuplicatedSignature + return "<Render %x of %r>" % (id(self), self.render_of) + + def __getstate__(self): #@DuplicatedSignature + if renpy.config.developer: + raise Exception("Can't pickle a Render.") + else: + return { } + + def __setstate__(self, state): #@DuplicatedSignature + return + + cpdef int blit(Render self, source, tuple pos, object focus=True, object main=True, object index=None): + """ + Blits `source` (a Render or Surface) to this Render, offset by + xo and yo. + + If `focus` is true, then focuses are added from the child to the + parent. + + This will only blit on integer pixel boundaries. + """ + + (xo, yo) = pos + + if source is self: + raise Exception("Blitting to self.") + + xo = int(xo) + yo = int(yo) + + if index is None: + self.children.append((source, xo, yo, focus, main)) + else: + self.children.insert(index, (source, xo, yo, focus, main)) + + if isinstance(source, Render): + self.depends_on_list.append(source) + source.parents.add(self) + + return 0 + + cpdef int subpixel_blit(Render self, source, tuple pos, object focus=True, object main=True, object index=None): + """ + Blits `source` (a Render or Surface) to this Render, offset by + xo and yo. + + If `focus` is true, then focuses are added from the child to the + parent. + + This blits at fractional pixel boundaries. + """ + + (xo, yo) = pos + + xo = float(xo) + yo = float(yo) + + if index is None: + self.children.append((source, xo, yo, focus, main)) + else: + self.children.insert(index, (source, xo, yo, focus, main)) + + if isinstance(source, Render): + self.depends_on_list.append(source) + source.parents.add(self) + + return 0 + + def get_size(self): + """ + Returns the size of this Render, a mostly ficticious value + that's taken from the inputs to the constructor. (As in, we + don't clip to this size.) + """ + + return self.width, self.height + + + def render_to_texture(self, alpha=True): + """ + Returns a texture constructed from this render. This may return + a cached textue, if one has already been rendered. + + `alpha` is a hint that controls if the surface should have + alpha or not. + """ + + if alpha: + if self.alpha_surface is not None: + return self.alpha_surface + else: + if self.surface is not None: + return self.surface + + rv = None + + opaque = self.is_opaque() + + # If we can, reuse a child's texture. + if opaque or alpha: + + if not self.forward and len(self.children) == 1: + child, x, y, focus, main = self.children[0] + cw, ch = child.get_size() + if x <= 0 and y <= 0 and cw + x >= self.width and ch + y >= self.height: + # Our single child overlaps us. + if isinstance(child, Render): + child = child.render_to_texture(alpha) + + if x != 0 or y != 0 or cw != self.width or ch != self.height: + rv = child.subsurface((-x, -y, self.width, self.height)) + else: + rv = child + + # Otherwise, render to a texture. + if rv is None: + # is_opaque has already been called. + rv = renpy.display.draw.render_to_texture(self, alpha) + + # Stash and return the surface. + if alpha: + self.alpha_surface = rv + else: + self.surface = rv + + return rv + + pygame_surface = render_to_texture + + def subsurface(self, rect, focus=False): + """ + Returns a subsurface of this render. If `focus` is true, then + the focuses are copied from this render to the child. + """ + + (x, y, w, h) = rect + rv = Render(w, h) + + reverse = self.reverse + + # This doesn't actually make a subsurface, as we can't easily do + # so for non-rectangle-aligned renders. + if (reverse is not None) and ( + reverse.xdx != 1.0 or + reverse.xdy != 0.0 or + reverse.ydx != 0.0 or + reverse.ydy != 1.0): + + rv.clipping = True + rv.blit(self, (-x, -y), focus=focus, main=True) + return rv + + # This is the path that executes for rectangle-aligned surfaces, + # making an actual subsurface. + + for child, cx, cy, cfocus, cmain in self.children: + + cw, ch = child.get_size() + xo, cx, cw = compute_subline(cx, cw, x, w) + yo, cy, ch = compute_subline(cy, ch, y, h) + + if cw <= 0 or ch <= 0: + continue + + crop = (cx, cy, cw, ch) + offset = (xo, yo) + + if isinstance(child, Render): + newchild = child.subsurface(crop, focus=focus) + newchild.render_of = child.render_of[:] + else: + newchild = child.subsurface(crop) + renpy.display.draw.mutated_surface(newchild) + + rv.blit(newchild, offset, focus=cfocus, main=cmain) + + if focus and self.focuses: + + for (d, arg, xo, yo, fw, fh, mx, my, mask) in self.focuses: + + if xo is None: + rv.add_focus(d, arg, xo, yo, fw, fh, mx, my, mask) + continue + + xo, cx, fw = compute_subline(xo, fw, x, w) + yo, cy, fh = compute_subline(yo, fh, y, h) + + if cw <= 0 or ch <= 0: + continue + + if mx is not None: + + mw, mh = mask.get_size() + + mx, mcx, mw = compute_subline(mx, mw, x, w) + my, mcy, mh = compute_subline(my, mh, y, h) + + if mw <= 0 or mh <= 0: + mx = None + my = None + mask = None + else: + mask = mask.subsurface((mcx, mcy, mw, mh)) + + rv.add_focus(d, arg, xo, yo, fw, fh, mx, my, mask) + + rv.depends_on(self) + rv.alpha = self.alpha + rv.operation = self.operation + rv.operation_alpha = self.operation_alpha + rv.operation_complete = self.operation_complete + + return rv + + + def depends_on(self, source, focus=False): + """ + Used to indicate that this render depends on another + render. Useful, for example, if we use pygame_surface to make + a surface, and then blit that surface into another render. + """ + + if source is self: + raise Exception("Render depends on itself.") + + self.depends_on_list.append(source) + source.parents.add(self) + + if focus: + if self.pass_focuses is None: + self.pass_focuses = [ source ] + else: + self.pass_focuses.append(source) + + + def kill_cache(self): + """ + Removes this render and its transitive parents from the cache. + """ + + if self.cache_killed: + return + + self.cache_killed = True + + for i in self.parents: + i.kill_cache() + + self.parents.clear() + + for i in self.depends_on_list: + if not i.cache_killed: + i.parents.discard(self) + + for ro in self.render_of: + cache = render_cache[ro] + for k, v in cache.items(): + if v is self: + del cache[k] + + if not cache: + del render_cache[ro] + + def kill(self): + """ + Retained for compatibility. + """ + + def add_focus(self, d, arg=None, x=0, y=0, w=None, h=None, mx=None, my=None, mask=None): + """ + This is called to indicate a region of the screen that can be + focused. + + `d` - the displayable that is being focused. + `arg` - an argument. + + The rest of the parameters are a rectangle giving the portion of + this region corresponding to the focus. If they are all None, than + this focus is assumed to be the singular full-screen focus. + """ + + if mask is not None and mask is not self: + self.depends_on(mask) + + t = (d, arg, x, y, w, h, mx, my, mask) + + if self.focuses is None: + self.focuses = [ t ] + else: + self.focuses.append(t) + + def take_focuses(self, cminx, cminy, cmaxx, cmaxy, reverse, x, y, focuses): #@DuplicatedSignature + """ + This adds to focuses Focus objects corresponding to the focuses + added to this object and its children, transformed into screen + coordinates. + + `cminx`, `cminy`, `cmaxx`, `cmaxy` - The clipping rectangle. + `reverse` - The transform from render to screen coordinates. + `x`, `y` - The offset of the upper-left corner of the render. + `focuses` - The list of focuses to add to. + """ + + if self.modal: + focuses[:] = [ ] + + if self.reverse: + reverse = reverse * self.reverse + + if self.focuses: + + for (d, arg, xo, yo, w, h, mx, my, mask) in self.focuses: + + if xo is None: + focuses.append(renpy.display.focus.Focus(d, arg, None, None, None, None)) + continue + + x1, y1 = reverse.transform(xo, yo) + x2, y2 = reverse.transform(xo + w, yo + h) + + minx = min(x1, x2) + x + miny = min(y1, y2) + y + maxx = max(x1, x2) + x + maxy = max(y1, y2) + y + + minx = max(minx, cminx) + miny = max(miny, cminy) + maxx = min(maxx, cmaxx) + maxy = min(maxy, cmaxy) + + if minx >= maxx or miny >= maxy: + continue + + focuses.append(renpy.display.focus.Focus(d, arg, minx, miny, maxx - minx, maxy - miny)) + + if self.clipping: + cminx = max(cminx, x) + cminy = max(cminy, y) + cmaxx = min(cmaxx, x + self.width) + cmaxy = min(cmaxx, x + self.height) + + for child, xo, yo, focus, main in self.children: + if not focus or not isinstance(child, Render): + continue + + xo, yo = reverse.transform(xo, yo) + child.take_focuses(cminx, cminy, cmaxx, cmaxy, reverse, x + xo, y + yo, focuses) + + if self.pass_focuses: + for child in self.pass_focuses: + child.take_focuses(cminx, cminy, cmaxx, cmaxy, reverse, x, y, focuses) + + def focus_at_point(self, x, y): #@DuplicatedSignature + """ + This returns the focus of this object at the given point. + """ + + if self.clipping: + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return None + + rv = None + + if self.focuses: + for (d, arg, xo, yo, w, h, mx, my, mask) in self.focuses: + + if xo is None: + continue + + elif mx is not None: + cx = x - mx + cy = y - my + + if self.forward: + cx, cy = self.forward.transform(cx, cy) + + if mask.is_pixel_opaque(cx, cy): + rv = d, arg + + elif xo <= x < xo + w and yo <= y < yo + h: + rv = d, arg + + for child, xo, yo, focus, main in self.children: + + if not focus or not isinstance(child, Render): + continue + + cx = x - xo + cy = y - yo + + if self.forward: + cx, cy = self.forward.transform(cx, cy) + + cf = child.focus_at_point(cx, cy) + if cf is not None: + rv = cf + + if self.pass_focuses: + for child in self.pass_focuses: + cf = child.focus_at_point(x, y) + if cf is not None: + rv = cf + + if rv is None and self.modal: + rv = Modal + + return rv + + + def main_displayables_at_point(self, x, y, layers, depth=None): + """ + Returns the displayable at `x`, `y` on one of the layers in + the set or list `layers`. + """ + + rv = [ ] + + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return rv + + if depth is not None: + for d in self.render_of: + rv.append((depth, self.width, self.height, d)) + depth += 1 + elif self.layer_name in layers: + depth = 0 + + for (child, xo, yo, focus, main) in self.children: + if not main or not isinstance(child, Render): + continue + + cx = x - xo + cy = y - yo + + if self.forward: + cx, cy = self.forward.transform(cx, cy) + + cf = child.main_displayables_at_point(cx, cy, layers, depth) + rv.extend(cf) + + return rv + + + def is_opaque(self): + """ + Returns true if this displayable is opaque, or False otherwise. + Also sets self.visible_children. + """ + + if self.opaque is not None: + return self.opaque + + # A rotated image is never opaque. (This isn't actually true, but it + # saves us from the expensive calculations require to prove it is.) + if self.forward: + self.opaque = False + return False + + rv = False + vc = [ ] + + for i in self.children: + child, xo, yo, focus, main = i + + if xo <= 0 and yo <= 0: + cw, ch = child.get_size() + if cw + xo < self.width or ch + yo < self.height: + if child.is_opaque(): + vc = [ ] + rv = True + + vc.append(i) + + self.visible_children = vc + self.opaque = rv + return rv + + + def is_pixel_opaque(self, x, y): + """ + Determine if the pixel at x and y is opaque or not. + """ + + if x < 0 or y < 0 or x >= self.width or y >= self.height: + return False + + if self.is_opaque(): + return True + + return renpy.display.draw.is_pixel_opaque(self, x, y) + + + def fill(self, color): + """ + Fills this Render with the given color. + """ + + color = renpy.easy.color(color) + solid = renpy.display.imagelike.Solid(color) + surf = render(solid, self.width, self.height, 0, 0) + self.blit(surf, (0, 0), focus=False, main=False) + + + def canvas(self): + """ + Returns a canvas object that draws to this Render. + """ + + surf = renpy.display.pgrender.surface((self.width, self.height), True) + + mutated_surface(surf) + + self.blit(surf, (0, 0)) + + return Canvas(surf) + + +class Canvas(object): + + def __init__(self, surf): #@DuplicatedSignature + self.surf = surf + + def rect(self, color, rect, width=0): + + try: + blit_lock.acquire() + pygame.draw.rect(self.surf, + renpy.easy.color(color), + rect, + width) + finally: + blit_lock.release() + + def polygon(self, color, pointlist, width=0): + try: + blit_lock.acquire() + pygame.draw.polygon(self.surf, + renpy.easy.color(color), + pointlist, + width) + finally: + blit_lock.release() + + def circle(self, color, pos, radius, width=0): + + try: + blit_lock.acquire() + pygame.draw.circle(self.surf, + renpy.easy.color(color), + pos, + radius, + width) + + finally: + blit_lock.release() + + def ellipse(self, color, rect, width=0): + try: + blit_lock.acquire() + pygame.draw.ellipse(self.surf, + renpy.easy.color(color), + rect, + width) + finally: + blit_lock.release() + + + def arc(self, color, rect, start_angle, stop_angle, width=1): + try: + blit_lock.acquire() + pygame.draw.arc(self.surf, + renpy.easy.color(color), + rect, + start_angle, + stop_angle, + width) + finally: + blit_lock.release() + + + def line(self, color, start_pos, end_pos, width=1): + try: + blit_lock.acquire() + pygame.draw.line(self.surf, + renpy.easy.color(color), + start_pos, + end_pos, + width) + finally: + blit_lock.release() + + def lines(self, color, closed, pointlist, width=1): + try: + blit_lock.acquire() + pygame.draw.lines(self.surf, + renpy.easy.color(color), + closed, + pointlist, + width) + finally: + blit_lock.release() + + def aaline(self, color, startpos, endpos, blend=1): + try: + blit_lock.acquire() + pygame.draw.aaline(self.surf, + renpy.easy.color(color), + startpos, + endpos, + blend) + finally: + blit_lock.release() + + def aalines(self, color, closed, pointlist, blend=1): + try: + blit_lock.acquire() + pygame.draw.aalines(self.surf, + renpy.easy.color(color), + closed, + pointlist, + blend) + finally: + blit_lock.release() diff --git a/unrpyc/renpy/display/scale.py b/unrpyc/renpy/display/scale.py new file mode 100644 index 0000000..c9efbcc --- /dev/null +++ b/unrpyc/renpy/display/scale.py @@ -0,0 +1,109 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This used to hack pygame to support resolution-scaling. Now it just kinda +# sits here, to provide compatibility with what it used to be. + +import pygame +import renpy.display +import renpy.display.pgrender as pgrender + +import _renpy + +############################################################################## +# The scaling API that's used if we don't enable scaling. + +# Gets the real pygame surface. +def real(s): + return s + +# Scales the number, n. +def scale(n): + return n + +def real_bilinear(src, size): + rv = pgrender.surface_unscaled(size, src) + renpy.display.module.bilinear_scale(src, rv) + return rv + +# Does pygame.transform.scale. +def real_transform_scale(surf, size): + return pgrender.transform_scale_unscaled(surf, size) + +# Loads an image, without scaling it. +def image_load_unscaled(f, hint, convert=True): + rv = pgrender.load_image_unscaled(f, hint) + return rv + +# Saves an image without rescaling. +def image_save_unscaled(surf, filename): + pygame.image.save(surf, renpy.exports.fsencode(filename)) + +# Scales down a surface. +def surface_scale(full): + return full + +real_renpy_pixellate = _renpy.pixellate +real_renpy_transform = _renpy.transform + +def real_smoothscale(src, size, dest=None): + """ + This scales src up or down to size. This uses both the pixellate + and the transform operations to handle the scaling. + """ + + width, height = size + srcwidth, srcheight = src.get_size() + iwidth, iheight = srcwidth, srcheight + + if dest is None: + dest = pgrender.surface_unscaled(size, src) + + if width == 0 or height == 0: + return dest + + xshrink = 1 + yshrink = 1 + + while iwidth >= width * 2: + xshrink *= 2 + iwidth /= 2 + + while iheight >= height * 2: + yshrink *= 2 + iheight /= 2 + + if iwidth != srcwidth or iheight != srcheight: + inter = pgrender.surface_unscaled((iwidth, iheight), src) + real_renpy_pixellate(src, inter, xshrink, yshrink, 1, 1) + src = inter + + real_renpy_transform(src, dest, + 0, 0, + 1.0 * iwidth / width , 0, + 0, 1.0 * iheight / height, + precise=1, + ) + + return dest + +smoothscale = real_smoothscale + diff --git a/unrpyc/renpy/display/screen.py b/unrpyc/renpy/display/screen.py new file mode 100644 index 0000000..8fd529b --- /dev/null +++ b/unrpyc/renpy/display/screen.py @@ -0,0 +1,639 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import renpy.display + +class Screen(renpy.object.Object): + """ + A screen is a collection of widgets that are displayed together. + This class stores information about the screen. + """ + + def __init__(self, + name, + function, + modal="False", + zorder="0", + tag=None, + predict=None, + variant=None, + parameters=False): + + # The name of this screen. + if isinstance(name, str): + name = tuple(name.split()) + + self.name = name + + screens[name[0], variant] = self + + # The function that is called to display this screen. + self.function = function + + # Expression: Are we modal? (A modal screen ignores screens under it.) + self.modal = modal + + # Expression: Our zorder. + self.zorder = zorder + + # The tag associated with the screen. + self.tag = tag or name[0] + + # Can this screen be predicted? + if predict is None: + predict = renpy.config.predict_screens + + self.predict = predict + + # True if this screen takes parameters via _args and _kwargs. + self.parameters = parameters + + +class ScreenDisplayable(renpy.display.layout.Container): + """ + A screen is a collection of widgets that are displayed together. This + class is responsible for managing the display of a screen. + """ + + nosave = [ 'screen', 'child', 'transforms', 'widgets', 'old_widgets', 'old_transforms' ] + + restarting = False + + def after_setstate(self): + self.screen = get_screen_variant(self.screen_name[0]) + self.child = None + self.transforms = { } + self.widgets = { } + self.old_widgets = None + self.old_transforms = None + + def __init__(self, screen, tag, layer, widget_properties={}, scope={}, **properties): + + super(ScreenDisplayable, self).__init__(**properties) + + # Stash the properties, so we can re-create the screen. + self.properties = properties + + # The screen, and it's name. (The name is used to look up the + # screen on save.) + self.screen = screen + self.screen_name = screen.name + + # The tag and layer screen was displayed with. + self.tag = tag + self.layer = layer + + # The scope associated with this statement. This is passed in + # as keyword arguments to the displayable. + self.scope = renpy.python.RevertableDict(scope) + + # The child associated with this screen. + self.child = None + + # Widget properties given to this screen the last time it was + # shown. + self.widget_properties = widget_properties + + # A map from name to the widget with that name. + self.widgets = { } + + if tag and layer: + old_screen = get_screen(tag, layer) + else: + old_screen = None + + # A map from name to the transform with that name. (This is + # taken from the old version of the screen, if it exists. + if old_screen is not None: + self.transforms = old_screen.transforms + else: + self.transforms = { } + + # What widgets and transforms were the last time this screen was + # updated. Used to communicate with the ui module, and only + # valid during an update - not used at other times. + self.old_widgets = None + self.old_transforms = None + + # Should we transfer data from the old_screen? This becomes + # true once this screen finishes updating for the first time, + # and also while we're using something. + self.old_transfers = (old_screen and old_screen.screen_name == self.screen_name) + + # The current transform event, and the last transform event to + # be processed. + self.current_transform_event = None + + # A dict-set of widgets (by id) that have been hidden from us. + self.hidden_widgets = { } + + # Are we hiding? + self.hiding = False + + # Are we restarting? + self.restarting = False + + # Modal and zorder. + self.modal = renpy.python.py_eval(self.screen.modal, locals=self.scope) + self.zorder = renpy.python.py_eval(self.screen.zorder, locals=self.scope) + + def __repr__(self): + return "<ScreenDisplayable: %r>" % (self.screen_name,) + + def visit(self): + return [ self.child ] + + def per_interact(self): + renpy.display.render.redraw(self, 0) + self.update() + + def set_transform_event(self, event): + super(ScreenDisplayable, self).set_transform_event(event) + self.current_transform_event = event + + def find_focusable(self, callback, focus_name): + if self.child and not self.hiding: + self.child.find_focusable(callback, focus_name) + + def _hide(self, st, at, kind): + + if self.hiding: + hid = self + else: + hid = ScreenDisplayable(self.screen, self.tag, self.layer, self.widget_properties, self.scope, **self.properties) + hid.transforms = self.transforms.copy() + hid.widgets = self.widgets.copy() + hid.old_transfers = True + + hid.hiding = True + + hid.current_transform_event = kind + hid.update() + + renpy.display.render.redraw(hid, 0) + + rv = None + + # Compute the reverse of transforms and widgets. + reverse_transforms = dict((id(v), k) for k, v in hid.transforms.items()) + reverse_widgets = dict((id(v), k) for k, v in hid.widgets.items()) + + # Assumption: the only displayables that can keep us around + # are Transforms that handle hide. + + # Iterate over our immediate children, trying to hide them. + for d in list(hid.child.children): + + id_d = id(d) + + # If we have a transform, call its _hide method. If that comes + # back non-None, store the new transform, and keep us alive. + # + # Otherwise, remove the child. + name = reverse_transforms.get(id_d, None) + + if name is not None: + c = d._hide(st, at, kind) + + if c is not None: + hid.transforms[name] = c + rv = hid + else: + hid.hidden_widgets[name] = True + hid.child.remove(d) + + continue + + # Remove any non-transform children. + name = reverse_widgets.get(id_d, None) + + if name is not None: + hid.hidden_widgets[name] = True + hid.child.remove(d) + + return rv + + def update(self): + + # If we're restarting, do not update - the update can use variables + # that are no longer in scope. + if self.restarting: + if not self.child: + self.child = renpy.display.layout.Null() + + return self.widgets + + # Update _current_screen + global _current_screen + old_screen = _current_screen + _current_screen = self + + # Cycle widgets and transforms. + self.old_widgets = self.widgets + self.old_transforms = self.transforms + self.widgets = { } + self.transforms = { } + + # Render the child. + old_ui_screen = renpy.ui.screen + renpy.ui.screen = self + + renpy.ui.detached() + self.child = renpy.ui.fixed(focus="_screen_" + "_".join(self.screen_name)) + self.children = [ self.child ] + + self.scope["_scope"] = self.scope + self.scope["_name"] = 0 + + self.screen.function(**self.scope) + + renpy.ui.close() + + renpy.ui.screen = old_ui_screen + _current_screen = old_screen + + # Visit all the children, to get them started. + self.child.visit_all(lambda c : c.per_interact()) + + # Finish up. + self.old_widgets = None + self.old_transforms = None + self.old_transfers = True + + if self.current_transform_event: + + for i in self.child.children: + i.set_transform_event(self.current_transform_event) + + self.current_transform_event = None + + return self.widgets + + def render(self, w, h, st, at): + + if not self.child: + self.update() + + child = renpy.display.render.render(self.child, w, h, st, at) + + rv = renpy.display.render.Render(w, h) + + rv.blit(child, (0, 0), focus=not self.hiding, main=not self.hiding) + rv.modal = self.modal and not self.hiding + + return rv + + def get_placement(self): + if not self.child: + self.update() + + return self.child.get_placement() + + def event(self, ev, x, y, st): + + if self.hiding: + return + + global _current_screen + old_screen = _current_screen + _current_screen = self + + rv = self.child.event(ev, x, y, st) + + _current_screen = old_screen + + if rv is not None: + return rv + + if self.modal: + raise renpy.display.layout.IgnoreLayers() + + +# The name of the screen that is currently being displayed, or +# None if no screen is being currently displayed. +_current_screen = None + +# A map from (screen_name, variant) tuples to screen. +screens = { } + +def get_screen_variant(name): + """ + Get a variant screen object for `name`. + """ + + for i in renpy.config.variants: + rv = screens.get((name, i), None) + if rv is not None: + return rv + + return None + +def define_screen(*args, **kwargs): + """ + :doc: screens + :args: (name, function, modal="False", zorder="0", tag=None, variant=None) + + Defines a screen with `name`, which should be a string. + + `function` + The function that is called to display the screen. The + function is called with the screen scope as keyword + arguments. It should ignore additional keyword arguments. + + The function should call the ui functions to add things to the + screen. + + `modal` + A string that, when evaluated, determines of the created + screen should be modal. A modal screen prevents screens + underneath it from receiving input events. + + `zorder` + A string that, when evaluated, should be an integer. The integer + controls the order in which screens are displayed. A screen + with a greater zorder number is displayed above screens with a + lesser zorder number. + + `tag` + The tag associated with this screen. When the screen is shown, + it replaces any other screen with the same tag. The tag + defaults to the name of the screen. + + `predict` + If true, this screen can be loaded for image prediction. If false, + it can't. Defaults to true. + + `variant` + String. Gives the variant of the screen to use. + + """ + + Screen(*args, **kwargs) + + + +def get_screen(name, layer="screens"): + """ + :doc: screens + + Returns the ScreenDisplayable with the given `tag`, on + `layer`. If no displayable with the tag is not found, it is + interpreted as screen name. If it's still not found, None is returned. + """ + + if isinstance(name, str): + name = tuple(name.split()) + + tag = name[0] + + sl = renpy.exports.scene_lists() + + sd = sl.get_displayable_by_tag(layer, tag) + + if sd is None: + sd = sl.get_displayable_by_name(layer, name) + + return sd + +def has_screen(name): + """ + Returns true if a screen with the given name exists. + """ + + if not isinstance(name, tuple): + name = tuple(name.split()) + + if not name: + return False + + if get_screen_variant(name[0]): + return True + else: + return False + +def show_screen(_screen_name, *_args, **kwargs): + """ + :doc: screens + + The programmatic equivalent of the show screen statement. + + Shows the named screen. This takes the following keyword arguments: + + `_screen_name` + The name of the screen to show. + `_layer` + The layer to show the screen on. + `_tag` + The tag to show the screen with. If not specified, defaults to + the tag associated with the screen. It that's not specified, + defaults to the name of the screen., + `_widget_properties` + A map from the id of a widget to a property name -> property + value map. When a widget with that id is shown by the screen, + the specified properties are added to it. + `_transient` + If true, the screen will be automatically hidden at the end of + the current interaction. + + Keyword arguments not beginning with underscore (_) are used to + initialize the screen's scope. + """ + + _layer = kwargs.pop("_layer", "screens") + _tag = kwargs.pop("_tag", None) + _widget_properties = kwargs.pop("_widget_properties", {}) + _transient = kwargs.pop("_transient", False) + + name = _screen_name + + if not isinstance(name, tuple): + name = tuple(name.split()) + + screen = get_screen_variant(name[0]) + + if screen is None: + raise Exception("Screen %s is not known.\n" % (name[0],)) + + if _tag is None: + _tag = screen.tag + + scope = { } + + if screen.parameters: + scope["_kwargs" ] = kwargs + scope["_args"] = _args + else: + scope.update(kwargs) + + d = ScreenDisplayable(screen, _tag, _layer, _widget_properties, scope) + renpy.exports.show(name, tag=_tag, what=d, layer=_layer, zorder=d.zorder, transient=_transient, munge_name=False) + + +def predict_screen(_screen_name, *_args, **kwargs): + """ + Predicts the displayables that make up the given screen. + + `_screen_name` + The name of the screen to show. + `_widget_properties` + A map from the id of a widget to a property name -> property + value map. When a widget with that id is shown by the screen, + the specified properties are added to it. + + Keyword arguments not beginning with underscore (_) are used to + initialize the screen's scope. + """ + + _widget_properties = kwargs.pop("_widget_properties", {}) + _scope = kwargs.pop + + kwargs["_kwargs" ] = kwargs.copy() + kwargs["_args"] = _args + + name = _screen_name + + if renpy.config.debug_image_cache: + renpy.display.ic_log.write("Predict screen %s", name) + + if not isinstance(name, tuple): + name = tuple(name.split()) + + screen = get_screen_variant(name[0]) + + scope = { } + + if screen.parameters: + scope["_kwargs" ] = kwargs + scope["_args"] = _args + else: + scope.update(kwargs) + + try: + + if screen is None: + raise Exception("Screen %s is not known.\n" % (name[0],)) + + if not screen.predict: + return + + d = ScreenDisplayable(screen, None, None, _widget_properties, scope) + + d.update() + renpy.display.predict.displayable(d) + + except: + if renpy.config.debug_image_cache: + import traceback + + print("While predicting screen", screen) + traceback.print_exc() + + renpy.ui.reset() + + +def hide_screen(tag, layer='screens'): + """ + :doc: screens + + The programmatic equivalent of the hide screen statement. + + Hides the screen with `tag` on `layer`. + """ + + screen = get_screen(tag, layer) + + if screen is not None: + renpy.exports.hide(screen.tag, layer=layer) + +def use_screen(_screen_name, *_args, **kwargs): + + _name = kwargs.pop("_name", ()) + _scope = kwargs.pop("_scope", { }) + + name = _screen_name + + if not isinstance(name, tuple): + name = tuple(name.split()) + + screen = get_screen_variant(name[0]) + + if screen is None: + raise Exception("Screen %r is not known." % name) + + old_transfers = _current_screen.old_transfers + _current_screen.old_transfers = True + + scope = _scope.copy() + + if screen.parameters: + scope["_kwargs"] = kwargs + scope["_args"] = _args + else: + scope.update(kwargs) + + scope["_scope"] = scope + scope["_name"] = (_name, name) + + screen.function(**scope) + + _current_screen.old_transfers = old_transfers + +def current_screen(): + return _current_screen + +def get_widget(screen, id, layer='screens'): #@ReservedAssignment + """ + :doc: screens + + From the `screen` on `layer`, returns the widget with + `id`. Returns None if the screen doesn't exist, or there is no + widget with that id on the screen. + """ + + if screen is None: + screen = current_screen() + else: + screen = get_screen(screen, layer) + + if not isinstance(screen, ScreenDisplayable): + return None + + if screen.child is None: + screen.update() + + rv = screen.widgets.get(id, None) + return rv + +def before_restart(): + """ + This is called before Ren'Py restarts to put the screens into restart + mode, which prevents crashes due to variables being used that are no + longer defined. + """ + + for k, layer in renpy.display.interface.old_scene.items(): + if k is None: + continue + + for i in layer.children: + if isinstance(i, ScreenDisplayable): + i.restarting = True + diff --git a/unrpyc/renpy/display/swdraw.py b/unrpyc/renpy/display/swdraw.py new file mode 100644 index 0000000..d3ecb51 --- /dev/null +++ b/unrpyc/renpy/display/swdraw.py @@ -0,0 +1,1102 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import renpy.display +import pygame +import math +import weakref +import time +import os + +from renpy.display.render import blit_lock, IDENTITY, BLIT, DISSOLVE, IMAGEDISSOLVE, PIXELLATE + +# A map from cached surface to rle version of cached surface. +rle_cache = weakref.WeakKeyDictionary() + +class Clipper(object): + """ + This is used to calculate the clipping rectangle and update rectangles + used for a particular draw of the screen. + """ + + def __init__(self): + + # Lists of (x0, y0, x1, y1, clip, surface, transform) tuples, + # representing how a displayable is drawn to the screen. + self.blits = [ ] + self.old_blits = [ ] + + # Sets of (x0, y0, x1, y1) tuples, representing areas that + # aren't part of any displayable. + self.forced = set() + self.old_forced = set() + + # The set of surfaces that have been mutated recently. + self.mutated = set() + + def compute(self, full_redraw): + """ + This returns a clipping rectangle, and a list of update rectangles + that cover the changes between the old and new frames. + """ + + # First, get things out of the fields, and update them. This + # allows us to just return without having to do any cleanup + # code. + bl0 = self.old_blits + bl1 = self.blits + old_forced = self.old_forced + forced = self.forced + mutated = self.mutated + + self.old_blits = bl1 + self.blits = [ ] + self.old_forced = forced + self.forced = set() + self.mutated = set() + + sw = renpy.config.screen_width + sh = renpy.config.screen_height + sa = sw * sh + + # A tuple representing the size of the fullscreen. + fullscreen = (0, 0, sw, sh) + + # Check to see if a full redraw has been forced, and return + # early. + if full_redraw: + return fullscreen, [ fullscreen ] + + # Quick checks to see if a dissolve is happening, or something like + # that. + changes = forced | old_forced + + if fullscreen in changes: + return fullscreen, [ fullscreen ] + + # Compute the differences between the two sets, and add those + # to changes. + i0 = 0 + i1 = 0 + bl1set = set(bl1) + + while True: + if i0 >= len(bl0) or i1 >= len(bl1): + break + + b0 = bl0[i0] + b1 = bl1[i1] + + if b0 == b1: + if id(b0[5]) in mutated: + changes.add(b0[:5]) + + i0 += 1 + i1 += 1 + + elif b0 not in bl1set: + changes.add(b0[:5]) + i0 += 1 + + else: + changes.add(b1[:5]) + i1 += 1 + + changes.update(i[:5] for i in bl0[i0:]) + changes.update(i[:5] for i in bl1[i1:]) + + # No changes? Quit. + if not changes: + return None, [ ] + + # Compute the sizes of the updated rectangles. + sized = [ ] + + for x0, y0, x1, y1, (sx0, sy0, sx1, sy1) in changes: + + # Round up by a pixel, to prevent visual artifacts when scaled down. + x1 += 1 + y1 += 1 + + if x0 < sx0: + x0 = sx0 + if y0 < sy0: + y0 = sy0 + if x1 > sx1: + x1 = sx1 + if y1 > sy1: + y1 = sy1 + + w = x1 - x0 + h = y1 - y0 + + if w <= 0 or h <= 0: + continue + + area = w * h + + if area >= sa: + return fullscreen, [ fullscreen ] + + sized.append((area, x0, y0, x1, y1)) + + sized.sort() + + # The list of non-contiguous updates. + noncont = [ ] + + # The total area of noncont. + nca = 0 + + # Pick the largest area, merge with all overlapping smaller areas, repeat + # until no merge possible. + while sized: + area, x0, y0, x1, y1 = sized.pop() + + + merged = False + + if nca + area >= sa: + return (0, 0, sw, sh), [ (0, 0, sw, sh) ] + + i = 0 + + while i < len(sized): + _iarea, ix0, iy0, ix1, iy1 = sized[i] + + if (x0 <= ix0 <= x1 or x0 <= ix1 <= x1) and \ + (y0 <= iy0 <= y1 or y0 <= iy1 <= y1): + + merged = True + x0 = min(x0, ix0) + x1 = max(x1, ix1) + y0 = min(y0, iy0) + y1 = max(y1, iy1) + + area = (x1 - x0) * (y1 - y0) + + sized.pop(i) + + else: + i += 1 + + if merged: + sized.append((area, x0, y0, x1, y1)) + else: + noncont.append((x0, y0, x1, y1)) + nca += area + + if not noncont: + return None, [ ] + + x0, y0, x1, y1 = noncont.pop() + x0 = int(x0) + y0 = int(y0) + x1 = int(math.ceil(x1)) + y1 = int(math.ceil(y1)) + + # A list of (x, y, w, h) tuples for each update. + updates = [ (x0, y0, x1 - x0, y1 - y0) ] + + for ix0, iy0, ix1, iy1 in noncont: + + ix0 = int(ix0) + iy0 = int(iy0) + ix1 = int(math.ceil(ix1)) + iy1 = int(math.ceil(iy1)) + + x0 = min(x0, ix0) + y0 = min(y0, iy0) + x1 = max(x1, ix1) + y1 = max(y1, iy1) + + updates.append((ix0, iy0, ix1 - ix0, iy1 - iy0)) + + return (x0, y0, x1 - x0, y1 - y0), updates + +clippers = [ Clipper() ] + +def surface(w, h, alpha): + """ + Creates a surface that shares a pixel format with the screen. The created + surface will + """ + + if alpha: + rv = pygame.Surface((w + 4, h + 4), pygame.SRCALPHA) + else: + rv = pygame.Surface((w + 4, h + 4), 0) + + return rv.subsurface((2, 2, w, h)) + +def copy_surface(surf): + w, h = surf.get_size() + rv = surface(w, h, True) + + renpy.display.accelerator.nogil_copy(surf, rv) # @UndefinedVariable + return rv + +def draw_special(what, dest, x, y): + """ + This handles the special drawing operations, such as dissolve and + image dissolve. `x` and `y` are the offsets of the thing to be drawn + relative to the destination rectangle, and are always negative. + """ + + dw, dh = dest.get_size() + + w = min(dw, what.width + x) + h = min(dh, what.height + y) + + if w <= 0 or h <= 0: + return + + if what.operation == DISSOLVE: + + bottom = what.children[0][0].render_to_texture(True) + top = what.children[1][0].render_to_texture(True) + + if what.operation_alpha: + target = surface(w, h, True) + else: + target = dest.subsurface((0, 0, w, h)) + + renpy.display.module.blend( + bottom.subsurface((-x, -y, w, h)), + top.subsurface((-x, -y, w, h)), + target, + int(what.operation_complete * 255)) + + if what.operation_alpha: + dest.blit(target, (0, 0)) + + elif what.operation == IMAGEDISSOLVE: + + image = what.children[0][0].render_to_texture(True) + bottom = what.children[1][0].render_to_texture(True) + top = what.children[2][0].render_to_texture(True) + + if what.operation_alpha: + target = surface(w, h, True) + else: + target = dest.subsurface((0, 0, w, h)) + + ramplen = what.operation_parameter + + ramp = "\x00" * 256 + + for i in range(0, ramplen): + ramp += chr(255 * i / ramplen) + + ramp += "\xff" * 256 + + step = int( what.operation_complete * (256 + ramplen) ) + ramp = ramp[step:step+256] + + renpy.display.module.imageblend( + bottom.subsurface((-x, -y, w, h)), + top.subsurface((-x, -y, w, h)), + target, + image.subsurface((-x, -y, w, h)), + ramp) + + if what.operation_alpha: + dest.blit(target, (0, 0)) + + elif what.operation == PIXELLATE: + + surf = what.children[0][0].render_to_texture(False) + + px = what.operation_parameter + + renpy.display.module.pixellate( + surf.subsurface((-x, -y, w, h)), + dest.subsurface((0, 0, w, h)), + px, px, px, px) + + else: + raise Exception("Unknown operation: %d" % what.operation) + + +def draw(dest, clip, what, xo, yo, screen): + """ + This is the simple draw routine, which only works when alpha is 1.0 + and the matrices are None. If those aren't the case, draw_complex + is used instead. + + `dest` - Either a destination surface, or a clipper. + `clip` - If None, we should draw. Otherwise we should clip, and this is + the rectangle to clip to. + `what` - The Render or Surface we're drawing to. + `xo` - The X offset. + `yo` - The Y offset. + `screen` - True if this is a blit to the screen, False otherwise. + """ + + if not isinstance(what, renpy.display.render.Render): + + # Pixel-Aligned blit. + if isinstance(xo, int) and isinstance(yo, int): + if screen: + what = rle_cache.get(what, what) + + if clip: + w, h = what.get_size() + dest.blits.append((xo, yo, xo + w, yo + h, clip, what, None)) + else: + try: + blit_lock.acquire() + dest.blit(what, (xo, yo)) + finally: + blit_lock.release() + + # Subpixel blit. + else: + if clip: + w, h = what.get_size() + dest.blits.append((xo, yo, xo + w, yo + h, clip, what, None)) + else: + renpy.display.module.subpixel(what, dest, xo, yo) + + return + + # Deal with draw functions. + if what.operation != BLIT: + + xo = int(xo) + yo = int(yo) + + if clip: + dx0, dy0, dx1, dy1 = clip + dw = dx1 - dx0 + dh = dy1 - dy0 + else: + dw, dh = dest.get_size() + + if xo >= 0: + newx = 0 + subx = xo + else: + newx = xo + subx = 0 + + if yo >= 0: + newy = 0 + suby = yo + else: + newy = yo + suby = 0 + + if subx >= dw or suby >= dh: + return + + # newx and newy are the offset of this render relative to the + # subsurface. They can only be negative or 0, as otherwise we + # would make a smaller subsurface. + + subw = min(dw - subx, what.width + newx) + subh = min(dh - suby, what.height + newy) + + if subw <= 0 or subh <= 0: + return + + if clip: + dest.forced.add((subx, suby, subx + subw, suby + subh, clip)) + else: + newdest = dest.subsurface((subx, suby, subw, subh)) + # what.draw_func(newdest, newx, newy) + draw_special(what, newdest, newx, newy) + + + return + + # Deal with clipping, if necessary. + if what.clipping: + + if clip: + cx0, cy0, cx1, cy1 = clip + + cx0 = max(cx0, xo) + cy0 = max(cy0, yo) + cx1 = min(cx1, xo + what.width) + cy1 = min(cy1, yo + what.height) + + if cx0 > cx1 or cy0 > cy1: + return + + clip = (cx0, cy0, cx1, cy1) + + dest.forced.add(clip + (clip,)) + return + + else: + + # After this code, x and y are the coordinates of the subsurface + # relative to the destination. xo and yo are the offset of the + # upper-left corner relative to the subsurface. + + if xo >= 0: + x = xo + xo = 0 + else: + x = 0 + # xo = xo + + if yo >= 0: + y = yo + yo = 0 + else: + y = 0 + # yo = yo + + dw, dh = dest.get_size() + + width = min(dw - x, what.width + xo) + height = min(dh - y, what.height + yo) + + if width < 0 or height < 0: + return + + dest = dest.subsurface((x, y, width, height)) + + # Deal with alpha and transforms by passing them off to draw_transformed. + if what.alpha != 1 or (what.forward is not None and what.forward is not IDENTITY): + for child, cxo, cyo, _focus, _main in what.visible_children: + draw_transformed(dest, clip, child, xo + cxo, yo + cyo, + what.alpha, what.forward, what.reverse) + return + + for child, cxo, cyo, _focus, _main in what.visible_children: + draw(dest, clip, child, xo + cxo, yo + cyo, screen) + +def draw_transformed(dest, clip, what, xo, yo, alpha, forward, reverse): + + # If our alpha has hit 0, don't do anything. + if alpha <= 0.003: # (1 / 256) + return + + if forward is None: + forward = IDENTITY + reverse = IDENTITY + + if not isinstance(what, renpy.display.render.Render): + + # Figure out where the other corner of the transformed surface + # is on the screen. + sw, sh = what.get_size() + if clip: + + dx0, dy0, dx1, dy1 = clip + dw = dx1 - dx0 + dh = dy1 - dy0 + + else: + dw, dh = dest.get_size() + + x0, y0 = 0.0, 0.0 + x1, y1 = reverse.transform(sw, 0.0) + x2, y2 = reverse.transform(sw, sh) + x3, y3 = reverse.transform(0.0, sh) + + minx = math.floor(min(x0, x1, x2, x3) + xo) + maxx = math.ceil(max(x0, x1, x2, x3) + xo) + miny = math.floor(min(y0, y1, y2, y3) + yo) + maxy = math.ceil(max(y0, y1, y2, y3) + yo) + + if minx < 0: + minx = 0 + if miny < 0: + miny = 0 + + if maxx > dw: + maxx = dw + if maxy > dh: + maxy = dh + + if minx > dw or miny > dh or maxx < 0 or maxy < 0: + return + + cx, cy = forward.transform(minx - xo, miny - yo) + + if clip: + + dest.blits.append( + (minx, miny, maxx + dx0, maxy + dy0, clip, what, + (cx, cy, + forward.xdx, forward.ydx, + forward.xdy, forward.ydy, + alpha))) + + else: + + dest = dest.subsurface((minx, miny, maxx - minx, maxy - miny)) + + renpy.display.module.transform( + what, dest, + cx, cy, + forward.xdx, forward.ydx, + forward.xdy, forward.ydy, + alpha, True) + + return + + if what.clipping: + + if reverse.xdy or reverse.ydx: + draw_transformed(dest, clip, what.pygame_surface(True), xo, yo, alpha, forward, reverse) + return + + width = what.width * reverse.xdx + height = what.height * reverse.ydy + + if clip: + cx0, cy0, cx1, cy1 = clip + + cx0 = max(cx0, xo) + cy0 = max(cy0, yo) + cx1 = min(cx1, xo + width) + cy1 = min(cy1, yo + height) + + if cx0 > cx1 or cy0 > cy1: + return + + clip = (cx0, cy0, cx1, cy1) + + dest.forced.add(clip + (clip,)) + return + + else: + + # After this code, x and y are the coordinates of the subsurface + # relative to the destination. xo and yo are the offset of the + # upper-left corner relative to the subsurface. + + if xo >= 0: + x = xo + xo = 0 + else: + x = 0 + # xo = xo + + if yo >= 0: + y = yo + yo = 0 + else: + y = 0 + # yo = yo + + dw, dh = dest.get_size() + + width = min(dw - x, width + xo) + height = min(dh - y, height + yo) + + if width < 0 or height < 0: + return + + dest = dest.subsurface((x, y, width, height)) + + if what.draw_func or what.operation != BLIT: + child = what.pygame_surface(True) + draw_transformed(dest, clip, child, xo, yo, alpha, forward, reverse) + return + + for child, cxo, cyo, _focus, _main in what.visible_children: + + cxo, cyo = reverse.transform(cxo, cyo) + + if what.forward: + child_forward = forward * what.forward + child_reverse = what.reverse * reverse + else: + child_forward = forward + child_reverse = reverse + + draw_transformed(dest, clip, child, xo + cxo, yo + cyo, alpha * what.alpha, child_forward, child_reverse) + + + +def do_draw_screen(screen_render, full_redraw, swdraw): + """ + Draws the render produced by render_screen to the screen. + """ + + yoffset = xoffset = 0 + + screen_render.is_opaque() + + clip = (xoffset, yoffset, xoffset + screen_render.width, yoffset + screen_render.height) + clipper = clippers[0] + + draw(clipper, clip, screen_render, xoffset, yoffset, True) + + cliprect, updates = clipper.compute(full_redraw) + + if cliprect is None: + return [ ] + + x, y, _w, _h = cliprect + + dest = swdraw.window.subsurface(cliprect) + draw(dest, None, screen_render, -x, -y, True) + + return updates + + +class SWDraw(object): + """ + This uses the software renderer to draw to the screen. + """ + + def __init__(self): + self.display_info = None + + self.reset() + + def reset(self): + + # Should we draw the screen? + self.suppressed_blit = False + + # The earliest time at which the next frame can be redrawn. + self.next_frame = 0 + + # Mouse re-drawing. + self.mouse_location = None + self.mouse_backing = None + self.mouse_backing_pos = None + self.mouse_info = None + + + # Is the mouse currently visible? + self.mouse_old_visible = None + + # This is used to cache the surface->texture operation. + self.texture_cache = weakref.WeakKeyDictionary() + + # This is used to display video to the screen. + self.fullscreen_surface = None + + # Info. + self.info = { "renderer" : "sw", "resizable" : False } + + pygame.display.init() + renpy.display.interface.post_init() + + if self.display_info is None: + self.display_info = pygame.display.Info() + + # The scale factor we use for this display. + self.scale_factor = 1.0 + + # Should we scale fast, or scale good-looking? + self.scale_fast = "RENPY_SCALE_FAST" in os.environ + + # The screen returned to us from pygame. + self.screen = None + + # The window that we render into, if not the screen. This has a + # 1px border around it iff we're scaling. + self.window = None + + def set_mode(self, virtual_size, physical_size, fullscreen): + + # Reset before resize. + renpy.display.interface.kill_textures_and_surfaces() + self.reset() + + width, height = virtual_size + + # Set up scaling, if necessary. + screen_width = self.display_info.current_w + screen_height = self.display_info.current_h + + if not fullscreen: + screen_height -= 102 + screen_width -= 102 + + scale_factor = min(1.0 * screen_width / width, 1.0 * screen_height / height, 1.0) + if "RENPY_SCALE_FACTOR" in os.environ: + scale_factor = float(os.environ["RENPY_SCALE_FACTOR"]) + self.scale_factor = scale_factor + + # Figure out the fullscreen info. + if fullscreen: + fsflag = pygame.FULLSCREEN + else: + fsflag = 0 + + # If a window exists of the right size and flags, use it. Otherwise, + # make our own window. + old_screen = pygame.display.get_surface() + + scaled_width = int(width * scale_factor) + scaled_height = int(height * scale_factor) + + if ((old_screen is not None) and + (old_screen.get_size() == (scaled_width, scaled_height)) and + (old_screen.get_flags() & pygame.FULLSCREEN == fsflag)): + + self.screen = old_screen + + else: + self.screen = pygame.display.set_mode((scaled_width, scaled_height), fsflag, 32) + + if scale_factor != 1.0: + self.window = surface(width, height, True) + else: + self.window = self.screen + + renpy.display.pgrender.set_rgba_masks() + + # Should we redraw the screen from scratch? + self.full_redraw = True + + # The surface used to display fullscreen video. + self.fullscreen_surface = self.screen + + # Reset this on a mode change. + self.mouse_location = None + self.mouse_backing = None + self.mouse_backing_pos = None + self.mouse_info = None + + return True + + # private + def show_mouse(self, pos, info): + """ + Actually shows the mouse. + """ + + self.mouse_location = pos + self.mouse_info = info + + mxo, myo, tex = info + + mx, my = pos + mw, mh = tex.get_size() + + bx = mx - mxo + by = my - myo + + self.mouse_backing_pos = (bx, by) + self.mouse_backing = surface(mw, mh, False) + self.mouse_backing.blit(self.window, (0, 0), (bx, by, mw, mh)) + + self.screen.blit(tex, (bx, by)) + + return bx, by, mw, mh + + # private + def hide_mouse(self): + """ + Actually hides the mouse. + """ + + size = self.mouse_backing.get_size() + self.screen.blit(self.mouse_backing, self.mouse_backing_pos) + + rv = self.mouse_backing_pos + size + + self.mouse_backing = None + self.mouse_backing_pos = None + self.mouse_location = None + + return rv + + # private + def draw_mouse(self, show_mouse): + """ + This draws the mouse to the screen, if necessary. It uses the + buffer to minimize the amount of the screen that needs to be + drawn, and only redraws if the mouse has actually been moved. + """ + + hardware, x, y, tex = renpy.game.interface.get_mouse_info() + + if self.mouse_old_visible != hardware: + pygame.mouse.set_visible(hardware) + self.mouse_old_visible = hardware + + # The rest of this is for the software mouse. + + if self.suppressed_blit: + return [ ] + + if not show_mouse: + tex = None + + info = (x, y, tex) + pos = pygame.mouse.get_pos() + + if (pos == self.mouse_location and tex and info == self.mouse_info): + return [ ] + + updates = [ ] + + if self.mouse_location: + updates.append(self.hide_mouse()) + + if tex and pos and renpy.game.interface.focused: + updates.append(self.show_mouse(pos, info)) + + return updates + + def update_mouse(self): + """ + Draws the mouse, and then updates the screen. + """ + + updates = self.draw_mouse(True) + + if updates: + pygame.display.update(updates) + + def mouse_event(self, ev): + x, y = getattr(ev, 'pos', pygame.mouse.get_pos()) + + x /= self.scale_factor + y /= self.scale_factor + + return x, y + + def get_mouse_pos(self): + x, y = pygame.mouse.get_pos() + + x /= self.scale_factor + y /= self.scale_factor + + return x, y + + + def screenshot(self, surftree, fullscreen_video): + """ + Returns a pygame surface containing a screenshot. + """ + + return self.window + + def should_redraw(self, needs_redraw, first_pass): + """ + Uses the framerate to determine if we can and should redraw. + """ + + if not needs_redraw: + return False + + framerate = renpy.config.framerate + + if framerate is None: + return True + + next_frame = self.next_frame + now = pygame.time.get_ticks() + + frametime = 1000.0 / framerate + + # Handle timer rollover. + if next_frame > now + frametime: + next_frame = now + + # It's not yet time for the next frame. + if now < next_frame and not first_pass: + return False + + # Otherwise, it is. Schedule the next frame. + # if next_frame + frametime < now: + next_frame = now + frametime + # else: + # next_frame += frametime + + self.next_frame = next_frame + + return True + + + def draw_screen(self, surftree, fullscreen_video): + """ + Draws the screen. + """ + + if not fullscreen_video: + + updates = [ ] + + updates.extend(self.draw_mouse(False)) + + damage = do_draw_screen(surftree, self.full_redraw, self) + + if damage: + updates.extend(damage) + + self.full_redraw = False + + if self.window is self.screen: + + updates.extend(self.draw_mouse(True)) + pygame.display.update(updates) + + else: + + if self.scale_fast: + pygame.transform.scale(self.window, self.screen.get_size(), self.screen) + else: + renpy.display.scale.smoothscale(self.window, self.screen.get_size(), self.screen) + + self.draw_mouse(True) + pygame.display.flip() + + else: + pygame.display.flip() + self.full_redraw = True + + self.suppressed_blit = fullscreen_video + + + def render_to_texture(self, render, alpha): + + rv = surface(render.width, render.height, alpha) + draw(rv, None, render, 0, 0, False) + + return rv + + def is_pixel_opaque(self, what, x, y): + + if x < 0 or y < 0 or x >= what.width or y >= what.height: + return 0 + + for (child, xo, yo, _focus, _main) in what.visible_children: + cx = x - xo + cy = y - yo + + if what.forward: + cx, cy = what.forward.transform(cx, cy) + + + if isinstance(child, renpy.display.render.Render): + if self.is_pixel_opaque(child, x, y): + return True + + else: + cx = int(cx) + cy = int(cy) + + cw, ch = child.get_size() + if cx >= cw or cy >= ch: + return False + + + + if not child.get_masks()[3] or child.get_at((cx, cy))[3]: + return True + + return False + + + def mutated_surface(self, surf): + """ + Called to indicate that the given surface has changed. + """ + + for i in clippers: + i.mutated.add(id(surf)) + + if surf in rle_cache: + del rle_cache[surf] + + + def load_texture(self, surf, transient=False): + """ + Creates a texture from the surface. In the software implementation, + the only difference between a texture and a surface is that a texture + is in the RLE cache. + """ + + surf = copy_surface(surf) + self.mutated_surface(surf) + + if transient: + return surf + + if renpy.game.less_memory: + return surf + + if surf not in rle_cache: + rle_surf = copy_surface(surf) + rle_surf.set_alpha(255, pygame.RLEACCEL) + self.mutated_surface(rle_surf) + + rle_cache[surf] = rle_surf + + return surf + + def solid_texture(self, w, h, color): + """ + Creates a texture filled to the edges with color. + """ + + surf = surface(w + 4, h + 4, True) + surf.fill(color) + self.mutated_surface(surf) + + surf = surf.subsurface((2, 2, w, h)) + + self.mutated_surface(surf) + return surf + + + def free_memory(self): + """ + Frees up memory. + """ + + rle_cache.clear() + + def deinit(self): + """ + Called when we're restarted. + """ + + renpy.display.render.free_memory() + + return + + def quit(self): #@ReservedAssignment + """ + Shuts down the drawing system. + """ + + pygame.display.quit() + + return + + def event_peek_sleep(self): + """ + Wait a little bit so the CPU doesn't speed up. + """ + + time.sleep(.0001) + + def get_physical_size(self): + """ + Return the physical width and height of the screen. + """ + return renpy.config.screen_width, renpy.config.screen_height diff --git a/unrpyc/renpy/display/transition.py b/unrpyc/renpy/display/transition.py new file mode 100644 index 0000000..7f93312 --- /dev/null +++ b/unrpyc/renpy/display/transition.py @@ -0,0 +1,922 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# NOTE: +# Transitions need to be able to work even when old_widget and new_widget +# are None, at least to the point of making it through __init__. This is +# so that prediction of images works. + +import renpy.display +from renpy.display.render import render + + +class Transition(renpy.display.core.Displayable): + """ + This is the base class of most transitions. It takes care of event + dispatching. + """ + + def __init__(self, delay, **properties): + super(Transition, self).__init__(**properties) + self.delay = delay + self.events = True + + def event(self, ev, x, y, st): + + if self.events or ev.type == renpy.display.core.TIMEEVENT: + return self.new_widget.event(ev, x, y, st) # E1101 + else: + return None + + def visit(self): + return [ self.new_widget, self.old_widget ] # E1101 + + +def null_render(d, width, height, st, at): + + d.events = True + surf = renpy.display.render.render(d.new_widget, + width, + height, + st, at) + + rv = renpy.display.render.Render(surf.width, surf.height) + rv.blit(surf, (0, 0)) + + return rv + +class NoTransition(Transition): + """ + :doc: transition function + :args: (delay) + + Returns a transition that only displays the new screen for `delay` seconds. + It can be useful as part of a MultipleTransition. + """ + + def __init__(self, delay, old_widget=None, new_widget=None, **properties): + super(NoTransition, self).__init__(delay, **properties) + + self.old_widget = old_widget + self.new_widget = new_widget + self.events = True + + def render(self, width, height, st, at): + return null_render(self, width, height, st, at) + + +class MultipleTransition(Transition): + """ + :doc: transition function + :args: (args) + + Returns a transition that allows multiple transitions to be displayed, one + after the other. + + `args` + A list containing an odd number of items. The first, third, and + other odd-numbered items must be scenes, and the even items + must be transitions. A scene can be one of: + + * A displayable. + * False, to use the old scene. + * True, to use the new scene. + + Almost always, the first argument will be False and the last True. + + The transitions in `args` are applied in order. For each transition, + the old scene is the screen preceding it, and the new scene is the + scene following it. For example:: + + define logodissolve = MultipleTransition( + False, Dissolve(0.5) + "logo.jpg", NoTransition(1.0), + "logo.jpg", dissolve, + True) + + This example will dissolve to logo.jpg, wait 1 second, and then + dissolve to the new scene. + """ + + def __init__(self, args, old_widget=None, new_widget=None, **properties): + + if len(args) % 2 != 1 or len(args) < 3: + raise Exception("MultipleTransition requires an odd number of arguments, and at least 3 arguments.") + + self.transitions = [ ] + + # The screens that we use for the transition. + self.screens = [ renpy.easy.displayable(i) for i in args[0::2] ] + + def oldnew(w): + if w is False: + return old_widget + if w is True: + return new_widget + + return w + + for old, trans, new in zip(self.screens[0:], args[1::2], self.screens[1:]): + old = oldnew(old) + new = oldnew(new) + + self.transitions.append(trans(old_widget=old, new_widget=new)) + + super(MultipleTransition, self).__init__(sum([i.delay for i in self.transitions]), **properties) + + self.new_widget = self.transitions[-1] + self.events = False + + def visit(self): + return [ i for i in self.screens if isinstance(i, renpy.display.core.Displayable)] + self.transitions + + def event(self, ev, x, y, st): + + if self.events or ev.type == renpy.display.core.TIMEEVENT: + return self.transitions[-1].event(ev, x, y, st) + else: + return None + + def render(self, width, height, st, at): + + if renpy.game.less_updates: + return null_render(self, width, height, st, at) + + for trans in self.transitions[:-1]: + + if trans.delay > st: + break + + st -= trans.delay + + else: + + trans = self.transitions[-1] + self.events = True + + if trans is not self.transitions[-1]: + renpy.display.render.render(self.transitions[-1], width, height, 0, 0) + + surf = renpy.display.render.render(trans, width, height, st, at) + width, height = surf.get_size() + rv = renpy.display.render.Render(width, height) + rv.blit(surf, (0, 0)) + + if st < trans.delay: + renpy.display.render.redraw(self, trans.delay - st) + + return rv + + +def Fade(out_time, + hold_time, + in_time, + old_widget=None, + new_widget=None, + color=None, + widget=None, + alpha=False, + ): + + """ + :doc: transition function + :args: (out_time, hold_time, in_time, color="#000") + :name: Fade + + Returns a transition that takes `out_time` seconds to fade to + a screen filled with `color`, holds at that screen for `hold_time` + seconds, and then takes `in_time` to fade to then new screen. + + :: + + # Fade to black and back. + define fade = Fade(0.5, 0.0, 0.5) + + # Hold at black for a bit. + define fadehold = Fade(0.5, 1.0, 0.5) + + # Camera flash - quickly fades to white, then back to the scene. + define flash = Fade(0.1, 0.0, 0.5, color="#fff") + """ + + dissolve = renpy.curry.curry(Dissolve) + notrans = renpy.curry.curry(NoTransition) + + widget = renpy.easy.displayable_or_none(widget) + + if color: + widget = renpy.display.image.Solid(color) + + if not widget: + widget = renpy.display.image.Solid((0, 0, 0, 255)) + + args = [ False, dissolve(out_time, alpha=alpha), widget ] + + if hold_time: + args.extend([ notrans(hold_time), widget, ]) + + args.extend([dissolve(in_time, alpha=alpha), True ]) + + return MultipleTransition(args, old_widget=old_widget, new_widget=new_widget) + + +class Pixellate(Transition): + """ + :doc: transition function + :args: (time, steps) + :name: Pixellate + + Returns a transition that pixellates out the old screen, and then + pixellates in the new screen. + + `time` + The total time the transition will take, in seconds. + + `steps` + The number of steps that will occur, in each direction. Each step + creates pixels about twice the size of those in the previous step, + so a 5-step pixellation will create 32x32 pixels. + """ + + def __init__(self, time, steps, old_widget=None, new_widget=None, **properties): + + time = float(time) + + super(Pixellate, self).__init__(time, **properties) + + self.time = time + self.steps = steps + + self.old_widget = old_widget + self.new_widget = new_widget + + self.events = False + + self.quantum = time / (2 * steps) + + def render(self, width, height, st, at): + + if renpy.game.less_updates: + return null_render(self, width, height, st, at) + + if st >= self.time: + self.events = True + return render(self.new_widget, width, height, st, at) + + step = st // self.quantum + 1 + visible = self.old_widget + + if step > self.steps: + step = (self.steps * 2) - step + 1 + visible = self.new_widget + self.events = True + + + rdr = render(visible, width, height, st, at) + rv = renpy.display.render.Render(rdr.width, rdr.height) + + rv.blit(rdr, (0, 0)) + + rv.operation = renpy.display.render.PIXELLATE + rv.operation_parameter = 2 ** step + + renpy.display.render.redraw(self, 0) + + return rv + + +class Dissolve(Transition): + """ + :doc: transition function + :args: (time, alpha=False, time_warp=None) + :name: Dissolve + + Returns a transition that dissolves from the old scene to the new scene. + + `time` + The time the dissolve will take. + + `alpha` + If true, the dissolve will alpha-composite the the result of the + transition with the screen. If false, the result of the transition + will replace the screen, which is more efficient. + + `time_warp` + A function that adjusts the timeline. If not None, this should be a + function that takes a fractional time between 0.0 and 1.0, and returns + a number in the same range. + """ + + __version__ = 1 + + def after_upgrade(self, version): + if version < 1: + self.alpha = False + + time_warp = None + + def __init__(self, time, old_widget=None, new_widget=None, alpha=False, time_warp=None, **properties): + super(Dissolve, self).__init__(time, **properties) + + self.time = time + self.old_widget = old_widget + self.new_widget = new_widget + self.events = False + self.alpha = alpha + self.time_warp = time_warp + + + def render(self, width, height, st, at): + + if renpy.game.less_updates: + return null_render(self, width, height, st, at) + + if st >= self.time: + self.events = True + return render(self.new_widget, width, height, st, at) + + complete = min(1.0, st / self.time) + + if self.time_warp is not None: + complete = self.time_warp(complete) + + bottom = render(self.old_widget, width, height, st, at) + top = render(self.new_widget, width, height, st, at) + + width = min(top.width, bottom.width) + height = min(top.height, bottom.height) + + rv = renpy.display.render.Render(width, height, opaque=not self.alpha) + + rv.operation = renpy.display.render.DISSOLVE + rv.operation_alpha = self.alpha + rv.operation_complete = complete + + rv.blit(bottom, (0, 0), focus=False, main=False) + rv.blit(top, (0, 0), focus=True, main=True) + + renpy.display.render.redraw(self, 0) + + return rv + + +class ImageDissolve(Transition): + """ + :doc: transition function + :args: (image, time, ramplen=8, reverse=False, alpha=True, time_warp=None) + :name: ImageDissolve + + Returns a transition that dissolves the old scene into the new scene, using + an image to control the dissolve process. This means that white pixels will + dissolve in first, and black pixels will dissolve in last. + + `image` + A control image to use. This must be either an image file or + image manipulator. The control image should be the size of + the scenes being dissolved. + + `time` + The time the dissolve will take. + + `ramplen` + The length of the ramp to use. This must be an integer power + of 2. When this is the default value of 8, when a white pixel + is fully dissolved, a pixel 8 shades of gray darker will have + completed one step of dissolving in. + + `reverse` + If true, black pixels will dissolve in before white pixels. + + `alpha` + If true, the dissolve will alpha-composite the the result of the + transition with the screen. If false, the result of the transition + will replace the screen, which is more efficient. + + `time_warp` + A function that adjusts the timeline. If not None, this should be a + function that takes a fractional time between 0.0 and 1.0, and returns + a number in the same range. + + :: + + define circirisout = ImageDissolve("circiris.png", 1.0) + define circirisin = ImageDissolve("circiris.png", 1.0, reverse=True) + define circiristbigramp = ImageDissolve("circiris.png", 1.0, ramplen=256) + """ + + __version__ = 1 + + def after_upgrade(self, version): + if version < 1: + self.alpha = False + + time_warp = None + + def __init__( + self, + image, + time, + ramplen=8, + ramptype='linear', + ramp=None, + reverse=False, + alpha=False, + old_widget=None, + new_widget=None, + time_warp=None, + **properties): + + # ramptype and ramp are now unused, but are kept for compatbility with + # older code. + + super(ImageDissolve, self).__init__(time, **properties) + + self.old_widget = old_widget + self.new_widget = new_widget + self.events = False + self.alpha = alpha + self.time_warp = time_warp + + if not reverse: + + # Copies red -> alpha + matrix = renpy.display.im.matrix( + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0) + + else: + + # Copies 1-red -> alpha + matrix = renpy.display.im.matrix( + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + - 1, 0, 0, 0, 1) + + self.image = renpy.display.im.MatrixColor(image, matrix) + + if ramp is not None: + ramplen = len(ramp) + + # The length of the ramp. + self.ramplen = max(ramplen, 1) + + + def visit(self): + return super(ImageDissolve, self).visit() + [ self.image ] + + + def render(self, width, height, st, at): + + if renpy.game.less_updates or renpy.display.less_imagedissolve: + return null_render(self, width, height, st, at) + + if st >= self.delay: + self.events = True + return render(self.new_widget, width, height, st, at) + + image = render(self.image, width, height, st, at) + bottom = render(self.old_widget, width, height, st, at) + top = render(self.new_widget, width, height, st, at) + + width = min(bottom.width, top.width, image.width) + height = min(bottom.height, top.height, image.height) + + rv = renpy.display.render.Render(width, height, opaque=not self.alpha) + + complete = st / self.delay + + if self.time_warp is not None: + complete = self.time_warp(complete) + + rv.operation = renpy.display.render.IMAGEDISSOLVE + rv.operation_alpha = self.alpha + rv.operation_complete = complete + rv.operation_parameter = self.ramplen + + rv.blit(image, (0, 0), focus=False, main=False) + rv.blit(bottom, (0, 0), focus=False, main=False) + rv.blit(top, (0, 0), focus=True, main=True) + + renpy.display.render.redraw(self, 0) + + return rv + + +class AlphaDissolve(Transition): + """ + :doc: transition function + :args: (control, delay=0.0, alpha=False, reverse=False) + + Returns a transition that uses a control displayable (almost always some + sort of animated transform) to transition from one screen to another. The + transform is evaluated. The new screen is used where the transform is + opaque, and the old image is used when it is transparent. + + `control` + The control transform. + + `delay` + The time the transition takes, before ending. + + `alpha` + If true, the image is composited with what's behind it. If false, + the default, the image is opaque and overwrites what's behind it. + + `reverse` + If true, the alpha channel is reversed. Opaque areas are taken + from the old image, while transparent areas are taken from the + new image. + """ + + def __init__( + self, + control, + delay=0.0, + old_widget=None, + new_widget=None, + alpha=False, + reverse=False, + **properties): + + super(AlphaDissolve, self).__init__(delay, **properties) + + self.control = renpy.display.layout.Fixed() + self.control.add(control) + + self.old_widget = renpy.easy.displayable(old_widget) + self.new_widget = renpy.easy.displayable(new_widget) + self.events = False + + self.alpha = alpha + self.reverse = reverse + + def visit(self): + return super(AlphaDissolve, self).visit() + [ self.control ] + + def render(self, width, height, st, at): + + if renpy.game.less_updates or renpy.display.less_imagedissolve: + return null_render(self, width, height, st, at) + + if st >= self.delay: + self.events = True + + bottom = render(self.old_widget, width, height, st, at) + top = render(self.new_widget, width, height, st, at) + + width = min(bottom.width, top.width) + height = min(bottom.height, top.height) + + control = render(self.control, width, height, st, at) + + rv = renpy.display.render.Render(width, height, opaque=not self.alpha) + + rv.operation = renpy.display.render.IMAGEDISSOLVE + rv.operation_alpha = self.alpha + rv.operation_complete = 256.0 / (256.0 + 256.0) + rv.operation_parameter = 256 + + rv.blit(control, (0, 0), focus=False, main=False) + + if not self.reverse: + rv.blit(bottom, (0, 0), focus=False, main=False) + rv.blit(top, (0, 0), focus=True, main=True) + else: + rv.blit(top, (0, 0), focus=True, main=True) + rv.blit(bottom, (0, 0), focus=False, main=False) + + return rv + + +class CropMove(Transition): + """ + :doc: transition function + :args: (time, mode="slideright", startcrop=(0.0, 0.0, 0.0, 1.0), startpos=(0.0, 0.0), endcrop=(0.0, 0.0, 1.0, 1.0), endpos=(0.0, 0.0), topnew=True) + :name: CropMove + + Returns a transition that works by cropping a scene and positioning it on the + screen. This can be used to implement a variety of effects, all of which + involved changing rectangular slices of scenes. + + `time` + The time the transition takes. + + `mode` + The name of the mode of the transition. There are three groups + of modes: wipes, slides, and other. This can also be "custom", + to allow a custom mode to be defined. + + In a wipe, the image stays fixed, and more of it is revealed as + the transition progresses. For example, in "wiperight", a wipe from left to right, first the left edge of the image is + revealed at the left edge of the screen, then the center of the image, + and finally the right side of the image at the right of the screen. + Other supported wipes are "wipeleft", "wipedown", and "wipeup". + + In a slide, the image moves. So in a "slideright", the right edge of the + image starts at the left edge of the screen, and moves to the right + as the transition progresses. Other slides are "slideleft", "slidedown", + and "slideup". + + There are also slideaways, in which the old image moves on top of + the new image. Slideaways include "slideawayright", "slideawayleft", + "slideawayup", and "slideawaydown". + + We also support a rectangular iris in with "irisin" and a + rectangular iris out with "irisout". + + The following parameters are only respected if the mode is "custom". Positions + are relative to the size of the screen, while the crops are relative to the + size of the image. So a crop of (0.25, 0.0, 0.5, 1.0) takes the middle + half of an image. + + `startcrop` + The starting rectangle that is cropped out of the + top image. A 4-element tuple containing x, y, width, and height. + + `startpos` + The starting place that the top image is drawn + to the screen at, a 2-element tuple containing x and y. + + `endcrop` + The ending rectangle that is cropped out of the + top image. A 4-element tuple containing x, y, width, and height. + + `endpos` + The ending place that the top image is drawn + to the screen at, a 2-element tuple containing x and y. + + `topnew` + If true, the scene that is cropped and moved (and is on top of + the other scene) is the new scene. If false, it is the old scene. + + :: + + define wiperight = CropMove(1.0, "wiperight") + define wipeleft = CropMove(1.0, "wipeleft") + define wipeup = CropMove(1.0, "wipeup") + define wipedown = CropMove(1.0, "wipedown") + + define slideright = CropMove(1.0, "slideright") + define slideleft = CropMove(1.0, "slideleft") + define slideup = CropMove(1.0, "slideup") + define slidedown = CropMove(1.0, "slidedown") + + define slideawayright = CropMove(1.0, "slideawayright") + define slideawayleft = CropMove(1.0, "slideawayleft") + define slideawayup = CropMove(1.0, "slideawayup") + define slideawaydown = CropMove(1.0, "slideawaydown") + + define irisout = CropMove(1.0, "irisout") + define irisin = CropMove(1.0, "irisin") + """ + + def __init__(self, time, + mode="slideright", + startcrop=(0.0, 0.0, 0.0, 1.0), + startpos=(0.0, 0.0), + endcrop=(0.0, 0.0, 1.0, 1.0), + endpos=(0.0, 0.0), + topnew=True, + old_widget=None, + new_widget=None, + **properties): + + super(CropMove, self).__init__(time, **properties) + self.time = time + + if mode == "wiperight": + startpos = (0.0, 0.0) + startcrop = (0.0, 0.0, 0.0, 1.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "wipeleft": + startpos = (1.0, 0.0) + startcrop = (1.0, 0.0, 0.0, 1.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "wipedown": + startpos = (0.0, 0.0) + startcrop = (0.0, 0.0, 1.0, 0.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "wipeup": + startpos = (0.0, 1.0) + startcrop = (0.0, 1.0, 1.0, 0.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "slideright": + startpos = (0.0, 0.0) + startcrop = (1.0, 0.0, 0.0, 1.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "slideleft": + startpos = (1.0, 0.0) + startcrop = (0.0, 0.0, 0.0, 1.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "slideup": + startpos = (0.0, 1.0) + startcrop = (0.0, 0.0, 1.0, 0.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "slidedown": + startpos = (0.0, 0.0) + startcrop = (0.0, 1.0, 1.0, 0.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "slideawayleft": + endpos = (0.0, 0.0) + endcrop = (1.0, 0.0, 0.0, 1.0) + startpos = (0.0, 0.0) + startcrop = (0.0, 0.0, 1.0, 1.0) + topnew = False + + elif mode == "slideawayright": + endpos = (1.0, 0.0) + endcrop = (0.0, 0.0, 0.0, 1.0) + startpos = (0.0, 0.0) + startcrop = (0.0, 0.0, 1.0, 1.0) + topnew = False + + elif mode == "slideawaydown": + endpos = (0.0, 1.0) + endcrop = (0.0, 0.0, 1.0, 0.0) + startpos = (0.0, 0.0) + startcrop = (0.0, 0.0, 1.0, 1.0) + topnew = False + + elif mode == "slideawayup": + endpos = (0.0, 0.0) + endcrop = (0.0, 1.0, 1.0, 0.0) + startpos = (0.0, 0.0) + startcrop = (0.0, 0.0, 1.0, 1.0) + topnew = False + + elif mode == "irisout": + startpos = (0.5, 0.5) + startcrop = (0.5, 0.5, 0.0, 0.0) + endpos = (0.0, 0.0) + endcrop = (0.0, 0.0, 1.0, 1.0) + topnew = True + + elif mode == "irisin": + startpos = (0.0, 0.0) + startcrop = (0.0, 0.0, 1.0, 1.0) + endpos = (0.5, 0.5) + endcrop = (0.5, 0.5, 0.0, 0.0) + topnew = False + + + elif mode == "custom": + pass + else: + raise Exception("Invalid mode %s passed into CropMove." % mode) + + self.delay = time + self.time = time + + self.startpos = startpos + self.endpos = endpos + + self.startcrop = startcrop + self.endcrop = endcrop + + self.topnew = topnew + + self.old_widget = old_widget + self.new_widget = new_widget + + self.events = False + + if topnew: + self.bottom = old_widget + self.top = new_widget + else: + self.bottom = new_widget + self.top = old_widget + + def render(self, width, height, st, at): + + if renpy.game.less_updates: + return null_render(self, width, height, st, at) + + time = 1.0 * st / self.time + + # Done rendering. + if time >= 1.0: + self.events = True + return render(self.new_widget, width, height, st, at) + + # How we scale each element of a tuple. + scales = (width, height, width, height) + + def interpolate_tuple(t0, t1): + return tuple([ int(s * (a * (1.0 - time) + b * time)) + for a, b, s in zip(t0, t1, scales) ]) + + crop = interpolate_tuple(self.startcrop, self.endcrop) + pos = interpolate_tuple(self.startpos, self.endpos) + + + top = render(self.top, width, height, st, at) + bottom = render(self.bottom, width, height, st, at) + + width = min(bottom.width, width) + height = min(bottom.height, height) + rv = renpy.display.render.Render(width, height) + + rv.blit(bottom, (0, 0), focus=not self.topnew) + + ss = top.subsurface(crop, focus=self.topnew) + rv.blit(ss, pos, focus=self.topnew) + + renpy.display.render.redraw(self, 0) + return rv + + +def ComposeTransition(trans, before=None, after=None, new_widget=None, old_widget=None): + """ + :doc: transition function + :args: (trans, before, after) + + Returns a transition that composes up to three transitions. If not None, + the `before` and `after` transitions are applied to the old and new + scenes, respectively. These updated old and new scenes are then supplied + to the `trans` transition. + + :: + + # Move the images in and out while dissolving. (This is a fairly expensive transition.) + define moveinoutdissolve = ComposeTransition(dissolve, before=moveoutleft, after=moveinright) + """ + + if before is not None: + old = before(new_widget=new_widget, old_widget=old_widget) + else: + old = old_widget + + if after is not None: + new = after(new_widget=new_widget, old_widget=old_widget) + else: + new = new_widget + + return trans(new_widget=new, old_widget=old) + + +def SubTransition(rect, trans, old_widget=None, new_widget=None, **properties): + """ + Applies a transition to a subset of the screen. Not documented. + """ + + x, y, _w, _h = rect + + old = renpy.display.layout.LiveCrop(rect, old_widget) + new = renpy.display.layout.LiveCrop(rect, new_widget) + + inner = trans(old_widget=old, new_widget=new) + delay = inner.delay + inner = renpy.display.layout.Position(inner, xpos=x, ypos=y, xanchor=0, yanchor=0) + + f = renpy.display.layout.MultiBox(layout='fixed') + f.add(new_widget) + f.add(inner) + + return NoTransition(delay, old_widget=f, new_widget=f) + diff --git a/unrpyc/renpy/display/video.py b/unrpyc/renpy/display/video.py new file mode 100644 index 0000000..5451e89 --- /dev/null +++ b/unrpyc/renpy/display/video.py @@ -0,0 +1,234 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import renpy.display +import renpy.audio + +# The movie displayable that's currently being shown on the screen. +current_movie = None + +# True if the movie that is currently displaying is in fullscreen mode, +# False if it's a smaller size. +fullscreen = False + +# The size of a Movie object that hasn't had an explicit size set. +default_size = (400, 300) + +# The file we allocated the surface for. +surface_file = None + +# The surface to display the movie on, if not fullscreen. +surface = None + +def movie_stop(clear=True, only_fullscreen=False): + """ + Stops the currently playing movie. + """ + + if (not fullscreen) and only_fullscreen: + return + + renpy.audio.music.stop(channel='movie') + + +def movie_start(filename, size=None, loops=0): + """ + This starts a movie playing. + """ + + if renpy.game.less_updates: + return + + global default_size + + if size is not None: + default_size = size + + filename = [ filename ] + + if loops == -1: + loop = True + else: + loop = False + filename = filename * (loops + 1) + + renpy.audio.music.play(filename, channel='movie', loop=loop) + +movie_start_fullscreen = movie_start +movie_start_displayable = movie_start + +def early_interact(): + """ + Called early in the interact process, to clear out the fullscreen + flag. + """ + + global fullscreen + global current_movie + + fullscreen = True + current_movie = None + + +def interact(): + """ + This is called each time the screen is redrawn. It helps us decide if + the movie should be displayed fullscreen or not. + """ + + global surface + global surface_file + + if not renpy.audio.music.get_playing("movie"): + surface = None + surface_file = None + return False + + if fullscreen: + return True + else: + return False + +def get_movie_texture(): + """ + Gets a movie texture we can draw to the screen. + """ + + global surface + global surface_file + + playing = renpy.audio.music.get_playing("movie") + + pss = renpy.audio.audio.pss + + if pss: + size = pss.movie_size() + else: + size = (64, 64) + + if (surface is None) or (surface.get_size() != size) or (surface_file != playing): + surface = renpy.display.pgrender.surface(size, False) + surface_file = playing + surface.fill((0, 0, 0, 255)) + + tex = None + + if playing is not None: + renpy.display.render.mutated_surface(surface) + tex = renpy.display.draw.load_texture(surface, True) + + return tex + + +def render_movie(width, height): + tex = get_movie_texture() + + if tex is None: + return None + + sw, sh = tex.get_size() + + scale = min(1.0 * width / sw, 1.0 * height / sh) + + dw = scale * sw + dh = scale * sh + + rv = renpy.display.render.Render(width, height, opaque=True) + rv.forward = renpy.display.render.Matrix2D(1.0 / scale, 0.0, 0.0, 1.0 / scale) + rv.reverse = renpy.display.render.Matrix2D(scale, 0.0, 0.0, scale) + rv.blit(tex, (int((width - dw) / 2), int((height - dh) / 2))) + + return rv + +class Movie(renpy.display.core.Displayable): + """ + This is a displayable that shows the current movie. + """ + + fullscreen = False + + def __init__(self, fps=24, size=None, **properties): + """ + @param fps: The framerate that the movie should be shown at. + """ + super(Movie, self).__init__(**properties) + self.size = size + + def render(self, width, height, st, at): + + size = self.size + + if size is None: + size = default_size + + width, height = size + + rv = render_movie(width, height) + + if rv is None: + rv = renpy.display.render.Render(0, 0) + + # Usually we get redrawn when the frame is ready - but we want + # the movie to disappear if it's ended, or if it hasn't started + # yet. + renpy.display.render.redraw(self, 0.1) + + return rv + + + def per_interact(self): + global fullscreen + fullscreen = False + + global current_movie + current_movie = self + + +def playing(): + return renpy.audio.music.get_playing("movie") + +def frequent(): + """ + Called to update the video playback. Returns true if a video refresh is + needed, false otherwise. + """ + + if not playing(): + return 0 + + pss = renpy.audio.audio.pss + + if pss.needs_alloc(): + + if renpy.display.video.fullscreen and renpy.display.draw.fullscreen_surface: + surf = renpy.display.draw.fullscreen_surface + else: + get_movie_texture() + surf = renpy.display.scale.real(surface) + + pss.alloc_event(surf) + + rv = pss.refresh_event() + + if rv and current_movie is not None: + renpy.display.render.redraw(current_movie, 0) + + return rv |