1# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us>
2#
3# Permission is hereby granted, free of charge, to any person
4# obtaining a copy of this software and associated documentation files
5# (the "Software"), to deal in the Software without restriction,
6# including without limitation the rights to use, copy, modify, merge,
7# publish, distribute, sublicense, and/or sell copies of the Software,
8# and to permit persons to whom the Software is furnished to do so,
9# subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be
12# included in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22# This file contains displayables that move, zoom, rotate, or otherwise
23# transform displayables. (As well as displayables that support them.)
24
25from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
26from renpy.compat import *
27
28# Some imports are here to handle pickles of a moved class.
29from renpy.display.transform import Transform, Proxy, TransformState, ATLTransform, null  # @UnusedImport
30
31import math
32
33import renpy.display
34
35from renpy.display.render import render
36from renpy.display.layout import Container
37
38
39class Motion(Container):
40    """
41    This is used to move a child displayable around the screen. It
42    works by supplying a time value to a user-supplied function,
43    which is in turn expected to return a pair giving the x and y
44    location of the upper-left-hand corner of the child, or a
45    4-tuple giving that and the xanchor and yanchor of the child.
46
47    The time value is a floating point number that ranges from 0 to
48    1. If repeat is True, then the motion repeats every period
49    sections. (Otherwise, it stops.) If bounce is true, the
50    time value varies from 0 to 1 to 0 again.
51
52    The function supplied needs to be pickleable, which means it needs
53    to be defined as a name in an init block. It cannot be a lambda or
54    anonymous inner function. If you can get away with using Pan or
55    Move, use them instead.
56
57    Please note that floats and ints are interpreted as for xpos and
58    ypos, with floats being considered fractions of the screen.
59    """
60
61    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):
62        """
63        @param child: The child displayable.
64
65        @param new_widget: If child is None, it is set to new_widget,
66        so that we can speak the transition protocol.
67
68        @param old_widget: Ignored, for compatibility with the transition protocol.
69
70        @param function: A function that takes a floating point value and returns
71        an xpos, ypos tuple.
72
73        @param period: The amount of time it takes to go through one cycle, in seconds.
74
75        @param repeat: Should we repeat after a period is up?
76
77        @param bounce: Should we bounce?
78
79        @param delay: How long this motion should take. If repeat is None, defaults to period.
80
81        @param anim_timebase: If True, use the animation timebase rather than the shown timebase.
82
83        @param time_warp: If not None, this is a function that takes a
84        fraction of the period (between 0.0 and 1.0), and returns a
85        new fraction of the period. Use this to warp time, applying
86        acceleration and deceleration to motions.
87
88        This can also be used as a transition. When used as a
89        transition, the motion is applied to the new_widget for delay
90        seconds.
91        """
92
93        if child is None:
94            child = new_widget
95
96        if delay is None and not repeat:
97            delay = period
98
99        super(Motion, self).__init__(style=style, **properties)
100
101        if child is not None:
102            self.add(child)
103
104        self.function = function
105        self.period = period
106        self.repeat = repeat
107        self.bounce = bounce
108        self.delay = delay
109        self.anim_timebase = anim_timebase
110        self.time_warp = time_warp
111        self.add_sizes = add_sizes
112
113        self.position = None
114
115    def update_position(self, t, sizes):
116
117        if renpy.game.less_updates:
118            if self.delay:
119                t = self.delay
120                if self.repeat:
121                    t = t % self.period
122            else:
123                t = self.period
124        elif self.delay and t >= self.delay:
125            t = self.delay
126            if self.repeat:
127                t = t % self.period
128        elif self.repeat:
129            t = t % self.period
130            renpy.display.render.redraw(self, 0)
131        else:
132            if t > self.period:
133                t = self.period
134            else:
135                renpy.display.render.redraw(self, 0)
136
137        if self.period > 0:
138            t /= self.period
139        else:
140            t = 1
141
142        if self.time_warp:
143            t = self.time_warp(t)
144
145        if self.bounce:
146            t = t * 2
147            if t > 1.0:
148                t = 2.0 - t
149
150        if self.add_sizes:
151            res = self.function(t, sizes)
152        else:
153            res = self.function(t)
154
155        res = tuple(res)
156
157        if len(res) == 2:
158            self.position = res + (self.style.xanchor or 0, self.style.yanchor or 0)
159        else:
160            self.position = res
161
162    def get_placement(self):
163
164        if self.position is None:
165            if self.add_sizes:
166                # Almost certainly gives the wrong placement, but there's nothing
167                # we can do.
168                return super(Motion, self).get_placement()
169            else:
170                self.update_position(0.0, None)
171
172        return self.position + (self.style.xoffset, self.style.yoffset, self.style.subpixel)
173
174    def render(self, width, height, st, at):
175
176        if self.anim_timebase:
177            t = at
178        else:
179            t = st
180
181        child = render(self.child, width, height, st, at)
182        cw, ch = child.get_size()
183
184        self.update_position(t, (width, height, cw, ch))
185
186        rv = renpy.display.render.Render(cw, ch)
187        rv.blit(child, (0, 0))
188
189        self.offsets = [ (0, 0) ]
190
191        return rv
192
193
194class Interpolate(object):
195
196    anchors = {
197        'top' : 0.0,
198        'center' : 0.5,
199        'bottom' : 1.0,
200        'left' : 0.0,
201        'right' : 1.0,
202        }
203
204    def __init__(self, start, end):
205
206        if len(start) != len(end):
207            raise Exception("The start and end must have the same number of arguments.")
208
209        self.start = [ self.anchors.get(i, i) for i in start ]
210        self.end = [ self.anchors.get(i, i) for i in end ]
211
212    def __call__(self, t, sizes=(None, None, None, None)):
213
214        types = (renpy.atl.position,) * len(self.start)
215        return renpy.atl.interpolate(t, tuple(self.start), tuple(self.end), types)
216
217
218def Pan(startpos, endpos, time, child=None, repeat=False, bounce=False,
219        anim_timebase=False, style='motion', time_warp=None, **properties):
220    """
221    This is used to pan over a child displayable, which is almost
222    always an image. It works by interpolating the placement of the
223    upper-left corner of the screen, over time. It's only really
224    suitable for use with images that are larger than the screen,
225    and we don't do any cropping on the image.
226
227    @param startpos: The initial coordinates of the upper-left
228    corner of the screen, relative to the image.
229
230    @param endpos: The coordinates of the upper-left corner of the
231    screen, relative to the image, after time has elapsed.
232
233    @param time: The time it takes to pan from startpos to endpos.
234
235    @param child: The child displayable.
236
237    @param repeat: True if we should repeat this forever.
238
239    @param bounce: True if we should bounce from the start to the end
240    to the start.
241
242    @param anim_timebase: True if we use the animation timebase, False to use the
243    displayable timebase.
244
245    @param time_warp: If not None, this is a function that takes a
246    fraction of the period (between 0.0 and 1.0), and returns a
247    new fraction of the period. Use this to warp time, applying
248    acceleration and deceleration to motions.
249
250    This can be used as a transition. See Motion for details.
251    """
252
253    x0, y0 = startpos
254    x1, y1 = endpos
255
256    return Motion(Interpolate((-x0, -y0), (-x1, -y1)),
257                  time,
258                  child,
259                  repeat=repeat,
260                  bounce=bounce,
261                  style=style,
262                  anim_timebase=anim_timebase,
263                  time_warp=time_warp,
264                  **properties)
265
266
267def Move(startpos, endpos, time, child=None, repeat=False, bounce=False,
268         anim_timebase=False, style='motion', time_warp=None, **properties):
269    """
270    This is used to pan over a child displayable relative to
271    the containing area. It works by interpolating the placement of the
272    the child, over time.
273
274    @param startpos: The initial coordinates of the child
275    relative to the containing area.
276
277    @param endpos: The coordinates of the child at the end of the
278    move.
279
280    @param time: The time it takes to move from startpos to endpos.
281
282    @param child: The child displayable.
283
284    @param repeat: True if we should repeat this forever.
285
286    @param bounce: True if we should bounce from the start to the end
287    to the start.
288
289    @param anim_timebase: True if we use the animation timebase, False to use the
290    displayable timebase.
291
292    @param time_warp: If not None, this is a function that takes a
293    fraction of the period (between 0.0 and 1.0), and returns a
294    new fraction of the period. Use this to warp time, applying
295    acceleration and deceleration to motions.
296
297    This can be used as a transition. See Motion for details.
298    """
299
300    return Motion(Interpolate(startpos, endpos),
301                  time,
302                  child,
303                  repeat=repeat,
304                  bounce=bounce,
305                  anim_timebase=anim_timebase,
306                  style=style,
307                  time_warp=time_warp,
308                  **properties)
309
310
311class Revolver(object):
312
313    def __init__(self, start, end, child, around=(0.5, 0.5), cor=(0.5, 0.5), pos=None):
314        self.start = start
315        self.end = end
316        self.around = around
317        self.cor = cor
318        self.pos = pos
319        self.child = child
320
321    def __call__(self, t, rect):
322
323        (w, h, cw, ch) = rect
324
325        # Converts a float to an integer in the given range, passes
326        # integers through unchanged.
327        def fti(x, r):
328            if x is None:
329                x = 0
330
331            if isinstance(x, float):
332                return int(x * r)
333            else:
334                return x
335
336        if self.pos is None:
337            pos = self.child.get_placement()
338        else:
339            pos = self.pos
340
341        xpos, ypos, xanchor, yanchor, _xoffset, _yoffset, _subpixel = pos
342
343        xpos = fti(xpos, w)
344        ypos = fti(ypos, h)
345        xanchor = fti(xanchor, cw)
346        yanchor = fti(yanchor, ch)
347
348        xaround, yaround = self.around
349
350        xaround = fti(xaround, w)
351        yaround = fti(yaround, h)
352
353        xcor, ycor = self.cor
354
355        xcor = fti(xcor, cw)
356        ycor = fti(ycor, ch)
357
358        angle = self.start + (self.end - self.start) * t
359        angle *= math.pi / 180
360
361        # The center of rotation, relative to the xaround.
362        x = xpos - xanchor + xcor - xaround
363        y = ypos - yanchor + ycor - yaround
364
365        # Rotate it.
366        nx = x * math.cos(angle) - y * math.sin(angle)
367        ny = x * math.sin(angle) + y * math.cos(angle)
368
369        # Project it back.
370        nx = nx - xcor + xaround
371        ny = ny - ycor + yaround
372
373        return (renpy.display.core.absolute(nx), renpy.display.core.absolute(ny), 0, 0)
374
375
376def Revolve(start, end, time, child, around=(0.5, 0.5), cor=(0.5, 0.5), pos=None, **properties):
377
378    return Motion(Revolver(start, end, child, around=around, cor=cor, pos=pos),
379                  time,
380                  child,
381                  add_sizes=True,
382                  **properties)
383
384
385def zoom_render(crend, x, y, w, h, zw, zh, bilinear):
386    """
387    This creates a render that zooms its child.
388
389    `crend` - The render of the child.
390    `x`, `y`, `w`, `h` - A rectangle inside the child.
391    `zw`, `zh` - The size the rectangle is rendered to.
392    `bilinear` - Should we be rendering in bilinear mode?
393    """
394
395    rv = renpy.display.render.Render(zw, zh)
396
397    if zw == 0 or zh == 0 or w == 0 or h == 0:
398        return rv
399
400    rv.forward = renpy.display.render.Matrix2D(w / zw, 0, 0, h / zh)
401    rv.reverse = renpy.display.render.Matrix2D(zw / w, 0, 0, zh / h)
402
403    rv.xclipping = True
404    rv.yclipping = True
405
406    rv.blit(crend, rv.reverse.transform(-x, -y))
407
408    return rv
409
410
411class ZoomCommon(renpy.display.core.Displayable):
412
413    def __init__(self,
414                 time, child,
415                 end_identity=False,
416                 after_child=None,
417                 time_warp=None,
418                 bilinear=True,
419                 opaque=True,
420                 anim_timebase=False,
421                 repeat=False,
422                 style='motion',
423                 **properties):
424        """
425        @param time: The amount of time it will take to
426        interpolate from the start to the end rectange.
427
428        @param child: The child displayable.
429
430        @param after_child: If present, a second child
431        widget. This displayable will be rendered after the zoom
432        completes. Use this to snap to a sharp displayable after
433        the zoom is done.
434
435        @param time_warp: If not None, this is a function that takes a
436        fraction of the period (between 0.0 and 1.0), and returns a
437        new fraction of the period. Use this to warp time, applying
438        acceleration and deceleration to motions.
439        """
440
441        super(ZoomCommon, self).__init__(style=style, **properties)
442
443        child = renpy.easy.displayable(child)
444
445        self.time = time
446        self.child = child
447        self.repeat = repeat
448
449        if after_child:
450            self.after_child = renpy.easy.displayable(after_child)
451        else:
452            if end_identity:
453                self.after_child = child
454            else:
455                self.after_child = None
456
457        self.time_warp = time_warp
458        self.bilinear = bilinear
459        self.opaque = opaque
460        self.anim_timebase = anim_timebase
461
462    def visit(self):
463        return [ self.child, self.after_child ]
464
465    def render(self, width, height, st, at):
466
467        if self.anim_timebase:
468            t = at
469        else:
470            t = st
471
472        if self.time:
473            done = min(t / self.time, 1.0)
474        else:
475            done = 1.0
476
477        if self.repeat:
478            done = done % 1.0
479
480        if renpy.game.less_updates:
481            done = 1.0
482
483        self.done = done
484
485        if self.after_child and done == 1.0:
486            return renpy.display.render.render(self.after_child, width, height, st, at)
487
488        if self.time_warp:
489            done = self.time_warp(done)
490
491        rend = renpy.display.render.render(self.child, width, height, st, at)
492
493        rx, ry, rw, rh, zw, zh = self.zoom_rectangle(done, rend.width, rend.height)
494
495        if rx < 0 or ry < 0 or rx + rw > rend.width or ry + rh > rend.height:
496            raise Exception("Zoom rectangle %r falls outside of %dx%d parent surface." % ((rx, ry, rw, rh), rend.width, rend.height))
497
498        rv = zoom_render(rend, rx, ry, rw, rh, zw, zh, self.bilinear)
499
500        if self.done < 1.0:
501            renpy.display.render.redraw(self, 0)
502
503        return rv
504
505    def event(self, ev, x, y, st):
506
507        if not self.time:
508            done = 1.0
509        else:
510            done = min(st / self.time, 1.0)
511
512        if done == 1.0 and self.after_child:
513            return self.after_child.event(ev, x, y, st)
514        else:
515            return None
516
517
518class Zoom(ZoomCommon):
519
520    def __init__(self, size, start, end, time, child, **properties):
521
522        end_identity = (end == (0.0, 0.0) + size)
523
524        super(Zoom, self).__init__(time, child, end_identity=end_identity, **properties)
525
526        self.size = size
527        self.start = start
528        self.end = end
529
530    def zoom_rectangle(self, done, width, height):
531
532        rx, ry, rw, rh = [ (a + (b - a) * done) for a, b in zip(self.start, self.end) ]
533
534        return rx, ry, rw, rh, self.size[0], self.size[1]
535
536
537class FactorZoom(ZoomCommon):
538
539    def __init__(self, start, end, time, child, **properties):
540
541        end_identity = (end == 1.0)
542
543        super(FactorZoom, self).__init__(time, child, end_identity=end_identity, **properties)
544
545        self.start = start
546        self.end = end
547
548    def zoom_rectangle(self, done, width, height):
549
550        factor = self.start + (self.end - self.start) * done
551
552        return 0, 0, width, height, factor * width, factor * height
553
554
555class SizeZoom(ZoomCommon):
556
557    def __init__(self, start, end, time, child, **properties):
558
559        end_identity = False
560
561        super(SizeZoom, self).__init__(time, child, end_identity=end_identity, **properties)
562
563        self.start = start
564        self.end = end
565
566    def zoom_rectangle(self, done, width, height):
567
568        sw, sh = self.start
569        ew, eh = self.end
570
571        zw = sw + (ew - sw) * done
572        zh = sh + (eh - sh) * done
573
574        return 0, 0, width, height, zw, zh
575
576
577class RotoZoom(renpy.display.core.Displayable):
578
579    transform = None
580
581    def __init__(self,
582                 rot_start,
583                 rot_end,
584                 rot_delay,
585                 zoom_start,
586                 zoom_end,
587                 zoom_delay,
588                 child,
589                 rot_repeat=False,
590                 zoom_repeat=False,
591                 rot_bounce=False,
592                 zoom_bounce=False,
593                 rot_anim_timebase=False,
594                 zoom_anim_timebase=False,
595                 rot_time_warp=None,
596                 zoom_time_warp=None,
597                 opaque=False,
598                 style='motion',
599                 **properties):
600
601        super(RotoZoom, self).__init__(style=style, **properties)
602
603        self.rot_start = rot_start
604        self.rot_end = rot_end
605        self.rot_delay = rot_delay
606
607        self.zoom_start = zoom_start
608        self.zoom_end = zoom_end
609        self.zoom_delay = zoom_delay
610
611        self.child = renpy.easy.displayable(child)
612
613        self.rot_repeat = rot_repeat
614        self.zoom_repeat = zoom_repeat
615
616        self.rot_bounce = rot_bounce
617        self.zoom_bounce = zoom_bounce
618
619        self.rot_anim_timebase = rot_anim_timebase
620        self.zoom_anim_timebase = zoom_anim_timebase
621
622        self.rot_time_warp = rot_time_warp
623        self.zoom_time_warp = zoom_time_warp
624
625        self.opaque = opaque
626
627    def visit(self):
628        return [ self.child ]
629
630    def render(self, width, height, st, at):
631
632        if self.rot_anim_timebase:
633            rot_time = at
634        else:
635            rot_time = st
636
637        if self.zoom_anim_timebase:
638            zoom_time = at
639        else:
640            zoom_time = st
641
642        if self.rot_delay == 0:
643            rot_time = 1.0
644        else:
645            rot_time /= self.rot_delay
646
647        if self.zoom_delay == 0:
648            zoom_time = 1.0
649        else:
650            zoom_time /= self.zoom_delay
651
652        if self.rot_repeat:
653            rot_time %= 1.0
654
655        if self.zoom_repeat:
656            zoom_time %= 1.0
657
658        if self.rot_bounce:
659            rot_time *= 2
660            rot_time = min(rot_time, 2.0 - rot_time)
661
662        if self.zoom_bounce:
663            zoom_time *= 2
664            zoom_time = min(zoom_time, 2.0 - zoom_time)
665
666        if renpy.game.less_updates:
667            rot_time = 1.0
668            zoom_time = 1.0
669
670        rot_time = min(rot_time, 1.0)
671        zoom_time = min(zoom_time, 1.0)
672
673        if self.rot_time_warp:
674            rot_time = self.rot_time_warp(rot_time)
675
676        if self.zoom_time_warp:
677            zoom_time = self.zoom_time_warp(zoom_time)
678
679        angle = self.rot_start + (1.0 * self.rot_end - self.rot_start) * rot_time
680        zoom = self.zoom_start + (1.0 * self.zoom_end - self.zoom_start) * zoom_time
681        # angle = -angle * math.pi / 180
682
683        zoom = max(zoom, 0.001)
684
685        if self.transform is None:
686            self.transform = Transform(self.child)
687
688        self.transform.rotate = angle
689        self.transform.zoom = zoom
690
691        rv = renpy.display.render.render(self.transform, width, height, st, at)
692
693        if rot_time <= 1.0 or zoom_time <= 1.0:
694            renpy.display.render.redraw(self.transform, 0)
695
696        return rv
697
698
699# For compatibility with old games.
700renpy.display.layout.Transform = Transform
701renpy.display.layout.RotoZoom = RotoZoom
702renpy.display.layout.SizeZoom = SizeZoom
703renpy.display.layout.FactorZoom = FactorZoom
704renpy.display.layout.Zoom = Zoom
705renpy.display.layout.Revolver = Revolver
706renpy.display.layout.Motion = Motion
707renpy.display.layout.Interpolate = Interpolate
708
709# Leave these functions around - they might have been pickled somewhere.
710renpy.display.layout.Revolve = Revolve  # function
711renpy.display.layout.Move = Move  # function
712renpy.display.layout.Pan = Pan  # function
713