Related
This python code is made to show the animation of 3D solar system with 2d projection.I make two file named solar_system_3d.py and simple_solar_system.py to write the code.But it cannot show properly due to the error:
raceback (most recent call last):
File ~/simple_solar_system.py:14 in
sun = Sun(solar_system)
File ~/solar_system_3d.py:143 in init
super(Sun, self).init(solar_system, mass, position, velocity)
File ~/solar_system_3d.py:89 in init
self.velocity = Vector(*velocity)
TypeError: Vector() takes no arguments
Here is the code from "solar_system_3d.py":
updated version:
# solar_system_3d.py
import itertools
import math
import matplotlib.pyplot as plt
from vectors import Vector
class SolarSystem:
def __init__(self, size, projection_2d=False):
self.size = size
self.projection_2d = projection_2d
self.bodies = []
self.fig, self.ax = plt.subplots(
1,
1,
subplot_kw={"projection": "3d"},
figsize=(self.size / 50, self.size / 50),
)
self.fig.tight_layout()
if self.projection_2d:
self.ax.view_init(10, 0)
else:
self.ax.view_init(0, 0)
def add_body(self, body):
self.bodies.append(body)
def update_all(self):
self.bodies.sort(key=lambda item: item.position[0])
for body in self.bodies:
body.move()
body.draw()
def draw_all(self):
self.ax.set_xlim((-self.size / 2, self.size / 2))
self.ax.set_ylim((-self.size / 2, self.size / 2))
self.ax.set_zlim((-self.size / 2, self.size / 2))
if self.projection_2d:
self.ax.xaxis.set_ticklabels([])
self.ax.yaxis.set_ticklabels([])
self.ax.zaxis.set_ticklabels([])
else:
self.ax.axis(False)
plt.pause(0.001)
self.ax.clear()
def calculate_all_body_interactions(self):
bodies_copy = self.bodies.copy()
for idx, first in enumerate(bodies_copy):
for second in bodies_copy[idx + 1:]:
first.accelerate_due_to_gravity(second)
class SolarSystemBody:
min_display_size = 10
display_log_base = 1.3
def __init__(
self,
solar_system,
mass,
position=(0, 0, 0),
velocity=(0, 0, 0),
):
self.solar_system = solar_system
self.mass = mass
self.position = position
self.velocity = Vector(*velocity)
self.display_size = max(
math.log(self.mass, self.display_log_base),
self.min_display_size,
)
self.colour = "black"
self.solar_system.add_body(self)
def move(self):
self.position = (
self.position[0] + self.velocity[0],
self.position[1] + self.velocity[1],
self.position[2] + self.velocity[2],
)
def draw(self):
self.solar_system.ax.plot(
*self.position,
marker="o",
markersize=self.display_size + self.position[0] / 30,
color=self.colour
)
if self.solar_system.projection_2d:
self.solar_system.ax.plot(
self.position[0],
self.position[1],
-self.solar_system.size / 2,
marker="o",
markersize=self.display_size / 2,
color=(.5, .5, .5),
)
def accelerate_due_to_gravity(self, other):
distance = Vector(*other.position) - Vector(*self.position)
distance_mag = distance.get_magnitude()
force_mag = self.mass * other.mass / (distance_mag ** 2)
force = distance.normalize() * force_mag
reverse = 1
for body in self, other:
acceleration = force / body.mass
body.velocity += acceleration * reverse
reverse = -1
class Vector:
def __sub__(self, vec):
return Vector(self.x - vec.x)
class Sun(SolarSystemBody):
def __init__(
self,
solar_system,
mass=10_000,
position=(0, 0, 0),
velocity=(0, 0, 0),
):
super(Sun, self).__init__(solar_system, mass, position, velocity)
self.colour = "yellow"
class Planet(SolarSystemBody):
colours = itertools.cycle([(1, 0, 0), (0, 1, 0), (0, 0, 1)])
def __init__(
self,
solar_system,
mass=10,
position=(0, 0, 0),
velocity=(0, 0, 0),
):
super(Planet, self).__init__(solar_system, mass, position, velocity)
self.colour = next(Planet.colours)
here is the code from simple_solar_system.py:
# simple_solar_system.py
from solar_system_3d import SolarSystem, Sun, Planet
solar_system = SolarSystem(400, projection_2d=True)
sun = Sun(solar_system)
planets = (
Planet(
solar_system,
position=(150, 50, 0),
velocity=(0, 5, 5),
),
Planet(
solar_system,
mass=20,
position=(100, -50, 150),
velocity=(5, 0, 0)
)
)
while True:
solar_system.calculate_all_body_interactions()
solar_system.update_all()
solar_system.draw_all()
Plz help me, thx a lot!
Python objects have what's called dunder methods, or double underscore. aka magic methods.
Heres what some look like
__lt__(), __le__(), __gt__(), or __ge__()
class Vector:
def __add__(self, vec):
return Vector(self.x + vec.x)
I could say something like and then my Vector classes could be added with the + operator
Heres a better example docs.python - datamodel
I assume you are using this vectors library.
Say you have two vectors:
from vectors import Vector
a = Vector(1, 1, 1)
b = Vector(2, 2, 2)
The Vector class from the vectors library has a function substract(other). You can then use this function to substract vectors in the following way:
a_minus_b = b.substract(a) # a - b
b_minus_a = a.substract(b) # b - a
You could replace all vector minuses in your code with the above code. However, you can also define the __sub__() function on the Vector class like this:
class Vector(Vector):
def __sub__(self, other):
return other.substract(self)
That way a - b will implicitly call the __sub()__ function.
Now you can substract vectors simply as:
a_minus_b = a - b
b_minus_a = b - a
I follow a youtube video 'https://www.youtube.com/watch?v=cCiXqK9c18g' and in the video he make a class that represent a ball and he used pymunk to make a body and added it to the space and after that he created a method inside the ball class that will use pygame to draw the ball and I did almost like him
import pygame
import pymunk
pygame.init()
fps = 60
dt = 1/fps
dsX = 800 # screen width
dsY = 500 # screen height
display = pygame.display.set_mode((dsX, dsY))
space = pymunk.Space()
clock = pygame.time.Clock()
def convert_cor(point): # convet the coordinates from pymunk to pygame coordinates
return point[0], dsY - point[1]
class Particle: # v: velocity, pos: position[x, y], r: radius of particle(Circle)
def __init__(self, pos = [0, 0], v = [0, 0], r = 10, color = (255, 0, 0)):
self.pos = pos
self.v = v
self.r = r
self.color = color
self.body = pymunk.Body()
self.body.position = self.pos
self.body.velocity = self.v # this is the veclocity
self.shape = pymunk.Circle(self.body, self.r)
self.shape.dencity = 1
self.shape.elasticity = 1
space.add(self.body, self.shape)
def draw(self):
pygame.draw.circle(display, self.color, convert_cor(self.pos), self.r)
class Box: # thickness of the sides of the box and L1, L2, L3, L4 are the sides of the box
def __init__(self, thickness, color):
self.thickness = thickness
self.color = color
L1 = pymunk.Body(body_type = pymunk.Body.STATIC)
L2 = pymunk.Body(body_type = pymunk.Body.STATIC)
L3 = pymunk.Body(body_type = pymunk.Body.STATIC)
L4 = pymunk.Body(body_type = pymunk.Body.STATIC)
L1_shape = pymunk.Segment(L1, (0, 0), (dsX, 0), self.thickness)
L2_shape = pymunk.Segment(L2, (dsX, 0), (dsX, dsY), self.thickness)
L3_shape = pymunk.Segment(L3, (dsX, dsY), (0, dsY), self.thickness)
L4_shape = pymunk.Segment(L4, (0, dsY), (0, 0), self.thickness)
space.add(L1, L1_shape)
space.add(L2, L2_shape)
space.add(L3, L3_shape)
space.add(L4, L4_shape)
def draw(self):
pygame.draw.line(display, self.color, convert_cor((0, 0)), convert_cor((dsX, 0)), self.thickness * 2)
pygame.draw.line(display, self.color, convert_cor((dsX, 0)), convert_cor((dsX, dsY)), self.thickness * 2)
pygame.draw.line(display, self.color, convert_cor((dsX, dsY)), convert_cor((0, dsY)), self.thickness * 2)
pygame.draw.line(display, self.color, convert_cor((0, dsY)), convert_cor((0, 0)), self.thickness * 2)
def Sim(): # the infinite while loop as a function
box = Box(2, (0, 255, 255))
particle = Particle(pos =[dsX/2, dsY/2], v = [-200, 500]) # here i gave the position and the velocity
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
display.fill((255, 255, 255))
box.draw()
particle.draw()
clock.tick(fps)
space.step(dt)
pygame.display.update()
Sim()
pygame.quit()
The thing is, I did also a class that will add a rigid sides for the display and i drew the sides from the Box class using the method 'draw' The problem is in the time 5:58 in the video he gave the ball velocity and it start moving and in my code it does not move. any idea why it doen't move?
note: I called the ball particle in my code
You error is both a typo and using the wrong variable.
Inside your particles draw function...
# OLD
def draw(self):
pygame.draw.circle(display, self.color, convert_cor(self.pos), self.r)
# New
def draw(self):
pygame.draw.circle(display, self.color, convert_cor(self.body.position), self.r)
You have to use the body's position cause that is the position of the physics body in pymunk's space.
Secondly...
class Particle: # v: velocity, pos: position[x, y], r: radius of particle(Circle)
def __init__(self, pos, v, r=10, color=(255, 0, 0)):
...
# Old
self.shape.dencity = 1
# New
self.shape.density = 1
Since density was not set to anything Pymunk was having a divide by zero error so it wouldn't update the body's position.
self.body.position = self.pos
To be clear about the problem:
From what I could find in the documentation, pymunk.Body.position is a property; it expects you to pass either an ordinary Python tuple or a Vec2d (not a list, although anything it's relying on internally to handle a tuple probably handles a list just fine), and calls some internal code written in another programming language. The effect is that, instead of storing your list object and then making changes to the list (which would be visible in your class, since it would be the same object), it just gets the initial values out of your list, and then doesn't use the list object.
This means that when Pymunk applies physics calculations, self.body.position changes, but self.pos does not. So the current self.pos is useless; we can't use it to check the object position.
If you don't need to do that, then there is no need to create a self.pos at all - just feed self.body.position = pos directly, and make sure to use self.body.position when drawing.
If you do, I recommend using your own property instead of trying to set self.pos. Do the above, and then add to the class:
#property
def pos(self):
return self.body.position
And if you want to change the position from your code (but you probably shouldn't! Why else are you using the physics engine in the first place?), also add:
#pos.setter
def pos(self, value):
self.body.position = value
I want to draw a triangle from a class, so I call the function
pygame.draw.polygon()
Now, the problem is that I need to pass the points in a manner that will allow me to calculate the centre of the triangle.
I was trying to pass the tuples one by one in this way
self.first_point = (int, int)
self.second_point = (int, int)
self.third_point = (int, int)
so that I can then access the single tuple values.
Then pass the three points like this
self.position = [self.first_point, self.second_point, self.third_point]
But for some reason, it doesn't work.
This is the error I get
File "C:/Users/oricc/PycharmProjects/designAChessboardChallange/display.py", line 178, in <module>
white_archer_3 = Archer(white, [(100, 100), (200, 200), (300, 300)])
[(100, 100), (200, 200), (300, 300)]
File "C:/Users/oricc/PycharmProjects/designAChessboardChallange/display.py", line 132, in __init__
self.triangle = pygame.draw.polygon(game_window, colour, self.position)
TypeError: points must be number pairs
By number of pairs, the Pygame documentation gives as an example
e.g. [(x1, y1), (x2, y2), (x3, y3)]
In fact, when I print the position I pass I get, as you can see from the error above
[(100, 100), (200, 200), (300, 300)]
Anyone can help with this?
Is there another manner to calculate the centre without accessing the xs and ys like that?
Full code here
import pygame
import sys
from coordinator import coordinator
# set up the display
pygame.init()
window_size = (800, 800)
game_window = pygame.display.set_mode(size=window_size)
pygame.display.set_caption('My Game')
# defines classes and related methods
class WhiteSquare:
def __init__(self):
self.height = int(window_size[0] / 8)
self.width = int(window_size[1] / 8)
self.white_square = pygame.Surface((self.height, self.width))
self.white_square.fill((255, 255, 255))
class BlackSquare:
def __init__(self):
self.height = int(window_size[0] / 8)
self.width = int(window_size[1] / 8)
self.black_square = pygame.Surface((self.height, self.width))
self.black_square.fill((0, 0, 0))
class ChessBoard:
def __init__(self):
self.ws = ws
self.bs = bs
self.white_columns = white_columns
self.black_columns = black_columns
def draw(self):
for w_columns in self.white_columns:
game_window.blit(self.ws.white_square, w_columns)
for b_columns in self.black_columns:
game_window.blit(self.bs.black_square, b_columns)
# declare letters and numbers
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
numbers = ['1', '2', '3', '4', '5', '6', '7', '8']
# create coordinates
coordinates = []
for item_letter in letters:
letter = item_letter
for item_number in numbers:
number = item_number
coordinates.append(letter + number)
# create coordinates values components
x_values = []
for number in range(0, 800, 100):
x = number
x_values.append(x)
y_values = []
for number in range(0, 800, 100):
y = number
y_values.append(y)
# create coordinate values
coordinate_values = []
for x in x_values:
for y in y_values:
coordinate_values.append((x, y))
# assign values to coordinates
squares_coordinates = dict(zip(coordinates, coordinate_values))
# Background for units
class CircleSurface:
def __init__(self):
self.circle_surface = pygame.Surface((100, 100), flags=pygame.SRCALPHA)
pygame.draw.circle(self.circle_surface, (255, 0, 0), (50, 50), 45)
# define colours
black = (0, 0, 0)
white = (255, 255, 255)
gold = (153, 153, 0)
class Unit:
def __init__(self, colour, position):
# define Unit colour
self.colour = colour
# define Unit position
self.position = position
class Knight(Unit):
def __init__(self, colour, position):
# draw circle, inline, and outline
super().__init__(colour, position)
self.center_x = position[0]
self.center_y = position[1]
self.colour = colour
self.position = position
circle_radius = 40
self.circle = pygame.draw.circle(game_window, colour, self.position, circle_radius)
self.circle_outline = pygame.draw.circle(game_window, gold, self.position, circle_radius, 5)
self.circle_inline = pygame.draw.circle(game_window, gold, self.position, (circle_radius - 10), 5)
# draw letter
pygame.font.init()
my_font_size = 50
my_font = pygame.font.SysFont('Time New Roman', my_font_size)
text_surface = my_font.render('K', 1, gold)
center_text = text_surface.get_rect(center=(self.center_x, self.center_y))
game_window.blit(text_surface, center_text)
class Archer(Unit):
def __init__(self, colour, position):
super().__init__(colour, position)
self.first_point = (int, int)
self.second_point = (int, int)
self.third_point = (int, int)
self.position = [self.first_point, self.second_point, self.third_point]
print(position)
self.triangle = pygame.draw.polygon(game_window, colour, self.position)
self.triangle_outline = pygame.draw.polygon(game_window, gold, self.position, 5)
self.triangle_inline = pygame.draw.polygon(game_window, gold, self.position, 5)
# draw letter
# pygame.font.init()
# my_font_size = 50
# my_font = pygame.font.SysFont('Time New Roman', my_font_size)
# text_surface = my_font.render('A', 1, gold)
# center_text = text_surface.get_rect(center=(self.center_x, self.center_y))
# game_window.blit(text_surface, center_text)
# Sets and gets the coordinates for black and white squares
coordinator = coordinator()
black_columns = coordinator[2] + coordinator[3]
white_columns = coordinator[0] + coordinator[1]
# Creates needed objects
ws = WhiteSquare()
bs = BlackSquare()
cb = ChessBoard()
cs = CircleSurface()
# Event loop (outer)
while 1:
# Event loop (inner)
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# Draws the chessboard
cb.draw()
# Draws white pieces in their initial position
white_knight_1 = Knight(white, (150, 650))
white_knight_2 = Knight(white, (650, 650))
white_archer_3 = Archer(white, [(100, 100), (200, 200), (300, 300)])
pygame.display.update()
Thank you
OK, I managed to make it work.
You guys are both right, I should know by now that I can't pass placeholders, so I figured out the solution to my problem as follow:
class Archer(Unit):
def __init__(self, colour, first_point, second_point, third_point):
self.first_point = first_point
self.second_point = second_point
self.third_point = third_point
position = [self.first_point, self.second_point, self.third_point]
super().__init__(colour, position)
print(self.position)
self.triangle = pygame.draw.polygon(game_window, colour, self.position)
self.triangle_outline = pygame.draw.polygon(game_window, gold, self.position, 5)
self.triangle_inline = pygame.draw.polygon(game_window, gold, self.position, 5)
Basically I have to declare the three points self variables, as well as position before the super function so that I can then pass them as 'position' to the parent class initializer. This has been really useful!!
Thanks both
you could also do this: self.first_point = (int(0), int(0)) as int is not a placeholder but to declare that a variable is an integer str('0') will print '0' you could also input this
zero = 0
str(zero) #'0'
int(zero) #0
and you dont need to put rgb tupels because you can store them in a variable like this
black = (0, 0, 0)
Display = pygame.display.set_mode((800, 600))
Display.fill(black)
If I draw rectangles with the following code:
def render(shape):
gl.glLoadIdentity()
gl.glVertexPointer(2, gl.GL_FLOAT, 0, 0)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, shape.vbo_id)
gl.glColor4ub(shape.color[0], shape.color[1], shape.color[2], 255)
gl.glDrawArrays(shape.draw_mode, 0, shape.size)
...The colors I get are one off. If I draw Red, Green, Blue, Violet squares, I actually get Green, Blue, Violet, Red.
If I swap the color and draw lines like this:
def render(shape):
gl.glLoadIdentity()
gl.glVertexPointer(2, gl.GL_FLOAT, 0, 0)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, shape.vbo_id)
# SWAPPED
gl.glDrawArrays(shape.draw_mode, 0, shape.size)
gl.glColor4ub(shape.color[0], shape.color[1], shape.color[2], 255)
Then all but the last will draw the colors correctly. The last defaults to white. UNLESS I draw over and over in a loop. Then it is ok.
What is happening? I'm not great at OpenGL and I'm not sure where to look.
Full example:
import pyglet
import ctypes
import pyglet.gl as gl
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
VIOLET = (255, 0, 255)
def start_render():
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
gl.glMatrixMode(gl.GL_MODELVIEW)
gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
def set_viewport(left, right, bottom, top):
# gl.glViewport(0, 0, _window.height, _window.height)
gl.glMatrixMode(gl.GL_PROJECTION)
gl.glLoadIdentity()
gl.glOrtho(left, right, bottom, top, -1, 1)
gl.glMatrixMode(gl.GL_MODELVIEW)
gl.glLoadIdentity()
def open_window(width, height, window_title):
window = pyglet.window.Window(width=width, height=height,
caption=window_title)
set_viewport(0, width - 1, 0, height - 1)
window.invalid = False
return window
class VertexBuffer:
def __init__(self, vbo_id: gl.GLuint, size: float, draw_mode: int):
self.vbo_id = vbo_id
self.size = size
self.draw_mode = draw_mode
self.color = None
self.line_width = 0
def create_rectangle(center_x, center_y, width, height, color):
x1 = -width / 2 + center_x
y1 = -height / 2 + center_y
x2 = width / 2 + center_x
y2 = -height / 2 + center_y
x3 = width / 2 + center_x
y3 = height / 2 + center_y
x4 = -width / 2 + center_x
y4 = height / 2 + center_y
data = [x1, y1,
x2, y2,
x3, y3,
x4, y4]
# print(data)
vbo_id = gl.GLuint()
gl.glGenBuffers(1, ctypes.pointer(vbo_id))
# Create a buffer with the data
# This line of code is a bit strange.
# (gl.GLfloat * len(data)) creates an array of GLfloats, one for each number
# (*data) initalizes the list with the floats. *data turns the list into a
# tuple.
data2 = (gl.GLfloat * len(data))(*data)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo_id)
gl.glBufferData(gl.GL_ARRAY_BUFFER, ctypes.sizeof(data2), data2,
gl.GL_STATIC_DRAW)
shape_mode = gl.GL_QUADS
shape = VertexBuffer(vbo_id, len(data) // 2, shape_mode)
shape.color = color
return shape
def render(shape):
gl.glLoadIdentity()
gl.glVertexPointer(2, gl.GL_FLOAT, 0, 0)
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, shape.vbo_id)
gl.glColor4ub(shape.color[0], shape.color[1], shape.color[2], 255)
gl.glDrawArrays(shape.draw_mode, 0, shape.size)
class MyApp:
def __init__(self):
self.r1 = None
self.r2 = None
self.r3 = None
self.r4 = None
self.window = None
def setup(self):
self.window = open_window(800, 600, "Test 2")
self.r1 = create_rectangle(100, 100, 50, 50, RED) # Green
self.r2 = create_rectangle(200, 200, 50, 50, GREEN) # Blue
self.r3 = create_rectangle(300, 300, 50, 50, BLUE) # Violet
self.r4 = create_rectangle(400, 400, 50, 50, VIOLET) # Red
pyglet.clock.schedule_interval(self.draw, 1/60)
def draw(self, dt):
start_render()
render(self.r1)
render(self.r2)
render(self.r3)
render(self.r4)
def main1():
# This just draws one frame and stops.
window = open_window(800, 600, "Test 1")
r1 = create_rectangle(100, 100, 50, 50, RED)
r2 = create_rectangle(200, 200, 50, 50, GREEN)
r3 = create_rectangle(300, 300, 50, 50, BLUE)
r4 = create_rectangle(400, 400, 50, 50, VIOLET)
start_render()
render(r1)
render(r2)
render(r3)
render(r4)
window.flip()
pyglet.app.run()
def main2():
# This main function repeatedly updates the screen.
app = MyApp()
app.setup()
pyglet.app.run()
main1()
gl.glVertexPointer(2, gl.GL_FLOAT, 0, 0)
^ what VBO is bound?
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, shape.vbo_id)
Wrong order. Right now glVertexPointer() is using whatever buffer you happened to have bound.
Bind then Pointer:
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, shape.vbo_id)
gl.glVertexPointer(2, gl.GL_FLOAT, 0, 0)
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()