about summary refs log tree commit diff stats
path: root/src/img/painter.nim
diff options
context:
space:
mode:
Diffstat (limited to 'src/img/painter.nim')
-rw-r--r--src/img/painter.nim207
1 files changed, 207 insertions, 0 deletions
diff --git a/src/img/painter.nim b/src/img/painter.nim
new file mode 100644
index 00000000..c4aef549
--- /dev/null
+++ b/src/img/painter.nim
@@ -0,0 +1,207 @@
+import algorithm
+import unicode
+
+import css/values
+import img/bitmap
+import img/path
+import img/png
+import types/color
+import types/line
+import types/vector
+
+# 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 0xFF))
+  let gy = uint64(64 + 16 * (u div 0xFF))
+  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)