pgfw/pgfw/extension.py

571 lines
20 KiB
Python

import itertools, random, os, glob
from math import sin, cos, atan2, radians, sqrt, pi
import pygame
from pygame import Surface, PixelArray, Color, Rect, draw, gfxdraw
from pygame.mixer import get_num_channels, Channel
from pygame.locals import *
from .Vector import Vector
def clamp(n, min_n, max_n):
if n < min_n:
return min_n
elif n > max_n:
return max_n
else:
return n
def get_step(start, end, speed):
x0, y0 = start
x1, y1 = end
angle = atan2(x1 - x0, y1 - y0)
return Vector(speed * sin(angle), speed * cos(angle))
def get_step_relative(start, end, step):
return get_step(start, end, get_distance(start, end) * step)
def get_segments(start, end, count):
rel_step = get_step_relative(start, end, 1 / float(count))
segs = [[Vector(start[0], start[1])]]
for ii in range(count):
seg_end = Vector(segs[-1][0].x + rel_step.x, segs[-1][0].y + rel_step.y)
segs[-1].append(seg_end)
if ii < count - 1:
segs.append([seg_end])
return segs
def get_points_on_line(start, end, count):
rel_step = get_step_relative(start, end, 1 / float(count - 1))
points = [Vector(start[0], start[1])]
for ii in range(count - 2):
points.append(Vector(points[-1][0] + rel_step[0], points[-1][1] + rel_step[1]))
points.append(Vector(end[0], end[1]))
return points
def get_angle(start, end, transpose=False):
"""counterclockwise, 0 is down"""
angle = atan2(end[0] - start[0], end[1] - start[1])
if transpose:
angle = -angle - pi
return angle
def get_endpoint(start, angle, magnitude, translate_angle=True):
"""clockwise, 0 is up"""
x0, y0 = start
dx, dy = get_delta(angle, magnitude, translate_angle)
return Vector(x0 + dx, y0 + dy)
def get_delta(angle, magnitude, translate_angle=True):
if translate_angle:
angle = radians(angle)
return Vector(sin(angle) * magnitude, -cos(angle) * magnitude)
def reflect_angle(angle, wall):
return wall - angle
def rotate_2d(point, center, angle, translate_angle=True):
if translate_angle:
angle = radians(angle)
x, y = point
cx, cy = center
return cos(angle) * (x - cx) - sin(angle) * (y - cy) + cx, \
sin(angle) * (x - cx) + cos(angle) * (y - cy) + cy
def get_points_on_circle(center, radius, count, offset=0):
angle_step = 360.0 / count
points = []
current_angle = 0
for _ in range(count):
points.append(get_point_on_circle(center, radius,
current_angle + offset))
current_angle += angle_step
return points
def get_point_on_circle(center, radius, angle, translate_angle=True):
if translate_angle:
angle = radians(angle)
return Vector(center[0] + sin(angle) * radius,
center[1] - cos(angle) * radius)
def get_range_steps(start, end, count, omit=[]):
'''
Iterator that yields evenly spaced steps from `start` to `end` as floats based on step `count`. Indicies in the
omit parameter will be skipped.
'''
# normalize array indicies (for example -1 becomes count - 1)
for ii in range(len(omit)):
omit[ii] = omit[ii] % count
for ii in range(count):
if ii in omit:
continue
else:
yield start + (end - start) * ii / float(count - 1)
def get_percent_way(iterable):
for ii in range(len(iterable)):
yield iterable[ii], float(ii) / (len(iterable) - 1)
def mirrored(iterable, full=False, tail=True):
for ii, item in enumerate(itertools.chain(iterable, reversed(iterable))):
if not full and ii == len(iterable):
continue
elif not tail and ii == len(iterable) * 2 - 1:
continue
else:
yield item
def get_distance(p0, p1):
"""
Return the distance between two points
"""
return sqrt((p0[0] - p1[0]) ** 2 + (p0[1] - p1[1]) ** 2)
def place_in_rect(rect, incoming, contain=True, *args):
while True:
incoming.center = random.randint(0, rect.w), random.randint(0, rect.h)
if not contain or rect.contains(incoming):
collides = False
for inner in args:
if inner.colliderect(incoming):
collides = True
break
if not collides:
break
def constrain_dimensions_2d(vector, container):
dw = vector[0] - container[0]
dh = vector[1] - container[1]
if dw > 0 or dh > 0:
if dw > dh:
size = Vector(container[0], int(round(container[0] / vector[0] * vector[1])))
else:
size = Vector(int(round(container[1] / vector[1] * vector[0])), container[1])
else:
size = Vector(vector[0], vector[1])
return size
# from http://www.realtimerendering.com/resources/GraphicsGems/gemsii/xlines.c
def get_intersection(p0, p1, p2, p3):
x0, y0 = p0
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
a0 = y1 - y0
b0 = x0 - x1
c0 = x1 * y0 - x0 * y1
r2 = a0 * x2 + b0 * y2 + c0
r3 = a0 * x3 + b0 * y3 + c0
if r2 != 0 and r3 != 0 and r2 * r3 > 0:
return None
a1 = y3 - y2
b1 = x2 - x3
c1 = x3 * y2 - x2 * y3
r0 = a1 * x0 + b1 * y0 + c1
r1 = a1 * x1 + b1 * y1 + c1
if r0 != 0 and r1 != 0 and r0 * r1 > 0:
return None
denominator = a0 * b1 - a1 * b0
if denominator == 0:
return (x0 + x1 + x2 + x3) / 4, (y0 + y1 + y2 + y3) / 4
if denominator < 0:
offset = -denominator / 2
else:
offset = denominator / 2
numerator = b0 * c1 - b1 * c0
x = ((-1, 1)[numerator < 0] * offset + numerator) / denominator
numerator = a1 * c0 - a0 * c1
y = ((-1, 1)[numerator < 0] * offset + numerator) / denominator
return x, y
def collide_line_with_rect(rect, p0, p1):
for line in ((rect.topleft, rect.topright),
(rect.topright, rect.bottomright),
(rect.bottomright, rect.bottomleft),
(rect.bottomleft, rect.topleft)):
if get_intersection(p0, p1, *line):
return True
def get_random_number_in_range(start, end):
return random.random() * (end - start) + start
def get_value_in_range(start, end, position, reverse=False):
if reverse:
position = 1 - position
return (end - start) * position + start
def fill_borders(surface, color=Color(0, 0, 0), thickness=1, rect=None, flags=0, include=(True, True, True, True)):
'''
Draw borders on a surface at rect position. A subset of the four borders can be drawn by passing the include
iterable with bools for each position (top, right, bottom, left)
'''
if rect is None:
rect = surface.get_rect()
# handle horizontal sides differently based on whether top and bottom are being drawn
horizontal_h = rect.h - thickness * 2
horizontal_y = rect.top + thickness
if not include[0]:
horizontal_y = rect.top
horizontal_h += thickness
if not include[2]:
horizontal_h += thickness
if include[0]:
surface.fill(color, (rect.left, rect.top, rect.w, thickness), flags)
if include[1]:
surface.fill(color, (rect.right - thickness, horizontal_y, thickness, horizontal_h), flags)
if include[2]:
surface.fill(color, (rect.left, rect.bottom - thickness, rect.w, thickness), flags)
if include[3]:
surface.fill(color, (rect.left, horizontal_y, thickness, horizontal_h), flags)
def get_boxed_surface(surface, background=None, border=None, border_width=1, padding=0):
if padding:
if isinstance(padding, int):
padding = [padding] * 2
padding = [x * 2 for x in padding]
rect = surface.get_rect()
padded_surface = Surface(rect.inflate(padding).size, SRCALPHA)
if background is not None:
padded_surface.fill(background)
rect.center = padded_surface.get_rect().center
padded_surface.blit(surface, rect)
surface = padded_surface
if border is not None:
if isinstance(border_width, int):
border_width = [border_width] * 2
border_width = [x * 2 for x in border_width]
rect = surface.get_rect()
bordered_surface = Surface(rect.inflate(border_width).size, SRCALPHA)
bordered_surface.fill(border)
rect.center = bordered_surface.get_rect().center
bordered_surface.fill((255, 255, 255, 255), rect)
bordered_surface.blit(surface, rect, None, BLEND_RGBA_MIN)
surface = bordered_surface
return surface
def render_box(font=None, text=None, antialias=True, color=(0, 0, 0), background=None, border=None,
border_width=1, padding=0, width=None, height=None):
if font is not None:
surface = font.render(text, antialias, color, background)
if width is not None or height is not None:
if width is None:
width = surface.get_width()
if height is None:
height = surface.get_height()
container = Surface((width, height), SRCALPHA)
if background is not None:
container.fill(background)
text_rect = surface.get_rect()
text_rect.center = container.get_rect().center
container.blit(surface, text_rect)
surface = container
else:
surface = pygame.Surface((width, height), SRCALPHA)
if background is not None:
surface.fill(background)
return get_boxed_surface(surface, background, border, border_width, padding)
def get_wrapped_text_surface(font, text, width, antialias=True, color=(0, 0, 0),
background=None, border=None, border_width=1,
padding=0, align="left"):
lines = []
height = 0
for chunk in text.split("\n"):
line_text = ""
ii = 0
finished = False
if chunk.startswith("\*") and chunk.endswith("\*"):
chunk = chunk.replace("\*", "*")
elif chunk.startswith("*") and chunk.endswith("*"):
chunk = chunk[1:-1]
font.set_italic(True)
words = chunk.split(" ")
while not finished:
line_width = font.size(line_text + " " + words[ii])[0]
if line_width > width or ii == len(words) - 1:
if ii == len(words) - 1 and line_width <= width:
if line_text != "":
line_text += " "
line_text += words[ii]
finished = True
line = font.render(line_text, antialias, color)
height += line.get_height()
lines.append(line)
line_text = ""
else:
if ii > 0:
line_text += " "
line_text += words[ii]
ii += 1
font.set_italic(False)
top = 0
surface = Surface((width, height), pygame.SRCALPHA)
# if background:
# surface.fill(background)
rect = surface.get_rect()
for line in lines:
line_rect = line.get_rect()
line_rect.top = top
if align == "center":
line_rect.centerx = rect.centerx
surface.blit(line, line_rect)
top += line_rect.h
return get_boxed_surface(surface, background, border, border_width, padding)
def replace_color(surface, color, replacement):
'''
Replace a color on a surface without creating a copy
'''
pixels = PixelArray(surface)
pixels.replace(color, replacement)
del pixels
def get_color_swapped_surface(surface, color, replacement):
'''
Get a copy of given Surface with given color replaced by given replacement
'''
swapped = surface.copy()
replace_color(swapped, color, replacement)
return swapped
def get_busy_channel_count():
'''
Return count of audio channels currently playing audio
'''
count = 0
for index in range(get_num_channels()):
count += Channel(index).get_busy()
return count
def get_hue_shifted_surface(base, offset):
surface = base.copy()
pixels = PixelArray(surface)
for x in range(surface.get_width()):
for y in range(surface.get_height()):
rgb = surface.unmap_rgb(pixels[x][y])
color = Color(*rgb)
h, s, l, a = color.hsla
if a and surface.get_colorkey() != rgb:
color.hsla = (int(h) + offset) % 360, int(s), int(l), int(a)
pixels[x][y] = color
del pixels
return surface
def get_inverted_color(color):
return Color(255 - color[0], 255 - color[1], 255 - color[2])
def get_inverted_surface(base):
surface = base.copy()
pixels = PixelArray(surface)
for x in range(surface.get_width()):
for y in range(surface.get_height()):
color = Color(*surface.unmap_rgb(pixels[x][y]))
if color.hsla[3]:
pixels[x][y] = get_inverted_color(color)
del pixels
return surface
def fill_tile(surface, tile, rect=None, flags=0, offset=Vector(0, 0)):
'''
Fill surface with tile surface. If rect is set, only fill in that section. If offset is set, start filling
with tile shifted by offset (-x, -y)
'''
w, h = tile.get_size()
surface.set_clip(rect)
for x in range(-offset.x, surface.get_width(), w):
for y in range(-offset.y, surface.get_height(), h):
surface.blit(tile, (x, y), None, flags)
surface.set_clip(None)
def load_frames(path, transparency=False, ppa=True, key=None, query=None):
if os.path.isfile(path):
paths = [path]
else:
if query:
paths = sorted(glob.glob(os.path.join(path, query)))
else:
paths = [os.path.join(path, name) for name in sorted(os.listdir(path))]
frames = []
for path in paths:
img = pygame.image.load(path)
if transparency:
if ppa:
frame = img.convert_alpha()
else:
frame = fill_colorkey(img, key)
else:
frame = img.convert()
frames.append(frame)
return frames
def fill_colorkey(img, key=None):
if key is None:
key = 255, 0, 255
img = img.convert_alpha()
frame = Surface(img.get_size())
frame.fill(key)
frame.set_colorkey(key)
frame.blit(img, (0, 0))
return frame
def get_shadowed_text(text, font, offset, color, antialias=True, shadow_color=(0, 0, 0),
colorkey=(255, 0, 255)):
foreground = font.render(text, antialias, color)
background = font.render(text, antialias, shadow_color)
alpha = SRCALPHA if antialias else 0
surface = Surface((foreground.get_width() + abs(offset[0]),
foreground.get_height() + abs(offset[1])), alpha)
if not antialias:
surface.set_colorkey(colorkey)
surface.fill(colorkey)
surface.blit(background, ((abs(offset[0]) + offset[0]) / 2,
(abs(offset[1]) + offset[1]) / 2))
surface.blit(foreground, ((abs(offset[0]) - offset[0]) / 2,
(abs(offset[1]) - offset[1]) / 2))
return surface
def get_blinds_rects(w, h, step=.05, count=4):
blinds = []
blind_h = int(round(h / float(count)))
for ii in range(1, count + 1):
blinds.append(Rect(0, blind_h * ii, w, 0))
inflate_h = int(round(blind_h * step))
if inflate_h < 1:
inflate_h = 1
rects = []
while blinds[0].h < blind_h:
rects.append([])
for blind in blinds:
bottom = blind.bottom
blind.inflate_ip(0, inflate_h)
blind.bottom = bottom
rects[-1].append(blind.copy())
return rects
def get_blinds_frames(surface, step=.05, count=4, fill=(0, 0, 0, 0)):
frames = []
rects = []
h = int(round(surface.get_height() / float(count)))
for ii in range(1, count + 1):
rects.append(Rect(0, h * ii, surface.get_width(), 0))
bar_h = int(round(h * step))
if bar_h < 1:
bar_h = 1
while rects[0].h < h:
frame = surface.copy()
for rect in rects:
bottom = rect.bottom
rect.inflate_ip(0, bar_h)
rect.bottom = bottom
frame.fill(fill, rect)
frames.append(frame)
return frames
def get_hsla_color(hue, saturation=100, lightness=50, alpha=100):
'''
Get a pygame Color object from HSLA value
'''
color = Color(0, 0, 0, 0)
color.hsla = hue % 360, clamp(saturation, 0, 100), clamp(lightness, 0, 100), clamp(alpha, 0, 100)
return color
def get_random_hsla_color(hue_range=(0, 359), saturation_range=(0, 100), lightness_range=(0, 100), alpha_range=(100, 100)):
'''
Get a random color with constrained hue, saturation, lightness and alpha
'''
return get_hsla_color(
random.randint(*hue_range), random.randint(*saturation_range), random.randint(*lightness_range),
random.randint(*alpha_range))
def get_hsva_color(hue, saturation=100, value=100, alpha=100):
'''
Get a pygame Color object from HSVA value
'''
color = Color(0, 0, 0, 0)
color.hsva = hue % 360, saturation, value, alpha
return color
def get_lightened_color(color, lightness):
'''
Return a copy of the provided color with specified lightness
'''
h, s, _, a = color.hsla
return get_hsla_color(h, s, clamp(lightness, 0, 100), a)
def get_glow_frames(radius, segments, colors=[(0, 0, 0), (255, 255, 255)], minsize=4, transparency=True):
frames = []
radius = int(round(radius))
sizes = [int(round(minsize + float(ii) / (segments - 1) * (radius - minsize))) for ii in range(segments)]
if transparency:
alpha_step = 255.0 / segments
alpha = alpha_step
else:
alpha = 255
for color_offset in range(len(colors)):
frame = Surface([radius * 2] * 2, SRCALPHA if transparency else None)
if transparency:
alpha = alpha_step
for segment_ii, segment_radius in enumerate(reversed(sizes)):
color = Color(*(colors[(color_offset + segment_ii) % len(colors)] + (int(round(alpha)),)))
gfxdraw.filled_circle(frame, radius, radius, int(round(segment_radius)), color)
if transparency:
alpha += alpha_step
frames.append(frame)
return frames
# http://www.pygame.org/wiki/BezierCurve
def compute_bezier_points(vertices, numPoints=60):
points = []
b0x = vertices[0][0]
b0y = vertices[0][1]
b1x = vertices[1][0]
b1y = vertices[1][1]
b2x = vertices[2][0]
b2y = vertices[2][1]
b3x = vertices[3][0]
b3y = vertices[3][1]
ax = -b0x + 3 * b1x + -3 * b2x + b3x
ay = -b0y + 3 * b1y + -3 * b2y + b3y
bx = 3 * b0x + -6 * b1x + 3 * b2x
by = 3 * b0y + -6 * b1y + 3 * b2y
cx = -3 * b0x + 3 * b1x
cy = -3 * b0y + 3 * b1y
dx = b0x
dy = b0y
numSteps = numPoints - 1
h = 1.0 / numSteps
pointX = dx
pointY = dy
firstFDX = ax * h ** 3 + bx * h ** 2 + cx * h
firstFDY = ay * h ** 3 + by * h ** 2 + cy * h
secondFDX = 6 * ax * h ** 3 + 2 * bx * h ** 2
secondFDY = 6 * ay * h ** 3 + 2 * by * h ** 2
thirdFDX = 6 * ax * h ** 3
thirdFDY = 6 * ay * h ** 3
points.append(Vector(pointX, pointY))
for i in range(numSteps):
pointX += firstFDX
pointY += firstFDY
firstFDX += secondFDX
firstFDY += secondFDY
secondFDX += thirdFDX
secondFDY += thirdFDY
points.append(Vector(pointX, pointY))
return points
def get_marching_ants_color(position, time=0, colors=((0, 0, 0), (255, 255, 255))):
return Color(*(colors[0], colors[1])[((position // 2) % 2 + (time % 2)) % 2])
def diagonal_to_rect(start, end):
sx, sy = start
ex, ey = end
return Rect(min(sx, ex), min(sy, ey), abs(sx - ex), abs(ex - ey))
def scale_2x_multiple(surface, times):
'''
Run the scale2x scale on a surface times number of times
'''
for _ in range(times):
surface = pygame.transform.scale2x(surface)
return surface