613 lines
17 KiB
Python
613 lines
17 KiB
Python
# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
|
|
#
|
|
# 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 print_function
|
|
|
|
import renpy.display
|
|
from renpy.display.render import render, Render, Matrix2D
|
|
|
|
# This file contains displayables that are image-like, because they take
|
|
# up a rectangular area of the screen, and do not respond to input.
|
|
|
|
|
|
class Solid(renpy.display.core.Displayable):
|
|
"""
|
|
:doc: disp_imagelike
|
|
|
|
A displayable that fills the area its assigned with `color`.
|
|
|
|
::
|
|
|
|
image white = Solid("#fff")
|
|
|
|
"""
|
|
|
|
def __init__(self, color, **properties):
|
|
|
|
super(Solid, self).__init__(**properties)
|
|
|
|
if color is not None:
|
|
self.color = renpy.easy.color(color)
|
|
else:
|
|
self.color = None
|
|
|
|
def __hash__(self):
|
|
return hash(self.color)
|
|
|
|
def __eq__(self, o):
|
|
if not self._equals(o):
|
|
return False
|
|
|
|
return (self.color == o.color)
|
|
|
|
def visit(self):
|
|
return [ ]
|
|
|
|
def render(self, width, height, st, at):
|
|
|
|
width = max(self.style.xminimum, width)
|
|
height = max(self.style.yminimum, height)
|
|
|
|
color = self.color or self.style.color
|
|
|
|
rv = Render(width, height)
|
|
|
|
if color is None or width <= 0 or height <= 0:
|
|
return rv
|
|
|
|
SIZE = 10
|
|
|
|
if width < SIZE or height < SIZE:
|
|
tex = renpy.display.draw.solid_texture(width, height, color)
|
|
else:
|
|
tex = renpy.display.draw.solid_texture(SIZE, SIZE, color)
|
|
rv.forward = Matrix2D(1.0 * SIZE / width, 0, 0, 1.0 * SIZE / height)
|
|
rv.reverse = Matrix2D(1.0 * width / SIZE, 0, 0, 1.0 * height / SIZE)
|
|
|
|
rv.blit(tex, (0, 0))
|
|
|
|
return rv
|
|
|
|
|
|
class Borders(object):
|
|
"""
|
|
:doc: disp_imagelike
|
|
|
|
This object provides border size and tiling information to a :func:`Frame`.
|
|
It can also provide padding information that can be supplied to the
|
|
:propref:`padding` style property of a window or frame.
|
|
|
|
`left`, `top`, `right`, `bottom`
|
|
These provide the size of the insets used by a frame, and are added
|
|
to the padding on each side. They should zero or a positive integer.
|
|
|
|
`pad_left`, `pad_top`, `pad_right`, `pad_bottom`
|
|
These are added to the padding on each side, and may be positive or
|
|
negative. (For example, if `left` is 5 and `pad_left` is -3, the final
|
|
padding is 2.)
|
|
|
|
The padding information is supplied via a field:
|
|
|
|
.. attribute:: padding
|
|
|
|
This is a four-element tuple containing the padding on each of the
|
|
four sides.
|
|
"""
|
|
|
|
def __init__(self, left, top, right, bottom, pad_left=0, pad_top=0, pad_right=0, pad_bottom=0):
|
|
|
|
self.left = left
|
|
self.top = top
|
|
self.right = right
|
|
self.bottom = bottom
|
|
|
|
self.pad_left = pad_left
|
|
self.pad_top = pad_top
|
|
self.pad_right = pad_right
|
|
self.pad_bottom = pad_bottom
|
|
|
|
@property
|
|
def padding(self):
|
|
return (
|
|
self.left + self.pad_left,
|
|
self.top + self.pad_top,
|
|
self.right + self.pad_right,
|
|
self.bottom + self.pad_bottom,
|
|
)
|
|
|
|
|
|
class Frame(renpy.display.core.Displayable):
|
|
"""
|
|
:doc: disp_imagelike
|
|
:args: (image, left=0, top=0, right=None, bottom=None, tile=False, **properties)
|
|
:name: Frame
|
|
|
|
A displayable that resizes an image to fill the available area,
|
|
while preserving the width and height of its borders. It is often
|
|
used as the background of a window or button.
|
|
|
|
.. figure:: frame_example.png
|
|
|
|
Using a frame to resize an image to double its size.
|
|
|
|
`image`
|
|
An image manipulator that will be resized by this frame.
|
|
|
|
`left`
|
|
The size of the border on the left side. This can also be a
|
|
:func:`Borders` object, in which case that object is used in place
|
|
of the other parameters.
|
|
|
|
`top`
|
|
The size of the border on the top.
|
|
|
|
`right`
|
|
The size of the border on the right side. If None, defaults
|
|
to `left`.
|
|
|
|
`bottom`
|
|
The side of the border on the bottom. If None, defaults to `top`.
|
|
|
|
`tile`
|
|
If set to True, tiling is used to resize sections of the image,
|
|
rather than scaling. If set to the string "integer", the nearest
|
|
integer number of tiles will be used in each direction. That set of
|
|
full tiles will then be scaled up or down to fit the required area.
|
|
|
|
::
|
|
|
|
# Resize the background of the text window if it's too small.
|
|
init python:
|
|
style.window.background = Frame("frame.png", 10, 10)
|
|
"""
|
|
|
|
__version__ = 1
|
|
|
|
properties = { }
|
|
tile_ratio = 0.5
|
|
|
|
def after_upgrade(self, version):
|
|
if version < 2:
|
|
self.left = self.xborder
|
|
self.right = self.xborder
|
|
self.top = self.yborder
|
|
self.bottom = self.yborder
|
|
|
|
def __init__(self, image, left=None, top=None, right=None, bottom=None,
|
|
xborder=0, yborder=0, bilinear=True, tile=False,
|
|
tile_ratio=0.5, **properties):
|
|
super(Frame, self).__init__(**properties)
|
|
|
|
self.image = renpy.easy.displayable(image)
|
|
self._duplicatable = self.image._duplicatable
|
|
|
|
if isinstance(left, Borders):
|
|
insets = left
|
|
|
|
left = insets.left
|
|
top = insets.top
|
|
right = insets.right
|
|
bottom = insets.bottom
|
|
|
|
self.tile = tile
|
|
# When tile="integer" the proportion of an edge tile that determines
|
|
# whether to use less tiles and stretch or more tiles and shrink
|
|
self.tile_ratio = float(tile_ratio)
|
|
|
|
# Compat for old argument names.
|
|
if left is None:
|
|
left = xborder
|
|
if top is None:
|
|
top = yborder
|
|
|
|
if right is None:
|
|
right = left
|
|
if bottom is None:
|
|
bottom = top
|
|
|
|
self.left = left
|
|
self.top = top
|
|
self.right = right
|
|
self.bottom = bottom
|
|
|
|
def __repr__(self):
|
|
return "<Frame {!r} ({},{},{},{}){}>".format(
|
|
self.image,
|
|
self.left,
|
|
self.top,
|
|
self.right,
|
|
self.bottom,
|
|
(" tile ({})".format(self.tile_ratio) if self.tile == "integer"
|
|
else " tile" if self.tile
|
|
else ""))
|
|
|
|
def __eq__(self, o):
|
|
if not self._equals(o):
|
|
return False
|
|
|
|
if self.image != o.image:
|
|
return False
|
|
|
|
if self.left != o.left:
|
|
return False
|
|
if self.top != o.top:
|
|
return False
|
|
if self.right != o.right:
|
|
return False
|
|
if self.bottom != o.bottom:
|
|
return False
|
|
|
|
if self.tile != o.tile:
|
|
return False
|
|
|
|
if self.tile_ratio != o.tile_ratio:
|
|
return False
|
|
|
|
return True
|
|
|
|
def render(self, width, height, st, at):
|
|
|
|
width = max(self.style.xminimum, width)
|
|
height = max(self.style.yminimum, height)
|
|
|
|
image = self.style.child or self.image
|
|
crend = render(image, width, height, st, at)
|
|
|
|
sw, sh = crend.get_size()
|
|
sw = int(sw)
|
|
sh = int(sh)
|
|
|
|
dw = int(width)
|
|
dh = int(height)
|
|
|
|
bw = self.left + self.right
|
|
bh = self.top + self.bottom
|
|
|
|
xborder = min(bw, sw - 2, dw)
|
|
if xborder and bw:
|
|
left = self.left * xborder / bw
|
|
right = self.right * xborder / bw
|
|
else:
|
|
left = 0
|
|
right = 0
|
|
|
|
yborder = min(bh, sh - 2, dh)
|
|
if yborder and bh:
|
|
top = self.top * yborder / bh
|
|
bottom = self.bottom * yborder / bh
|
|
else:
|
|
top = 0
|
|
bottom = 0
|
|
|
|
if renpy.display.draw.info["renderer"] == "sw":
|
|
return self.sw_render(crend, dw, dh, left, top, right, bottom)
|
|
|
|
def draw(x0, x1, y0, y1):
|
|
|
|
# Compute the coordinates of the left, right, top, and
|
|
# bottom sides of the region, for both the source and
|
|
# destination surfaces.
|
|
|
|
# left side.
|
|
if x0 >= 0:
|
|
dx0 = x0
|
|
sx0 = x0
|
|
else:
|
|
dx0 = dw + x0
|
|
sx0 = sw + x0
|
|
|
|
# right side.
|
|
if x1 > 0:
|
|
dx1 = x1
|
|
sx1 = x1
|
|
else:
|
|
dx1 = dw + x1
|
|
sx1 = sw + x1
|
|
|
|
# top side.
|
|
if y0 >= 0:
|
|
dy0 = y0
|
|
sy0 = y0
|
|
else:
|
|
dy0 = dh + y0
|
|
sy0 = sh + y0
|
|
|
|
# bottom side
|
|
if y1 > 0:
|
|
dy1 = y1
|
|
sy1 = y1
|
|
else:
|
|
dy1 = dh + y1
|
|
sy1 = sh + y1
|
|
|
|
# Quick exit.
|
|
if sx0 == sx1 or sy0 == sy1:
|
|
return
|
|
|
|
# Compute sizes.
|
|
csw = sx1 - sx0
|
|
csh = sy1 - sy0
|
|
cdw = dx1 - dx0
|
|
cdh = dy1 - dy0
|
|
|
|
if csw <= 0 or csh <= 0 or cdh <= 0 or cdw <= 0:
|
|
return
|
|
|
|
# Get a subsurface.
|
|
cr = crend.subsurface((sx0, sy0, csw, csh))
|
|
|
|
# Scale or tile if we have to.
|
|
if csw != cdw or csh != cdh:
|
|
|
|
if self.tile:
|
|
ctw, cth = cdw, cdh
|
|
|
|
xtiles = max(1, cdw // csw + (1 if cdw % csw else 0))
|
|
ytiles = max(1, cdh // csh + (1 if cdh % csh else 0))
|
|
|
|
if cdw % csw or cdh % csh:
|
|
# Area is not an exact integer number of tiles
|
|
|
|
if self.tile == "integer":
|
|
if cdw % csw / float(csw) < self.tile_ratio:
|
|
xtiles = max(1, xtiles-1)
|
|
if cdh % csh / float(csh) < self.tile_ratio:
|
|
ytiles = max(1, ytiles-1)
|
|
|
|
# Set size of the used tiles (ready to scale)
|
|
ctw, cth = csw * xtiles, csh * ytiles
|
|
|
|
newcr = Render(ctw, cth)
|
|
newcr.xclipping = True
|
|
newcr.yclipping = True
|
|
|
|
for x in xrange(0, xtiles):
|
|
for y in xrange(0, ytiles):
|
|
newcr.blit(cr, (x * csw, y * csh))
|
|
|
|
csw, csh = ctw, cth
|
|
cr = newcr
|
|
|
|
if csw != cdw or csh != cdh:
|
|
# Subsurface needs scaling
|
|
newcr = Render(cdw, cdh)
|
|
newcr.forward = Matrix2D(1.0 * csw / cdw, 0, 0, 1.0 * csh / cdh)
|
|
newcr.reverse = Matrix2D(1.0 * cdw / csw, 0, 0, 1.0 * cdh / csh)
|
|
newcr.blit(cr, (0, 0))
|
|
|
|
cr = newcr
|
|
|
|
# Blit.
|
|
rv.blit(cr, (dx0, dy0))
|
|
return
|
|
|
|
rv = Render(dw, dh)
|
|
|
|
self.draw_pattern(draw, left, top, right, bottom)
|
|
|
|
return rv
|
|
|
|
def draw_pattern(self, draw, left, top, right, bottom):
|
|
# Top row.
|
|
if top:
|
|
|
|
if left:
|
|
draw(0, left, 0, top)
|
|
|
|
draw(left, -right, 0, top)
|
|
|
|
if right:
|
|
draw(-right, 0, 0, top)
|
|
|
|
# Middle row.
|
|
if left:
|
|
draw(0, left, top, -bottom)
|
|
|
|
draw(left, -right, top, -bottom)
|
|
|
|
if right:
|
|
draw(-right, 0, top, -bottom)
|
|
|
|
# Bottom row.
|
|
if bottom:
|
|
if left:
|
|
draw(0, left, -bottom, 0)
|
|
|
|
draw(left, -right, -bottom, 0)
|
|
|
|
if right:
|
|
draw(-right, 0, -bottom, 0)
|
|
|
|
def sw_render(self, crend, dw, dh, left, top, right, bottom):
|
|
|
|
source = crend.render_to_texture(True)
|
|
sw, sh = source.get_size()
|
|
|
|
dest = renpy.display.swdraw.surface(dw, dh, True)
|
|
rv = dest
|
|
|
|
def draw(x0, x1, y0, y1):
|
|
|
|
# Compute the coordinates of the left, right, top, and
|
|
# bottom sides of the region, for both the source and
|
|
# destination surfaces.
|
|
|
|
# left side.
|
|
if x0 >= 0:
|
|
dx0 = x0
|
|
sx0 = x0
|
|
else:
|
|
dx0 = dw + x0
|
|
sx0 = sw + x0
|
|
|
|
# right side.
|
|
if x1 > 0:
|
|
dx1 = x1
|
|
sx1 = x1
|
|
else:
|
|
dx1 = dw + x1
|
|
sx1 = sw + x1
|
|
|
|
# top side.
|
|
if y0 >= 0:
|
|
dy0 = y0
|
|
sy0 = y0
|
|
else:
|
|
dy0 = dh + y0
|
|
sy0 = sh + y0
|
|
|
|
# bottom side
|
|
if y1 > 0:
|
|
dy1 = y1
|
|
sy1 = y1
|
|
else:
|
|
dy1 = dh + y1
|
|
|
|
sy1 = sh + y1
|
|
|
|
# Quick exit.
|
|
if sx0 == sx1 or sy0 == sy1 or dx1 <= dx0 or dy1 <= dy0:
|
|
return
|
|
|
|
# Compute sizes.
|
|
srcsize = (sx1 - sx0, sy1 - sy0)
|
|
dstsize = (int(dx1 - dx0), int(dy1 - dy0))
|
|
|
|
# Get a subsurface.
|
|
surf = source.subsurface((sx0, sy0, srcsize[0], srcsize[1]))
|
|
|
|
# Scale or tile if we have to.
|
|
if dstsize != srcsize:
|
|
|
|
if self.tile:
|
|
tilew, tileh = srcsize
|
|
dstw, dsth = dstsize
|
|
|
|
xtiles = max(
|
|
1, dstw // tilew + (1 if dstw % tilew else 0))
|
|
ytiles = max(
|
|
1, dsth // tileh + (1 if dsth % tileh else 0))
|
|
|
|
if dstw % tilew or dsth % tileh:
|
|
# Area is not an exact integer number of tiles
|
|
|
|
if self.tile == "integer":
|
|
if dstw % tilew / float(tilew) < self.tile_ratio:
|
|
xtiles = max(1, xtiles-1)
|
|
if dsth % tileh / float(tileh) < self.tile_ratio:
|
|
ytiles = max(1, ytiles-1)
|
|
|
|
# Tile at least one tile in each direction
|
|
surf2 = renpy.display.pgrender.surface_unscaled(
|
|
(tilew * xtiles, tileh * ytiles), surf)
|
|
|
|
for y in range(0, ytiles):
|
|
for x in range(0, xtiles):
|
|
surf2.blit(surf, (x*tilew, y*tileh))
|
|
|
|
if self.tile is True:
|
|
# Trim the tiled surface to required size
|
|
surf = surf2.subsurface((0, 0, dstw, dsth))
|
|
else:
|
|
# Using integer full 'tiles' per side
|
|
srcsize = (tilew * xtiles, tileh * ytiles)
|
|
surf = surf2
|
|
|
|
if dstsize != srcsize:
|
|
surf2 = renpy.display.scale.real_transform_scale(surf, dstsize)
|
|
surf = surf2
|
|
|
|
# Blit.
|
|
dest.blit(surf, (dx0, dy0))
|
|
|
|
self.draw_pattern(draw, left, top, right, bottom)
|
|
|
|
rrv = renpy.display.render.Render(dw, dh)
|
|
rrv.blit(rv, (0, 0))
|
|
rrv.depends_on(crend)
|
|
|
|
# And, finish up.
|
|
return rrv
|
|
|
|
def _duplicate(self, args):
|
|
image = self.image._duplicate(args)
|
|
|
|
if image is self.image:
|
|
return self
|
|
|
|
image._unique()
|
|
|
|
rv = self._copy(args)
|
|
rv.image = image
|
|
rv._duplicatable = image._duplicatable
|
|
return rv
|
|
|
|
def _in_current_store(self):
|
|
image = self.image._in_current_store()
|
|
|
|
if image is self.image:
|
|
return self
|
|
|
|
rv = self._copy()
|
|
rv.image = image
|
|
return rv
|
|
|
|
def visit(self):
|
|
rv = [ ]
|
|
self.style._visit_frame(rv)
|
|
return rv
|
|
|
|
|
|
class FileCurrentScreenshot(renpy.display.core.Displayable):
|
|
"""
|
|
:doc: file_action_function
|
|
|
|
A displayable that shows the screenshot that will be saved with the current
|
|
file, if a screenshot has been taken when entering a menu or with
|
|
:func:`FileTakeScreenshot`.
|
|
|
|
If there is no current screenshot, `empty` is shown in its place. (If `empty` is
|
|
None, it defaults to :func:`Null`.)
|
|
"""
|
|
|
|
def __init__(self, empty=None, **properties):
|
|
|
|
super(FileCurrentScreenshot, self).__init__(**properties)
|
|
|
|
if empty is None:
|
|
empty = renpy.display.layout.Null()
|
|
|
|
self.empty = empty
|
|
|
|
def render(self, width, height, st, at):
|
|
|
|
ss = renpy.display.interface.screenshot_surface
|
|
|
|
if ss is None:
|
|
return renpy.display.render.render(self.empty, width, height, st, at)
|
|
|
|
tex = renpy.display.draw.load_texture(ss)
|
|
w, h = tex.get_size()
|
|
|
|
rv = renpy.display.render.Render(w, h)
|
|
rv.blit(tex, (0, 0))
|
|
|
|
return rv
|