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