How can I draw an outline on the font?
I want to use black font, but the background has to be blackish so it's hard to see the font.
I assume myfont.render doesn't support drawing outline on the font.
Is there other way?
Pygame doesn't support this out of the box, but one way to do it is render the text in the outline color and blit it to the result surface shifted multiple times, then render the text in the desired color on top of it.
pgzero uses this technique; a trimmed down version of its code is shown below:
import pygame
_circle_cache = {}
def _circlepoints(r):
r = int(round(r))
if r in _circle_cache:
return _circle_cache[r]
x, y, e = r, 0, 1 - r
_circle_cache[r] = points = []
while x >= y:
points.append((x, y))
y += 1
if e < 0:
e += 2 * y - 1
else:
x -= 1
e += 2 * (y - x) - 1
points += [(y, x) for x, y in points if x > y]
points += [(-x, y) for x, y in points if x]
points += [(x, -y) for x, y in points if y]
points.sort()
return points
def render(text, font, gfcolor=pygame.Color('dodgerblue'), ocolor=(255, 255, 255), opx=2):
textsurface = font.render(text, True, gfcolor).convert_alpha()
w = textsurface.get_width() + 2 * opx
h = font.get_height()
osurf = pygame.Surface((w, h + 2 * opx)).convert_alpha()
osurf.fill((0, 0, 0, 0))
surf = osurf.copy()
osurf.blit(font.render(text, True, ocolor).convert_alpha(), (0, 0))
for dx, dy in _circlepoints(opx):
surf.blit(osurf, (dx + opx, dy + opx))
surf.blit(textsurface, (opx, opx))
return surf
def main():
pygame.init()
font = pygame.font.SysFont(None, 64)
screen = pygame.display.set_mode((350, 100))
clock = pygame.time.Clock()
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
screen.fill((30, 30, 30))
screen.blit(render('Hello World', font), (20, 20))
pygame.display.update()
clock.tick(60)
if __name__ == '__main__':
main()
If you are a beginer and are not looking for very complex code, and you are only using a simple font such as arial, helvetica, calibri, etc...
You can just blit the text in the outline colour 4 times in the 'corners', then blit the actual text over it:
white = pygame.Color(255,255,255)
black = pygame.Color(0,0,0)
def draw_text(x, y, string, col, size, window):
font = pygame.font.SysFont("Impact", size )
text = font.render(string, True, col)
textbox = text.get_rect()
textbox.center = (x, y)
window.blit(text, textbox)
x = 300
y = 300
# TEXT OUTLINE
# top left
draw_text(x + 2, y-2 , "Hello", black, 40, win)
# top right
draw_text(x +2, y-2 , "Hello", black, 40, win)
# btm left
draw_text(x -2, y +2 , "Hello", black, 40, win)
# btm right
draw_text(x-2, y +2 , "Hello", black, 40, win)
# TEXT FILL
draw_text(x, y, "Hello", white, 40, win)
You can use masks to add an outline to your text. Here is an example:
import pygame
def add_outline_to_image(image: pygame.Surface, thickness: int, color: tuple, color_key: tuple = (255, 0, 255)) -> pygame.Surface:
mask = pygame.mask.from_surface(image)
mask_surf = mask.to_surface(setcolor=color)
mask_surf.set_colorkey((0, 0, 0))
new_img = pygame.Surface((image.get_width() + 2, image.get_height() + 2))
new_img.fill(color_key)
new_img.set_colorkey(color_key)
for i in -thickness, thickness:
new_img.blit(mask_surf, (i + thickness, thickness))
new_img.blit(mask_surf, (thickness, i + thickness))
new_img.blit(image, (thickness, thickness))
return new_img
And here is how I use this function to create a white text with a 2px black outline:
text_surf = myfont.render("test", False, (255, 255, 255)).convert()
text_with_ouline = add_outline_to_image(text_surf, 2, (0, 0, 0))
Related
So I'm creating a game and I'm using Recursive backtracking algorithm to create the maze, however, I don't want it to show the maze generation and just to instantly generate the maze. I'm unsure of how to actually do this though so any help would be appreciated, I've already tried not drawing the generated white part but that then doesn't create the maze.
import pygame
import random
import time
class Cell(object):
def __init__(self, x, y, cell_size, screen, black, white, red, blue):
# position in matrix
self.x = x
self.y = y
# keeps track of which walls are still visible
self.walls = [True, True, True, True]
# checks if cell has been visited during generation
self.generated = False
# checks if cell is on path during solving
self.on_path = False
# checks if cell has been visited during solving
self.visited = False
self.cell_size = cell_size
self.screen = screen
self.black = black
self.white = white
self.red = red
self.blue = blue
def draw_cell(self):
# coordinates on screen
x = self.x * self.cell_size
y = self.y * self.cell_size
# draws a wall if it still exists
if self.walls[0]:
pygame.draw.line(self.screen, self.black, (x, y), (x + self.cell_size, y), 5)
if self.walls[1]:
pygame.draw.line(self.screen, self.black,
(x, y + self.cell_size), (x + self.cell_size, y + self.cell_size), 5)
if self.walls[2]:
pygame.draw.line(self.screen, self.black,
(x + self.cell_size, y), (x + self.cell_size, y + self.cell_size), 5)
if self.walls[3]:
pygame.draw.line(self.screen, self.black, (x, y), (x, y + self.cell_size), 5)
# marks out white if generated during generation
if self.generated:
pygame.draw.rect(self.screen, self.white, (x, y, self.cell_size, self.cell_size))
class Maze:
def __init__(self, screen, cell_size, rows, cols, white, black, red, blue):
self.screen = screen
self.cell_size = cell_size
self.rows = rows
self.cols = cols
self.state = None
self.maze = []
self.stack = []
self.current_x = 0
self.current_y = 0
self.row = []
self.neighbours = []
self.black = black
self.white = white
self.red = red
self.blue = blue
self.cell = None
def on_start(self):
# maintains the current state
# maze matrix of cell instances
self.maze = []
# stack of current cells on path
self.stack = []
self.current_x, self.current_y = 0, 0
self.maze.clear()
self.stack.clear()
for x in range(self.cols):
self.row = []
for y in range(self.rows):
self.cell = Cell(x, y, self.cell_size, self.screen, self.black, self.white, self.red, self.blue)
self.row.append(self.cell)
self.maze.append(self.row)
def in_bounds(self, x, y):
return 0 <= x < self.cols and 0 <= y < self.rows
def find_next_cell(self, x, y):
# keeps track of valid neighbors
self.neighbours = []
# loop through these two arrays to find all 4 neighbor cells
dx, dy = [1, -1, 0, 0], [0, 0, 1, -1]
for d in range(4):
# add cell to neighbor list if it is in bounds and not generated
if self.in_bounds(x + dx[d], y + dy[d]):
if not self.maze[x + dx[d]][y + dy[d]].generated:
self.neighbours.append((x + dx[d], y + dy[d]))
# returns a random cell in the neighbors list, or -1 -1 otherwise
if len(self.neighbours) > 0:
return self.neighbours[random.randint(0, len(self.neighbours) - 1)]
else:
return -1, -1
def remove_wall(self, x1, y1, x2, y2):
# x distance between original cell and neighbor cell
xd = self.maze[x1][y1].x - self.maze[x2][y2].x
# to the bottom
if xd == 1:
self.maze[x1][y1].walls[3] = False
self.maze[x2][y2].walls[1] = False
# to the top
elif xd == -1:
self.maze[x1][y1].walls[1] = False
self.maze[x2][y2].walls[3] = False
# y distance between original cell and neighbor cell
xy = self.maze[x1][y1].y - self.maze[x2][y2].y
# to the right
if xy == 1:
self.maze[x1][y1].walls[0] = False
self.maze[x2][y2].walls[2] = False
# to the left
elif xy == -1:
self.maze[x1][y1].walls[2] = False
self.maze[x2][y2].walls[0] = False
def create_maze(self):
self.maze[self.current_x][self.current_y].generated = True
# self.maze[self.current_x][self.current_y].draw_current()
next_cell = self.find_next_cell(self.current_x, self.current_y)
# checks if a neighbor was returned
if next_cell[0] >= 0 and next_cell[1] >= 0:
self.stack.append((self.current_x, self.current_y))
self.remove_wall(self.current_x, self.current_y, next_cell[0], next_cell[1])
self.current_x = next_cell[0]
self.current_y = next_cell[1]
# no neighbor, so go to the previous cell in the stack
elif len(self.stack) > 0:
previous = self.stack.pop()
self.current_x = previous[0]
self.current_y = previous[1]
def main():
WIDTH, HEIGHT = 800, 800
CELL_SIZE = 40
ROWS, COLUMNS = int(HEIGHT / CELL_SIZE), int(WIDTH / CELL_SIZE)
# color variables
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
# initialize pygame
pygame.init()
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
SCREEN.fill(WHITE)
pygame.display.set_caption("Maze Gen")
CLOCK = pygame.time.Clock()
FPS = 60
m = Maze(SCREEN, CELL_SIZE, ROWS, COLUMNS, WHITE, BLACK, RED, BLUE)
m.on_start()
running = True
while running:
CLOCK.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for i in range(m.cols):
for j in range(m.rows):
m.maze[i][j].draw_cell()
m.create_maze()
pygame.display.flip()
if __name__ == "__main__":
main()
pygame.quit()
Call m.create_maze() in a loop before the application loop. Terminate the loop when len(m.stack) == 0:
def main():
# [...]
m = Maze(SCREEN, CELL_SIZE, ROWS, COLUMNS, WHITE, BLACK, RED, BLUE)
m.on_start()
while True:
m.create_maze()
if len(m.stack) == 0:
break
running = True
while running:
CLOCK.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for i in range(m.cols):
for j in range(m.rows):
m.maze[i][j].draw_cell()
pygame.display.flip()
def star1():
global angle
x = int(math.cos(angle) * 100) + 800
y = int(math.sin(angle) * 65) + 400
pygame.draw.circle(screen, white, (x, y), 17)
angle += 0.02
The parameters pygame gives for the .draw.circle here only allow for an RGB color to be used to color the figure. I am wondering if there is a way I can pass an image as a color, or use a different method that allows me to place an image on the object/circle. The circles move around so the image also needs to stay with the circle as it moves.
Create a transparent pygame.Surface with the desired size
Draw a white circle in the middle of the Surface
Blend the image (my_image) on this Surface using the blend mode BLEND_RGBA_MIN:
size = 100
circular_image = pygame.Surface((size, size), pygame.SRCALPHA)
pygame.draw.circle(circular_image, (255, 255, 255), (size//2, size//2), size//2)
image_rect = my_image.get_rect(center = circular_image.get_rect().center)
circular_image.blit(my_image, image_rect, special_flags=pygame.BLEND_RGBA_MIN)
Minimal example:
import pygame
pygame.init()
window = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
def create_circular_image(size, image):
clip_image = pygame.Surface((size, size), pygame.SRCALPHA)
pygame.draw.circle(clip_image, (255, 255, 255), (size//2, size//2), size//2)
image_rect = my_image.get_rect(center = clip_image.get_rect().center)
clip_image.blit(my_image, image_rect, special_flags=pygame.BLEND_RGBA_MIN)
return clip_image
def create_test_image():
image = pygame.Surface((100, 100))
ts, w, h, c1, c2 = 25, 100, 100, (255, 64, 64), (32, 64, 255)
[pygame.draw.rect(image, c1 if (x+y) % 2 == 0 else c2, (x*ts, y*ts, ts, ts))
for x in range((w+ts-1)//ts) for y in range((h+ts-1)//ts)]
return image
my_image = create_test_image()
circular_image = create_circular_image(100, my_image)
rect = circular_image.get_rect(center = window.get_rect().center)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
keys = pygame.key.get_pressed()
rect.x += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 5
rect.y += (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * 5
window.fill((64, 64, 64))
window.blit(circular_image, rect)
pygame.display.flip()
pygame.quit()
exit()
See also:
how to make circular surface in PyGame
How to fill only certain circular parts of the window in PyGame?
Clipping
Write a function that draws an image on a given x, y and pass these x, y values used in the circle function.
Basicly it's just win.blit(image, (x,y)). Don't forget to subtruct radius value. So the image matchs with circle.
I am trying to understand how python and pygame works
So ,I am trying go build this square grid from:
http://programarcadegames.com/index.php?lang=en&chapter=array_backed_grids#step_07
Managed to do the row with 10 squares, but somehow i stuck on the part that i have to do the actual grid by creating another for loop and get this result
my code looks like this:
import pygame
pygame.init()
win = pygame.display.set_mode((728, 728))
tile_width = 64
tile_height = 64
margin = 8 # space between the boxes
white = (255, 255, 255)
clock = pygame.time.Clock()
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
x, y = margin, margin
for column in range(10):
for row in range(0, column):
rect = pygame.Rect(x, y, tile_width, tile_height)
pygame.draw.rect(win, white, rect)
x = x + tile_width + margin
y = y + tile_width + margin
pygame.display.update()
pygame.quit()
You have to reset y before the inner loop and you have to increment x after the inner loop rather than in the inner loop:
run = True
while run:
# [...]
x = margin
for column in range(10):
y = margin
for row in range(10):
rect = pygame.Rect(x, y, tile_width, tile_height)
pygame.draw.rect(win, white, rect)
y = y + tile_height + margin
x = x + tile_width + margin
pygame.display.update()
Alternatively you can compute x and y in the loop, dependent on row and column:
run = True
while run:
# [...]
for column in range(10):
x = margin + column * (tile_height + margin)
for row in range(10):
y = margin + row * (tile_width + margin)
rect = pygame.Rect(x, y, tile_width, tile_height)
pygame.draw.rect(win, white, rect)
pygame.display.update()
Right now, I have a ball that moves around the screen in a random diagonal direction and bounces off the wall when it collides with it.
What I want to do:
If the ball goes over a cell/box in the grid, have the cell/box turn red and then keep changing colors until the amount of times that specific spot is 10. If the value is equal to 10, the program ends.
Let me give you a scenario: let's say I have a 3x3 grid and each cell in that grid has a value of 0. Every time the ball goes over a cell, that value goes up by one until it hits 3. Every time the value goes up, the color for that cell changes. 0=white, 1=red, 2=green, and 3=blue. That's what I wanted the output to be.
What I have so far:
The entire screen turns red instead of just the individual cell.
I am just testing it for one color at the moment, but then want to add a multitude of colors.
for row in range(GRIDY):
for column in range(GRIDX):
rect = [(MARGIN + WIDTH) * column + MARGIN, (MARGIN + HEIGHT) * row + MARGIN, WIDTH, HEIGHT]
color = WHITE
if GRIDY or GRIDX == 1:
color = RED
pg.draw.rect(screen, color, rect)
Here's the rest of the code for reference:
import sys
import math
from random import randrange
import pygame as pg
# define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
# define measurements
WIDTH, HEIGHT, MARGIN = 10, 10, 1
GRIDX, GRIDY = 91, 36
class GridObject(pg.sprite.Sprite):
def __init__(self, pos, grid, *groups):
super().__init__(groups)
# create image from grid
self.grid = grid
self.gridsize = (len(grid[0]), len(grid))
imgsize = self.gridsize[0]*(WIDTH+MARGIN), self.gridsize[1]*(HEIGHT+MARGIN)
self.image = pg.Surface(imgsize, flags=pg.SRCALPHA)
self.image.fill((0, 0, 0, 0))
col = (235, 175, 76)
for c in range(self.gridsize[0]):
for r in range(self.gridsize[1]):
if self.grid[r][c] == 1:
rect = [(MARGIN + WIDTH) * c + MARGIN, (MARGIN + HEIGHT) * r + MARGIN, WIDTH, HEIGHT]
pg.draw.rect(self.image, col, rect)
self.rect = self.image.get_rect(center=pos)
self.vel = pg.math.Vector2(8, 0).rotate(randrange(360))
self.pos = pg.math.Vector2(pos)
def update(self, boundrect):
self.pos += self.vel
self.rect.center = self.pos
if self.rect.left <= boundrect.left or self.rect.right >= boundrect.right:
self.vel.x *= -1
if self.rect.top <= boundrect.top or self.rect.bottom >= boundrect.bottom:
self.vel.y *= -1
# align rect to grid
gridpos = round(self.rect.x / (WIDTH+MARGIN)), round(self.rect.y / (HEIGHT+MARGIN))
self.rect.topleft = gridpos[0] * (WIDTH+MARGIN), gridpos[1] * (HEIGHT+MARGIN)
ballGrid = [[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 0]]
def main():
#overlap = False
screen = pg.display.set_mode((GRIDX * (WIDTH+MARGIN) + MARGIN, GRIDY * (HEIGHT+MARGIN)))
# Set title of screen
pg.display.set_caption("Ball With Grid")
clock = pg.time.Clock()
sprite_group = pg.sprite.Group()
ball = GridObject((495, 193), ballGrid, sprite_group)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
# Used to track the grid coordinates
if event.type == pg.MOUSEBUTTONDOWN:
# Get the position is screen is clicked
pos = pg.mouse.get_pos()
# Change the x/y screen coordinates to grid coordinates
column = pos[0] // (WIDTH + MARGIN)
row = pos[1] // (HEIGHT + MARGIN)
# Set that location to one
grid[row][column] = 1
print("Click ", pos, "Grid coordinates: ", row, column)
screen.fill((0, 0, 0))
# Draw the grid and add values to the cells
for row in range(GRIDY):
for column in range(GRIDX):
rect = [(MARGIN + WIDTH) * column + MARGIN, (MARGIN + HEIGHT) * row + MARGIN, WIDTH, HEIGHT]
color = WHITE
if GRIDY or GRIDX == 1:
color = RED
pg.draw.rect(screen, color, rect)
sprite_group.update(screen.get_rect())
sprite_group.draw(screen)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
sys.exit()
btw if this looks similar, my other account posted this question: Using a matrix as a sprite and testing if two sprites overlap
Link to the images:
https://imgur.com/gallery/UicBs6q
let's say I have a 3x3 grid and each cell in that grid has a value of 0. Every time the ball goes over a cell, that value goes up by one until it hits 3. Every time the value goes up, the color for that cell changes. 0=white, 1=red, 2=green, and 3=blue. That's what I wanted the output to be.
You have how often the ball is on a cell, for each cell. Since the ball covers more than 1 cell (the size of the ball is greater than 1 cell), the cell counter would be incremented multiple times in consecutive frames, when the ball rolls over the cell.
So you have to ensure that a ball has left a cell, before its counter is allowed to be incremented again.
Create a 2 dimensional array (nested list) of integral values, initialized by 0 with the size of the field. The filed sores the hit counters for the cells. Furthermore, create a list (hitList) which stores the indices of the cells which have been hit in a frame.
hitGrid = [[0 for i in range(GRIDX)] for j in range(GRIDY)]
hitList = []
The grid (hitGrid) and the list (hitList) have to be passed to the update method of the class GridObject. If the ball touches a field and the field was not touched in the previous frame, then the corresponding entry in the gird is has to be incremented. Further more the function can set a global variable max_hit, wiht the highest fit count in the current frame:
class GridObject(pg.sprite.Sprite):
# [...]
def update(self, boundrect, gridHit):
# [...]
# increment touched filed
global max_hit
max_hit = 0
oldHitList = hitList[:]
hitList.clear()
for c in range(self.gridsize[0]):
for r in range(self.gridsize[1]):
p = gridpos[1] + r, gridpos[0] + c
if p in oldHitList:
hitList.append(p)
elif self.grid[r][c] == 1:
if p[0] < len(hitGrid) and p[1] < len(hitGrid[p[0]]):
hitList.append(p)
if p not in oldHitList:
hitGrid[p[0]][p[1]] +=1
max_hit = max(max_hit, hitGrid[p[0]][p[1]])
Evaluate max_hit after invoke update to the GridObject obejcts:
sprite_group.update(screen.get_rect(), hitGrid, hitList)
if max_hit >= 4:
print("game over")
done = True
Tint the color of a field dependent on the value in gridHit:
for row in range(GRIDY):
for column in range(GRIDX):
rect = [(MARGIN + WIDTH) * column + MARGIN, (MARGIN + HEIGHT) * row + MARGIN, WIDTH, HEIGHT]
colorlist = [WHITE, RED, GREEN, BLUE]
color = colorlist[min(len(colorlist)-1, hitGrid[row][column])]
pg.draw.rect(screen, color, rect)
See the example:
from random import randrange
import pygame as pg
# define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# define measurements
WIDTH, HEIGHT, MARGIN = 10, 10, 1
GRIDX, GRIDY = 36, 36
class GridObject(pg.sprite.Sprite):
def __init__(self, pos, grid, *groups):
super().__init__(groups)
# create image from grid
self.grid = grid
self.gridsize = (len(grid[0]), len(grid))
imgsize = self.gridsize[0]*(WIDTH+MARGIN), self.gridsize[1]*(HEIGHT+MARGIN)
self.image = pg.Surface(imgsize, flags=pg.SRCALPHA)
self.image.fill((0, 0, 0, 0))
col = (235, 175, 76)
for c in range(self.gridsize[0]):
for r in range(self.gridsize[1]):
if self.grid[r][c] == 1:
rect = [(MARGIN + WIDTH) * c + MARGIN, (MARGIN + HEIGHT) * r + MARGIN, WIDTH, HEIGHT]
pg.draw.rect(self.image, col, rect)
self.rect = self.image.get_rect(center=pos)
self.vel = pg.math.Vector2(8, 0).rotate(randrange(360))
self.pos = pg.math.Vector2(pos)
def update(self, boundrect, hitGrid, hitList):
self.pos += self.vel
self.rect.center = self.pos
if self.rect.left <= boundrect.left or self.rect.right >= boundrect.right:
self.vel.x *= -1
if self.rect.top <= boundrect.top or self.rect.bottom >= boundrect.bottom:
self.vel.y *= -1
# align rect to grid
gridpos = round(self.rect.x / (WIDTH+MARGIN)), round(self.rect.y / (HEIGHT+MARGIN))
self.rect.topleft = gridpos[0] * (WIDTH+MARGIN), gridpos[1] * (HEIGHT+MARGIN)
# increment touched filed
global max_hit
max_hit = 0
oldHitList = hitList[:]
hitList.clear()
for c in range(self.gridsize[0]):
for r in range(self.gridsize[1]):
p = gridpos[1] + r, gridpos[0] + c
if p in oldHitList:
hitList.append(p)
elif self.grid[r][c] == 1:
if p[0] < len(hitGrid) and p[1] < len(hitGrid[p[0]]):
hitList.append(p)
if p not in oldHitList:
hitGrid[p[0]][p[1]] +=1
max_hit = max(max_hit, hitGrid[p[0]][p[1]])
ballGrid = [[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 0]]
def main():
#overlap = False
screen = pg.display.set_mode((GRIDX * (WIDTH+MARGIN) + MARGIN, GRIDY * (HEIGHT+MARGIN)))
# Set title of screen
pg.display.set_caption("Ball With Grid")
clock = pg.time.Clock()
sprite_group = pg.sprite.Group()
ball = GridObject((screen.get_width()//2, screen.get_height()//2), ballGrid, sprite_group)
hitGrid = [[0 for i in range(GRIDX)] for j in range(GRIDY)]
hitList = []
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
if event.type == pg.KEYDOWN and event.key == pg.K_SPACE:
hitGrid = [[0 for i in range(GRIDX)] for j in range(GRIDY)]
screen.fill((0, 0, 0))
# Draw the grid and add values to the cells
for row in range(GRIDY):
for column in range(GRIDX):
rect = [(MARGIN + WIDTH) * column + MARGIN, (MARGIN + HEIGHT) * row + MARGIN, WIDTH, HEIGHT]
colorlist = [WHITE, RED, GREEN, BLUE]
color = colorlist[min(len(colorlist)-1, hitGrid[row][column])]
pg.draw.rect(screen, color, rect)
sprite_group.update(screen.get_rect(), hitGrid, hitList)
if max_hit >= 4:
print("game over")
done = True
sprite_group.draw(screen)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
sys.exit()
The title says it all really. The effect I'm desiring is going to be used for UI, since UI bubbles will appear, and I want to animate them stretching.
Chat bubbles in iOS messaging apps are a good example of this behavior, see
here for example. Here's the main image reproduced:
Notice the last chat bubbles wonky behavior. This is not normal in messaging apps, and the proper stretching is what I want to achieve with Pygame.
Is there any easy way to reproduce this specific kind of stretching in Pygame? Even if there are some constraints, like, all corners have to be the same size or something. I'd just like to know what is possible.
Thanks!
Based on what I had suggested in the comments, here is an implementation of the SliceSprite class that creates and renders a 9-sliced sprite in pygame. I have also included a sample to show how it might be used. It is definitely rough around the edges (does not check for invalid input like when you resize the sprite with a width less than your defined left and right slice sizes) but should still be a useful start. This code has been updated and polished to handle these edge cases and does not recreate nine subsurfaces on every draw call as suggested by #skrx in the comments.
slicesprite.py
import pygame
class SliceSprite(pygame.sprite.Sprite):
"""
SliceSprite extends pygame.sprite.Sprite to allow for 9-slicing of its contents.
Slicing of its image property is set using a slicing tuple (left, right, top, bottom).
Values for (left, right, top, bottom) are distances from the image edges.
"""
width_error = ValueError("SliceSprite width cannot be less than (left + right) slicing")
height_error = ValueError("SliceSprite height cannot be less than (top + bottom) slicing")
def __init__(self, image, slicing=(0, 0, 0, 0)):
"""
Creates a SliceSprite object.
_sliced_image is generated in _generate_slices() only when _regenerate_slices is True.
This avoids recomputing the sliced image whenever each SliceSprite parameter is changed
unless absolutely necessary! Additionally, _rect does not have direct #property access
since updating properties of the rect would not be trigger _regenerate_slices.
Args:
image (pygame.Surface): the original surface to be sliced
slicing (tuple(left, right, top, bottom): the 9-slicing margins relative to image edges
"""
pygame.sprite.Sprite.__init__(self)
self._image = image
self._sliced_image = None
self._rect = self.image.get_rect()
self._slicing = slicing
self._regenerate_slices = True
#property
def image(self):
return self._image
#image.setter
def image(self, new_image):
self._image = new_image
self._regenerate_slices = True
#property
def width(self):
return self._rect.width
#width.setter
def width(self, new_width):
self._rect.width = new_width
self._regenerate_slices = True
#property
def height(self):
return self._rect.height
#height.setter
def height(self, new_height):
self._rect.height = new_height
self._regenerate_slices = True
#property
def x(self):
return self._rect.x
#x.setter
def x(self, new_x):
self._rect.x = new_x
self._regenerate_slices = True
#property
def y(self):
return self._rect.y
#y.setter
def y(self, new_y):
self._rect.y = new_y
self._regenerate_slices = True
#property
def slicing(self):
return self._slicing
#slicing.setter
def slicing(self, new_slicing=(0, 0, 0, 0)):
self._slicing = new_slicing
self._regenerate_slices = True
def get_rect(self):
return self._rect
def set_rect(self, new_rect):
self._rect = new_rect
self._regenerate_slices = True
def _generate_slices(self):
"""
Internal method required to generate _sliced_image property.
This first creates nine subsurfaces of the original image (corners, edges, and center).
Next, each subsurface is appropriately scaled using pygame.transform.smoothscale.
Finally, each subsurface is translated in "relative coordinates."
Raises appropriate errors if rect cannot fit the center of the original image.
"""
num_slices = 9
x, y, w, h = self._image.get_rect()
l, r, t, b = self._slicing
mw = w - l - r
mh = h - t - b
wr = w - r
hb = h - b
rect_data = [
(0, 0, l, t), (l, 0, mw, t), (wr, 0, r, t),
(0, t, l, mh), (l, t, mw, mh), (wr, t, r, mh),
(0, hb, l, b), (l, hb, mw, b), (wr, hb, r, b),
]
x, y, w, h = self._rect
mw = w - l - r
mh = h - t - b
if mw < 0: raise SliceSprite.width_error
if mh < 0: raise SliceSprite.height_error
scales = [
(l, t), (mw, t), (r, t),
(l, mh), (mw, mh), (r, mh),
(l, b), (mw, b), (r, b),
]
translations = [
(0, 0), (l, 0), (l + mw, 0),
(0, t), (l, t), (l + mw, t),
(0, t + mh), (l, t + mh), (l + mw, t + mh),
]
self._sliced_image = pygame.Surface((w, h))
for i in range(num_slices):
rect = pygame.rect.Rect(rect_data[i])
surf_slice = self.image.subsurface(rect)
stretched_slice = pygame.transform.smoothscale(surf_slice, scales[i])
self._sliced_image.blit(stretched_slice, translations[i])
def draw(self, surface):
"""
Draws the SliceSprite onto the desired surface.
Calls _generate_slices only at draw time only if necessary.
Note that the final translation occurs here in "absolute coordinates."
Args:
surface (pygame.Surface): the parent surface for blitting SliceSprite
"""
x, y, w, h, = self._rect
if self._regenerate_slices:
self._generate_slices()
self._regenerate_slices = False
surface.blit(self._sliced_image, (x, y))
Example usage (main.py):
import pygame
from slicesprite import SliceSprite
if __name__ == "__main__":
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
done = False
outer_points = [(0, 20), (20, 0), (80, 0), (100, 20), (100, 80), (80, 100), (20, 100), (0, 80)]
inner_points = [(10, 25), (25, 10), (75, 10), (90, 25), (90, 75), (75, 90), (25, 90), (10, 75)]
image = pygame.Surface((100, 100), pygame.SRCALPHA)
pygame.draw.polygon(image, (20, 100, 150), outer_points)
pygame.draw.polygon(image, (0, 60, 120), inner_points)
button = SliceSprite(image, slicing=(25, 25, 25, 25))
button.set_rect((50, 100, 500, 200))
#Alternate version if you hate using rects for some reason
#button.x = 50
#button.y = 100
#button.width = 500
#button.height = 200
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
screen.fill((0, 0, 0))
button.draw(screen)
pygame.display.flip()
clock.tick()
Here's a solution in which I create an enlarged version of the surface by splitting it into three parts and blitting the middle line repeatedly. Vertical enlargement would work similarly.
import pygame as pg
def enlarge_horizontal(image, width=None):
"""A horizontally enlarged version of the image.
Blit the middle line repeatedly to enlarge the image.
Args:
image (pygame.Surface): The original image/surface.
width (int): Desired width of the scaled surface.
"""
w, h = image.get_size()
# Just return the original image, if the desired width is too small.
if width is None or width < w:
return image
mid_point = w//2
# Split the image into 3 parts (left, mid, right).
# `mid` is just the middle vertical line.
left = image.subsurface((0, 0, w//2, h))
mid = image.subsurface((mid_point, 0, 1, h))
right = image.subsurface((mid_point, 0, w//2, h))
surf = pg.Surface((width, h), pg.SRCALPHA)
# Join the parts (blit them onto the new surface).
surf.blit(left, (0, 0))
for i in range(width-w+1):
surf.blit(mid, (mid_point+i, 0))
surf.blit(right, (width-w//2, 0))
return surf
def main():
screen = pg.display.set_mode((800, 800))
clock = pg.time.Clock()
image = pg.Surface((100, 100), pg.SRCALPHA)
pg.draw.circle(image, (20, 100, 150), (50, 50), 50)
pg.draw.circle(image, (0, 60, 120), (50, 50), 45)
surfs = [enlarge_horizontal(image, width=i) for i in range(0, 701, 140)]
while True:
for event in pg.event.get():
if event.type == pg.QUIT:
return
screen.fill((30, 30, 40))
for i, surf in enumerate(surfs):
screen.blit(surf, (20, i*109 + 5))
pg.display.flip()
clock.tick(60)
if __name__ == '__main__':
pg.init()
main()
pg.quit()