# fx-92 Scientifique Collège+ language interpreter: On-screen drawing from sdl2 import * from fx92.util import rndcoord import math import os class InterruptException(Exception): pass class Window: """ An SDL window with pixel scaling. """ BLACK = (0, 0, 0, 255) WHITE = (255, 255, 255, 255) def __init__(self, width, height, scale, quiet=False, stream=False): self.width = width self.height = height self.scale = scale self.quiet = quiet self.stream = stream self.streamid = 1 def __enter__(self): """ Enter window context: allocate and create a window. """ if SDL_WasInit(SDL_INIT_VIDEO): raise Exception("Cannot create two windows") if SDL_Init(SDL_INIT_VIDEO) < 0: raise Exception("Failed to initialize SDL") # Create the window mode = SDL_WINDOW_HIDDEN if self.quiet else SDL_WINDOW_SHOWN # TODO: If the window is hidden, nothing is rendered mode = SDL_WINDOW_SHOWN self.w = SDL_CreateWindow("fx-92 Scientifique Collège+".encode(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, self.width*self.scale, self.height*self.scale, mode) if self.w is None: raise Exception("Failed to create window") # Create the renderer self.r = SDL_CreateRenderer(self.w, -1, SDL_RENDERER_ACCELERATED) if self.r is None: SDL_DestroyWindow(self.w) raise Exception("Failed to create renderer") # Create the base texture where things will be rendered at scale 1 self.t = SDL_CreateTexture(self.r, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.width, self.height) if self.t is None: SDL_DestroyRenderer(r) SDL_DestroyWindow(w) raise Exception("Failed to create texture") SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, b'0') SDL_SetRenderTarget(self.r, self.t) self.clear(self.WHITE) self.update() return self def __exit__(self, *args): """ Exit context: close and destroy window. """ if self.t is not None: SDL_SetRenderTarget(self.r, None) SDL_DestroyTexture(self.t) if self.r is not None: SDL_DestroyRenderer(self.r) if self.w is not None: SDL_DestroyWindow(self.w) SDL_Quit() def _point(self, x, y): # It's important to divide with //, otherwise when the width or height # is odd this transformation loses rounding information and can alter # the position of the final pixel. x = self.width // 2 + x - 1 y = self.height // 2 - y x = rndcoord(x) y = rndcoord(y) SDL_RenderDrawPoint(self.r, x, y) def clear(self, color): """Clear screen in a uniform color.""" SDL_SetRenderDrawColor(self.r, *color) SDL_RenderClear(self.r) def _line(self, x1, y1, x2, y2, color): """Draw a straight line.""" SDL_SetRenderDrawColor(self.r, *color) # Bresenham line drawing algorithm sgn = lambda x: -1 if x < 0 else (1 if x > 0 else 0) x, y = x1, y1 dx, dy = x2 - x1, y2 - y1 sx, sy = sgn(dx), sgn(dy) dx, dy = abs(dx), abs(dy) cumul = 0 self._point(x, y) if dx > dy: cumul = (dx+1) // 2 for i in range(dx): x += sx cumul += dy if cumul > dx: cumul -= dx y += sy self._point(x, y) elif dx == dy: for i in range(dx): x += sx y += sy self._point(x, y) else: cumul = (dy+1) // 2 for i in range(dy): y += sy cumul += dx if cumul > dy: cumul -= dy x += sx self._point(x, y) def line(self, x1, y1, x2, y2, color): x1, y1 = rndcoord(x1), rndcoord(y1) x2, y2 = rndcoord(x2), rndcoord(y2) self._line(x1, y1, x2, y2, color) def linef(self, x, y, angle, dist, color): """Draw a line forward.""" x, y, angle, dist = float(x), float(y), float(angle), float(dist) cos = math.cos(angle * math.pi / 180) sin = math.sin(angle * math.pi / 180) x2 = x + dist * cos y2 = y + dist * sin x1, y1 = rndcoord(x), rndcoord(y) x2, y2 = rndcoord(x2), rndcoord(y2) self._line(x1, y1, x2, y2, color) def wait(self, allow_any=False): """Wait for the window to be closed.""" if self.quiet: return event = SDL_Event() while 1: SDL_WaitEvent(event) if event.type == SDL_QUIT: break if event.type == SDL_KEYDOWN and event.key.keysym.sym==SDLK_ESCAPE: raise InterruptException() if event.type == SDL_KEYDOWN and allow_any: break def pause(self): """Display a pause message.""" # TODO: Display "paused" self.wait(allow_any=True) def save(self, out): """Save the output into a BMP file.""" SDL_SetRenderTarget(self.r, self.t) surface = SDL_CreateRGBSurface(0, self.width, self.height, 32,0,0,0,0) fmt = surface.contents.format.contents.format pixels = surface.contents.pixels pitch = surface.contents.pitch SDL_RenderReadPixels(self.r, None, fmt, pixels, pitch); SDL_SaveBMP(surface, out.encode()) SDL_FreeSurface(surface) def update(self): """Push window contents on-screen.""" if self.stream: name = "{:04d}.bmp".format(self.streamid) self.save(os.path.join(self.stream, name)) self.streamid += 1 # Target the window with scaling SDL_SetRenderTarget(self.r, None) SDL_RenderSetScale(self.r, self.scale, self.scale) # Push that to the screen SDL_RenderCopy(self.r, self.t, None, None) SDL_RenderPresent(self.r) # Go back to the texture SDL_SetRenderTarget(self.r, self.t) SDL_RenderSetScale(self.r, 1, 1)