# # # Nimrod's Runtime Library # (c) Copyright 2010 Andreas Rumpf, Dominik Picheta # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## This module implements graphical output for Nimrod; the current ## implementation uses SDL but the interface is meant to support multiple ## backends some day. import colors, math from sdl import PSurface # Bug from sdl_ttf import OpenFont type TRect* = tuple[x, y, width, height: int] TPoint* = tuple[x, y: int] PSurface* = ref TSurface ## a surface to draw onto TSurface {.pure, final.} = object w, h: int s: sdl.PSurface ESDLError = object of EBase proc surfaceFinalizer(s: PSurface) = sdl.freeSurface(s.s) proc newSurface*(width, height: int): PSurface = ## creates a new surface. new(result, surfaceFinalizer) result.w = width result.h = height result.s = SDL.CreateRGBSurface(SDL.SWSURFACE, width, height, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0) assert(not sdl.MustLock(result.s)) proc newScreenSurface(width, height: int): PSurface = ## Creates a new screen surface new(result, surfaceFinalizer) result.w = width result.h = height result.s = SDL.SetVideoMode(width, height, 0, 0) template withEvents(surf: PSurface, event: expr, actions: stmt): stmt = while True: var event: SDL.TEvent if SDL.PollEvent(addr(event)) == 1: actions proc writeToBMP*(sur: PSurface, filename: string) = ## Saves the contents of the surface `sur` to the file `filename` as a ## BMP file. if sdl.saveBMP(sur.s, filename) != 0: raise newException(EIO, "cannot write: " & filename) type TPixels = array[0..1000_000-1, int32] PPixels = ptr TPixels template setPix(video, pitch, x, y, col: expr): stmt = video[y * pitch + x] = int32(col) template getPix(video, pitch, x, y: expr): expr = colors.TColor(video[y * pitch + x]) const ColSize = 4 proc getPixel(sur: PSurface, x, y: Natural): colors.TColor = result = getPix(cast[PPixels](sur.s.pixels), sur.s.pitch div ColSize, x, y) proc setPixel(sur: PSurface, x, y: Natural, col: colors.TColor) = var pixs = cast[PPixels](sur.s.pixels) #pixs[y * (sur.s.pitch div colSize) + x] = int(col) setPix(pixs, sur.s.pitch div ColSize, x, y, col) proc `[]`*(sur: PSurface, p: TPoint): TColor = result = getPixel(sur, p.x, p.y) #proc `[,]`*(sur: PSurface, x, y: int): TColor = # result = setPixel(sur, x, y) proc `[]=`*(sur: PSurface, p: TPoint, col: TColor) = setPixel(sur, p.x, p.y, col) #proc `[,]=`*(sur: PSurface, x, y: int, col: TColor) = # setPixel(sur, x, y, col) proc blitSurface*(destSurf: PSurface, destRect: TRect, srcSurf: PSurface, srcRect: TRect) = ## Merges ``srcSurf`` into ``destSurf`` var destTRect, srcTRect: SDL.TRect destTRect.x = int16(destRect.x) destTRect.y = int16(destRect.y) destTRect.w = int16(destRect.width) destTRect.h = int16(destRect.height) srcTRect.x = int16(srcRect.x) srcTRect.y = int16(srcRect.y) srcTRect.w = int16(srcRect.width) srcTRect.h = int16(srcRect.height) if SDL.blitSurface(srcSurf.s, addr(srcTRect), destSurf.s, addr(destTRect)) != 0: raise newException(ESDLError, $SDL.GetError()) proc textBounds*(font: string, fontSize: int, text: string): tuple[width, height: int] = var fontFile = OpenFont(font, fontSize) # Open the font file if fontFile == nil: raise newException(ESDLError, "Could not open font file") var w, h: cint if sdl_ttf.SizeUTF8(fontFile, text, w, h) < 0: raise newException(ESDLError, $SDL.GetError()) return (int(w), int(h)) proc drawText*(sur: PSurface, p: TPoint, font: string, text: string, fg: TColor = colBlack, fontSize: int = 9) = ## Draws text with a transparent background, at location ``p``. ## ``font`` specifies the path to the ttf file, ## ``fontSize`` specifies the font size in pt, and ``fg`` ## specifies the foreground color. var fontFile = OpenFont(font, fontSize) # Open the font file if fontFile == nil: raise newException(ESDLError, "Could not open font file") var textSur: PSurface # This surface will have the text drawn on it new(textSur, surfaceFinalizer) var RGBfg = fg.extractRGB # Convert the colors.TColor to SDL.TColor var SDLfg: SDL.TColor SDLfg.r = toU8(RGBfg.r) SDLfg.g = toU8(RGBfg.g) SDLfg.b = toU8(RGBfg.b) # Render the text textSur.s = sdl_ttf.RenderTextBlended(fontFile, text, SDLfg) # Merge the text surface with sur sur.blitSurface((p.x, p.y, sur.w, sur.h), textSur, (0, 0, sur.w, sur.h)) # Free the surface SDL.FreeSurface(sur.s) proc drawText*(sur: PSurface, p: TPoint, font: string, text: string, bg: TColor, fg: TColor = colBlack, fontSize: int = 9) = ## Draws text, at location ``p``, ``font`` specifies the path to ## the ttf file, ``fontSize`` specifies the font size in pt, and ``fg`` ## and ``bg`` specify the foreground and background colors. var fontFile = OpenFont(font, fontSize) # Open the font file if fontFile == nil: raise newException(ESDLError, "Could not open font file") var textSur: PSurface # This surface will have the text drawn on it new(textSur, surfaceFinalizer) var RGBfg = fg.extractRGB var RGBbg = bg.extractRGB # Convert the colors.TColor to SDL.TColor var SDLfg: SDL.TColor SDLfg.r = toU8(RGBfg.r) SDLfg.g = toU8(RGBfg.g) SDLfg.b = toU8(RGBfg.b) var SDLbg: SDL.TColor SDLbg.r = toU8(RGBbg.r) SDLbg.g = toU8(RGBbg.g) SDLbg.b = toU8(RGBbg.b) # Render the text textSur.s = sdl_ttf.RenderTextShaded(fontFile, text, SDLfg, SDLbg) # Merge the text surface with sur sur.blitSurface((p.x, p.y, sur.w, sur.h), textSur, (0, 0, sur.w, sur.h)) # Free the surface SDL.FreeSurface(sur.s) proc drawCircle*(sur: PSurface, p: TPoint, r: Natural, color: TColor) = ## draws a circle with center `p` and radius `r` with the given color ## onto the surface `sur`. var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize var a = 1 - r var py = r var px = 0 var x = p.x var y = p.y while px <= py + 1: setPix(video, pitch, x + px, y + py, color) setPix(video, pitch, x + px, y - py, color) setPix(video, pitch, x - px, y + py, color) setPix(video, pitch, x - px, y - py, color) setPix(video, pitch, x + py, y + px, color) setPix(video, pitch, x + py, y - px, color) setPix(video, pitch, x - py, y + px, color) setPix(video, pitch, x - py, y - px, color) if a < 0: a = a + (2 * px + 3) else: a = a + (2 * (px - py) + 5) py = py - 1 px = px + 1 proc drawLine*(sur: PSurface, p1, p2: TPoint, color: TColor) = ## draws a line between the two points `p1` and `p2` with the given color ## onto the surface `sur`. var stepx, stepy: int = 0 var x0: int = p1.x var x1: int = p2.x var y0: int = p1.y var y1: int = p2.y var dy: int = y1 - y0 var dx: int = x1 - x0 if dy < 0: dy = -dy stepy = -1 else: stepy = 1 if dx < 0: dx = -dx stepx = -1 else: stepx = 1 dy = dy * 2 dx = dx * 2 var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize setPix(video, pitch, x0, y0, color) if dx > dy: var fraction = dy - (dx div 2) while x0 != x1: if fraction >= 0: y0 = y0 + stepy fraction = fraction - dx x0 = x0 + stepx fraction = fraction + dy setPix(video, pitch, x0, y0, color) else: var fraction = dx - (dy div 2) while y0 != y1: if fraction >= 0: x0 = x0 + stepx fraction = fraction - dy y0 = y0 + stepy fraction = fraction + dx setPix(video, pitch, x0, y0, color) proc drawHorLine*(sur: PSurface, x, y, w: Natural, Color: TColor) = ## draws a horizontal line from (x,y) to (x+w-1, h). var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize if y >= 0 and y <= sur.s.h: for i in 0 .. min(sur.s.w-x, w-1)-1: setPix(video, pitch, x + i, y, color) proc drawVerLine*(sur: PSurface, x, y, h: Natural, Color: TColor) = ## draws a vertical line from (x,y) to (x, y+h-1). var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize if x >= 0 and x <= sur.s.w: for i in 0 .. min(sur.s.h-y, h-1)-1: setPix(video, pitch, x, y + i, color) proc drawLine2*(sur: PSurface, p0: TPoint, p1: TPoint, color: TColor) = ## Draws a line from ``p0`` to ``p1``, using the Bresenham's line algorithm var (x0, x1, y0, y1) = (p0.x, p1.x, p0.y, p1.y) var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize var steep = abs(y1 - y0) > abs(x1 - x0) if steep: swap(x0, y0) swap(x1, y1) if x0 > x1: swap(x0, x1) swap(y0, y1) var deltax = x1 - x0 var deltay = abs(y1 - y0) var error = deltax div 2 var ystep: int var y = y0 if y0 < y1: ystep = 1 else: ystep = -1 for x in x0..x1: if steep: setPix(video, pitch, y, x, color) else: setPix(video, pitch, x, y, color) error = error - deltay if error < 0: y = y + ystep error = error + deltax proc fillCircle*(s: PSurface, p: TPoint, r: Natural, color: TColor) = ## draws a circle with center `p` and radius `r` with the given color ## onto the surface `sur` and fills it. var a = 1 - r var py: int = r var px = 0 var x = p.x var y = p.y while px <= py: # Fill up the middle half of the circle DrawVerLine(s, x + px, y, py + 1, color) DrawVerLine(s, x + px, y - py, py, color) if px != 0: DrawVerLine(s, x - px, y, py + 1, color) DrawVerLine(s, x - px, y - py, py, color) if a < 0: a = a + (2 * px + 3) else: a = a + (2 * (px - py) + 5) py = py - 1 # Fill up the left/right half of the circle if py >= px: DrawVerLine(s, x + py + 1, y, px + 1, color) DrawVerLine(s, x + py + 1, y - px, px, color) DrawVerLine(s, x - py - 1, y, px + 1, color) DrawVerLine(s, x - py - 1, y - px, px, color) px = px + 1 proc drawRect*(sur: PSurface, r: TRect, color: TColor) = ## draws a rectangle. var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize if (r.x >= 0 and r.x <= sur.s.w) and (r.y >= 0 and r.y <= sur.s.h): var minW = min(sur.s.w - r.x, r.width - 1) var minH = min(sur.s.h - r.y, r.height - 1) # Draw Top for i in 0 .. minW - 1: setPix(video, pitch, r.x + i, r.y, color) setPix(video, pitch, r.x + i, r.y + minH - 1, color) # Draw bottom # Draw left side for i in 0 .. minH - 1: setPix(video, pitch, r.x, r.y + i, color) setPix(video, pitch, r.x + minW - 1, r.y + i, color) # Draw right side proc fillRect*(sur: PSurface, r: TRect, col: TColor) = ## draws and fills a rectangle. var video = cast[PPixels](sur.s.pixels) assert video != nil var pitch = sur.s.pitch div ColSize for i in r.y..min(sur.s.h, r.y+r.height-1)-1: for j in r.x..min(sur.s.w, r.x+r.width-1)-1: setPix(video, pitch, j, i, col) proc Plot4EllipsePoints(sur: PSurface, CX, CY, X, Y: Natural, col: TColor) = var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize if CX+X < sur.s.w and CY+Y < sur.s.h: setPix(video, pitch, CX+X, CY+Y, col) if CX-X < sur.s.w and CY+Y < sur.s.h: setPix(video, pitch, CX-X, CY+Y, col) if CX-X < sur.s.w and CY-Y < sur.s.h: setPix(video, pitch, CX-X, CY-Y, col) if CX+X < sur.s.w and CY-Y < sur.s.h: setPix(video, pitch, CX+X, CY-Y, col) proc drawEllipse*(sur: PSurface, CX, CY, XRadius, YRadius: Natural, col: TColor) = ## Draws an ellipse, ``CX`` and ``CY`` specify the center X and Y of ## the ellipse, ``XRadius`` and ``YRadius`` specify the width and height ## of the ellipse. var X, Y: Natural XChange, YChange: Natural EllipseError: Natural TwoASquare, TwoBSquare: Natural StoppingX, StoppingY: Natural TwoASquare = 2 * XRadius * XRadius TwoBSquare = 2 * YRadius * YRadius X = XRadius Y = 0 XChange = YRadius * YRadius * (1 - 2 * XRadius) YChange = XRadius * XRadius EllipseError = 0 StoppingX = TwoBSquare * XRadius StoppingY = 0 while StoppingX >= StoppingY: # 1st set of points, y` > - 1 sur.Plot4EllipsePoints(CX, CY, X, Y, col) inc(Y) inc(StoppingY, TwoASquare) inc(EllipseError, YChange) inc(YChange, TwoASquare) if (2 * EllipseError + XChange) > 0 : dec(x) dec(StoppingX, TwoBSquare) inc(EllipseError, XChange) inc(XChange, TwoBSquare) # 1st point set is done; start the 2nd set of points X = 0 Y = YRadius XChange = YRadius * YRadius YChange = XRadius * XRadius * (1 - 2 * YRadius) EllipseError = 0 StoppingX = 0 StoppingY = TwoASquare * YRadius while StoppingX <= StoppingY: sur.Plot4EllipsePoints(CX, CY, X, Y, col) inc(X) inc(StoppingX, TwoBSquare) inc(EllipseError, XChange) inc(XChange,TwoBSquare) if (2 * EllipseError + YChange) > 0: dec(Y) dec(StoppingY, TwoASquare) inc(EllipseError, YChange) inc(YChange,TwoASquare) proc plotAA(sur: PSurface, x, y, c: float, color: TColor) = if (x.toInt() > 0 and x.toInt() < sur.s.w) and (y.toInt() > 0 and y.toInt() < sur.s.h): var video = cast[PPixels](sur.s.pixels) var pitch = sur.s.pitch div ColSize var pixColor = getPix(video, pitch, x.toInt, y.toInt) setPix(video, pitch, x.toInt(), y.toInt(), pixColor.intensity(1.0 - c) + color.intensity(c)) proc ipart(x: float): float = return x.trunc() proc fpart(x: float): float = return x - ipart(x) proc rfpart(x: float): float = return 1.0 - fpart(x) proc drawLineAA(sur: PSurface, p1: TPoint, p2: TPoint, color: TColor) = ## Draws a anti-aliased line from ``p1`` to ``p2``, using Xiaolin Wu's line algorithm var (x1, x2, y1, y2) = (p1.x.toFloat(), p2.x.toFloat(), p1.y.toFloat(), p2.y.toFloat()) var dx = x2 - x1 var dy = y2 - y1 if abs(dx) < abs(dy): swap(x1, y1) swap(x2, y2) if x2 < x1: swap(x1, x2) swap(y1, y2) var gradient = dy / dx # handle first endpoint var xend = x1 # Should be round(x1), but since this is an int anyway.. var yend = y1 + gradient * (xend - x1) var xgap = rfpart(x1 + 0.5) var xpxl1 = xend # this will be used in the main loop var ypxl1 = ipart(yend) sur.plotAA(xpxl1, ypxl1, rfpart(yend) * xgap, color) sur.plotAA(xpxl1, ypxl1 + 1.0, fpart(yend) * xgap, color) var intery = yend + gradient # first y-intersection for the main loop # handle second endpoint xend = x2 # Should be round(x1), but since this is an int anyway.. yend = y2 + gradient * (xend - x2) xgap = fpart(x2 + 0.5) var xpxl2 = xend # this will be used in the main loop var ypxl2 = ipart(yend) sur.plotAA(xpxl2, ypxl2, rfpart(yend) * xgap, color) sur.plotAA(xpxl2, ypxl2 + 1.0, fpart(yend) * xgap, color) # main loop for x in xpxl1.toInt + 1..xpxl2.toInt - 1: sur.plotAA(x.toFloat(), ipart(intery), rfpart(intery), color) sur.plotAA(x.toFloat(), ipart(intery) + 1.0, fpart(intery), color) intery = intery + gradient if sdl.Init(sdl.INIT_VIDEO) < 0: echo "sdl init failed: " & $SDL.GetError() if sdl_ttf.Init() < 0: echo "sdl_ttf init failed: " & $SDL.GetError() when isMainModule: #var txtSurf = drawText( var surf = newScreenSurface(800, 600) var r: TRect = (0, 0, 900, 900) # Draw the shapes surf.fillRect(r, colWhite) surf.drawLineAA((400, 599), (100, 170), colTan) surf.drawEllipse(200, 300, 200, 30, colSeaGreen) surf.drawHorLine(1, 300, 400, colViolet) # Check if the ellipse is the size it's suppose to be. surf.drawVerLine(200, 300 - 30 + 1, 60, colViolet) # ^^ | i suppose it is surf.drawEllipse(400, 300, 300, 300, colOrange) surf.drawEllipse(5, 5, 5, 5, colGreen) surf.drawHorLine(5, 5, 900, colRed) surf.drawVerLine(5, 60, 800, colRed) surf.drawCircle((600, 500), 60, colRed) surf.drawText((300, 300), "VeraMono.ttf", "TEST", colTan, colMidnightBlue, 150) var textSize = textBounds("VeraMono.ttf", 150, "TEST") surf.drawText((300, 300 + textSize.height), "VeraMono.ttf", $textSize.width & ", " & $textSize.height, colDarkGreen, 50) var mouseStartX = 0 var mouseStartY = 0 withEvents(surf, event): case event.theType: of SDL.QUITEV: break of SDL.KEYDOWN: if event.key.keysym.sym == SDL.K_LEFT: echo(event.key.keysym.sym) surf.drawHorLine(395, 300, 5, colPaleGoldenRod) echo("Drawing") else: echo(event.key.keysym.sym) of SDL.MOUSEBUTTONDOWN: # button.x/y is F* UP! echo("MOUSEDOWN", event.button.x) mouseStartX = event.button.x mouseStartY = event.button.y of SDL.MOUSEBUTTONUP: echo("MOUSEUP ", mouseStartX) if mouseStartX != 0 and mouseStartY != 0: echo(mouseStartX, "->", int(event.button.x)) surf.drawLineAA((mouseStartX, MouseStartY), (int(event.button.x), int(event.button.y)), colPaleGoldenRod) mouseStartX = 0 mouseStartY = 0 else: echo(event.theType) SDL.UpdateRect(surf.s, int32(0), int32(0), int32(800), int32(600)) surf.writeToBMP("test.bmp") SDL.Quit()