# Copyright 2004-2021 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. from __future__ import division, absolute_import, with_statement, print_function, unicode_literals from renpy.compat import * # 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.layout import Container from renpy.display.accelerator import transform_render from renpy.atl import position, any_object, bool_or_none, float_or_none, matrix, mesh # 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() renpy.display.motion.null = 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): last_angle = None def __init__(self): # Most fields on this object are set by add_property, at the bottom # of this file. # An xpos (etc) inherited from our child overrides an xpos inherited # from an old transform, but not an xpos set in the current transform. # # inherited_xpos stores the inherited_xpos, which is overridden by the # xpos, if not None. self.inherited_xpos = None self.inherited_ypos = None self.inherited_xanchor = None self.inherited_yanchor = None # The last angle that was rotated to. self.last_angle = None def take_state(self, ts): d = self.__dict__ for k in all_properties: d[k] = getattr(ts, k) self.last_angle = ts.last_angle # Set the position and anchor to None, so inheritance works. if self.perspective is None: self.xpos = None self.ypos = None self.xanchor = None self.yanchor = None # Take the computed position properties, not the # raw ones. (self.inherited_xpos, self.inherited_ypos, self.inherited_xanchor, self.inherited_yanchor, _, _, _) = ts.get_placement() self.xoffset = ts.xoffset self.yoffset = ts.yoffset self.subpixel = ts.subpixel # 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 = { } for prop in diff2_properties: new = getattr(newts, prop) old = getattr(self, prop) if new != old: rv[prop] = (old, new) for prop in diff4_properties: new = getattr(newts, prop) old = getattr(self, prop) if new is None: new = getattr(newts, "inherited_" + prop) if old is None: old = getattr(self, "inherited_" + prop) if new != old: rv[prop] = (old, new) return rv def get_placement(self, cxoffset=0, cyoffset=0): if self.perspective is not None: return ( 0, 0, 0, 0, cxoffset, cyoffset, False, ) return ( first_not_none(self.xpos, self.inherited_xpos), first_not_none(self.ypos, self.inherited_ypos), first_not_none(self.xanchor, self.inherited_xanchor), first_not_none(self.yanchor, self.inherited_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.inherited_xpos, 0) ypos = first_not_none(self.ypos, self.inherited_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.inherited_xpos, 0) ypos = first_not_none(self.ypos, self.inherited_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.inherited_xpos, 0) ypos = first_not_none(self.ypos, self.inherited_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.inherited_xpos, 0) ypos = first_not_none(self.ypos, self.inherited_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 get_xysize(self): return self.xsize, self.ysize def set_xysize(self, value): if value is None: value = (None, None) self.xsize, self.ysize = value xysize = property(get_xysize, set_xysize) def set_size(self, value): if value is None: self.xysize = None else: self.xysize = tuple(int(x) if isinstance(x, float) else x for x in value) size = property(get_xysize, set_size) 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) def get_xycenter(self): return self.xcenter, self.ycenter def set_xycenter(self, value): if value is None: value = (None, None) self.xcenter, self.ycenter = value xycenter = property(get_xycenter, set_xycenter) 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 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 # Default before we set this. child_size = (0, 0) def __init__(self, child=None, function=None, style="default", focus=None, default=False, _args=None, **kwargs): self.kwargs = kwargs self.style_arg = style super(Transform, self).__init__(style=style, focus=focus, default=default, _args=_args) self.function = function child = renpy.easy.displayable_or_none(child) if child is not None: self.add(child) self.state = TransformState() if kwargs: # A map from prefix -> (prop -> value) self.arguments = { } # Fill self.arguments with a for k, v in kwargs.items(): prefix = "" prop = k while True: if prop in renpy.atl.PROPERTIES and (not prefix or prefix in Transform.DEFAULT_ARGUMENTS): if prefix not in self.arguments: self.arguments[prefix] = { } self.arguments[prefix][prop] = v break new_prefix, _, prop = prop.partition("_") if not prop: raise Exception("Unknown transform property: %r" % k) if prefix: prefix = prefix + "_" + new_prefix else: prefix = new_prefix if "" in self.arguments: for k, v in self.arguments[""].items(): setattr(self.state, k, v) else: self.arguments = None # 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): if self.arguments is None: return None prefix = self.style.prefix.strip("_") prefixes = [ ] while prefix: prefixes.insert(0, prefix) _, _, prefix = prefix.partition("_") prefixes.insert(0, "") for i in prefixes: d = self.arguments.get(i, None) if d is None: continue for k, v in d.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) self.last_child_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. """ if self is t: return if not isinstance(t, Transform): return self.state.take_state(t.state) if isinstance(self.child, Transform) and isinstance(t.child, Transform): self.child.take_state(t.child) if (self.child is None) and (t.child is not None): self.add(t.child) self.child_st_base = t.child_st_base # 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. """ if self is t: return if not isinstance(t, Transform): return 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 self.child_st_base = t.child_st_base 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 _handles_event(self, event): if (event == "replaced") and (not self.active): return True if self.function is not None: return True if self.child and self.child._handles_event(event): return True return False def _hide(self, st, at, kind): # 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 if not self.active: self.update_state() 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 isinstance(self, ATLTransform): d.atl_st_offset = self.atl_st_offset if (self.atl_st_offset is not None) else self.st_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, at) elif isinstance(d, ATLTransform): d.execute(d, st, at) new_child = d.child._hide(st - self.st_offset, at - self.st_offset, 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, duplicate=True): child = renpy.easy.displayable(child) if duplicate and child._duplicatable: child = child._duplicate(self._args) child._unique() if child._duplicatable: self._duplicatable = True self.child = child self.children = [ child ] self.child_st_base = self.st child.per_interact() renpy.display.render.invalidate(self) def update_state(self): """ This updates the state to that at self.st, self.at. """ # NOTE: This function is duplicated (more or less) in ATLTransform. self.hide_response = True self.replaced_response = True # If we have to, call the function that updates this transform. if self.arguments is not None: self.default_function(self, self.st, self.at) if self.function is not None: fr = self.function(self, self.st, self.at) # Order a redraw, if necessary. if fr is not None: renpy.display.render.redraw(self, fr) self.active = True # The render method is now defined in accelerator.pyx. def render(self, width, height, st, at): return transform_render(self, width, height, st, at) def event(self, ev, x, y, st): if self.hide_request: return None if not self.state.events: return 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, _args=None): if child is None: child = self.child if (child is not None) and (child._duplicatable): child = child._duplicate(_args) rv = Transform( child=child, function=self.function, style=self.style_arg, _args=_args, **self.kwargs) rv.take_state(self) return rv def _unique(self): if self.child and self.child._duplicatable: self._duplicatable = True else: self._duplicatable = False 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() # Use non-None elements of the child placement as defaults. state = self.state if renpy.config.transform_uses_child_position: if cxpos is not None: state.inherited_xpos = cxpos if cxanchor is not None: state.inherited_xanchor = cxanchor if cypos is not None: state.inherited_ypos = cypos if cyanchor is not None: state.inherited_yanchor = cyanchor state.subpixel |= csubpixel 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 xanchor.__class__ is float: xanchor *= cw if yanchor.__class__ is 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) _duplicatable = True def _duplicate(self, args): if args and args.args: args.extraneous() if not self._duplicatable: return self rv = self(_args=args) rv.take_execution_state(self) rv._unique() return rv def _in_current_store(self): if self.child is None: return self child = self.child._in_current_store() if child is self.child: return self rv = self() rv.take_execution_state(self) rv.child = child rv._unique() return rv 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, **properties) self.raw_child = self.child def update_state(self): """ This updates the state to that at self.st, self.at. """ self.hide_response = True self.replaced_response = True fr = self.execute(self, self.st, self.at) # Order a redraw, if necessary. if fr is not None: renpy.display.render.redraw(self, fr) self.active = True def __repr__(self): return "".format(id(self), self.atl.loc) # Names of transform properties, and if the property should be handles with # diff2 or diff2. all_properties = set() diff2_properties = set() diff4_properties = set() # Uniforms and GL properties. uniforms = set() gl_properties = set() def add_property(name, atl=any_object, default=None, diff=2): """ Adds an ATL property. """ if name in all_properties: return all_properties.add(name) setattr(TransformState, name, default) setattr(Transform, name, Proxy(name)) renpy.atl.PROPERTIES[name] = atl if diff == 2: diff2_properties.add(name) elif diff == 4: diff4_properties.add(name) def add_uniform(name): """ Adds a uniform with `name` to Transform and ATL. """ if not name.startswith("u_"): return if name in renpy.gl2.gl2draw.standard_uniforms: return add_property(name, diff=2) uniforms.add(name) def add_gl_property(name): """ Adds a GL property with `name` to Transform and ATL. """ add_property(name, diff=None) gl_properties.add(name) add_property("additive", float, 0.0) add_property("alpha", float, 1.0) add_property("blend", any_object, None) add_property("blur", float_or_none, None) add_property("corner1", (float, float), None) add_property("corner2", (float, float), None) add_property("crop", (float, float, float, float), None) add_property("crop_relative", bool, False) add_property("debug", any_object, None) add_property("delay", float, 0) add_property("events", bool, True) add_property("fit", str, None) add_property("matrixanchor", (position, position), None) add_property("matrixcolor", matrix, None) add_property("matrixtransform", matrix, None) add_property("maxsize", (int, int), None) add_property("mesh", mesh, False, diff=None) add_property("mesh_pad", any_object, None) add_property("nearest", bool_or_none, None) add_property("perspective", any_object, None) add_property("rotate", float, None) add_property("rotate_pad", bool, True) add_property("shader", any_object, None, diff=None) add_property("subpixel", bool, False) add_property("transform_anchor", bool, False) add_property("zoom", float, 1.0) add_property("xanchoraround", float, 0.0) add_property("xanchor", position, None, diff=4) add_property("xaround", position, 0.0) add_property("xoffset", float, 0.0) add_property("xpan", float_or_none, None) add_property("xpos", position, None, diff=4) add_property("xsize", position, None) add_property("xtile", int, 1) add_property("xzoom", float, 1.0) add_property("yanchoraround", float, 0.0) add_property("yanchor", position, None, diff=4) add_property("yaround", position, 0.0) add_property("yoffset", float, 0.0) add_property("ypan", float_or_none, None) add_property("ypos", position, None, diff=4) add_property("ysize", position, None) add_property("ytile", int, 1) add_property("yzoom", float, 1.0) add_property("zpos", float, 0.0) add_property("zzoom", bool, False) add_gl_property("gl_anisotropic") add_gl_property("gl_blend_func") add_gl_property("gl_color_mask") add_gl_property("gl_depth") add_gl_property("gl_mipmap") add_gl_property("gl_pixel_perfect") add_gl_property("gl_texture_scaling") add_gl_property("gl_texture_wrap") ALIASES = { "alignaround" : (float, float), "align" : (float, float), "anchor" : (position, position), "angle" : float, "around" : (position, position), "offset" : (int, int), "pos" : (position, position), "radius" : float, "size" : (int, int), "xalign" : float, "xcenter" : position, "xycenter" : (position, position), "xysize" : (position, position), "yalign" : float, "ycenter" : position, } renpy.atl.PROPERTIES.update(ALIASES) for name in ALIASES: setattr(Transform, name, Proxy(name))