I am using Pyglet to create a main menu for my python game. I want to draw text on top of a box that would act as its container. Whenever I render the text, instead of it having a transparent background, it draws whatever the glClearColor is set to. This also happens whenever I try and draw an image that has no background.
I am currently using these two lines for my textbox and the text.
self.text_box = pyglet.sprite.Sprite(pyglet.image.load('../Resources/Textures/Menu/text_box.png'),640-150,360-25, batch=self.menuBatch,group=boxGroup)
self.play_text = pyglet.text.Label("Play", font_name="Arial", font_size=32, x=640, y=360, anchor_x='center', anchor_y='center', color=(255,255,255,255), batch=self.menuBatch,group=textGroup)
Then I just call self.menuBatch.draw(). A picture of the problem I am having is:
For transparency effects to work, 'blend' should be enabled in OpenGL.
To enable blend:
glEnable(GL_BLEND) # transparency
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) # transparency
This complete snippet is working for me:
import pyglet
from pyglet.gl import *
# Pyglet Window stuff ---------------------------------------------------------
batch = pyglet.graphics.Batch() # holds all graphics
config = Config(sample_buffers=1, samples=4,depth_size=16, double_buffer=True, mouse_visible=False)
window = pyglet.window.Window(fullscreen=False, config=config)
glClearColor( 0, 100, 0, 255) # background color
glEnable(GL_LINE_SMOOTH)
glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE)
glEnable(GL_BLEND) # transparency
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) # transparency
play_text = pyglet.text.Label("Play", font_name="Arial", font_size=32, x=240, y=140, anchor_x='center', anchor_y='center', color=(255,0,255,255), batch=batch,group=None)
text_box = pyglet.sprite.Sprite(pyglet.image.load('text_box.png'),240-150,160-25, batch=batch,group=None)
#window.event
def draw(dt):
window.clear()
batch.draw()
if __name__ == "__main__":
pyglet.clock.schedule_interval(draw, 1.0/60)
pyglet.app.run()
I have done something very similar to this before when making the GUI for one of my 3D programs. To make things very simple, I firstly turned off GL_DEPTH_TEST before the draw() call and then I re-enabled GL_DEPTH_TEST after the draw() call. I believe that the reason why the previous answer was working for them is because they didn't have GL_DEPTH_TEST enabled in the first place. I made a button class that you could maybe use as well (It's important to note that the argument "pos" is for the (x, y) coordinate of the lower left corner of the button and the argument "dim" is for the (width, height) dimensions of the button. The "texture_coords" argument refers to the texture coordinates of the background image of the button. If you want the full image, just leave it at the default. The rest is pretty self explanatory). So here's the code:
class Button:
def __init__(self, btn_img_file, text = "Button", pos = (0, 0), dim = (10, 10), text_color = (255, 255, 255), texture_coords = (0, 0, 1, 0, 1, 1, 0, 1)):
self.x, self.y = pos
self.w, self.h = dim
self.text = text
self.img = btn_img_file
self.tex = get_tex(self.img)
self.coords = texture_coords
self.color = text_color
self.batch = pyglet.graphics.Batch()
def on_click(self, mouse_x, mouse_y, function, *args, **kwargs):
x, y = self.x, self.y
X, Y = x + self.w, y + self.h
if mouse_x > x and mouse_x < X and mouse_y > y and mouse_y < Y:
function(*args, **kwargs)
def draw(self):
x, y = self.x, self.y
X, Y = x + self.w, y + self.h
font_size = (self.w // len(self.text)) - 5
label = pyglet.text.Label(self.text,
font_name = 'Times New Roman',
font_size = font_size,
x = x + (self.w // 2), y = y + (self.h // 2),
anchor_x = 'center', anchor_y = 'center',
color = (*self.color, 255))
self.batch.add(4, GL_QUADS, self.tex, ('v2f', (x, y, X, y, X, Y, x, Y)), ('t2f', self.coords))
glDisable(GL_DEPTH_TEST)
self.batch.draw()
label.draw()
glEnable(GL_DEPTH_TEST)
I hope this was helpful!
~ Rick Bowen
Related
I'm doing a fun exercise where I draw a spline and control it using four handle points. I want to be able to drag any of the four points individually, and this works fine if I hardcode a specific state for each instance of the handle object, but I'd like to make the drag event part of the point class so I can spawn as many handles as I'd like. But now I'm at a bit of a loss because while I can get the most recently defined object to show the correct behavior, the cv2 mouse callback seems to only honor one object at a time.
import cv2
import numpy as np
# Lerp for handles
def lerp(start, end, dt):
lerp_list = []
t = 1
while t >= 0:
x = start[0] + (end[0] - start[0]) * t
y = start[1] + (end[1] - start[1]) * t
lerp_list.append([x, y])
t = round(t-dt, 2)
return np.int32(lerp_list)
# Lerp for spline
def lerp_spline(l1, l2):
lerp_list = []
for pt in range(len(l1)):
t = pt/(len(l1)-1)
x = l1[pt][0] + (l2[pt][0] - l1[pt][0]) * t
y = l1[pt][1] + (l2[pt][1] - l1[pt][1]) * t
lerp_list.append([x, y])
return np.int32(lerp_list)
class Handle:
def __init__(self, x, y):
self.dragging = False
self.x = x + origin[0]
self.y = y + origin[1]
self.pt = [self.x, self.y]
self.radius = 5
def on_mouse_event(self, event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
if (x - self.x)**2 + (y - self.y)**2 < self.radius**2:
self.dragging = True
print("click")
elif event == cv2.EVENT_MOUSEMOVE:
if self.dragging:
self.x = x
self.y = y
self.pt = [self.x, self.y]
print("dragging")
elif event == cv2.EVENT_LBUTTONUP:
self.dragging = False
print("release")
# Canvas Params
height = 512
width = 512
canvas = np.zeros((height, width, 3), dtype=np.uint8)
origin = [int(width/2), int(height/2)]
# Initial Points for Handles
radius = 5
p0 = Handle(0, -150)
p1 = Handle(-224, 200)
p2 = Handle(192, 0)
p3 = Handle(232, 192)
# Mouse events
cv2.namedWindow('Spline')
cv2.setMouseCallback('Spline', p0.on_mouse_event, param=p0)
cv2.setMouseCallback('Spline', p1.on_mouse_event, param=p1)
cv2.setMouseCallback('Spline', p2.on_mouse_event, param=p2)
cv2.setMouseCallback('Spline', p3.on_mouse_event, param=p3)
while True:
# Wipe canvas each frame
canvas = np.zeros((height, width, 3), dtype=np.uint8)
# Spline Functions
t = .05
lp0 = lerp(p0.pt, p1.pt, t)
lp1 = lerp(p3.pt, p2.pt, t)
spline = lerp_spline(lp0, lp1)
# Draw Spline
canvas = cv2.polylines(canvas, [spline], False, (255, 255, 0), 1, lineType=cv2.LINE_AA)
# Draw Handles
canvas = cv2.circle(canvas, lp0[0], 2, (255, 0, 0), -1, lineType=cv2.LINE_AA)
canvas = cv2.circle(canvas, lp1[0], 2, (255, 255, 0), -1, lineType=cv2.LINE_AA)
canvas = cv2.circle(canvas, lp0[-1], 2, (0, 255, 255), -1, lineType=cv2.LINE_AA)
canvas = cv2.circle(canvas, lp1[-1], 2, (0, 0, 255), -1, lineType=cv2.LINE_AA)
canvas = cv2.line(canvas, p0.pt, p1.pt, (255, 255, 255), 1, lineType=cv2.LINE_AA)
canvas = cv2.line(canvas, p2.pt, p3.pt, (255, 255, 255), 1, lineType=cv2.LINE_AA)
# Show Canvas
cv2.imshow('Spline', canvas)
if cv2.waitKey(16) == ord('q'):
break
cv2.destroyAllWindows()
Apologies for the large amount of code. It is fully operational. The part I'm concerned with is from the Handle class down.
Currently, only the most recently referenced Handle object, p3, shows correct functionality. In order to simplify the example I've repeated the mouse callback four times, once for each Handle object. Not an elegant solution but hopefully illustrates the issue.
Intention and result:
I was hoping that the Handle class might behave something like a CSS class, where the mouse event is automatically assigned to all instances of the class, and each instance performs individually. This of course was wishful thinking. What ended up happening was that only the final mouse callback seems to count. So when the script is checking if the mouse is close to self.x or self.y, it's only checking against the most recently defined coordinates.
Thanks for the help!
I figured out a solution. By calling on_mouse_event() as a recursive function, it worked.
def on_mouse_event(event, x, y, flags, param):
for handle in handles:
handle.on_mouse_event(event, x, y, flags, param)
Then later called this function in a single mouse callback:
cv2.setMouseCallback('Spline', on_mouse_event)
Hopefully this obscure little problem will help somebody out someday :)
In this program when the user types more text I want the rectangle to automatically get longer when the user types to keep the letters inside of the rectangle. However, it doesn't update the rectangle when the text gets longer. How do I fix this?
from pygame import *
init()
screen = display.set_mode((800, 600))
name_font = font.Font(None, 32)
name_text = ''
class Rectangle:
def __init__(self, x, y):
self.x = x
self.y = y
self.input_rect = Rect(x, y, 140, 32)
self.text_surface = name_font.render(name_text, True, (255, 255, 255))
color = Color('lightskyblue3')
draw.rect(screen, color, self.input_rect, 2)
self.input_rect.w = self.text_surface.get_width() + 10
screen.blit(self.text_surface, (self.input_rect.x + 5, self.input_rect.y + 5))
def naming():
global name_text
if events.type == KEYDOWN:
if keys[K_BACKSPACE]:
name_text = name_text[:-1]
screen.fill((0, 0, 0))
rect_1 = Rectangle(200, 200)
else:
name_text += events.unicode
while True:
rect_1 = Rectangle(200, 200)
for events in event.get():
keys = key.get_pressed()
naming()
if events.type == QUIT:
quit()
display.update()
time.delay(1)
The Rectangle.text_surface is a PyGame Surface. So you can easily get the precise width of the bounding box by simply calling self.text_surface.get_width().
But you start the size of the border-rect at 140, so this size has to be the maximum of 140 or whatever the new (longer) width is. Another problem is that when the rectangle re-sizes, the old rectangle is left behind. So whenever we now re-draw the rectangle, it erases the background to black.
This is all pretty easily encapsulated into the exiting __init__():
def __init__(self, x, y):
self.x = x
self.y = y
self.text_surface = name_font.render(name_text, True, (255, 255, 255))
rect_width = max( 140, 10 + self.text_surface.get_width() ) # Adjust the width
color = Color('lightskyblue3')
self.input_rect = Rect(x, y, rect_width, 32) # Use new width (if any)
draw.rect(screen, (0,0,0) , self.input_rect, 0) # Erase any existing rect
draw.rect(screen, color, self.input_rect, 2)
self.input_rect.w = self.text_surface.get_width() + 10
screen.blit(self.text_surface, (self.input_rect.x + 5, self.input_rect.y + 5))
I'm trying to generate multiple rectangles which translate across the screen with varying distance of separation between any two consecutive rectangles.
Here's the snippet of the code-
win = pygame.display.set_mode((500, 500)) #canvas size is 500x500
width = 40
height = 60
x = 500 - width
y = 500 - height
vel = 5
state = True
while(state):
pygame.time.delay(50)
x -= vel
pygame.draw.rect(win, (0, 0, 255), (x, y, width, height))
pygame.display.update()
#I have not included the pygame exit code
Now, how do I go about this, without making the rectangles disappear everytime I try to generate a new one?
Create a list of rectangles:
rect_list = []
When you want to add a new rectangle, the append an new pygame.Rect object to the list:
rect_list.append(pygame.Rect(x, y, width, height))
Change the location of the rectangle and draw the rectangles in a loop, in the main application loop:
state = True
while state:
# [...]
for rect_obj in rect_list:
rect_obj.x -= vel
pygame.draw.rect(win, (0, 0, 255), rect_obj)
# [...]
This answer is derived off Rabbid76's answer, but with a slight modification that allows each individual rect to have it's own vel speed and color:
class rectangle:
# You can add as many new values as you like, just be wary about changing the other magic methods
def __init__(top, left, width, height, vel, color=(0, 0, 255)):
self.vel = vel
self.pos = (top, left)
self.size = (width, height)
self.rect = self.pos + self.size
def __iter__(self):
return self.rect
def __getitem__(self, key):
if type(key)!=int:
raise TypeError('invalid key!')
return self.rect[key]
def __len__(self):
return len(self.rect)
def __reversed__(self):
return reversed(self.rect)
def update_rect(self): # This should be called every time you make an adjustment to pos or size
self.rect = self.pos+self.size
def move(self, x, y):
self.pos[0] += x
self.pos[1] += y
self.update_rect
def resize(self, width, height):
self.size = (width, height)
self.update_rect
Now, you store a list of instances:
rect_list = []
rect_list.append(rectangle(top, left, width, height, vel, color)) # You can call this as many times as you want
of these classes instead of a list of pygame.Rects, and when it comes time to draw it, the magic methods will come into play, and magically make it a pygame.Rect object:
for rect in rect_list:
rect.move(x=-rect.vel, y=0) # Move it negative x
pygame.draw.rect(win, rect.color, pygame.Rect(rect)) # Draw it.
I copied this function from one of my older projects, on which it works perfectly, but it doesn't work anymore. The button is supposed to detect when the cursor is above it, and redraw itself in a lighter colour, and then when the cursor moves off, it redraws itself in the usual darker colour. But now when the cursor is above it, it doesn't change. It doesn't respond to clicking either. Here is the code
def button(text, x, y, width, height, inactive_color, active_color, action = None):
cur = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()
if x + width > cur[0] > x and y + height > cur[1] > y:
pygame.draw.rect(gameDisplay, active_color, (x, y, width, height))
pygame.display.update()
if click[0] == 1 and action != None:
if action == 'correct':
print('correct!')
else:
pygame.draw.rect(gameDisplay, inactive_color, (x, y, width, height))
text_to_button(text, black, x, y, width, height)
pygame.display.update()
button('test', 100, 100, 100, 50, darkGreen, green, action = 'correct')
You are calling the button function once. It runs through the code of the function and terminates, and does not respond further to any inputs -- because it only ran once (almost instantly).
Perhaps if you called this method every time a mouse movement event occurs in the Pygame evnet queue it may work.
Alternatively, consider using an object instead of a function for this, such as:
class Button():
def __init__(self, x, y, width, height, text, action):
self.x = x
self.y = y
self.width = width
self.height = height
self.text = text
self.action = action
self.label = myfont.render(self.text, 1, (0,0,0))
def render(self, window):
if self.visible == True:
pygame.draw.rect(window, (255,255,255), [self.x, self.y, self.width, self.height])
window.blit(self.label, (self.x, self.y))
def pressed(self):
self.action(self.arguments)
def hover(self):
#Here you can implement your code that handles when the mouse hovers over the button. This method can be called by checking mouse movement events in your main loop and seeing if they lie within the coordinates, width, and height of the button.
I am trying to have a circle that, when clicked, moves somewhere else on the screen. However, when I click the circle, nothing happens.
#IMPORT STUFF
import pyglet as pg
from random import randint
mouse = pg.window.mouse
#VARS
window = pg.window.Window(width = 640, height = 480)
score = 0
circleImg = pg.image.load("circle.png")
circle = pg.sprite.Sprite(circleImg, randint(1, window.width), randint(1, window.height))
text = pg.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
#DETECT MOUSE PRESS ON CIRCLE
#window.event
def on_mouse_press(x, y, button, modifiers):
if x == circle.x and y == circle.y:
circle.x = randint(1, window.width)
circle.y = randint(1, window.height)
#window.event
def on_draw():
window.clear()
text.draw()
circle.draw()
pg.app.run()
import pyglet
from pyglet.gl import *
from random import randint
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
class Circle(pyglet.sprite.Sprite):
def __init__(self, radiance=5, x=0, y=0):
self.texture = pyglet.image.load('circle.png')
super(Circle, self).__init__(self.texture)
def click(self, x, y):
if x >= self.x and y >= self.y:
if x <= self.x + self.texture.width and y <= self.y + self.texture.height:
return self
mouse = pyglet.window.mouse
#VARS
window = pyglet.window.Window(width = 640, height = 480)
score = 0
#circleImg = pyglet.image.load("circle.png")
#circle = pyglet.sprite.Sprite(circleImg, randint(1, window.width), randint(1, window.height))
circle = Circle(x=50, y=50)
text = pyglet.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
#DETECT MOUSE PRESS ON CIRCLE
#window.event
def on_mouse_press(x, y, button, modifiers):
if circle.click(x, y):
print('Clicked in circle')
circle.x = randint(0, window.width - 10)
circle.y = randint(0, window.height - 10)
#window.event
def on_draw():
window.clear()
text.draw()
circle.draw()
pyglet.app.run()
A short description of what this does is it creates a custom class called Circle that inherits the Sprite class. It loads the circle.png as a texture with a alpha channel that gets blended by the GL library.
We add a custom function called click that checks if the lowest x,y coordinates are higher than the circles lowest x,y, then we check if the cursor is below x+width and same for y of the image region.
If that's the case, we return the circle sprite class as a True value in case we want to use the sprite.
Future enhancements:
You should draw the circle using gl functions, hence why I've defined radiance in the class definitions. However radiance here is never used, it's a placeholder for the future.
This is so you can use math to defined if you actually clicked within the circle, but this is beyond my scope of quick answers.. I would have to do a lot of debugging myself in order to get the math to add up (it's not my strong side).
What makes it work now is that we use the image width, height, x and y data to crudely check if we're within the image, aka "the circle".
trying to draw over sprite or change picture pyglet
As a bonus, I'll add this answer to the list of enhancements because it contains some stuff that might be useful. One would be to replace 90% of your code with a custom pyglet.window.Window class to replace global variables and decorators and stuff.
And it would look something like this:
import pyglet
from pyglet.gl import *
from random import randint
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
key = pyglet.window.key
class Circle(pyglet.sprite.Sprite):
def __init__(self, radiance=5, x=0, y=0):
self.texture = pyglet.image.load('circle.png')
super(Circle, self).__init__(self.texture)
def click(self, x, y):
if x >= self.x and y >= self.y:
if x <= self.x + self.texture.width and y <= self.y + self.texture.height:
return self
class MainScreen(pyglet.window.Window):
def __init__ (self):
super(MainScreen, self).__init__(800, 600, fullscreen = False)
self.x, self.y = 0, 0
self.bg = pyglet.sprite.Sprite(pyglet.image.load('background.jpg'))
self.sprites = {}
self.sprites['circle'] = Circle(x=50, y=50)
self.sprites['label'] = pyglet.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_mouse_press(self, x, y, button, modifiers):
if self.sprites['circle'].click(x, y):
print('Clicked in circle')
self.sprites['circle'].x = randint(0, self.width - 10)
self.sprites['circle'].y = randint(0, self.height - 10)
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
def render(self):
self.clear()
self.bg.draw()
for sprite_name, sprite_obj in self.sprites.items():
sprite_obj.draw()
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
x = MainScreen()
x.run()
I'm not familiar with pyglet, but I'm guessing the problem is that you're checking whether x == circle.x etc, which means it only moves when you click the single pixel at the exact centre of the circle. Try some kind of maximum distance from the centre (e.g. a hypotenuse math.sqrt( (x-circle.x)**2 + (y-circle.y)**2) < circle.radius