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/anim.py | |
parent | ece6cf9fbfdba9dac8d7bf98516a840c955a4853 (diff) | |
download | html5ks-718936110b9511631fa1f4396be992752bf8b719.tar.xz html5ks-718936110b9511631fa1f4396be992752bf8b719.zip |
include renpy
Diffstat (limited to 'unrpyc/renpy/display/anim.py')
-rw-r--r-- | unrpyc/renpy/display/anim.py | 634 |
1 files changed, 634 insertions, 0 deletions
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) |