From 89e9d7c441e68f76639539276a712a8fd7be17fe Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Mon, 14 May 2012 20:10:01 +0200 Subject: [PATCH] Changed font file to a public-domain one. Added custom events/callbacks. Split project into library and run script to demonstrate and test the event API. --- font.png | Bin 3667 -> 3696 bytes pixelflut.py | 134 +++++++++++++++++++++++++-------------------------- run.py | 54 +++++++++++++++++++++ 3 files changed, 121 insertions(+), 67 deletions(-) create mode 100644 run.py diff --git a/font.png b/font.png index fb7686ea62f82c10514eec305e751821b305bf29..e496e2bc51e13930320d5938f18b67728f75f02e 100644 GIT binary patch literal 3696 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 diff --git a/pixelflut.py b/pixelflut.py index b48f274..a232944 100755 --- a/pixelflut.py +++ b/pixelflut.py @@ -9,6 +9,7 @@ from gevent.coros import Semaphore from gevent.queue import Queue from collections import deque +async = spawn class Client(object): px_per_tick = 10 @@ -38,8 +39,8 @@ class Client(object): readline = self.socket.makefile().readline try: while True: - # Idea: Sand first, recieve later. If the client is to - # slow to get the sendbuffer empty, he cannot send. + # Idea: Send first, receive later. If the client is to + # slow to get the send-buffer empty, he cannot send. while self.sendbuffer: sendall(self.sendbuffer.popleft()) line = readline() @@ -51,6 +52,8 @@ class Client(object): self.on_PX(arguments) elif command == 'SIZE': self.on_SIZE(arguments) + else: + self.canvas.fire('COMMAND-%s' % command.upper, self, *arguments) finally: self.disconnect() @@ -81,7 +84,6 @@ class Client(object): - import pygame import cairo import math @@ -101,25 +103,8 @@ class Canvas(object): self.width = self.screen.get_width() self.height = self.screen.get_height() self.clients = {} - - def load_font(self, fname): - self.font_img = pygame.image.load(fname) - self.font_res = int(self.font_img.get_width())/16 - - def putc(self, x, y, c): - if not self.font_img: return - fpos = ord(c) - fx = (fpos%16) * self.font_res - fy = (fpos/16) * self.font_res - self.screen.blit(self.font_img, (x,y), - (fx,fy,self.font_res,self.font_res)) - - def text(self, x, y, text, delay=0): - for i, line in enumerate(text.splitlines()): - for j, c in enumerate(line): - self.putc(x+j*self.font_res, y+i*self.font_res, c) - if delay: gsleep(delay) - + self.events = {} + def serve(self, host, port): self.server = StreamServer((host, port), self.make_client) self.server.start() @@ -129,45 +114,55 @@ class Canvas(object): if address in self.clients: self.clients[address].disconnect() self.clients[address] = client = Client(self, socket, address) + self.fire('CONNECT', client) client.serve() # This blocks until ready + self.fire('DISCONNECT', client) def _loop(self): + self.fire('START') while True: gsleep(0.01) # Required to allow other tasks to run - for e in pygame.event.get(): - if e.type == pygame.VIDEORESIZE: - self.on_resize(e.size) - if e.type == pygame.KEYDOWN and e.unicode == 'q': - return - if e.type == pygame.KEYDOWN and e.unicode == 'c': - self.clear() - if e.type == pygame.QUIT: - return + if not self.ticks % 10: + for e in pygame.event.get(): + if e.type == pygame.VIDEORESIZE: + old = self.screen.copy() + self.screen = pygame.display.set_mode(e.size, self.flags) + self.screen.blit(old, (0,0)) + self.width = self.screen.get_width() + self.height = self.screen.get_height() + self.fire('RESIZE') + elif e.type == pygame.QUIT: + self.fire('QUIT') + return + elif e.type == pygame.KEYDOWN: + self.fire('KEYDOWN-' + e.unicode) self.ticks += 1 - if 0 and self.ticks % 1000 == 0: - pygame.image.save(self.screen, - 'hist%000000d.png' % (self.ticks/1000)) + self.fire('TICK', self.ticks) pygame.display.flip() - for client in self.clients.itervalues(): - client.tick() - def on_resize(self, size): - old = self.screen.copy() - self.screen = pygame.display.set_mode(size, self.flags) - self.screen.blit(old, (0,0)) - self.width = self.screen.get_width() - self.height = self.screen.get_height() + def on(self, name): + ''' If used as a decorator, binds a function to an event. ''' + def decorator(func): + self.events[name] = func + return func + return decorator - def clear(self): - self.screen.fill((0,0,0)) + def fire(self, name, *a, **ka): + ''' Fire an event. ''' + if name in self.events: + self.events[name](self, *a, **ka) def get_size(self): + ''' Get the current screen dimension as a (width, height) tuple.''' return self.width, self.height def get_pixel(self, x, y): + ''' Get colour of a pixel as an (r,g,b) tuple. ''' return self.screen.get_at((x,y)) def set_pixel(self, x, y, r, g, b, a=255): + ''' Change the colour of a pixel. If an alpha value is given, the new + colour is mixed with the old colour accordingly. ''' if a == 0: return if a == 0xff: self.screen.set_at((x,y), (r,g,b)) @@ -178,29 +173,34 @@ class Canvas(object): b = (b2*(0xff-a)+(b*a)) / 0xff self.screen.set_at((x, y), (r,g,b)) + def clear(self, r=0, g=0, b=0): + ''' Fill the entire screen with a solid colour (default: black)''' + self.screen.fill((r,g,b)) + + def save_as(self, filename): + ''' Save screen to disk. ''' + pygame.image.save(self.screen, filename) + + def load_font(self, fname): + ''' Load a font image with 16x16 sprites. ''' + self.font_img = pygame.image.load(fname).convert() + self.font_res = int(self.font_img.get_width())/16 + + def putc(self, x, y, c): + if not self.font_img: + self.load_font('font.png') + fx = (c%16) * self.font_res + fy = (c/16) * self.font_res + self.screen.blit(self.font_img, (x,y), + (fx,fy,self.font_res,self.font_res)) + + def text(self, x, y, text, delay=0, linespace=1): + for i, line in enumerate(text.splitlines()): + line += ' ' + for j, c in enumerate(line): + self.putc(x+j*self.font_res, y+i*self.font_res, ord(c)) + gsleep(delay) + y += linespace -def guess_IP(): - import socket - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect(("google.com", 80)) - return s.getsockname()[0] - finally: - s.close() - -if __name__ == '__main__': - port = 2342 - text = 'P1XELFLUT! v%s\n' % __version__ - text += 'Connect to %s:%d\n\n' % (guess_IP(), port) - text += '>>> SIZE\n' - text += '>>> PX x y hex-color\n' - text += '... and more ...\n\n' - text += 'H A C K O N\n' - - canvas = Canvas() - task = canvas.serve('0.0.0.0', port) - canvas.load_font('./font.png') - canvas.text(5, 5, text, 0.1) - task.join() diff --git a/run.py b/run.py new file mode 100644 index 0000000..01be0b4 --- /dev/null +++ b/run.py @@ -0,0 +1,54 @@ +import pixelflut + + +def guess_IP(): + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("google.com", 80)) + return s.getsockname()[0] + finally: + s.close() + + +port = 2342 +text = 'P1XELFLUT! v%s\n' % pixelflut.__version__ +text += 'Connect to %s:%d\n\n' % (guess_IP(), port) +text += '>>> SIZE\n' +text += '>>> PX x y hex-color\n' +text += '... and more ...\n\n' +text += 'H A C K O N\n' + +canvas = pixelflut.Canvas() + +@canvas.on('START') +def callback(c): + c.load_font('./font.png') + pixelflut.async(c.text, 5, 5, text, delay=0.1) + +@canvas.on('RESIZE') +def callback(c): + c.text(5, 5, 'Screen Size: %dx%d' % c.get_size()) + +@canvas.on('CONNECT') +def callback(c, client): + c.text(5, 5, 'Connect: %s' % client.address[0]) + +@canvas.on('KEYDOWN-c') +def callback(c): + c.clear() + +@canvas.on('KEYDOWN-s') +def callback(c): + import os + i, mask = 0, 'screen%05d.png' + while os.path.exists(mask%i): i += 1 + c.save_as(mask%i) + c.text(5,5, 'Saved as %s' % mask % i) + +@canvas.on('COMMAND-CLEAR') +def callback(canvas, client, *args): + canvas.clear() + +task = canvas.serve('0.0.0.0', port) +task.join()