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