Search code examples
pythonpygamecropviewport

Creating a 'frame' that hides all but what it contains in pygame


I just started with pyGame and I would like to create a frame that crops whatever I display on screen. i.e. independently of what my world contains, this frame should cover everything but what it contains.

Think about it as the frame used to crop images on the iphone (see picture below). Eventually I want to make the frame interactive, so that I can move it around or change its size, but for the time being a static frame would do. And I have no idea where to start from.. any help is appreciated!

[example of cropping frame[1]


Solution

  • There are multiple ways to do this; one way is to draw a black Surface on the entire screen, but have a "hole" in that black Surface.

    An easy way to create that "hole" is to simply draw a rectangle with the right colorkey. What is the colorkey?

    When blitting this Surface onto a destination, any pixels that have the same color as the colorkey will be transparent

    Sounds useful, so let's give it a try:

    import random
    import pygame as pg
    
    IMAGE = pg.Surface((50, 50), pg.SRCALPHA)
    pg.draw.polygon(IMAGE, (240, 120, 0), [(0, 50), (25, 0), (50, 50)])
    
    class Actor(pg.sprite.Sprite):
    
        def __init__(self, grp, bounds, pos):
            self._layer = 0
            pg.sprite.Sprite.__init__(self, grp)
            self.image = IMAGE
            self.rect = self.image.get_rect(center=pos)
            self.vec = pg.math.Vector2()
            # just a random directon for movement
            self.vec.from_polar((10, random.randrange(0, 360)))
            self.bounds = bounds
    
        def update(self):
            self.rect.move_ip(*self.vec)
            # try staying on screen
            if not self.bounds.contains(self.rect):
                self.vec.from_polar((10, random.randrange(0, 360)))
    
    class Cursor(pg.sprite.Sprite):
        def __init__(self, grp, bounds):
            self._layer = 1000
            pg.sprite.Sprite.__init__(self, grp)
            self.image = pg.Surface((bounds.width, bounds.height))
            self.image.set_colorkey(pg.Color('yellow'))
            # we start with the entire screen black
            # if the screen should be fully visible at the start, we could use yellow instead
            self.image.fill(pg.Color('black'))
            self.rect = self.image.get_rect()
            # here we store the position of the mouse when we start drawing the hole
            self.start = None
            # here we store the entire rect of the hole so we can later move it around
            self.inner_rect = None
    
        def mousedown(self):
            self.start = pg.mouse.get_pos()
    
        def mouseup(self):
            self.start = None
    
        def move(self, rel):
            self.inner_rect.move_ip(rel)
            self.recreate()
    
        def update(self):
            if not self.start:
                return
            pos = pg.mouse.get_pos()
            p = pos[0] - self.start[0], pos[1] - self.start[1]
            self.inner_rect = pg.Rect(self.start, p)
            self.recreate()
    
        def recreate(self):
            # here we update our hole
            self.image.fill(pg.Color('black'))
            pg.draw.rect(self.image, pg.Color('yellow'), self.inner_rect)
            pg.draw.rect(self.image, pg.Color('white'), self.inner_rect, 2)
    
    def main():
        screen = pg.display.set_mode((640, 480))
        clock = pg.time.Clock()
        all_sprites = pg.sprite.LayeredUpdates()
        cursor = Cursor(all_sprites, screen.get_rect())
        for _ in range(20):
            Actor(all_sprites, screen.get_rect(), (random.randrange(600), random.randrange(440)))
    
        done = False
    
        while not done:
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    done = True
                if event.type == pg.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        cursor.mousedown()
                if event.type == pg.MOUSEBUTTONUP:
                    if event.button == 1:
                        cursor.mouseup()    
                if event.type == pg.MOUSEMOTION:
                    if pg.mouse.get_pressed()[2]:
                        cursor.move(event.rel)
    
            all_sprites.update()
            screen.fill(pg.Color('darkblue'))
            all_sprites.draw(screen)
    
            pg.display.flip()
            clock.tick(60)
    
    
    if __name__ == '__main__':
        pg.init()
        main()
        pg.quit()
    

    enter image description here

    Use the left mouse button to start drawing your frame, and use the right mouse button to move it.

    As you can see, we have a black Surface with yellow as colorkey. When we want to make a part of the screen visible, we draw a yellow rectangle, which in turn will be transparent, making the scene underneath it visible.