From 9f453ca3997528252eb28268e38480f58fbce4f6 Mon Sep 17 00:00:00 2001 From: bptato Date: Sun, 15 Sep 2024 18:13:00 +0200 Subject: Refactor img/* I've moved most image logic to adapter, so it doesn't really make sense to have this subdir anymore. --- src/css/cssvalues.nim | 2 +- src/html/dom.nim | 16 +- src/img/bitmap.nim | 39 ----- src/img/painter.nim | 211 ---------------------- src/img/path.nim | 398 ------------------------------------------ src/layout/box.nim | 2 +- src/layout/engine.nim | 2 +- src/layout/renderdocument.nim | 2 +- src/local/container.nim | 2 +- src/local/pager.nim | 2 +- src/types/bitmap.nim | 7 + src/types/canvastypes.nim | 15 ++ src/types/path.nim | 398 ++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 434 insertions(+), 662 deletions(-) delete mode 100644 src/img/bitmap.nim delete mode 100644 src/img/painter.nim delete mode 100644 src/img/path.nim create mode 100644 src/types/bitmap.nim create mode 100644 src/types/canvastypes.nim create mode 100644 src/types/path.nim (limited to 'src') diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim index a0bb1f1f..3faf13e5 100644 --- a/src/css/cssvalues.nim +++ b/src/css/cssvalues.nim @@ -6,7 +6,7 @@ import std/tables import css/cssparser import css/selectorparser -import img/bitmap +import types/bitmap import layout/layoutunit import types/color import types/opt diff --git a/src/html/dom.nim b/src/html/dom.nim index 146056a9..9275538c 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -19,9 +19,8 @@ import html/catom import html/enums import html/event import html/script -import img/bitmap -import img/painter -import img/path +import types/bitmap +import types/path import io/bufwriter import io/dynstream import io/promise @@ -40,6 +39,7 @@ import monoucha/jsutils import monoucha/quickjs import monoucha/tojs import types/blob +import types/canvastypes import types/color import types/line import types/matrix @@ -63,10 +63,10 @@ type fetMultipart = "multipart/form-data", fetTextPlain = "text/plain" -type DocumentReadyState* = enum - rsLoading = "loading" - rsInteractive = "interactive" - rsComplete = "complete" + DocumentReadyState* = enum + rsLoading = "loading" + rsInteractive = "interactive" + rsComplete = "complete" type DependencyType* = enum @@ -368,7 +368,7 @@ type CanvasRenderingContext2D = ref object of RenderingContext canvas {.jsget.}: HTMLCanvasElement - bitmap: Bitmap + bitmap: NetworkBitmap state: DrawingState stateStack: seq[DrawingState] ps*: PosixStream diff --git a/src/img/bitmap.nim b/src/img/bitmap.nim deleted file mode 100644 index 343e9ecf..00000000 --- a/src/img/bitmap.nim +++ /dev/null @@ -1,39 +0,0 @@ -import types/color - -type - Bitmap* = ref object of RootObj - px*: seq[RGBAColorBE] - width*: int - height*: int - - ImageBitmap* = ref object of Bitmap - - NetworkBitmap* = ref object of Bitmap - cacheId*: int - imageId*: int - contentType*: string - -proc newBitmap*(width, height: int): ImageBitmap = - return ImageBitmap( - px: newSeq[RGBAColorBE](width * height), - width: width, - height: height - ) - -proc setpx*(bmp: Bitmap; x, y: int; color: RGBAColorBE) {.inline.} = - bmp.px[bmp.width * y + x] = color - -proc setpx*(bmp: Bitmap; x, y: int; color: ARGBColor) {.inline.} = - bmp.px[bmp.width * y + x] = rgba_be(color.r, color.g, color.b, color.a) - -proc getpx*(bmp: Bitmap; x, y: int): RGBAColorBE {.inline.} = - return bmp.px[bmp.width * y + x] - -proc setpxb*(bmp: Bitmap; x, y: int; c: RGBAColorBE) {.inline.} = - if c.a == 255: - bmp.setpx(x, y, c) - else: - bmp.setpx(x, y, bmp.getpx(x, y).blend(c)) - -proc setpxb*(bmp: Bitmap; x, y: int; c: ARGBColor) {.inline.} = - bmp.setpxb(x, y, rgba_be(c.r, c.g, c.b, c.a)) diff --git a/src/img/painter.nim b/src/img/painter.nim deleted file mode 100644 index 40f2c79a..00000000 --- a/src/img/painter.nim +++ /dev/null @@ -1,211 +0,0 @@ -import std/algorithm - -import img/bitmap -import img/path -import types/color -import types/line -import types/vector -import utils/twtuni - -type - CanvasFillRule* = enum - cfrNonZero = "nonzero" - cfrEvenOdd = "evenodd" - - PaintCommand* = enum - pcSetDimensions, pcFillRect, pcStrokeRect, pcFillPath, pcStrokePath, - pcFillText, pcStrokeText - - CanvasTextAlign* = enum - ctaStart = "start" - ctaEnd = "end" - ctaLeft = "left" - ctaRight = "right" - ctaCenter = "center" - -# https://en.wikipedia.org/wiki/Bresenham's_line_algorithm#All_cases -proc plotLineLow(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) = - var dx = x2 - x1 - var dy = y2 - y1 - var yi = 1 - if dy < 0: - yi = -1 - dy = -dy - var D = 2 * dy - dx; - var y = y1; - for x in x1 ..< x2: - if x < 0 or y < 0 or x >= bmp.width or y >= bmp.height: - break - bmp.setpxb(x, y, color) - if D > 0: - y = y + yi; - D = D - 2 * dx; - D = D + 2 * dy; - -proc plotLineHigh(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) = - var dx = x2 - x1 - var dy = y2 - y1 - var xi = 1 - if dx < 0: - xi = -1 - dx = -dx - var D = 2 * dx - dy - var x = x1 - for y in y1 ..< y2: - if x < 0 or y < 0 or x >= bmp.width or y >= bmp.height: - break - bmp.setpxb(x, y, color) - if D > 0: - x = x + xi - D = D - 2 * dy - D = D + 2 * dx - -proc plotLine(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) = - if abs(y2 - y1) < abs(x2 - x1): - if x1 > x2: - bmp.plotLineLow(x2, y2, x1, y1, color) - else: - bmp.plotLineLow(x1, y1, x2, y2, color) - else: - if y1 > y2: - bmp.plotLineHigh(x2, y2, x1, y1, color) - else: - bmp.plotLineHigh(x1, y1, x2, y2, color) - -proc plotLine(bmp: Bitmap; a, b: Vector2D; color: ARGBColor) = - bmp.plotLine(int(a.x), int(a.y), int(b.x), int(b.y), color) - -proc plotLine(bmp: Bitmap; line: Line; color: ARGBColor) = - bmp.plotLine(line.p0, line.p1, color) - -proc strokePath*(bmp: Bitmap; lines: seq[Line]; color: ARGBColor) = - for line in lines: - bmp.plotLine(line, color) - -func isInside(windingNumber: int; fillRule: CanvasFillRule): bool = - return case fillRule - of cfrNonZero: windingNumber != 0 - of cfrEvenOdd: windingNumber mod 2 == 0 - -# Algorithm originally from SerenityOS. -proc fillPath*(bmp: Bitmap; lines: PathLines; color: ARGBColor; - fillRule: CanvasFillRule) = - var i = 0 - var ylines: seq[LineSegment] = @[] - for y in int(lines.miny) .. int(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 == cfrNonZero: 1 else: 0 - for k in 0 ..< ylines.high: - let a = ylines[k] - let b = ylines[k + 1] - let sx = int(a.minyx) - let ex = int(b.minyx) - if w.isInside(fillRule) and y > 0: - for x in sx .. ex: - if x > 0: - bmp.setpxb(x, y, color) - if int(a.p0.y) != y and int(a.p1.y) != y and int(b.p0.y) != y and - int(b.p1.y) != y and sx != ex or a.islope * b.islope < 0: - case fillRule - of cfrEvenOdd: inc w - of cfrNonZero: - 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; x1, y1, x2, y2: int; color: ARGBColor) = - for y in y1 ..< y2: - for x in x1 ..< x2: - bmp.setpxb(x, y, color) - -proc strokeRect*(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) = - for x in x1 ..< x2: - bmp.setpxb(x, y1, color) - bmp.setpxb(x, y2, color) - for y in y1 ..< y2: - bmp.setpxb(x1, y, color) - bmp.setpxb(x2, y, color) - -type GlyphCacheItem = object - u: uint32 - bmp: Bitmap - -var unifontBitmap*: Bitmap = nil -var glyphCache: seq[GlyphCacheItem] = @[] -var glyphCacheI = 0 -proc getCharBmp(u: uint32): Bitmap = - # We only have the BMP. - let u = if u <= 0xFFFF: u else: 0xFFFD - for it in glyphCache: - if it.u == u: - return it.bmp - # Unifont glyphs start at x: 32, y: 64, and are of 8x16/16x16 size - let gx = int(32 + 16 * (u mod 0x100)) - let gy = int(64 + 16 * (u div 0x100)) - var fullwidth = false - const white = rgba_be(255, 255, 255, 255) - block loop: - # hack to recognize full width characters - for y in 0 ..< 16: - for x in 8 ..< 16: - 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(GlyphCacheItem(u: u, bmp: bmp)) - else: - glyphCache[glyphCacheI] = GlyphCacheItem(u: u, bmp: 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 = int(p.x) + x - let ay = int(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: ARGBColor; - textAlign: CanvasTextAlign) = - var w = 0f64 - var glyphs: seq[Bitmap] = @[] - for u in text.points: - let glyph = getCharBmp(u) - glyphs.add(glyph) - w += float64(glyph.width) - var x = x - #TODO rtl - case textAlign - of ctaLeft, ctaStart: discard - of ctaRight, ctaEnd: x -= w - of ctaCenter: x -= w / 2 - 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: ARGBColor; - textAlign: CanvasTextAlign) = - #TODO - bmp.fillText(text, x, y, color, textAlign) diff --git a/src/img/path.nim b/src/img/path.nim deleted file mode 100644 index c5db0bea..00000000 --- a/src/img/path.nim +++ /dev/null @@ -1,398 +0,0 @@ -import std/algorithm -import std/deques -import std/math - -import types/line -import types/vector - -type - Path* = ref object - subpaths: seq[Subpath] - needsNewSubpath: bool - tempClosed: bool - - PathLines* = object - lines*: seq[LineSegment] - miny*: float64 - maxy*: float64 - - PathSegmentType = enum - pstStraight, pstQuadratic, pstBezier, pstArc, pstEllipse - - PathSegment = object - case t: PathSegmentType - of pstQuadratic: - cp: Vector2D - of pstBezier: - cp0: Vector2D - cp1: Vector2D - of pstArc: - oa: Vector2D - r: float64 - ia: bool - of pstEllipse: - oe: Vector2D - rx: float64 - ry: float64 - else: discard - - Subpath* = object - points: seq[Vector2D] - segments: seq[PathSegment] - closed: bool - -proc newPath*(): Path = - return Path( - needsNewSubpath: true - ) - -proc addSubpathAt(path: Path; p: Vector2D) = - path.subpaths.add(Subpath(points: @[p])) - -proc addSegment(path: Path; segment: PathSegment; p: Vector2D) = - path.subpaths[^1].segments.add(segment) - path.subpaths[^1].points.add(p) - -proc addStraightSegment(path: Path; p: Vector2D) = - let segment = PathSegment(t: pstStraight) - path.addSegment(segment, p) - -proc addQuadraticSegment(path: Path; cp, p: Vector2D) = - let segment = PathSegment( - t: pstQuadratic, - cp: cp - ) - path.addSegment(segment, p) - -proc addBezierSegment(path: Path; cp0, cp1, p: Vector2D) = - let segment = PathSegment( - t: pstBezier, - cp0: cp0, - cp1: cp1 - ) - path.addSegment(segment, p) - -# Goes from start tangent point to end tangent point -proc addArcSegment(path: Path; o, etan: Vector2D; r: float64; ia: bool) = - let segment = PathSegment( - t: pstArc, - oa: o, - r: r, - ia: ia - ) - path.addSegment(segment, etan) - -proc addEllipseSegment(path: Path; o, etan: Vector2D; rx, ry: float64) = - #TODO simplify to bezier? - let segment = PathSegment( - t: pstEllipse, - oe: o, - rx: rx, - ry: ry - ) - path.addSegment(segment, etan) - -# https://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf -func flatEnough(a, b, c: Vector2D): bool = - let ux = 3 * c.x - 2 * a.x - b.x - let uy = 3 * c.y - 2 * a.y - b.y - let vx = 3 * c.x - 2 * b.x - b.x - let vy = 3 * c.y - 2 * b.y - b.y - return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02 - -func flatEnough(a, b, c0, c1: Vector2D): bool = - let ux = 3 * c0.x - 2 * a.x - b.x - let uy = 3 * c0.y - 2 * a.y - b.y - let vx = 3 * c1.x - a.x - 2 * b.x - let vy = 3 * c1.y - a.y - 2 * b.y - return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02 - -iterator items*(pl: PathLines): LineSegment {.inline.} = - for line in pl.lines: - yield line - -func `[]`*(pl: PathLines; i: int): LineSegment = pl.lines[i] -func `[]`*(pl: PathLines; i: BackwardsIndex): LineSegment = pl.lines[i] -func `[]`*(pl: PathLines; s: Slice[int]): seq[LineSegment] = pl.lines[s] -func len*(pl: PathLines): int = pl.lines.len - -iterator quadraticLines(a, b, c: Vector2D): Line {.inline.} = - var points: Deque[tuple[a, b, c: Vector2D]] - let tup = (a, b, c) - points.addFirst(tup) - while points.len > 2: - let (a, b, c) = points.popFirst() - if flatEnough(a, b, c): - yield Line(p0: a, p1: b) - else: - let mid1 = (c + a) / 2 - let mid2 = (c + b) / 2 - let s = (mid1 + mid2) / 2 - points.addFirst((a, s, mid1)) - points.addFirst((s, b, mid2)) - -iterator bezierLines(p0, p1, c0, c1: Vector2D): Line {.inline.} = - var points: Deque[tuple[p0, p1, c0, c1: Vector2D]] - let tup = (p0, p1, c0, c1) - points.addLast(tup) - while points.len > 0: - let (p0, p1, c0, c1) = points.popFirst() - if flatEnough(p0, p1, c0, c1): - yield Line(p0: p0, p1: p1) - else: - let mida1 = (p0 + c0) / 2 - let mida2 = (c0 + c1) / 2 - let mida3 = (c1 + p1) / 2 - let midb1 = (mida1 + mida2) / 2 - let midb2 = (mida2 + mida3) / 2 - let midc = (midb1 + midb2) / 2 - points.addLast((p0, midc, mida1, midb1)) - points.addLast((midc, p1, midb2, mida3)) - -# https://stackoverflow.com/a/44829356 -func arcControlPoints(p1, p4, o: Vector2D): tuple[c0, c1: Vector2D] = - let a = p1 - o - let b = p4 - o - let q1 = a.x * a.x + a.y * a.y - let q2 = q1 + a.x * b.x + a.y * b.y - let k2 = (4 / 3) * (sqrt(2 * q1 * q2) - q2) / a.cross(b) - let c0 = o + a + Vector2D(x: -k2 * a.y, y: k2 * a.x) - let c1 = o + b + Vector2D(x: k2 * b.y, y: -k2 * b.x) - return (c0, c1) - -iterator arcLines(p0, p1, o: Vector2D; r: float64; i: bool): Line {.inline.} = - var p0 = p0 - let pp0 = p0 - o - let pp1 = p1 - o - var theta = pp0.innerAngle(pp1) - if not i: - theta = PI * 2 - theta - while theta > 0: - let step = if theta > PI / 2: PI / 2 else: theta - var p1 = p0 - o - p1 = p1.rotate(step) - p1 += o - let (c0, c1) = arcControlPoints(p0, p1, o) - for line in bezierLines(p0, p1, c0, c1): - yield line - p0 = p1 - theta -= step - -proc addLines(lines: var seq[Line]; subpath: Subpath; i: int) = - let p0 = subpath.points[i] - let p1 = subpath.points[i + 1] - case subpath.segments[i].t - of pstStraight: - if line.p0 != line.p1: - lines.add(Line(p0: p0, p1: p1)) - of pstQuadratic: - let c = subpath.segments[i].cp - for line in quadraticLines(p0, p1, c): - if line.p0 != line.p1: - lines.add(line) - of pstBezier: - let c0 = subpath.segments[i].cp0 - let c1 = subpath.segments[i].cp1 - for line in bezierLines(p0, p1, c0, c1): - if line.p0 != line.p1: - lines.add(line) - of pstArc: - let o = subpath.segments[i].oa - let r = subpath.segments[i].r - let i = subpath.segments[i].ia - for line in arcLines(p0, p1, o, r, i): - if line.p0 != line.p1: - lines.add(line) - of pstEllipse: - discard #TODO - -proc getLines*(path: Path): seq[Line] = - var lines: seq[Line] = @[] - for subpath in path.subpaths: - assert subpath.points.len == subpath.segments.len + 1 - for i in 0 ..< subpath.segments.len: - lines.addLines(subpath, i) - return lines - -proc getLineSegments*(path: Path): PathLines = - if path.subpaths.len == 0: - return PathLines() - var miny = Inf - var maxy = -Inf - let lines = path.getLines() - var segments: seq[LineSegment] = @[] - for line in lines: - let ls = line.toLineSegment() - miny = min(miny, ls.miny) - maxy = max(maxy, ls.maxy) - segments.add(ls) - segments.sort(cmpLineSegmentY) - return PathLines( - miny: miny, - maxy: maxy, - lines: segments - ) - -proc moveTo(path: Path; v: Vector2D) = - path.addSubpathAt(v) - path.needsNewSubpath = false #TODO TODO TODO ???? why here - -proc beginPath*(path: Path) = - path.subpaths.setLen(0) - -proc moveTo*(path: Path; x, y: float64) = - for v in [x, y]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - path.moveTo(Vector2D(x: x, y: y)) - -proc ensureSubpath(path: Path; x, y: float64) = - if path.needsNewSubpath: - path.moveTo(x, y) - path.needsNewSubpath = false - -proc closePath*(path: Path) = - let lsp = path.subpaths[^1] - if path.subpaths.len > 0 and (lsp.points.len > 0 or lsp.closed): - path.subpaths[^1].closed = true - path.addSubpathAt(path.subpaths[^1].points[0]) - -#TODO this is a hack, and breaks as soon as any draw command is issued -# between tempClosePath and tempOpenPath -proc tempClosePath*(path: Path) = - if path.subpaths.len > 0 and not path.subpaths[^1].closed: - path.subpaths[^1].closed = true - let lsp = path.subpaths[^1] - path.addSubpathAt(lsp.points[^1]) - path.addStraightSegment(lsp.points[0]) - path.tempClosed = true - -proc tempOpenPath*(path: Path) = - if path.tempClosed: - path.subpaths.setLen(path.subpaths.len - 1) - path.subpaths[^1].closed = false - path.tempClosed = false - -proc lineTo*(path: Path; x, y: float64) = - for v in [x, y]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - if path.subpaths.len == 0: - path.ensureSubpath(x, y) - else: - path.addStraightSegment(Vector2D(x: x, y: y)) - -proc quadraticCurveTo*(path: Path; cpx, cpy, x, y: float64) = - for v in [cpx, cpy, x, y]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - path.ensureSubpath(cpx, cpy) - let cp = Vector2D(x: cpx, y: cpy) - let p = Vector2D(x: x, y: y) - path.addQuadraticSegment(cp, p) - -proc bezierCurveTo*(path: Path; cp0x, cp0y, cp1x, cp1y, x, y: float64) = - for v in [cp0x, cp0y, cp1x, cp1y, x, y]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - path.ensureSubpath(cp0x, cp0y) - let cp0 = Vector2D(x: cp0x, y: cp0y) - let cp1 = Vector2D(x: cp1x, y: cp1y) - let p = Vector2D(x: x, y: y) - path.addBezierSegment(cp0, cp1, p) - -proc arcTo*(path: Path; x1, y1, x2, y2, radius: float64) = - for v in [x1, y1, x2, y2, radius]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - path.ensureSubpath(x1, y1) - #TODO this should be transformed by the inverse of the transformation matrix - let v0 = path.subpaths[^1].points[^1] - let v1 = Vector2D(x: x1, y: y1) - let v2 = Vector2D(x: x2, y: y2) - if v0.x == x1 and v0.y == y1 or x1 == x2 and y1 == y2 or radius == 0: - path.addStraightSegment(v1) - elif collinear(v0, v1, v2): - path.addStraightSegment(v1) - else: - let pv0 = v0 - v1 - let pv2 = v2 - v1 - let tv0 = v1 + pv0 * radius * 2 / pv0.norm() - let tv2 = v1 + pv2 * radius * 2 / pv2.norm() - let q = -(pv0.x * tv0.x + pv0.y * tv0.y) - let p = -(pv2.x * tv2.x + pv2.y * tv2.y) - let cr = pv0.cross(pv2) - let origin = Vector2D( - x: (pv0.y * p - pv2.y * q) / cr, - y: (pv2.x * q - pv0.x * p) / cr - ) - path.addStraightSegment(tv0) - path.addArcSegment(origin, tv2, radius, true) #TODO always inner? - -func resolveEllipsePoint(o: Vector2D; angle, radiusX, radiusY, - rotation: float64): Vector2D = - # Stolen from SerenityOS - let tanrel = tan(angle) - let tan2 = tanrel * tanrel - let ab = radiusX * radiusY - let a2 = radiusX * radiusX - let b2 = radiusY * radiusY - let sq = sqrt(b2 + a2 * tan2) - let sn = if cos(angle) >= 0: 1f64 else: -1f64 - let relx = ab / sq * sn - let rely = ab * tanrel / sq * sn - return Vector2D(x: relx, y: rely).rotate(rotation) + o - -proc arc*(path: Path; x, y, radius, startAngle, endAngle: float64; - counterclockwise: bool) = - for v in [x, y, radius, startAngle, endAngle]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - let o = Vector2D(x: x, y: y) - var s = resolveEllipsePoint(o, startAngle, radius, radius, 0) - var e = resolveEllipsePoint(o, endAngle, radius, radius, 0) - if counterclockwise: - let tmp = s - e = s - s = tmp - if path.subpaths.len > 0: - path.addStraightSegment(s) - else: - path.moveTo(s) - path.addArcSegment(o, e, radius, abs(startAngle - endAngle) < PI) - -proc ellipse*(path: Path; x, y, radiusX, radiusY, rotation, startAngle, - endAngle: float64; counterclockwise: bool) = - for v in [x, y, radiusX, radiusY, rotation, startAngle, endAngle]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - let o = Vector2D(x: x, y: y) - var s = resolveEllipsePoint(o, startAngle, radiusX, radiusY, rotation) - var e = resolveEllipsePoint(o, endAngle, radiusX, radiusY, rotation) - if counterclockwise: - let tmp = s - s = e - e = tmp - if path.subpaths.len > 0: - path.addStraightSegment(s) - else: - path.moveTo(s) - path.addEllipseSegment(o, e, radiusX, radiusY) - -proc rect*(path: Path; x, y, w, h: float64) = - for v in [x, y, w, h]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - path.addSubpathAt(Vector2D(x: x, y: y)) - path.addStraightSegment(Vector2D(x: x + w, y: y)) - path.addStraightSegment(Vector2D(x: x + w, y: y + h)) - path.addStraightSegment(Vector2D(x: x, y: y + h)) - path.addStraightSegment(Vector2D(x: x, y: y)) - path.addSubpathAt(Vector2D(x: x, y: y)) - -proc roundRect*(path: Path; x, y, w, h, radii: float64) = - for v in [x, y, w, h]: - if classify(v) in {fcInf, fcNegInf, fcNan}: - return - #TODO implement - path.rect(x, y, w, h) # :P diff --git a/src/layout/box.nim b/src/layout/box.nim index cd94ca49..b9d18f79 100644 --- a/src/layout/box.nim +++ b/src/layout/box.nim @@ -1,6 +1,6 @@ import css/cssvalues import css/stylednode -import img/bitmap +import types/bitmap import layout/layoutunit type diff --git a/src/layout/engine.nim b/src/layout/engine.nim index 831df62e..8ba12729 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -3,7 +3,7 @@ import std/math import css/cssvalues import css/stylednode -import img/bitmap +import types/bitmap import layout/box import layout/layoutunit import types/winattrs diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim index 2d1df349..9622d305 100644 --- a/src/layout/renderdocument.nim +++ b/src/layout/renderdocument.nim @@ -2,7 +2,7 @@ import std/strutils import css/cssvalues import css/stylednode -import img/bitmap +import types/bitmap import layout/box import layout/engine import layout/layoutunit diff --git a/src/local/container.nim b/src/local/container.nim index 54d9bedd..c6907982 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -7,7 +7,7 @@ import std/tables import chagashi/charset import config/config import config/mimetypes -import img/bitmap +import types/bitmap import io/dynstream import io/promise import io/serversocket diff --git a/src/local/pager.nim b/src/local/pager.nim index c0034a7f..8b449f24 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -12,7 +12,7 @@ import chagashi/charset import config/chapath import config/config import config/mailcap -import img/bitmap +import types/bitmap import io/bufreader import io/dynstream import io/promise diff --git a/src/types/bitmap.nim b/src/types/bitmap.nim new file mode 100644 index 00000000..55a7794f --- /dev/null +++ b/src/types/bitmap.nim @@ -0,0 +1,7 @@ +type + NetworkBitmap* = ref object + width*: int + height*: int + cacheId*: int + imageId*: int + contentType*: string diff --git a/src/types/canvastypes.nim b/src/types/canvastypes.nim new file mode 100644 index 00000000..350d8cbb --- /dev/null +++ b/src/types/canvastypes.nim @@ -0,0 +1,15 @@ +type + CanvasFillRule* = enum + cfrNonZero = "nonzero" + cfrEvenOdd = "evenodd" + + PaintCommand* = enum + pcSetDimensions, pcFillRect, pcStrokeRect, pcFillPath, pcStrokePath, + pcFillText, pcStrokeText + + CanvasTextAlign* = enum + ctaStart = "start" + ctaEnd = "end" + ctaLeft = "left" + ctaRight = "right" + ctaCenter = "center" diff --git a/src/types/path.nim b/src/types/path.nim new file mode 100644 index 00000000..c5db0bea --- /dev/null +++ b/src/types/path.nim @@ -0,0 +1,398 @@ +import std/algorithm +import std/deques +import std/math + +import types/line +import types/vector + +type + Path* = ref object + subpaths: seq[Subpath] + needsNewSubpath: bool + tempClosed: bool + + PathLines* = object + lines*: seq[LineSegment] + miny*: float64 + maxy*: float64 + + PathSegmentType = enum + pstStraight, pstQuadratic, pstBezier, pstArc, pstEllipse + + PathSegment = object + case t: PathSegmentType + of pstQuadratic: + cp: Vector2D + of pstBezier: + cp0: Vector2D + cp1: Vector2D + of pstArc: + oa: Vector2D + r: float64 + ia: bool + of pstEllipse: + oe: Vector2D + rx: float64 + ry: float64 + else: discard + + Subpath* = object + points: seq[Vector2D] + segments: seq[PathSegment] + closed: bool + +proc newPath*(): Path = + return Path( + needsNewSubpath: true + ) + +proc addSubpathAt(path: Path; p: Vector2D) = + path.subpaths.add(Subpath(points: @[p])) + +proc addSegment(path: Path; segment: PathSegment; p: Vector2D) = + path.subpaths[^1].segments.add(segment) + path.subpaths[^1].points.add(p) + +proc addStraightSegment(path: Path; p: Vector2D) = + let segment = PathSegment(t: pstStraight) + path.addSegment(segment, p) + +proc addQuadraticSegment(path: Path; cp, p: Vector2D) = + let segment = PathSegment( + t: pstQuadratic, + cp: cp + ) + path.addSegment(segment, p) + +proc addBezierSegment(path: Path; cp0, cp1, p: Vector2D) = + let segment = PathSegment( + t: pstBezier, + cp0: cp0, + cp1: cp1 + ) + path.addSegment(segment, p) + +# Goes from start tangent point to end tangent point +proc addArcSegment(path: Path; o, etan: Vector2D; r: float64; ia: bool) = + let segment = PathSegment( + t: pstArc, + oa: o, + r: r, + ia: ia + ) + path.addSegment(segment, etan) + +proc addEllipseSegment(path: Path; o, etan: Vector2D; rx, ry: float64) = + #TODO simplify to bezier? + let segment = PathSegment( + t: pstEllipse, + oe: o, + rx: rx, + ry: ry + ) + path.addSegment(segment, etan) + +# https://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf +func flatEnough(a, b, c: Vector2D): bool = + let ux = 3 * c.x - 2 * a.x - b.x + let uy = 3 * c.y - 2 * a.y - b.y + let vx = 3 * c.x - 2 * b.x - b.x + let vy = 3 * c.y - 2 * b.y - b.y + return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02 + +func flatEnough(a, b, c0, c1: Vector2D): bool = + let ux = 3 * c0.x - 2 * a.x - b.x + let uy = 3 * c0.y - 2 * a.y - b.y + let vx = 3 * c1.x - a.x - 2 * b.x + let vy = 3 * c1.y - a.y - 2 * b.y + return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02 + +iterator items*(pl: PathLines): LineSegment {.inline.} = + for line in pl.lines: + yield line + +func `[]`*(pl: PathLines; i: int): LineSegment = pl.lines[i] +func `[]`*(pl: PathLines; i: BackwardsIndex): LineSegment = pl.lines[i] +func `[]`*(pl: PathLines; s: Slice[int]): seq[LineSegment] = pl.lines[s] +func len*(pl: PathLines): int = pl.lines.len + +iterator quadraticLines(a, b, c: Vector2D): Line {.inline.} = + var points: Deque[tuple[a, b, c: Vector2D]] + let tup = (a, b, c) + points.addFirst(tup) + while points.len > 2: + let (a, b, c) = points.popFirst() + if flatEnough(a, b, c): + yield Line(p0: a, p1: b) + else: + let mid1 = (c + a) / 2 + let mid2 = (c + b) / 2 + let s = (mid1 + mid2) / 2 + points.addFirst((a, s, mid1)) + points.addFirst((s, b, mid2)) + +iterator bezierLines(p0, p1, c0, c1: Vector2D): Line {.inline.} = + var points: Deque[tuple[p0, p1, c0, c1: Vector2D]] + let tup = (p0, p1, c0, c1) + points.addLast(tup) + while points.len > 0: + let (p0, p1, c0, c1) = points.popFirst() + if flatEnough(p0, p1, c0, c1): + yield Line(p0: p0, p1: p1) + else: + let mida1 = (p0 + c0) / 2 + let mida2 = (c0 + c1) / 2 + let mida3 = (c1 + p1) / 2 + let midb1 = (mida1 + mida2) / 2 + let midb2 = (mida2 + mida3) / 2 + let midc = (midb1 + midb2) / 2 + points.addLast((p0, midc, mida1, midb1)) + points.addLast((midc, p1, midb2, mida3)) + +# https://stackoverflow.com/a/44829356 +func arcControlPoints(p1, p4, o: Vector2D): tuple[c0, c1: Vector2D] = + let a = p1 - o + let b = p4 - o + let q1 = a.x * a.x + a.y * a.y + let q2 = q1 + a.x * b.x + a.y * b.y + let k2 = (4 / 3) * (sqrt(2 * q1 * q2) - q2) / a.cross(b) + let c0 = o + a + Vector2D(x: -k2 * a.y, y: k2 * a.x) + let c1 = o + b + Vector2D(x: k2 * b.y, y: -k2 * b.x) + return (c0, c1) + +iterator arcLines(p0, p1, o: Vector2D; r: float64; i: bool): Line {.inline.} = + var p0 = p0 + let pp0 = p0 - o + let pp1 = p1 - o + var theta = pp0.innerAngle(pp1) + if not i: + theta = PI * 2 - theta + while theta > 0: + let step = if theta > PI / 2: PI / 2 else: theta + var p1 = p0 - o + p1 = p1.rotate(step) + p1 += o + let (c0, c1) = arcControlPoints(p0, p1, o) + for line in bezierLines(p0, p1, c0, c1): + yield line + p0 = p1 + theta -= step + +proc addLines(lines: var seq[Line]; subpath: Subpath; i: int) = + let p0 = subpath.points[i] + let p1 = subpath.points[i + 1] + case subpath.segments[i].t + of pstStraight: + if line.p0 != line.p1: + lines.add(Line(p0: p0, p1: p1)) + of pstQuadratic: + let c = subpath.segments[i].cp + for line in quadraticLines(p0, p1, c): + if line.p0 != line.p1: + lines.add(line) + of pstBezier: + let c0 = subpath.segments[i].cp0 + let c1 = subpath.segments[i].cp1 + for line in bezierLines(p0, p1, c0, c1): + if line.p0 != line.p1: + lines.add(line) + of pstArc: + let o = subpath.segments[i].oa + let r = subpath.segments[i].r + let i = subpath.segments[i].ia + for line in arcLines(p0, p1, o, r, i): + if line.p0 != line.p1: + lines.add(line) + of pstEllipse: + discard #TODO + +proc getLines*(path: Path): seq[Line] = + var lines: seq[Line] = @[] + for subpath in path.subpaths: + assert subpath.points.len == subpath.segments.len + 1 + for i in 0 ..< subpath.segments.len: + lines.addLines(subpath, i) + return lines + +proc getLineSegments*(path: Path): PathLines = + if path.subpaths.len == 0: + return PathLines() + var miny = Inf + var maxy = -Inf + let lines = path.getLines() + var segments: seq[LineSegment] = @[] + for line in lines: + let ls = line.toLineSegment() + miny = min(miny, ls.miny) + maxy = max(maxy, ls.maxy) + segments.add(ls) + segments.sort(cmpLineSegmentY) + return PathLines( + miny: miny, + maxy: maxy, + lines: segments + ) + +proc moveTo(path: Path; v: Vector2D) = + path.addSubpathAt(v) + path.needsNewSubpath = false #TODO TODO TODO ???? why here + +proc beginPath*(path: Path) = + path.subpaths.setLen(0) + +proc moveTo*(path: Path; x, y: float64) = + for v in [x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.moveTo(Vector2D(x: x, y: y)) + +proc ensureSubpath(path: Path; x, y: float64) = + if path.needsNewSubpath: + path.moveTo(x, y) + path.needsNewSubpath = false + +proc closePath*(path: Path) = + let lsp = path.subpaths[^1] + if path.subpaths.len > 0 and (lsp.points.len > 0 or lsp.closed): + path.subpaths[^1].closed = true + path.addSubpathAt(path.subpaths[^1].points[0]) + +#TODO this is a hack, and breaks as soon as any draw command is issued +# between tempClosePath and tempOpenPath +proc tempClosePath*(path: Path) = + if path.subpaths.len > 0 and not path.subpaths[^1].closed: + path.subpaths[^1].closed = true + let lsp = path.subpaths[^1] + path.addSubpathAt(lsp.points[^1]) + path.addStraightSegment(lsp.points[0]) + path.tempClosed = true + +proc tempOpenPath*(path: Path) = + if path.tempClosed: + path.subpaths.setLen(path.subpaths.len - 1) + path.subpaths[^1].closed = false + path.tempClosed = false + +proc lineTo*(path: Path; x, y: float64) = + for v in [x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + if path.subpaths.len == 0: + path.ensureSubpath(x, y) + else: + path.addStraightSegment(Vector2D(x: x, y: y)) + +proc quadraticCurveTo*(path: Path; cpx, cpy, x, y: float64) = + for v in [cpx, cpy, x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.ensureSubpath(cpx, cpy) + let cp = Vector2D(x: cpx, y: cpy) + let p = Vector2D(x: x, y: y) + path.addQuadraticSegment(cp, p) + +proc bezierCurveTo*(path: Path; cp0x, cp0y, cp1x, cp1y, x, y: float64) = + for v in [cp0x, cp0y, cp1x, cp1y, x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.ensureSubpath(cp0x, cp0y) + let cp0 = Vector2D(x: cp0x, y: cp0y) + let cp1 = Vector2D(x: cp1x, y: cp1y) + let p = Vector2D(x: x, y: y) + path.addBezierSegment(cp0, cp1, p) + +proc arcTo*(path: Path; x1, y1, x2, y2, radius: float64) = + for v in [x1, y1, x2, y2, radius]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.ensureSubpath(x1, y1) + #TODO this should be transformed by the inverse of the transformation matrix + let v0 = path.subpaths[^1].points[^1] + let v1 = Vector2D(x: x1, y: y1) + let v2 = Vector2D(x: x2, y: y2) + if v0.x == x1 and v0.y == y1 or x1 == x2 and y1 == y2 or radius == 0: + path.addStraightSegment(v1) + elif collinear(v0, v1, v2): + path.addStraightSegment(v1) + else: + let pv0 = v0 - v1 + let pv2 = v2 - v1 + let tv0 = v1 + pv0 * radius * 2 / pv0.norm() + let tv2 = v1 + pv2 * radius * 2 / pv2.norm() + let q = -(pv0.x * tv0.x + pv0.y * tv0.y) + let p = -(pv2.x * tv2.x + pv2.y * tv2.y) + let cr = pv0.cross(pv2) + let origin = Vector2D( + x: (pv0.y * p - pv2.y * q) / cr, + y: (pv2.x * q - pv0.x * p) / cr + ) + path.addStraightSegment(tv0) + path.addArcSegment(origin, tv2, radius, true) #TODO always inner? + +func resolveEllipsePoint(o: Vector2D; angle, radiusX, radiusY, + rotation: float64): Vector2D = + # Stolen from SerenityOS + let tanrel = tan(angle) + let tan2 = tanrel * tanrel + let ab = radiusX * radiusY + let a2 = radiusX * radiusX + let b2 = radiusY * radiusY + let sq = sqrt(b2 + a2 * tan2) + let sn = if cos(angle) >= 0: 1f64 else: -1f64 + let relx = ab / sq * sn + let rely = ab * tanrel / sq * sn + return Vector2D(x: relx, y: rely).rotate(rotation) + o + +proc arc*(path: Path; x, y, radius, startAngle, endAngle: float64; + counterclockwise: bool) = + for v in [x, y, radius, startAngle, endAngle]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + let o = Vector2D(x: x, y: y) + var s = resolveEllipsePoint(o, startAngle, radius, radius, 0) + var e = resolveEllipsePoint(o, endAngle, radius, radius, 0) + if counterclockwise: + let tmp = s + e = s + s = tmp + if path.subpaths.len > 0: + path.addStraightSegment(s) + else: + path.moveTo(s) + path.addArcSegment(o, e, radius, abs(startAngle - endAngle) < PI) + +proc ellipse*(path: Path; x, y, radiusX, radiusY, rotation, startAngle, + endAngle: float64; counterclockwise: bool) = + for v in [x, y, radiusX, radiusY, rotation, startAngle, endAngle]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + let o = Vector2D(x: x, y: y) + var s = resolveEllipsePoint(o, startAngle, radiusX, radiusY, rotation) + var e = resolveEllipsePoint(o, endAngle, radiusX, radiusY, rotation) + if counterclockwise: + let tmp = s + s = e + e = tmp + if path.subpaths.len > 0: + path.addStraightSegment(s) + else: + path.moveTo(s) + path.addEllipseSegment(o, e, radiusX, radiusY) + +proc rect*(path: Path; x, y, w, h: float64) = + for v in [x, y, w, h]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.addSubpathAt(Vector2D(x: x, y: y)) + path.addStraightSegment(Vector2D(x: x + w, y: y)) + path.addStraightSegment(Vector2D(x: x + w, y: y + h)) + path.addStraightSegment(Vector2D(x: x, y: y + h)) + path.addStraightSegment(Vector2D(x: x, y: y)) + path.addSubpathAt(Vector2D(x: x, y: y)) + +proc roundRect*(path: Path; x, y, w, h, radii: float64) = + for v in [x, y, w, h]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + #TODO implement + path.rect(x, y, w, h) # :P -- cgit 1.4.1-2-gfad0