# Copyright 2004-2013 Tom Rothamel # # 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