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# NOTE:
23# Transitions need to be able to work even when old_widget and new_widget
24# are None, at least to the point of making it through __init__. This is
25# so that prediction of images works.
26
27from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
28from renpy.compat import *
29
30import renpy.display
31
32# Utility function used by MoveTransition et al.
33
34
35def position(d):
36
37    xpos, ypos, xanchor, yanchor, _xoffset, _yoffset, _subpixel = d.get_placement()
38
39    if xpos is None:
40        xpos = 0
41    if ypos is None:
42        ypos = 0
43    if xanchor is None:
44        xanchor = 0
45    if yanchor is None:
46        yanchor = 0
47
48    return xpos, ypos, xanchor, yanchor
49
50
51def offsets(d):
52
53    _xpos, _ypos, _xanchor, _yanchor, xoffset, yoffset, _subpixel = d.get_placement()
54
55    if renpy.config.movetransition_respects_offsets:
56        return { 'xoffset' : xoffset, 'yoffset' : yoffset }
57    else:
58        return { }
59
60
61# These are used by MoveTransition.
62def MoveFactory(pos1, pos2, delay, d, **kwargs):
63    if pos1 == pos2:
64        return d
65
66    return renpy.display.motion.Move(pos1, pos2, delay, d, **kwargs)
67
68
69def default_enter_factory(pos, delay, d, **kwargs):
70    return d
71
72
73def default_leave_factory(pos, delay, d, **kwargs):
74    return None
75
76# These can be used to move things in and out of the screen.
77
78
79def MoveIn(pos, pos1, delay, d, **kwargs):
80
81    def aorb(a, b):
82        if a is None:
83            return b
84        return a
85
86    pos = tuple([aorb(a, b) for a, b in zip(pos, pos1)])
87    return renpy.display.motion.Move(pos, pos1, delay, d, **kwargs)
88
89
90def MoveOut(pos, pos1, delay, d, **kwargs):
91
92    def aorb(a, b):
93        if a is None:
94            return b
95        return a
96
97    pos = tuple([aorb(a, b) for a, b in zip(pos, pos1)])
98    return renpy.display.motion.Move(pos1, pos, delay, d, **kwargs)
99
100
101def ZoomInOut(start, end, pos, delay, d, **kwargs):
102
103    xpos, ypos, xanchor, yanchor = pos
104
105    FactorZoom = renpy.display.motion.FactorZoom
106
107    if end == 1.0:
108        return FactorZoom(start, end, delay, d, after_child=d, opaque=False,
109                          xpos=xpos, ypos=ypos, xanchor=xanchor, yanchor=yanchor, **kwargs)
110    else:
111        return FactorZoom(start, end, delay, d, opaque=False,
112                          xpos=xpos, ypos=ypos, xanchor=xanchor, yanchor=yanchor, **kwargs)
113
114
115def RevolveInOut(start, end, pos, delay, d, **kwargs):
116    return renpy.display.motion.Revolve(start, end, delay, d, pos=pos, **kwargs)
117
118
119def OldMoveTransition(delay, old_widget=None, new_widget=None, factory=None, enter_factory=None, leave_factory=None, old=False, layers=[ 'master' ]):
120    """
121    Returns a transition that attempts to find images that have changed
122    position, and moves them from the old position to the new transition, taking
123    delay seconds to complete the move.
124
125    If `factory` is given, it is expected to be a function that takes as
126    arguments: an old position, a new position, the delay, and a
127    displayable, and to return a displayable as an argument. If not
128    given, the default behavior is to move the displayable from the
129    starting to the ending positions. Positions are always given as
130    (xpos, ypos, xanchor, yanchor) tuples.
131
132    If `enter_factory` or `leave_factory` are given, they are expected
133    to be functions that take as arguments a position, a delay, and a
134    displayable, and return a displayable. They are applied to
135    displayables that are entering or leaving the scene,
136    respectively. The default is to show in place displayables that
137    are entering, and not to show those that are leaving.
138
139    If `old` is True, then factory moves the old displayable with the
140    given tag. Otherwise, it moves the new displayable with that
141    tag.
142
143    `layers` is a list of layers that the transition will be applied
144    to.
145
146    Images are considered to be the same if they have the same tag, in
147    the same way that the tag is used to determine which image to
148    replace or to hide. They are also considered to be the same if
149    they have no tag, but use the same displayable.
150
151    Computing the order in which images are displayed is a three-step
152    process. The first step is to create a list of images that
153    preserves the relative ordering of entering and moving images. The
154    second step is to insert the leaving images such that each leaving
155    image is at the lowest position that is still above all images
156    that were below it in the original scene. Finally, the list
157    is sorted by zorder, to ensure no zorder violations occur.
158
159    If you use this transition to slide an image off the side of the
160    screen, remember to hide it when you are done. (Or just use
161    a leave_factory.)
162    """
163
164    if factory is None:
165        factory = MoveFactory
166
167    if enter_factory is None:
168        enter_factory = default_enter_factory
169
170    if leave_factory is None:
171        leave_factory = default_leave_factory
172
173    use_old = old
174
175    def merge_slide(old, new):
176
177        # If new does not have .layers or .scene_list, then we simply
178        # insert a move from the old position to the new position, if
179        # a move occured.
180
181        if (not isinstance(new, renpy.display.layout.MultiBox)
182                or (new.layers is None and new.layer_name is None)):
183
184            if use_old:
185                child = old
186            else:
187                child = new
188
189            old_pos = position(old)
190            new_pos = position(new)
191
192            if old_pos != new_pos:
193                return factory(old_pos,
194                               new_pos,
195                               delay,
196                               child,
197                               **offsets(child)
198                               )
199
200            else:
201                return child
202
203        # If we're in the layers_root widget, merge the child widgets
204        # for each layer.
205        if new.layers:
206
207            rv = renpy.display.layout.MultiBox(layout='fixed')
208            rv.layers = { }
209
210            for layer in renpy.config.layers:
211
212                f = new.layers[layer]
213
214                if (isinstance(f, renpy.display.layout.MultiBox)
215                    and layer in layers
216                        and f.scene_list is not None):
217
218                    f = merge_slide(old.layers[layer], new.layers[layer])
219
220                rv.layers[layer] = f
221                rv.add(f)
222
223            return rv
224
225        # Otherwise, we recompute the scene list for the two widgets, merging
226        # as appropriate.
227
228        # Wraps the displayable found in SLE so that the various timebases
229        # are maintained.
230        def wrap(sle):
231            return renpy.display.layout.AdjustTimes(sle.displayable, sle.show_time, sle.animation_time)
232
233        def tag(sle):
234            return sle.tag or sle.displayable
235
236        def merge(sle, d):
237            rv = sle.copy()
238            rv.show_time = 0
239            rv.displayable = d
240            return rv
241
242        def entering(sle):
243            new_d = wrap(new_sle)
244            move = enter_factory(position(new_d), delay, new_d, **offsets(new_d))
245
246            if move is None:
247                return
248
249            rv_sl.append(merge(new_sle, move))
250
251        def leaving(sle):
252            old_d = wrap(sle)
253            move = leave_factory(position(old_d), delay, old_d, **offsets(old_d))
254
255            if move is None:
256                return
257
258            move = renpy.display.layout.IgnoresEvents(move)
259            rv_sl.append(merge(old_sle, move))
260
261        def moving(old_sle, new_sle):
262            old_d = wrap(old_sle)
263            new_d = wrap(new_sle)
264
265            if use_old:
266                child = old_d
267            else:
268                child = new_d
269
270            move = factory(position(old_d), position(new_d), delay, child, **offsets(child))
271            if move is None:
272                return
273
274            rv_sl.append(merge(new_sle, move))
275
276        # The old, new, and merged scene_lists.
277        old_sl = old.scene_list[:]
278        new_sl = new.scene_list[:]
279        rv_sl = [ ]
280
281        # A list of tags in old_sl, new_sl, and rv_sl.
282        old_map = dict((tag(i), i) for i in old_sl if i is not None)
283        new_tags = set(tag(i) for i in new_sl if i is not None)
284        rv_tags = set()
285
286        while old_sl or new_sl:
287
288            # If we have something in old_sl, then
289            if old_sl:
290
291                old_sle = old_sl[0]
292                old_tag = tag(old_sle)
293
294                # If the old thing has already moved, then remove it.
295                if old_tag in rv_tags:
296                    old_sl.pop(0)
297                    continue
298
299                # If the old thing does not match anything in new_tags,
300                # have it enter.
301                if old_tag not in new_tags:
302                    leaving(old_sle)
303                    rv_tags.add(old_tag)
304                    old_sl.pop(0)
305                    continue
306
307            # Otherwise, we must have something in new_sl. We want to
308            # either move it or have it enter.
309
310            new_sle = new_sl.pop(0)
311            new_tag = tag(new_sle)
312
313            # If it exists in both, move.
314            if new_tag in old_map:
315                old_sle = old_map[new_tag]
316
317                moving(old_sle, new_sle)
318                rv_tags.add(new_tag)
319                continue
320
321            else:
322                entering(new_sle)
323                rv_tags.add(new_tag)
324                continue
325
326        # Sort everything by zorder, to ensure that there are no zorder
327        # violations in the result.
328        rv_sl.sort(key=lambda a : a.zorder)
329
330        layer = new.layer_name
331        rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **renpy.game.interface.layer_properties[layer])
332        rv.append_scene_list(rv_sl)
333        rv.layer_name = layer
334
335        return rv
336
337    # This calls merge_slide to actually do the merging.
338
339    rv = merge_slide(old_widget, new_widget)
340    rv.delay = delay # W0201
341
342    return rv
343
344##############################################################################
345# New Move Transition (since 6.14)
346
347
348class MoveInterpolate(renpy.display.core.Displayable):
349    """
350    This displayable has two children. It interpolates between the positions
351    of its two children to place them on the screen.
352    """
353
354    def __init__(self, delay, old, new, use_old, time_warp):
355        super(MoveInterpolate, self).__init__()
356
357        # The old and new displayables.
358        self.old = old
359        self.new = new
360
361        # Should we display the old displayable?
362        self.use_old = use_old
363
364        # Time warp function or None.
365        self.time_warp = time_warp
366
367        # The width of the screen.
368        self.screen_width = 0
369        self.screen_height = 0
370
371        # The width of the selected child.
372        self.child_width = 0
373        self.child_height = 0
374
375        # The delay and st.
376        self.delay = delay
377        self.st = 0
378
379    def render(self, width, height, st, at):
380        self.screen_width = width
381        self.screen_height = height
382
383        old_r = renpy.display.render.render(self.old, width, height, st, at)
384        new_r = renpy.display.render.render(self.new, width, height, st, at)
385
386        if self.use_old:
387            cr = old_r
388        else:
389            cr = new_r
390
391        self.child_width, self.child_height = cr.get_size()
392        self.st = st
393
394        if self.st < self.delay:
395            renpy.display.render.redraw(self, 0)
396
397        return cr
398
399    def child_placement(self, child):
400
401        def based(v, base):
402            if v is None:
403                return 0
404            elif isinstance(v, int):
405                return v
406            elif isinstance(v, renpy.display.core.absolute):
407                return v
408            else:
409                return v * base
410
411        xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = child.get_placement()
412
413        xpos = based(xpos, self.screen_width)
414        ypos = based(ypos, self.screen_height)
415        xanchor = based(xanchor, self.child_width)
416        yanchor = based(yanchor, self.child_height)
417
418        return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel
419
420    def get_placement(self):
421
422        if self.st > self.delay:
423            done = 1.0
424        else:
425            done = self.st / self.delay
426
427        if self.time_warp is not None:
428            done = self.time_warp(done)
429
430        absolute = renpy.display.core.absolute
431
432        def I(a, b):
433            return absolute(a + done * (b - a))
434
435        old_xpos, old_ypos, old_xanchor, old_yanchor, old_xoffset, old_yoffset, old_subpixel = self.child_placement(self.old)
436        new_xpos, new_ypos, new_xanchor, new_yanchor, new_xoffset, new_yoffset, new_subpixel = self.child_placement(self.new)
437
438        xpos = I(old_xpos, new_xpos)
439        ypos = I(old_ypos, new_ypos)
440        xanchor = I(old_xanchor, new_xanchor)
441        yanchor = I(old_yanchor, new_yanchor)
442        xoffset = I(old_xoffset, new_xoffset)
443        yoffset = I(old_yoffset, new_yoffset)
444        subpixel = old_subpixel or new_subpixel
445
446        return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel
447
448
449def MoveTransition(delay, old_widget=None, new_widget=None, enter=None, leave=None, old=False, layers=[ 'master' ], time_warp=None, enter_time_warp=None, leave_time_warp=None):
450    """
451    :doc: transition function
452    :args: (delay, enter=None, leave=None, old=False, layers=['master'], time_warp=None, enter_time_warp=None, leave_time_warp=None)
453    :name: MoveTransition
454
455    Returns a transition that interpolates the position of images (with the
456    same tag) in the old and new scenes.
457
458    `delay`
459        The time it takes for the interpolation to finish.
460
461    `enter`
462        If not None, images entering the scene will also be moved. The value
463        of `enter` should be a transform that is applied to the image to
464        get its starting position.
465
466    `leave`
467        If not None, images leaving the scene will also be move. The value
468        of `leave` should be a transform that is applied to the image to
469        get its ending position.
470
471    `old`
472        If true, the old image will be used in preference to the new one.
473
474    `layers`
475        A list of layers that moves are applied to.
476
477    `time_warp`
478        A time warp function that's applied to the interpolation. This
479        takes a number between 0.0 and 1.0, and should return a number in
480        the same range.
481
482    `enter_time_warp`
483        A time warp function that's applied to images entering the scene.
484
485    `leave_time_warp`
486        A time warp function that's applied to images leaving the scene.
487
488    """
489
490    use_old = old
491
492    def merge_slide(old, new, merge_slide):
493
494        # This function takes itself as an argument to prevent a reference
495        # loop that occurs when it refers to itself in the it's parent's
496        # scope.
497
498        # If new does not have .layers or .scene_list, then we simply
499        # insert a move from the old position to the new position, if
500        # a move occured.
501
502        if (not isinstance(new, renpy.display.layout.MultiBox)
503                or (new.layers is None and new.layer_name is None)):
504
505            if old is new:
506                return new
507            else:
508                return MoveInterpolate(delay, old, new, use_old, time_warp)
509
510        # If we're in the layers_root widget, merge the child widgets
511        # for each layer.
512        if new.layers:
513
514            rv = renpy.display.layout.MultiBox(layout='fixed')
515
516            rv.raw_layers = { }
517            rv.layers = { }
518
519            for layer in renpy.config.layers:
520
521                f = new.layers[layer]
522                d = new.raw_layers[layer]
523
524                if (isinstance(d, renpy.display.layout.MultiBox)
525                    and layer in layers
526                    and d.scene_list is not None):
527
528                    d = merge_slide(old.raw_layers[layer], new.raw_layers[layer], merge_slide)
529
530                    adjust = renpy.display.layout.AdjustTimes(d, None, None)
531                    f = renpy.game.context().scene_lists.transform_layer(layer, adjust)
532
533                    if f is adjust:
534                        f = d
535                    else:
536                        f = renpy.display.layout.MatchTimes(f, adjust)
537
538                rv.raw_layers[layer] = d
539                rv.layers[layer] = f
540                rv.add(f)
541
542            return rv
543
544        # Otherwise, we recompute the scene list for the two widgets, merging
545        # as appropriate.
546
547        # Wraps the displayable found in SLE so that the various timebases
548        # are maintained.
549        def wrap(sle):
550            return renpy.display.layout.AdjustTimes(sle.displayable, sle.show_time, sle.animation_time)
551
552        def tag(sle):
553            return sle.tag or sle.displayable
554
555        def merge(sle, d):
556            rv = sle.copy()
557            rv.show_time = 0
558            rv.displayable = d
559            return rv
560
561        def entering(sle):
562
563            if not enter:
564                return
565
566            new_d = wrap(new_sle)
567            move = MoveInterpolate(delay, enter(new_d), new_d, False, enter_time_warp)
568            rv_sl.append(merge(new_sle, move))
569
570        def leaving(sle):
571
572            if not leave:
573                return
574
575            old_d = wrap(sle)
576            move = MoveInterpolate(delay, old_d, leave(old_d), True, leave_time_warp)
577            move = renpy.display.layout.IgnoresEvents(move)
578            rv_sl.append(merge(old_sle, move))
579
580        def moving(old_sle, new_sle):
581
582            if old_sle.displayable is new_sle.displayable:
583                rv_sl.append(new_sle)
584                return
585
586            old_d = wrap(old_sle)
587            new_d = wrap(new_sle)
588
589            move = MoveInterpolate(delay, old_d, new_d, use_old, time_warp)
590
591            rv_sl.append(merge(new_sle, move))
592
593        # The old, new, and merged scene_lists.
594        old_sl = old.scene_list[:]
595        new_sl = new.scene_list[:]
596        rv_sl = [ ]
597
598        # A list of tags in old_sl, new_sl, and rv_sl.
599        old_map = dict((tag(i), i) for i in old_sl if i is not None)
600        new_tags = set(tag(i) for i in new_sl if i is not None)
601        rv_tags = set()
602
603        while old_sl or new_sl:
604
605            # If we have something in old_sl, then
606            if old_sl:
607
608                old_sle = old_sl[0]
609                old_tag = tag(old_sle)
610
611                # If the old thing has already moved, then remove it.
612                if old_tag in rv_tags:
613                    old_sl.pop(0)
614                    continue
615
616                # If the old thing does not match anything in new_tags,
617                # have it enter.
618                if old_tag not in new_tags:
619                    leaving(old_sle)
620                    rv_tags.add(old_tag)
621                    old_sl.pop(0)
622                    continue
623
624            # Otherwise, we must have something in new_sl. We want to
625            # either move it or have it enter.
626
627            new_sle = new_sl.pop(0)
628            new_tag = tag(new_sle)
629
630            # If it exists in both, move.
631            if new_tag in old_map:
632                old_sle = old_map[new_tag]
633
634                moving(old_sle, new_sle)
635                rv_tags.add(new_tag)
636                continue
637
638            else:
639                entering(new_sle)
640                rv_tags.add(new_tag)
641                continue
642
643        # Sort everything by zorder, to ensure that there are no zorder
644        # violations in the result.
645        rv_sl.sort(key=lambda a : a.zorder)
646
647        layer = new.layer_name
648        rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **renpy.game.interface.layer_properties[layer])
649        rv.append_scene_list(rv_sl)
650
651        return rv
652
653    # Call merge_slide to actually do the merging.
654    rv = merge_slide(old_widget, new_widget, merge_slide)
655    rv.delay = delay
656
657    return rv
658