I have an almost-working piece of code (I hope). In the update method of this class, random black points should be drawn at locations bounded by the width and height of the window - the problem is that the points are not drawn. A gtk window containing the background image that is loaded with the cairo ImageSurface.create_from_png(BG_IMG) is displayed and I've also verified that the update function is called (every 17ms with a gobject.timeout_add callback function). I've searched here and elsewhere, but I can't quite see what's wrong with this code..
class Screen(gtk.DrawingArea):
__gsignals__ = {"expose-event": "override"}
def do_expose_event(self, event):
self.cr = self.window.cairo_create()
self.cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
self.cr.clip()
self.draw(*self.window.get_size())
def draw(self, width, height):
x = y = 0
self.bg = c.ImageSurface.create_from_png(BG_IMG)
self.cr.set_source_surface(self.bg, x, y)
self.cr.paint()
def update(self):
x = randint(0, DOCK_W)
y = randint(0, DOCK_H)
self.cr.rectangle(x, y, 1, 1)
self.cr.set_source_rgba(0, 0, 0, 1)
self.cr.fill()
self.cr.paint()
Anybody have some insights into why this is code is failing? Big thanks in advance!
Solved
I was unaware that a new cairo context could be used at each draw operation. That turned out to be the main problem.
Generally speaking, you should not draw directly to the window outside of an expose event. And do not keep the cairo context for later use: create one for each event run.
When you want to draw your points, just do: widget.queue_draw(), and a new expose event will be delivered to you ASAP. But note that in the expose event you will have to paint all the points, not just the new one.
There a useful optimization to your type of code: from the timer do not call queue_draw as it is fairly inefficient. Instead just draw the new point. However that doesn't excuse you to draw all the points in the do_expose_event, as an expose event can happen at any time and you do not want to lose the already painted points.
To do the one-point draw you have to create a new cairo context, but you do not need to save it:
def update(self):
cr = self.window.cairo_create()
x = randint(0, DOCK_W)
y = randint(0, DOCK_H)
self.points.append((x,y)) #for the expose event ;-)
cr.rectangle(x, y, 1, 1)
cr.set_source_rgba(0, 0, 0, 1)
cr.fill()
cr.paint()
Another common optimization, particularly if you have a lot of points is to keep the painted image in a bitmap, so when the expose event happens, you simply blit the bitmap, instead of iterating all along the list of points.
Related
I have a code here that will add an ellipse and line when mouse is clicked.
class Viewer(QtWidgets.QGraphicsView):
def __init__(self, parent):
super(leftImagePhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtWidgets.QGraphicsScene(self)
self.setGeometry(QtCore.QRect(20, 90, 451, 421))
self.setSceneRect(20, 90, 451, 421)
I have an MouseRelase Event
def mouseReleaseEvent(self,event):
pos = self.mapToScene(event.pos())
point = self._scene.addEllipse(self._size/2, self._size/2, 10, 10, QPen(Qt.black), QBrush(Qt.green))
point.setPos(QPointF(pos.x(),pos.y()))
self._scene.addLine(pos.x(),pos.y(), self.posprev.x(), self.posprev.y(), QPen(Qt.green))
When I clicked the line, its position is similar to the mouse positon, but the ellipse positon has few gap or difference to the exact mouse position.The center of the ellipse should be the endpoitn of the line or where the mouse position is.
See image here:
Can someone help me what is wrong why the ellipse will not add on the exact position to the mouse?
As the documentation of addEllipse() explains:
Note that the item's geometry is provided in item coordinates, and its position is initialized to (0, 0).
This is actually valid for all QGraphicsScene functions that add basic shapes, and the initialized position is always (0, 0) for all QGraphicsItems in general.
Consider the following:
point = scene.addEllipse(5, 5, 10, 10)
The above will create an ellipse enclosed in a rectangle that starts at (5, 5) relative to its position. Since we've not moved it yet, that position is the origin point of the scene.
The ellipse as it as soon as it's created, with the rectangle shown as a reference of its boundaries.
Then, we set its position (assuming the mouse is at 20, 20 of the scene):
point.setPos(QPointF(20, 20))
The result will be an ellipse enclosed in a rectangle that has its top left corner at (25, 25), which is the rectangle position relative to the item position: (5, 5) + (20, 20).
Note that the above shows both the ellipse in the original position and the result of setPos().
If you want an ellipse that will be centered on its position, you must create one with negative x and y coordinates that are half of the width and height of its rectangle.
Considering the case above, the following will properly show the ellipse centered at (20, 20):
point = scene.addEllipse(-5, -5, 10, 10)
point.setPos(QPointF(20, 20))
Notes:
as the documentation shows, mapToScene() already returns a QPointF, there's no point in doing setPos(QPointF(pos.x(), pos.y())): just do setPos(pos);
remember what said above: all items have a starting position at (0, 0); this is valid also for the line you're creating after that point, which will be drawn between pos and self.posprev, but will still be at (0, 0) in scene coordinates;
the view and the scene might need mouse events, especially if you're going to add movable items; you should always call the base implementation (in your case, super().mouseReleaseEvent(event)) when you override functions, unless you really know what you're doing;
as already suggested to you, it is of utmost importance that you read and understand the whole graphics view documentation, especially how its coordinate system works; the graphics view framework is as much powerful as it is complex, and cannot be learnt just by trial and error: being able to use it requires a lot of patience in understanding how it works by carefully studying the documentation of each of its classes and all functions you are going to use;
I want to show an image recreated from an img-vector, everything fine.
now I edit the Vector and want to show the new image, and that multiple times per second.
My actual code open tons of windows, with the new picture in it.
loop
{
rearr0 = generateNewImageVector()
reimg0 = Image.fromarray(rearr0, 'RGB')
reimg0.show()
}
What can I do to create just one Window and always show just the new image?
Another way of doing this is to take advantage of OpenCV's imshow() function which will display a numpy image in a window and redraw it each time you update the data.
Note that OpenCV is quite a beast of an installation, but maybe you use it already. So, the code is miles simpler than my pygame-based answer, but the installation of OpenCV could take many hours/days...
#!/usr/local/bin/python3
import numpy as np
import cv2
def sin2d(x,y):
"""2-d sine function to plot"""
return np.sin(x) + np.cos(y)
def getFrame():
"""Generate next frame of simulation as numpy array"""
# Create data on first call only
if getFrame.z is None:
xx, yy = np.meshgrid(np.linspace(0,2*np.pi,w), np.linspace(0,2*np.pi,h))
getFrame.z = sin2d(xx, yy)
getFrame.z = cv2.normalize(getFrame.z,None,alpha=0,beta=1,norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_32F)
# Just roll data for subsequent calls
getFrame.z = np.roll(getFrame.z,(1,2),(0,1))
return getFrame.z
# Frame size
w, h = 640, 480
getFrame.z = None
while True:
# Get a numpy array to display from the simulation
npimage=getFrame()
cv2.imshow('image',npimage)
cv2.waitKey(1)
That looks like this:
It is dead smooth and has no "banding" effects in real life, but there is a 2MB limit on StackOverflow, so I had to decrease the quality and frame rate to keep the size down.
You can do that pretty simply and pretty fast with pygame.
You write a function called getFrame() that returns a numpy array containing an image that is calculated by your simulation.
By way of example, I create a 2-d sine wave on the first pass then roll that 1 pixel down and 2 pixels across on subsequent calls to simulate movement.
#!/usr/local/bin/python3
import numpy as np
import pygame
h,w=480,640
border=50
N=0
getFrame.z = None
def sin2d(x,y):
"""2-d sine function to plot"""
return np.sin(x) + np.cos(y)
def getFrame():
"""Generate next frame of simulation as numpy array"""
# Create data on first call only
if getFrame.z is None:
xx, yy = np.meshgrid(np.linspace(0,2*np.pi,h), np.linspace(0,2*np.pi,w))
getFrame.z = sin2d(xx, yy)
getFrame.z = 255*getFrame.z/getFrame.z.max()
# Just roll data for subsequent calls
getFrame.z = np.roll(getFrame.z,(1,2),(0,1))
return getFrame.z
pygame.init()
screen = pygame.display.set_mode((w+(2*border), h+(2*border)))
pygame.display.set_caption("Serious Work - not games")
done = False
clock = pygame.time.Clock()
# Get a font for rendering the frame number
basicfont = pygame.font.SysFont(None, 32)
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# Clear screen to white before drawing
screen.fill((255, 255, 255))
# Get a numpy array to display from the simulation
npimage=getFrame()
# Convert to a surface and splat onto screen offset by border width and height
surface = pygame.surfarray.make_surface(npimage)
screen.blit(surface, (border, border))
# Display and update frame counter
text = basicfont.render('Frame: ' + str(N), True, (255, 0, 0), (255, 255, 255))
screen.blit(text, (border,h+border))
N = N + 1
pygame.display.flip()
clock.tick(60)
That looks like this. In real life it is very fast and very smooth, but there is a 2MB size limit for videos on StackOverflow, so I have generated a GIF with a low-ish frame rate and small-ish size just to keep it under 2MB.
Obviously you can add in detection of Up and Down arrows to speed up or slow down the animation, and you could detect left/right arrow and Spacebar to go backwards/forwards or pause the animation.
According to Pillow documentation the method Image.show() is mainly intended for debugging purposes. On Windows, it saves the image to a temporary BMP file, and uses the standard BMP display utility to show it.
You should take a look at the matplotlib image library.
You can try:
import matplotlib.pyplot as plt
imgplot = plt.imshow(rearr0) # "plot" the image.
For each iteration, you can call imgplot.clf(), it will clear the plot keeping the axis. I have not tried it, but it should work
I want to add a grid to my level that stays with the terrain and not the screen. The way I thought of doing it is to add all the lines that form the grid as sprites and move them with the terrain, but I can't figure out how to represent the line as an image.
I tried to do this myself, but had no success.
EDIT: Here's what I've tried
class Grid():
def __init__(self):
self.grid = pygame.Surface(size)
self.grid.set_colorkey((0,0,0))
def draw(self):
# DRAW TILE LINES ----------------------------------------------------------
grid_x = 0
grid_y = 0
for i in range(total_level_width // TILE_SIZE):
pygame.draw.aaline(self.grid,BLACK,[grid_x,0],[grid_x,total_level_height])
pygame.draw.aaline(self.grid,BLACK,[0,grid_x],[total_level_width,grid_y])
grid_x += TILE_SIZE
grid_y += TILE_SIZE
# tile test
pygame.draw.rect(screen,BLACK,(49*TILE_SIZE,34*TILE_SIZE,TILE_SIZE,TILE_SIZE))
screen.blit(self.grid,(0,0))
Creating the object:
grid = Grid()
Calling class: (in main program loop)
grid.draw()
I had a similar problem while i was trying to do a project. I used the following code to get a line onto a surface and then bliting it onto my screen. I hope this entire function might help you.
def blitBoundary(self):
""" helper function to blit boundary on screen """
# create a surface
self.boundSurf=pygame.Surface((1024,768))
self.boundSurf.set_colorkey((0,0,0))
"""
if not self.boundary.closePoly:
(x,y)=pygame.mouse.get_pos()
pointList=self.boundary.pointList +[[x,y]]
else:
pointList=self.boundary.pointList"""
if len(pointList)>1:
pygame.draw.aalines(self.boundSurf, (255,255,255),
self.boundary.closePoly , pointList, 1)
self.screen.blit(self.boundSurf,(0,0))
I was trying to draw a polygon. The commented out the if statement that would be most probably not useful for you.
All my lines were in a polygon class object.
You might want to look into pygame.draw.aalines function.
I thought you guys might be able to help me wrap my head around this. I want to be able to generate rects and assign images to those rects. I've been doing this for the whole project and isn't too hard. The hard part here is that I want this particular function to be able to generate as many different rects as I want. The project is a game that takes place on like a chess board. I figure I can write like... if statements for every single space and then have like a bazillion parameters in the function that dictate which rects get generated and where, but I was hoping someone might be able to think of a more elegant solution.
You could use two nested "for" loops --
def make_chessboard(upper_x=0, upper_y=0, size=30):
chessboard = []
for y in range(8):
row = []
for x in range(8):
coords = (upper_x + x * size, upper_y + y * size)
row.append(pygame.Rect(coords, (size, size)))
chessboard.append(row)
return chessboard
Then, to get the rect that's in the top-left corner, you could do chessboard[0][0]. To get the rect that's in the top-right corner, you could do chessboard[0][7].
You wouldn't be able to explicitly name each rect, but then again, you really wouldn't need to.
Note: I'm assuming that you wanted to create a chessboard-like pattern of rects of some kind. I can edit my question if you detail specifically what you're trying to do.
class ChessTile(pygame.sprite.Sprite):
def __init__(self, image, location):
pygame.sprite.Sprite.__init__(self)
self.image = image.convert()
self.mask = pygame.mask.from_surface(self.image)
self.rect = pygame.Rect(location, self.image.get_size())
Then make another method called like "MakeBoard". Call MakeBoad and have a loop setup with the size of the board. so the pseudo code would be:
(let's assume "img" is a 32x32 white or black square)
for y in range(0,7):
for x in range(0,7):
# alternate the tile image from black/white before calling ChessTile with it
# the location parameter is going to be x*32,y*32.. something like that
# so you'd have a tile at (0,0) then at (32,0), then (64,0), etc...
# after the first "j" run is done, "i" increments so now we have
# (0, 32), (32, 32), etc etc.
#
tile = ChessTile(img, (x,y))
then just draw the tile object as you normall would in some render method!
hope that helps!!!
This code displays the image assassin1.png on a black screen. This image has a pymunk body and shape associated with it. There is also an invisible static pymunk object called floor present beneath it. Gravity is induced on the image and it is resting on the invisible floor.
I would like to make my image jump naturally when I press the UP key. How can I implement this?
import pyglet
import pymunk
def assassin_space(space):
mass = 91
radius = 14
inertia = pymunk.moment_for_circle(mass, 0, radius)
body = pymunk.Body(mass, inertia)
body.position = 50, 80
shape = pymunk.Circle(body, radius)
space.add(body, shape)
return shape
def add_static_line(space):
body = pymunk.Body()
body.position = (0,0)
floor = pymunk.Segment(body, (0, 20), (300, 20), 0)
space.add_static(floor)
return floor
class Assassin(pyglet.sprite.Sprite):
def __init__(self, batch, img, space):
self.space = space
pyglet.sprite.Sprite.__init__(self, img, self.space.body.position.x, self.space.body.position.y)
def update(self):
self.x = self.space.body.position.x
self.y = self.space.body.position.y
class Game(pyglet.window.Window):
def __init__(self):
pyglet.window.Window.__init__(self, width = 315, height = 220)
self.batch_draw = pyglet.graphics.Batch()
self.player1 = Assassin(batch = self.batch_draw, img = pyglet.image.load("assassin1.png"), space = assassin_space(space))
pyglet.clock.schedule(self.update)
add_static_line(space)
def on_draw(self):
self.clear()
self.batch_draw.draw()
self.player1.draw()
space.step(1/50.0)
def on_key_press(self, symbol, modifiers):
if symbol == pyglet.window.key.UP:
print "The 'UP' key was pressed"
def update(self, dt):
self.player1.update()
space.step(dt)
if __name__ == "__main__":
space = pymunk.Space() #
space.gravity = (0.0, -900.) #
window = Game()
pyglet.app.run()
You need to apply an impulse to the body. Impulse is a change in momentum, which is mass times velocity. I assume you want your asassin to jump straight up. If that is the case, you have to apply the impulse to the center of the body.
body.apply_impulse(pymunk.Vec2d(0, 60), (0, 0))
Writing a platformer using a physics library like PyMunk is really difficult. I would strongly advise against it, and instead to manage physics like jumping in your own code.
The problem is that realistic physics like pymunk's come with many side effects that you really don't want. For example, when your character is running sideways, they will experience a frictional drag against the floor, which cannot operate through their center of mass, so will tend to make them rotate or fall over. You may find ways to counteract this, but these will have other undesirable side-effects. For example, if you make your character a squat shape with a large flat bottom edge, then this will also affect collision detection. If you reduce their friction with the floor, this will mean they don't slow down over time. You may add still more refinements to correct for these things, but they will have yet more side-effects. The examples I give here are just the tip of the iceberg. There are many more complex effects that will cause big problems.
Instead, it is massively simpler to have a variable representing your character's velocity, and add that to their position every frame. Detect if they have hit anything like a platform, and if so, set their horizontal or vertical velocity such that they do not move into the platform. This also comes with some subtle problems, but they are generally much easier to fix than a physics simulation.