219 lines
6.2 KiB
Python
219 lines
6.2 KiB
Python
# 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)
|