Add Image and Paths bones.
This commit is contained in:
parent
f148595203
commit
003e7bfac1
3 changed files with 951 additions and 1 deletions
|
@ -8,5 +8,4 @@ srcDir = "src"
|
|||
requires "nim >= 1.2.6"
|
||||
requires "vmath >= 0.3.2"
|
||||
requires "chroma >= 0.1.5"
|
||||
requires "supersnappy >= 1.0.0"
|
||||
requires "zippy >= 0.3.5"
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import strformat, chroma, vmath
|
||||
|
||||
type
|
||||
Image* = ref object
|
||||
## Main image object that holds the bitmap data in RGBA format.
|
||||
width*, height*: int
|
||||
data*: seq[ColorRGBA]
|
||||
|
||||
Mask* = ref object
|
||||
## Main image object that holds the mask data.
|
||||
width*, height*: int
|
||||
data*: seq[uint8]
|
||||
|
||||
proc newImage*(width, height: int): Image =
|
||||
## Creates a new image with appropriate dimensions.
|
||||
result = Image()
|
||||
result.width = width
|
||||
result.height = height
|
||||
result.data = newSeq[ColorRGBA](width * height)
|
||||
|
||||
proc copy*(image: Image): Image =
|
||||
## Copies an image creating a new image.
|
||||
result = newImage(image.width, image.height)
|
||||
result.data = image.data
|
||||
|
||||
proc `$`*(image: Image): string =
|
||||
## Display the image size and channels.
|
||||
"<Image " & $image.width & "x" & $image.height & ">"
|
||||
|
||||
proc decodeImage(data: seq[uint8]): Image =
|
||||
## Loads an image from a memory.
|
||||
discard
|
||||
|
||||
proc readImage*(filePath: string): Image =
|
||||
## Loads an image from a file.
|
||||
discard
|
||||
|
||||
proc encodeImage(image: Image): seq[uint8] =
|
||||
## Encodes an image into a memory.
|
||||
discard
|
||||
|
||||
proc writeFile*(image: Image, filePath: string): Image =
|
||||
## Writes an image to a file.
|
||||
discard
|
||||
|
||||
proc inside*(image: Image, x, y: int): bool {.inline.} =
|
||||
## Returns true if (x, y) is inside the image.
|
||||
x >= 0 and x < image.width and y >= 0 and y < image.height
|
||||
|
||||
proc getRgbaUnsafe*(image: Image, x, y: int): ColorRGBA {.inline.} =
|
||||
## Gets a color from (x, y) coordinates.
|
||||
## * No bounds checking *
|
||||
## Make sure that x, y are in bounds.
|
||||
## Failure in the assumptions will case unsafe memory reads.
|
||||
result = image.data[image.width * y + x]
|
||||
|
||||
proc `[]`*(image: Image, x, y: int): ColorRGBA {.inline.} =
|
||||
## Gets a pixel at (x, y) or returns transparent black if outside of bounds.
|
||||
if image.inside(x, y):
|
||||
return image.getRgbaUnsafe(x, y)
|
||||
|
||||
proc setRgbaUnsafe*(image: Image, x, y: int, rgba: ColorRGBA) {.inline.} =
|
||||
## Sets a color from (x, y) coordinates.
|
||||
## * No bounds checking *
|
||||
## Make sure that x, y are in bounds.
|
||||
## Failure in the assumptions will case unsafe memory writes.
|
||||
image.data[image.width * y + x] = rgba
|
||||
|
||||
proc `[]=`*(image: Image, x, y: int, rgba: ColorRGBA) {.inline.} =
|
||||
## Sets a pixel at (x, y) or does nothing if outside of bounds.
|
||||
if image.inside(x, y):
|
||||
image.setRgbaUnsafe(x, y, rgba)
|
||||
|
||||
proc fill*(image: Image, rgba: ColorRgba) =
|
||||
## Fills the image with a solid color.
|
||||
for i in 0 ..< image.data.len:
|
||||
image.data[i] = rgba
|
|
@ -0,0 +1,874 @@
|
|||
import vmath, images, chroma, strutils, algorithm
|
||||
|
||||
type
|
||||
Segment* = object
|
||||
## A math segment from point "at" to point "to"
|
||||
at*: Vec2
|
||||
to*: Vec2
|
||||
|
||||
PathCommandKind* = enum
|
||||
## Type of path commands
|
||||
Start, End
|
||||
Move, Line, HLine, VLine, Cubic, SCurve, Quad, TQuad, Arc,
|
||||
RMove, RLine, RHLine, RVLine, RCubic, RSCurve, RQuad, RTQuad, RArc
|
||||
|
||||
PathCommand* = object
|
||||
## Binary version of an SVG command
|
||||
kind*: PathCommandKind
|
||||
numbers*: seq[float32]
|
||||
|
||||
Path* = ref object
|
||||
at*: Vec2
|
||||
commands*: seq[PathCommand]
|
||||
|
||||
proc segment(at, to: Vec2): Segment =
|
||||
result.at = at
|
||||
result.to = to
|
||||
|
||||
proc newPath*(): Path =
|
||||
result = Path()
|
||||
|
||||
proc commandNumbers(kind: PathCommandKind): int =
|
||||
## How many numbers does a command take:
|
||||
case kind:
|
||||
of Start, End: 0
|
||||
of Move, Line, RMove, RLine: 2
|
||||
of HLine, VLine, RHLine, RVLine: 1
|
||||
of Cubic, RCubic: 6
|
||||
of SCurve, RSCurve, Quad, RQuad: 4
|
||||
of TQuad, RTQuad: 2
|
||||
of Arc, RArc: 7
|
||||
|
||||
proc parsePath*(path: string): Path =
|
||||
## Converts a SVG style path into seq of commands.
|
||||
result = newPath()
|
||||
var command = Start
|
||||
var number = ""
|
||||
var numbers = newSeq[float32]()
|
||||
|
||||
template finishDigit() =
|
||||
if number.len > 0:
|
||||
numbers.add(parseFloat(number))
|
||||
number = ""
|
||||
|
||||
template finishCommand() =
|
||||
finishDigit()
|
||||
if command != Start:
|
||||
let num = commandNumbers(command)
|
||||
if num > 0:
|
||||
assert numbers.len mod num == 0
|
||||
for batch in 0 ..< numbers.len div num:
|
||||
result.commands.add PathCommand(
|
||||
kind: command,
|
||||
numbers: numbers[batch*num ..< (batch+1)*num]
|
||||
)
|
||||
numbers.setLen(0)
|
||||
else:
|
||||
assert numbers.len == 0
|
||||
result.commands.add PathCommand(kind: command)
|
||||
|
||||
for c in path:
|
||||
case c:
|
||||
# Relative.
|
||||
of 'm':
|
||||
finishCommand()
|
||||
command = RMove
|
||||
of 'l':
|
||||
finishCommand()
|
||||
command = RLine
|
||||
of 'h':
|
||||
finishCommand()
|
||||
command = RHLine
|
||||
of 'v':
|
||||
finishCommand()
|
||||
command = RVLine
|
||||
of 'c':
|
||||
finishCommand()
|
||||
command = RCubic
|
||||
of 's':
|
||||
finishCommand()
|
||||
command = RSCurve
|
||||
of 'q':
|
||||
finishCommand()
|
||||
command = RQuad
|
||||
of 't':
|
||||
finishCommand()
|
||||
command = RTQuad
|
||||
of 'a':
|
||||
finishCommand()
|
||||
command = RArc
|
||||
of 'z':
|
||||
finishCommand()
|
||||
command = End
|
||||
# Absolute
|
||||
of 'M':
|
||||
finishCommand()
|
||||
command = Move
|
||||
of 'L':
|
||||
finishCommand()
|
||||
command = Line
|
||||
of 'H':
|
||||
finishCommand()
|
||||
command = HLine
|
||||
of 'V':
|
||||
finishCommand()
|
||||
command = VLine
|
||||
of 'C':
|
||||
finishCommand()
|
||||
command = Cubic
|
||||
of 'S':
|
||||
finishCommand()
|
||||
command = SCurve
|
||||
of 'Q':
|
||||
finishCommand()
|
||||
command = Quad
|
||||
of 'T':
|
||||
finishCommand()
|
||||
command = TQuad
|
||||
of 'A':
|
||||
finishCommand()
|
||||
command = Arc
|
||||
of 'Z':
|
||||
finishCommand()
|
||||
command = End
|
||||
# Punctuation
|
||||
of '-', '+':
|
||||
if number.len > 0 and number[^1] in {'e', 'E'}:
|
||||
number &= c
|
||||
else:
|
||||
finishDigit()
|
||||
number = $c
|
||||
of ' ', ',', '\r', '\n', '\t':
|
||||
finishDigit()
|
||||
else:
|
||||
if command == Move and numbers.len == 2:
|
||||
finishCommand()
|
||||
command = Line
|
||||
elif command == Line and numbers.len == 2:
|
||||
finishCommand()
|
||||
command = Line
|
||||
number &= c
|
||||
|
||||
finishCommand()
|
||||
|
||||
proc `$`*(path: Path): string =
|
||||
for command in path.commands:
|
||||
case command.kind:
|
||||
of Move:
|
||||
result.add "M"
|
||||
of Arc:
|
||||
result.add "A"
|
||||
of Line:
|
||||
result.add "L"
|
||||
of End:
|
||||
result.add "Z"
|
||||
else:
|
||||
result.add "?"
|
||||
for number in command.numbers:
|
||||
if floor(number) == number:
|
||||
result.add $(number.int)
|
||||
else:
|
||||
result.add $number
|
||||
result.add " "
|
||||
|
||||
## See https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
|
||||
type ArcParams = object
|
||||
s: float32
|
||||
rx, ry: float32
|
||||
rotation: float32
|
||||
cx, cy: float32
|
||||
theta, delta: float32
|
||||
|
||||
proc svgAngle (ux, uy, vx, vy: float32): float32 =
|
||||
var u = vec2(ux, uy);
|
||||
var v = vec2(vx, vy);
|
||||
# (F.6.5.4)
|
||||
var dot = dot(u,v);
|
||||
var len = length(u) * length(v);
|
||||
var ang = arccos( clamp(dot / len,-1,1) ); # floating point precision, slightly over values appear
|
||||
if (u.x*v.y - u.y*v.x) < 0:
|
||||
ang = -ang
|
||||
return ang;
|
||||
|
||||
proc endpointToCenterArcParams(
|
||||
ax, ay, rx, ry, rotation, large, sweep, bx, by: float32
|
||||
): ArcParams =
|
||||
|
||||
var r = vec2(rx, ry)
|
||||
var p1 = vec2(ax, ay)
|
||||
var p2 = vec2(bx, by)
|
||||
var xAngle = rotation/180*PI
|
||||
var flagA = large == 1.0
|
||||
var flagS = sweep == 1.0
|
||||
var rX = abs(r.x)
|
||||
var rY = abs(r.y)
|
||||
|
||||
# (F.6.5.1)
|
||||
var dx2 = (p1.x - p2.x) / 2.0
|
||||
var dy2 = (p1.y - p2.y) / 2.0
|
||||
var x1p = cos(xAngle)*dx2 + sin(xAngle)*dy2;
|
||||
var y1p = -sin(xAngle)*dx2 + cos(xAngle)*dy2;
|
||||
|
||||
# (F.6.5.2)
|
||||
var rxs = rX * rX;
|
||||
var rys = rY * rY;
|
||||
var x1ps = x1p * x1p;
|
||||
var y1ps = y1p * y1p;
|
||||
# check if the radius is too small `pq < 0`, when `dq > rxs * rys` (see below)
|
||||
# cr is the ratio (dq : rxs * rys)
|
||||
var cr = x1ps/rxs + y1ps/rys;
|
||||
var s = 1.0
|
||||
if cr > 1:
|
||||
# scale up rX,rY equally so cr == 1
|
||||
s = sqrt(cr);
|
||||
rX = s * rX;
|
||||
rY = s * rY;
|
||||
rxs = rX * rX;
|
||||
rys = rY * rY;
|
||||
|
||||
var dq = (rxs * y1ps + rys * x1ps);
|
||||
var pq = (rxs*rys - dq) / dq;
|
||||
var q = sqrt(max(0,pq)); # use Max to account for float precision
|
||||
if flagA == flagS:
|
||||
q = -q;
|
||||
var cxp = q * rX * y1p / rY;
|
||||
var cyp = - q * rY * x1p / rX;
|
||||
|
||||
# (F.6.5.3)
|
||||
var cx = cos(xAngle)*cxp - sin(xAngle)*cyp + (p1.x + p2.x)/2;
|
||||
var cy = sin(xAngle)*cxp + cos(xAngle)*cyp + (p1.y + p2.y)/2;
|
||||
|
||||
# (F.6.5.5)
|
||||
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY );
|
||||
# (F.6.5.6)
|
||||
var delta = svgAngle(
|
||||
(x1p - cxp)/rX, (y1p - cyp)/rY,
|
||||
(-x1p - cxp)/rX, (-y1p-cyp)/rY);
|
||||
delta = delta mod (PI * 2)
|
||||
|
||||
if not flagS:
|
||||
delta -= 2 * PI;
|
||||
|
||||
# normalize the delta
|
||||
while delta > PI*2:
|
||||
delta -= PI*2
|
||||
while delta < -PI*2:
|
||||
delta += PI*2
|
||||
|
||||
r = vec2(rX, rY);
|
||||
|
||||
return ArcParams(
|
||||
s: s, rx: rX, rY: ry, rotation: xAngle, cx: cx, cy: cy,
|
||||
theta: theta, delta: delta
|
||||
)
|
||||
|
||||
proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] =
|
||||
## Converts SVG-like commands to simpler polygon
|
||||
|
||||
var start, at, to, ctr, ctr2: Vec2
|
||||
var prevCommand: PathCommandKind
|
||||
|
||||
var polygon: seq[Vec2]
|
||||
|
||||
proc drawLine(at, to: Vec2) =
|
||||
# Don't add any 0 length lines.
|
||||
if at - to != vec2(0, 0):
|
||||
# Don't double up points.
|
||||
if polygon.len == 0 or polygon[^1] != at:
|
||||
polygon.add(at)
|
||||
polygon.add(to)
|
||||
|
||||
proc getCurvePoint(points: seq[Vec2], t: float32): Vec2 =
|
||||
if points.len == 1:
|
||||
return points[0]
|
||||
else:
|
||||
var newPoints = newSeq[Vec2](points.len - 1)
|
||||
for i in 0 ..< newPoints.len:
|
||||
newPoints[i] = points[i] * (1-t) + points[i + 1] * t
|
||||
return getCurvePoint(newPoints, t)
|
||||
|
||||
proc drawCurve(points: seq[Vec2]) =
|
||||
let n = 10
|
||||
var a = at
|
||||
for t in 1..n:
|
||||
var b = getCurvePoint(points, float32(t) / float32(n))
|
||||
drawLine(a, b)
|
||||
a = b
|
||||
|
||||
proc drawQuad(p0, p1, p2: Vec2) =
|
||||
let devx = p0.x - 2.0 * p1.x + p2.x
|
||||
let devy = p0.y - 2.0 * p1.y + p2.y
|
||||
let devsq = devx * devx + devy * devy
|
||||
if devsq < 0.333:
|
||||
drawLine(p0, p2)
|
||||
return
|
||||
let tol = 3.0
|
||||
let n = 1 + (tol * (devsq)).sqrt().sqrt().floor()
|
||||
var p = p0
|
||||
let nrecip = 1 / n
|
||||
var t = 0.0
|
||||
for i in 0 ..< int(n):
|
||||
t += nrecip
|
||||
let pn = lerp(lerp(p0, p1, t), lerp(p1, p2, t), t)
|
||||
drawLine(p, pn)
|
||||
p = pn
|
||||
|
||||
drawLine(p, p2)
|
||||
|
||||
for command in commands:
|
||||
case command.kind
|
||||
of Move:
|
||||
assert command.numbers.len == 2
|
||||
at.x = command.numbers[0]
|
||||
at.y = command.numbers[1]
|
||||
start = at
|
||||
|
||||
of Line:
|
||||
assert command.numbers.len == 2
|
||||
to.x = command.numbers[0]
|
||||
to.y = command.numbers[1]
|
||||
drawLine(at, to)
|
||||
at = to
|
||||
|
||||
of VLine:
|
||||
assert command.numbers.len == 1
|
||||
to.x = at.x
|
||||
to.y = command.numbers[0]
|
||||
drawLine(at, to)
|
||||
at = to
|
||||
|
||||
of HLine:
|
||||
assert command.numbers.len == 1
|
||||
to.x = command.numbers[0]
|
||||
to.y = at.y
|
||||
drawLine(at, to)
|
||||
at = to
|
||||
|
||||
of Quad:
|
||||
assert command.numbers.len mod 4 == 0
|
||||
var i = 0
|
||||
while i < command.numbers.len:
|
||||
ctr.x = command.numbers[i+0]
|
||||
ctr.y = command.numbers[i+1]
|
||||
to.x = command.numbers[i+2]
|
||||
to.y = command.numbers[i+3]
|
||||
|
||||
drawQuad(at, ctr, to)
|
||||
at = to
|
||||
i += 4
|
||||
|
||||
of TQuad:
|
||||
if prevCommand != Quad and prevCommand != TQuad:
|
||||
ctr = at
|
||||
assert command.numbers.len == 2
|
||||
to.x = command.numbers[0]
|
||||
to.y = command.numbers[1]
|
||||
ctr = at - (ctr - at)
|
||||
drawQuad(at, ctr, to)
|
||||
at = to
|
||||
|
||||
of Cubic:
|
||||
assert command.numbers.len == 6
|
||||
ctr.x = command.numbers[0]
|
||||
ctr.y = command.numbers[1]
|
||||
ctr2.x = command.numbers[2]
|
||||
ctr2.y = command.numbers[3]
|
||||
to.x = command.numbers[4]
|
||||
to.y = command.numbers[5]
|
||||
drawCurve(@[at, ctr, ctr2, to])
|
||||
at = to
|
||||
|
||||
of Arc:
|
||||
var arc = endpointToCenterArcParams(
|
||||
at.x,
|
||||
at.y,
|
||||
command.numbers[0],
|
||||
command.numbers[1],
|
||||
command.numbers[2],
|
||||
command.numbers[3],
|
||||
command.numbers[4],
|
||||
command.numbers[5],
|
||||
command.numbers[6],
|
||||
)
|
||||
let steps = int(abs(arc.delta)/PI*180/5)
|
||||
let step = arc.delta / steps.float32
|
||||
var a = arc.theta
|
||||
var rotMat = rotationMat3(arc.rotation)
|
||||
for i in 0 .. steps:
|
||||
# polygon.add(polygon[^1])
|
||||
polygon.add(rotMat * vec2(
|
||||
cos(a)*arc.rx,
|
||||
sin(a)*arc.ry) + vec2(arc.cx, arc.cy)
|
||||
)
|
||||
a += step
|
||||
at = polygon[^1]
|
||||
|
||||
of End:
|
||||
assert command.numbers.len == 0
|
||||
if at != start:
|
||||
if prevCommand == Quad or prevCommand == TQuad:
|
||||
drawQuad(at, ctr, start)
|
||||
else:
|
||||
drawLine(at, start)
|
||||
if polygon.len > 0:
|
||||
result.add(polygon)
|
||||
polygon = newSeq[Vec2]()
|
||||
at = start
|
||||
|
||||
of RMove:
|
||||
assert command.numbers.len == 2
|
||||
at.x += command.numbers[0]
|
||||
at.y += command.numbers[1]
|
||||
start = at
|
||||
|
||||
of RLine:
|
||||
assert command.numbers.len == 2
|
||||
to.x = at.x + command.numbers[0]
|
||||
to.y = at.y + command.numbers[1]
|
||||
drawLine(at, to)
|
||||
at = to
|
||||
|
||||
of RVLine:
|
||||
assert command.numbers.len == 1
|
||||
to.x = at.x
|
||||
to.y = at.y + command.numbers[0]
|
||||
drawLine(at, to)
|
||||
at = to
|
||||
|
||||
of RHLine:
|
||||
assert command.numbers.len == 1
|
||||
to.x = at.x + command.numbers[0]
|
||||
to.y = at.y
|
||||
drawLine(at, to)
|
||||
at = to
|
||||
|
||||
of RQuad:
|
||||
assert command.numbers.len == 4
|
||||
ctr.x = at.x + command.numbers[0]
|
||||
ctr.y = at.y + command.numbers[1]
|
||||
to.x = at.x + command.numbers[2]
|
||||
to.y = at.y + command.numbers[3]
|
||||
drawQuad(at, ctr, to)
|
||||
at = to
|
||||
|
||||
of RTQuad:
|
||||
if prevCommand != RQuad and prevCommand != RTQuad:
|
||||
ctr = at
|
||||
assert command.numbers.len == 2
|
||||
to.x = at.x + command.numbers[0]
|
||||
to.y = at.y + command.numbers[1]
|
||||
ctr = at - (ctr - at)
|
||||
drawQuad(at, ctr, to)
|
||||
at = to
|
||||
|
||||
of RCubic:
|
||||
assert command.numbers.len == 6
|
||||
ctr.x = at.x + command.numbers[0]
|
||||
ctr.y = at.y + command.numbers[1]
|
||||
ctr2.x = at.x + command.numbers[2]
|
||||
ctr2.y = at.y + command.numbers[3]
|
||||
to.x = at.x + command.numbers[4]
|
||||
to.y = at.y + command.numbers[5]
|
||||
drawCurve(@[at, ctr, ctr2, to])
|
||||
at = to
|
||||
|
||||
of RSCurve:
|
||||
assert command.numbers.len == 4
|
||||
ctr = at
|
||||
ctr2.x = at.x + command.numbers[0]
|
||||
ctr2.y = at.y + command.numbers[1]
|
||||
to.x = at.x + command.numbers[2]
|
||||
to.y = at.y + command.numbers[3]
|
||||
drawCurve(@[at, ctr, ctr2, to])
|
||||
at = to
|
||||
|
||||
else:
|
||||
raise newException(ValueError, "not supported path command " & $command)
|
||||
|
||||
prevCommand = command.kind
|
||||
|
||||
if polygon.len > 0:
|
||||
result.add(polygon)
|
||||
|
||||
iterator zipline*[T](s: seq[T]): (T, T) =
|
||||
## Return elements in pairs: (1st, 2nd), (2nd, 3rd) ... (nth, last).
|
||||
for i in 0 ..< s.len - 1:
|
||||
yield(s[i], s[i + 1])
|
||||
|
||||
iterator zipwise*[T](s: seq[T]): (T, T) =
|
||||
## Return elements in pairs: (1st, 2nd), (2nd, 3rd) ... (last, 1st).
|
||||
for i in 0 ..< s.len - 1:
|
||||
yield(s[i], s[i + 1])
|
||||
if s.len > 0:
|
||||
yield(s[^1], s[0])
|
||||
|
||||
proc intersects*(a, b: Segment, at: var Vec2): bool =
|
||||
## Checks if the a segment intersects b segment.
|
||||
## If it returns true, at will have point of intersection
|
||||
var s1x, s1y, s2x, s2y: float32
|
||||
s1x = a.to.x - a.at.x
|
||||
s1y = a.to.y - a.at.y
|
||||
s2x = b.to.x - b.at.x
|
||||
s2y = b.to.y - b.at.y
|
||||
|
||||
var s, t: float32
|
||||
s = (-s1y * (a.at.x - b.at.x) + s1x * (a.at.y - b.at.y)) /
|
||||
(-s2x * s1y + s1x * s2y)
|
||||
t = (s2x * (a.at.y - b.at.y) - s2y * (a.at.x - b.at.x)) /
|
||||
(-s2x * s1y + s1x * s2y)
|
||||
|
||||
if s >= 0 and s < 1 and t >= 0 and t < 1:
|
||||
at.x = a.at.x + (t * s1x)
|
||||
at.y = a.at.y + (t * s1y)
|
||||
return true
|
||||
return false
|
||||
|
||||
proc strokePolygons*(ps: seq[seq[Vec2]], strokeWidthR, strokeWidthL: float32): seq[seq[Vec2]] =
|
||||
## Converts simple polygons into stroked versions:
|
||||
# TODO: Stroke location, add caps and joins.
|
||||
for p in ps:
|
||||
var poly: seq[Vec2]
|
||||
var back: seq[Vec2] # Back side of poly.
|
||||
var prevRSeg: Segment
|
||||
var prevLSeg: Segment
|
||||
var first = true
|
||||
for (at, to) in p.zipline:
|
||||
#echo at, ":", to
|
||||
let tangent = (at - to).normalize()
|
||||
let normal = vec2(-tangent.y, tangent.x)
|
||||
#print tangent, normal
|
||||
|
||||
var
|
||||
rSeg = segment(at + normal * strokeWidthR, to + normal * strokeWidthR)
|
||||
lSeg = segment(at - normal * strokeWidthL, to - normal * strokeWidthL)
|
||||
|
||||
if first:
|
||||
first = false
|
||||
# TODO: draw start cap
|
||||
else:
|
||||
# as previous lines
|
||||
var touch: Vec2
|
||||
if intersects(prevRSeg, rSeg, touch):
|
||||
rSeg.at = touch
|
||||
poly.setLen(poly.len - 1)
|
||||
else:
|
||||
discard # TODO: draw joint
|
||||
|
||||
if intersects(prevLSeg, lSeg, touch):
|
||||
lSeg.at = touch
|
||||
back.setLen(back.len - 1)
|
||||
else:
|
||||
discard # TODO: draw joint
|
||||
|
||||
poly.add rSeg.at
|
||||
back.add lSeg.at
|
||||
poly.add rSeg.to
|
||||
back.add lSeg.to
|
||||
|
||||
prevRSeg = rSeg
|
||||
prevLSeg = lSeg
|
||||
|
||||
# Add the backside reversed:
|
||||
for i in 1 .. back.len:
|
||||
poly.add back[^i]
|
||||
|
||||
# TODO: draw end cap
|
||||
# Cap it at the end:
|
||||
poly.add poly[0]
|
||||
|
||||
result.add(poly)
|
||||
|
||||
|
||||
{.push checks: off, stacktrace: off.}
|
||||
|
||||
proc fillPolygons*(
|
||||
image: Image,
|
||||
polys: seq[seq[Vec2]],
|
||||
color: ColorRGBA,
|
||||
quality = 4,
|
||||
) =
|
||||
const ep = 0.0001 * PI
|
||||
|
||||
if polys.len == 0:
|
||||
image.fill(rgba(0, 0, 0, 0))
|
||||
|
||||
proc scanLineHits(
|
||||
polys: seq[seq[Vec2]],
|
||||
hits: var seq[(float32, bool)],
|
||||
y: int,
|
||||
shiftY: float32
|
||||
) =
|
||||
hits.setLen(0)
|
||||
var yLine = (float32(y) + ep) + shiftY
|
||||
var scan = Segment(at: vec2(-10000, yLine), to: vec2(100000, yLine))
|
||||
|
||||
for poly in polys:
|
||||
for (at, to) in poly.zipwise:
|
||||
let line = Segment(at: at, to: to)
|
||||
var at: Vec2
|
||||
if line.intersects(scan, at):
|
||||
let winding = line.at.y > line.to.y
|
||||
let x = at.x.clamp(0, image.width.float32)
|
||||
hits.add((x, winding))
|
||||
|
||||
hits.sort(proc(a, b: (float32, bool)): int = cmp(a[0], b[0]))
|
||||
|
||||
var hits: seq[(float32, bool)]
|
||||
|
||||
var alphas = newSeq[float32](image.width)
|
||||
for y in 0 ..< image.height:
|
||||
for x in 0 ..< image.width:
|
||||
alphas[x] = 0
|
||||
for m in 0 ..< quality:
|
||||
polys.scanLineHits(hits, y, float32(m)/float32(quality))
|
||||
if hits.len == 0:
|
||||
continue
|
||||
var
|
||||
penFill = 0.0
|
||||
curHit = 0
|
||||
for x in 0 ..< image.width:
|
||||
var penEdge = penFill
|
||||
while true:
|
||||
if curHit >= hits.len:
|
||||
break
|
||||
if x != hits[curHit][0].int:
|
||||
break
|
||||
let cover = hits[curHit][0] - x.float32
|
||||
let winding = hits[curHit][1]
|
||||
if winding == false:
|
||||
penFill += 1.0
|
||||
penEdge += 1.0 - cover
|
||||
else:
|
||||
penFill -= 1.0
|
||||
penEdge -= 1.0 - cover
|
||||
inc curHit
|
||||
alphas[x] += penEdge
|
||||
for x in 0 ..< image.width:
|
||||
var a = clamp(abs(alphas[x]) / float32(quality), 0.0, 1.0)
|
||||
var colorWithAlpha = color
|
||||
colorWithAlpha.a = uint8(clamp(a, 0, 1) * 255.0)
|
||||
image[x, y] = colorWithAlpha
|
||||
|
||||
{.pop.}
|
||||
|
||||
proc fillPath*(
|
||||
image: Image,
|
||||
path: Path,
|
||||
color: ColorRGBA
|
||||
) =
|
||||
let polys = commandsToPolygons(path.commands)
|
||||
image.fillPolygons(polys, color)
|
||||
|
||||
proc fillPath*(
|
||||
image: Image,
|
||||
path: string,
|
||||
color: ColorRGBA
|
||||
) =
|
||||
image.fillPath(parsePath(path), color)
|
||||
|
||||
proc fillPath*(
|
||||
image: Image,
|
||||
path: string,
|
||||
color: ColorRGBA,
|
||||
pos: Vec2
|
||||
) =
|
||||
var polys = commandsToPolygons(parsePath(path).commands)
|
||||
for poly in polys.mitems:
|
||||
for i, p in poly.mpairs:
|
||||
poly[i] = p + pos
|
||||
image.fillPolygons(polys, color)
|
||||
|
||||
proc fillPath*(
|
||||
image: Image,
|
||||
path: Path,
|
||||
color: ColorRGBA,
|
||||
mat: Mat3
|
||||
) =
|
||||
var polys = commandsToPolygons(path.commands)
|
||||
for poly in polys.mitems:
|
||||
for i, p in poly.mpairs:
|
||||
poly[i] = mat * p
|
||||
image.fillPolygons(polys, color)
|
||||
|
||||
proc fillPath*(
|
||||
image: Image,
|
||||
path: string,
|
||||
color: ColorRGBA,
|
||||
mat: Mat3
|
||||
) =
|
||||
image.fillPath(parsePath(path), color, mat)
|
||||
|
||||
proc strokePath*(
|
||||
image: Image,
|
||||
path: Path,
|
||||
color: ColorRGBA,
|
||||
strokeWidth: float32,
|
||||
# strokeLocation: StrokeLocation,
|
||||
# strokeCap: StorkeCap,
|
||||
# strokeJoin: StorkeJoin
|
||||
) =
|
||||
let polys = commandsToPolygons(path.commands)
|
||||
let (strokeL, strokeR) = (strokeWidth/2, strokeWidth/2)
|
||||
let polys2 = strokePolygons(polys, strokeL, strokeR)
|
||||
image.fillPolygons(polys2, color)
|
||||
|
||||
proc strokePath*(
|
||||
image: Image,
|
||||
path: string,
|
||||
color: ColorRGBA,
|
||||
strokeWidth: float32
|
||||
) =
|
||||
image.strokePath(parsePath(path), color, strokeWidth)
|
||||
|
||||
proc strokePath*(
|
||||
image: Image,
|
||||
path: string,
|
||||
color: ColorRGBA,
|
||||
strokeWidth: float32,
|
||||
pos: Vec2
|
||||
) =
|
||||
var polys = commandsToPolygons(parsePath(path).commands)
|
||||
let (strokeL, strokeR) = (strokeWidth/2, strokeWidth/2)
|
||||
var polys2 = strokePolygons(polys, strokeL, strokeR)
|
||||
for poly in polys2.mitems:
|
||||
for i, p in poly.mpairs:
|
||||
poly[i] = p + pos
|
||||
image.fillPolygons(polys2, color)
|
||||
|
||||
proc strokePath*(
|
||||
image: Image,
|
||||
path: string,
|
||||
color: ColorRGBA,
|
||||
strokeWidth: float32,
|
||||
mat: Mat3
|
||||
) =
|
||||
var polys = commandsToPolygons(parsePath(path).commands)
|
||||
let (strokeL, strokeR) = (strokeWidth/2, strokeWidth/2)
|
||||
var polys2 = strokePolygons(polys, strokeL, strokeR)
|
||||
for poly in polys2.mitems:
|
||||
for i, p in poly.mpairs:
|
||||
poly[i] = mat * p
|
||||
image.fillPolygons(polys2, color)
|
||||
|
||||
proc addPath*(path: Path, other: Path) =
|
||||
## Adds a path to the current path.
|
||||
path.commands &= other.commands
|
||||
|
||||
proc closePath*(path: Path) =
|
||||
## Causes the point of the pen to move back to the start of the current sub-path. It tries to draw a straight line from the current point to the start. If the shape has already been closed or has only one point, this function does nothing.
|
||||
path.commands.add PathCommand(kind: End)
|
||||
|
||||
proc moveTo*(path: Path, x, y: float32) =
|
||||
## Moves the starting point of a new sub-path to the (x, y) coordinates.
|
||||
path.commands.add PathCommand(kind: Move, numbers: @[x, y])
|
||||
path.at = vec2(x, y)
|
||||
|
||||
proc lineTo*(path: Path, x, y: float32) =
|
||||
## Connects the last point in the subpath to the (x, y) coordinates with a straight line.
|
||||
path.commands.add PathCommand(kind: Line, numbers: @[x, y])
|
||||
path.at = vec2(x, y)
|
||||
|
||||
proc bezierCurveTo*(path: Path) =
|
||||
## Adds a cubic Bézier curve to the path. It requires three points. The first two points are control points and the third one is the end point. The starting point is the last point in the current path, which can be changed using moveTo() before creating the Bézier curve.
|
||||
raise newException(ValueError, "not implemented")
|
||||
|
||||
proc quadraticCurveTo*(path: Path) =
|
||||
## Adds a quadratic Bézier curve to the current path.
|
||||
raise newException(ValueError, "not implemented")
|
||||
|
||||
proc arc*(path: Path) =
|
||||
## Adds an arc to the path which is centered at (x, y) position with radius r starting at startAngle and ending at endAngle going in the given direction by anticlockwise (defaulting to clockwise).
|
||||
raise newException(ValueError, "not implemented")
|
||||
|
||||
proc arcTo*(path: Path, x1, y1, x2, y2, r: float32) =
|
||||
## Adds a circular arc to the path with the given control points and radius, connected to the previous point by a straight line.
|
||||
|
||||
var
|
||||
x0 = path.at.x
|
||||
y0 = path.at.y
|
||||
x21 = x2 - x1
|
||||
y21 = y2 - y1
|
||||
x01 = x0 - x1
|
||||
y01 = y0 - y1
|
||||
l01_2 = x01 * x01 + y01 * y01
|
||||
|
||||
const epsilon: float32 = 1e-6
|
||||
|
||||
# if x1 != path.at.x or y1 != path.at.y:
|
||||
# path.commands.add(PathCommand(kind: Line, numbers: @[x1, y1]))
|
||||
# path.at.x = x1
|
||||
# path.at.y = y1
|
||||
var r = r
|
||||
if r < 0:
|
||||
# Is the radius negative? Error.
|
||||
r = -r
|
||||
#raise newException(ValueError, "negative radius: " & $r)
|
||||
|
||||
if path.commands.len == 0:
|
||||
# Is this path empty? Move to (x1,y1).
|
||||
path.commands.add(PathCommand(kind: Move, numbers: @[x1,y1]))
|
||||
|
||||
elif not (l01_2 > epsilon):
|
||||
# Or, is (x1,y1) coincident with (x0,y0)? Do nothing.
|
||||
discard
|
||||
|
||||
elif not (abs(y01 * x21 - y21 * x01) > epsilon) or r == 0:
|
||||
# // Or, are (x0,y0), (x1,y1) and (x2,y2) collinear?
|
||||
# // Equivalently, is (x1,y1) coincident with (x2,y2)?
|
||||
# // Or, is the radius zero? Line to (x1,y1).
|
||||
|
||||
#this._ += "L" + (this._x1 = x1) + "," + (this._y1 = y1);
|
||||
path.commands.add(PathCommand(kind: Line, numbers: @[x1, y1]))
|
||||
path.at.x = x1
|
||||
path.at.y = y1
|
||||
|
||||
else:
|
||||
# Otherwise, draw an arc!
|
||||
var
|
||||
x20 = x2 - x0
|
||||
y20 = y2 - y0
|
||||
l21_2 = x21 * x21 + y21 * y21
|
||||
l20_2 = x20 * x20 + y20 * y20
|
||||
l21 = sqrt(l21_2)
|
||||
l01 = sqrt(l01_2)
|
||||
l:float32 = r * tan((PI - arccos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2)
|
||||
t01 = l / l01
|
||||
t21 = l / l21
|
||||
|
||||
# If the start tangent is not coincident with (x0,y0), line to.
|
||||
if abs(t01 - 1) > epsilon:
|
||||
#this._ += "L" + (x1 + t01 * x01) + "," + (y1 + t01 * y01)
|
||||
path.commands.add(PathCommand(kind: Line, numbers: @[x1 + t01 * x01, y1 + t01 * y01]))
|
||||
discard
|
||||
|
||||
# this._ += "A" + r + "," + r + ",0,0," + (+(y01 * x20 > x01 * y20)) + "," + (this._x1 = x1 + t21 * x21) + "," + (this._y1 = y1 + t21 * y21);
|
||||
|
||||
path.at.x = x1 + t21 * x21
|
||||
path.at.y = y1 + t21 * y21
|
||||
path.commands.add(PathCommand(
|
||||
kind: Arc,
|
||||
numbers: @[
|
||||
r,
|
||||
r,
|
||||
0,
|
||||
0,
|
||||
if y01 * x20 > x01 * y20: 1 else: 0,
|
||||
path.at.x,
|
||||
path.at.y
|
||||
]
|
||||
))
|
||||
|
||||
|
||||
proc ellipse*(path: Path) =
|
||||
## Adds an elliptical arc to the path which is centered at (x, y) position with the radii radiusX and radiusY starting at startAngle and ending at endAngle going in the given direction by anticlockwise (defaulting to clockwise).
|
||||
raise newException(ValueError, "not implemented")
|
||||
|
||||
proc rect*(path: Path, x, y, w, h: float32) =
|
||||
## Creates a path for a rectangle at position (x, y) with a size that is determined by width and height.
|
||||
path.moveTo(x, y)
|
||||
path.lineTo(x+w, y)
|
||||
path.lineTo(x+w, y+h)
|
||||
path.lineTo(x, y+h)
|
||||
path.lineTo(x, y)
|
||||
path.closePath()
|
Loading…
Reference in a new issue