import std/algorithm import std/unicode import css/values import img/bitmap import img/path import img/png import types/color import types/line import types/vector type CanvasFillRule* = enum NON_ZERO = "nonzero" EVEN_ODD = "evenodd" # https://en.wikipedia.org/wiki/Bresenham's_line_algorithm#All_cases proc plotLineLow(bmp: Bitmap, x0, y0, x1, y1: int64, color: RGBAColor) = var dx = x1 - x0 var dy = y1 - y0 var yi = 1 if dy < 0: yi = -1 dy = -dy var D = 2 * dy - dx; var y = y0; for x in x0 ..< x1: if x < 0 or y < 0 or uint64(x) >= bmp.width or uint64(y) >= bmp.height: break bmp.setpxb(uint64(x), uint64(y), color) if D > 0: y = y + yi; D = D - 2 * dx; D = D + 2 * dy; proc plotLineHigh(bmp: Bitmap, x0, y0, x1, y1: int64, color: RGBAColor) = var dx = x1 - x0 var dy = y1 - y0 var xi = 1 if dx < 0: xi = -1 dx = -dx var D = 2 * dx - dy var x = x0 for y in y0 ..< y1: if x < 0 or y < 0 or uint64(x) >= bmp.width or uint64(y) >= bmp.height: break bmp.setpxb(uint64(x), uint64(y), color) if D > 0: x = x + xi D = D - 2 * dy D = D + 2 * dx #TODO should be uint64... proc plotLine(bmp: Bitmap, x0, y0, x1, y1: int64, color: RGBAColor) = if abs(y1 - y0) < abs(x1 - x0): if x0 > x1: bmp.plotLineLow(x1, y1, x0, y0, color) else: bmp.plotLineLow(x0, y0, x1, y1, color) else: if y0 > y1: bmp.plotLineHigh(x1, y1, x0, y0, color) else: bmp.plotLineHigh(x0, y0, x1, y1, color) proc plotLine(bmp: Bitmap, a, b: Vector2D, color: RGBAColor) = bmp.plotLine(int64(a.x), int64(a.y), int64(b.x), int64(b.y), color) proc plotLine(bmp: Bitmap, line: Line, color: RGBAColor) = bmp.plotLine(line.p0, line.p1, color) proc strokePath*(bmp: Bitmap, path: Path, color: RGBAColor) = for line in path.lines: bmp.plotLine(line, color) func isInside(windingNumber: int, fillRule: CanvasFillRule): bool = return case fillRule of NON_ZERO: windingNumber != 0 of EVEN_ODD: windingNumber mod 2 == 0 # Mainly adapted from SerenityOS. proc fillPath*(bmp: Bitmap, path: Path, color: RGBAColor, fillRule: CanvasFillRule) = let lines = path.getLineSegments() var i = 0 var ylines: seq[LineSegment] for y in int64(lines.miny) .. int64(lines.maxy): for k in countdown(ylines.high, 0): if ylines[k].maxy < float64(y): ylines.del(k) # we'll sort anyways, so del is fine for j in i ..< lines.len: if lines[j].miny > float64(y): break if lines[j].maxy > float64(y): ylines.add(lines[j]) inc i ylines.sort(cmpLineSegmentX) var w = if fillRule == NON_ZERO: 1 else: 0 for k in 0 ..< ylines.high: let a = ylines[k] let b = ylines[k + 1] let sx = int64(a.minyx) let ex = int64(b.minyx) if isInside(w, fillRule) and y > 0: for x in sx .. ex: if x > 0: bmp.setpxb(uint64(x), uint64(y), color) if int64(a.p0.y) != y and int64(a.p1.y) != y and int64(b.p0.y) != y and int64(b.p1.y) != y and sx != ex or a.islope * b.islope < 0: case fillRule of EVEN_ODD: inc w of NON_ZERO: if a.p0.y < a.p1.y: inc w else: dec w ylines[k].minyx += ylines[k].islope if ylines.len > 0: ylines[^1].minyx += ylines[^1].islope proc fillRect*(bmp: Bitmap, x0, x1, y0, y1: uint64, color: RGBAColor) = for y in y0 ..< y1: for x in x0 ..< x1: bmp.setpxb(x, y, color) proc strokeRect*(bmp: Bitmap, x0, x1, y0, y1: uint64, color: RGBAColor) = for x in x0 ..< x1: bmp.setpxb(x, y0, color) bmp.setpxb(x, y1, color) for y in y0 ..< y1: bmp.setpxb(x0, y, color) bmp.setpxb(x1, y, color) proc clearRect*(bmp: Bitmap, x0, x1, y0, y1: uint64) = for y in y0 ..< y1: for x in x0 ..< x1: bmp.setpx(x, y, rgba(0, 0, 0, 0)) proc clear*(bmp: Bitmap) = bmp.clearRect(0, bmp.width, 0, bmp.height) const unifont = readFile"res/unifont_jp-15.0.05.png" var unifontBitmap: Bitmap var glyphCache: seq[tuple[u: uint32, bmp: Bitmap]] var glyphCacheI = 0 proc getCharBmp(u: uint32): Bitmap = # We only have the BMP. let u = if u <= 0xFFFF: u else: 0xFFFD if unifontBitmap == nil: unifontBitmap = fromPNG(toOpenArrayByte(unifont, 0, unifont.high)) for (cu, bmp) in glyphCache: if cu == u: return bmp # Unifont glyphs start at x: 32, y: 64, and are of 8x16/16x16 size let gx = uint64(32 + 16 * (u mod 0x100)) let gy = uint64(64 + 16 * (u div 0x100)) var fullwidth = false const white = rgba(255, 255, 255, 255) block loop: # hack to recognize full width characters for y in 0 ..< 16u64: for x in 8 ..< 16u64: if unifontBitmap.getpx(gx + x, gy + y) != white: fullwidth = true break loop let bmp = newBitmap(if fullwidth: 16 else: 8, 16) for y in 0 ..< bmp.height: for x in 0 ..< bmp.width: let c = unifontBitmap.getpx(gx + x, gy + y) if c != white: bmp.setpx(x, y, c) if glyphCache.len < 256: glyphCache.add((u, bmp)) else: glyphCache[glyphCacheI] = (u, bmp) inc glyphCacheI if glyphCacheI >= glyphCache.len: glyphCacheI = 0 return bmp proc drawBitmap(a, b: Bitmap, p: Vector2D) = for y in 0 ..< b.height: for x in 0 ..< b.width: let ax = uint64(p.x) + x let ay = uint64(p.y) + y if ax >= 0 and ay >= y and ax < a.width and ay < a.height: a.setpxb(ax, ay, b.getpx(x, y)) proc fillText*(bmp: Bitmap, text: string, x, y: float64, color: RGBAColor, textAlign: CSSTextAlign) = var w = 0f64 var glyphs: seq[Bitmap] for r in text.runes: let glyph = getCharBmp(uint32(r)) glyphs.add(glyph) w += float64(glyph.width) var x = x #TODO rtl case textAlign of TEXT_ALIGN_LEFT, TEXT_ALIGN_START: discard of TEXT_ALIGN_RIGHT, TEXT_ALIGN_END: x -= w of TEXT_ALIGN_CENTER: x -= w / 2 else: doAssert false for glyph in glyphs: bmp.drawBitmap(glyph, Vector2D(x: x, y: y - 8)) x += float64(glyph.width) proc strokeText*(bmp: Bitmap, text: string, x, y: float64, color: RGBAColor, textAlign: CSSTextAlign) = #TODO bmp.fillText(text, x, y, color, textAlign)