Merge pull request #74 from guzba/master

0.0.19 new scanline+fill
This commit is contained in:
treeform 2021-01-24 21:37:05 -08:00 committed by GitHub
commit 4a2dbb5c49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 280 additions and 193 deletions

View file

@ -1,4 +1,4 @@
version = "0.0.18"
version = "0.0.19"
author = "Andre von Houck and Ryan Oldenburg"
description = "Full-featured 2d graphics library for Nim."
license = "MIT"

View file

@ -335,6 +335,16 @@ proc blendNormal*(backdrop, source: ColorRGBA): ColorRGBA =
result = source
result = alphaFix(backdrop, source, result)
when defined(amd64) and not defined(pixieNoSimd):
proc blendNormalSimd*(backdrop, source: M128i): M128i =
let
backdrops = cast[array[4, ColorRGBA]](backdrop)
sources = cast[array[4, ColorRGBA]](source)
var blended: array[4, ColorRGBA]
for i in 0 ..< 4:
blended[i] = blendNormal(backdrops[i], sources[i])
cast[M128i](blended)
proc blendDarken(backdrop, source: ColorRGBA): ColorRGBA =
result.r = min(backdrop.r, source.r)
result.g = min(backdrop.g, source.g)

View file

@ -24,7 +24,7 @@ proc toPremultipliedAlpha*(c: ColorRGBA): ColorRGBA {.inline.} =
result.a = c.a
proc toStraightAlpha*(c: ColorRGBA): ColorRGBA {.inline.} =
## Converts a color to from premultiplied alpha to straight alpha.
## Converts a color from premultiplied alpha to straight alpha.
result = c
if result.a != 0 and result.a != 255:
let multiplier = ((255 / c.a.float32) * 255).uint32

View file

@ -1,4 +1,7 @@
import vmath, images, chroma, strutils, algorithm, common, bumpy, blends
import vmath, images, chroma, strutils, common, bumpy, blends, common
when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
type
WindingRule* = enum
@ -28,11 +31,10 @@ when defined(release):
proc parameterCount(kind: PathCommandKind): int =
case kind:
of Close: 0
of Move, Line, RMove, RLine: 2
of Move, Line, RMove, RLine, TQuad, RTQuad: 2
of HLine, VLine, RHLine, RVLine: 1
of Cubic, RCubic: 6
of SCubic, RSCubic, Quad, RQuad: 4
of TQuad, RTQuad: 2
of Arc, RArc: 7
proc parsePath*(path: string): Path =
@ -368,7 +370,7 @@ proc polygon*(path: var Path, x, y, size: float32, sides: int) =
)
proc commandsToShapes*(path: Path): seq[seq[Segment]] =
## Converts SVG-like commands to simpler polygon
## Converts SVG-like commands to line segments.
var
start, at: Vec2
@ -709,6 +711,264 @@ proc commandsToShapes*(path: Path): seq[seq[Segment]] =
if shape.len > 0:
result.add(shape)
proc quickSort(a: var seq[(float32, bool)], inl, inr: int) =
var
r = inr
l = inl
let n = r - l + 1
if n < 2:
return
let p = a[l + 3 * n div 4][0]
while l <= r:
if a[l][0] < p:
inc l
elif a[r][0] > p:
dec r
else:
swap(a[l], a[r])
inc l
dec r
quickSort(a, inl, r)
quickSort(a, l, inr)
proc computeBounds(shape: seq[Segment]): Rect =
var
xMin = float32.high
xMax = float32.low
yMin = float32.high
yMax = float32.low
for segment in shape:
xMin = min(xMin, min(segment.at.x, segment.to.x))
xMax = max(xMax, max(segment.at.x, segment.to.x))
yMin = min(yMin, min(segment.at.y, segment.to.y))
yMax = max(yMax, max(segment.at.y, segment.to.y))
xMin = floor(xMin)
xMax = ceil(xMax)
yMin = floor(yMin)
yMax = ceil(yMax)
result.x = xMin
result.y = yMin
result.w = xMax - xMin
result.h = yMax - yMin
proc fillShapes(
image: Image,
shapes: seq[seq[Segment]],
color: ColorRGBA,
windingRule: WindingRule
) =
var sortedShapes = newSeq[seq[(Segment, bool)]](shapes.len)
for i, sorted in sortedShapes.mpairs:
for j, segment in shapes[i]:
if segment.at.y == segment.to.y or segment.at - segment.to == Vec2():
# Skip horizontal and zero-length
continue
var
segment = segment
winding = segment.at.y > segment.to.y
if winding:
swap(segment.at, segment.to)
sorted.add((segment, winding))
# Compute the bounds of each shape
var bounds = newSeq[Rect](shapes.len)
for i, shape in shapes:
bounds[i] = computeBounds(shape)
# Figure out the total bounds of all the shapes
var
minX = float32.high
minY = float32.high
maxY = float32.low
for bounds in bounds:
minX = min(minX, bounds.x)
minY = min(minY, bounds.y)
maxY = max(maxY, bounds.y + bounds.h)
# Rasterize only within the total bounds
let
startX = max(0, minX.int)
startY = max(0, miny.int)
stopY = min(image.height, maxY.int)
const
quality = 5 # Must divide 255 cleanly
sampleCoverage = 255.uint8 div quality
ep = 0.0001 * PI
offset = 1 / quality.float32
initialOffset = offset / 2
var
hits = newSeq[(float32, bool)](4)
coverages = newSeq[uint32](image.width)
numHits: int
for y in startY ..< stopY:
# Reset buffer for this row
zeroMem(coverages[0].addr, coverages.len * 4)
# Do scanlines for this row
for m in 0 ..< quality:
let
yLine = y.float32 + initialOffset + offset * m.float32 + ep
scanline = Line(a: vec2(0, yLine), b: vec2(1000, yLine))
numHits = 0
for i, shape in sortedShapes:
let bounds = bounds[i]
if bounds.y > y.float32 or bounds.y + bounds.h < y.float32:
continue
for (segment, winding) in shape:
var at: Vec2
if scanline.intersects(segment, at):# and segment.to != at:
if numHits == hits.len:
hits.setLen(hits.len * 2)
hits[numHits] = (at.x.clamp(0, image.width.float32), winding)
inc numHits
quickSort(hits, 0, numHits - 1)
proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} =
case windingRule:
of wrNonZero:
count != 0
of wrEvenOdd:
count mod 2 != 0
var
x: float32
count: int
for i in 0 ..< numHits:
let (at, winding) = hits[i]
var
fillStart = x.int
leftCover = if at.int - x.int > 0: ceil(x) - x else: at - x
if leftCover != 0:
inc fillStart
if shouldFill(windingRule, count):
coverages[x.int] += (leftCover * sampleCoverage.float32).uint8
if at.int - x.int > 0:
let rightCover = at - trunc(at)
if rightCover > 0 and shouldFill(windingRule, count):
coverages[at.int] += (rightCover * sampleCoverage.float32).uint8
let fillLen = at.int - fillStart
if fillLen > 0 and shouldFill(windingRule, count):
var i = fillStart
when defined(amd64) and not defined(pixieNoSimd):
let m = mm_set1_epi32(sampleCoverage.int32)
for j in countup(i, fillStart + fillLen - 4, 4):
let current = mm_loadu_si128(coverages[j].addr)
mm_storeu_si128(coverages[j].addr, mm_add_epi32(m, current))
i += 4
for j in i ..< fillStart + fillLen:
coverages[j] += sampleCoverage
count += (if winding: -1 else: 1)
x = at
# Apply the coverage and blend
var x = startX
when defined(amd64) and not defined(pixieNoSimd):
# When supported, SIMD blend as much as possible
let
alphaMask = mm_set1_epi32(cast[int32](0xff000000))
colorMask = mm_set1_epi32(cast[int32](0x00ffffff))
div255 = mm_set1_epi16(cast[int16](0x8081))
zero = mm_set1_epi32(0)
v255 = mm_set1_epi32(255)
for _ in countup(x, coverages.len - 4, 4):
var coverage = mm_loadu_si128(coverages[x].addr)
if mm_movemask_epi8(mm_cmpeq_epi32(coverage, zero)) != 0xffff:
# If the coverages are not all zero
var source = mm_set1_epi32(cast[int32](color))
if mm_movemask_epi8(mm_cmpeq_epi32(coverage, v255)) != 0xffff:
# If the coverages are not all 255
# Shift the coverages to `a` for multiplying
coverage = mm_slli_epi32(coverage, 24)
var alpha = mm_and_si128(source, alphaMask)
alpha = mm_mulhi_epu16(alpha, coverage)
alpha = mm_srli_epi16(mm_mulhi_epu16(alpha, div255), 7)
alpha = mm_slli_epi32(alpha, 8)
source = mm_or_si128(mm_and_si128(source, colorMask), alpha)
let
index = image.dataIndex(x, y)
backdrop = mm_loadu_si128(image.data[index].addr)
mm_storeu_si128(image.data[index].addr, blendNormalSimd(backdrop, source))
x += 4
break
while x < image.width:
if x + 2 <= coverages.len:
let peeked = cast[ptr uint64](coverages[x].addr)[]
if peeked == 0:
x += 2
continue
let coverage = coverages[x]
if coverage != 0:
var source = color
if coverage != 255:
source.a = ((color.a.uint16 * coverage) div 255).uint8
let backdrop = image.getRgbaUnsafe(x, y)
image.setRgbaUnsafe(x, y, blendNormal(backdrop, source))
inc x
proc parseSomePath(path: SomePath): seq[seq[Segment]] =
when type(path) is string:
parsePath(path).commandsToShapes()
elif type(path) is Path:
path.commandsToShapes()
elif type(path) is seq[seq[Segment]]:
path
proc fillPath*(
image: Image,
path: SomePath,
color: ColorRGBA,
windingRule = wrNonZero
) {.inline.} =
image.fillShapes(parseSomePath(path), color, windingRule)
proc fillPath*(
image: Image,
path: SomePath,
color: ColorRGBA,
pos: Vec2,
windingRule = wrNonZero
) =
var shapes = parseSomePath(path)
for shape in shapes.mitems:
for segment in shape.mitems:
segment += pos
image.fillShapes(shapes, color, windingRule)
proc fillPath*(
image: Image,
path: SomePath,
color: ColorRGBA,
mat: Mat3,
windingRule = wrNonZero
) =
var shapes = parseSomePath(path)
for shape in shapes.mitems:
for segment in shape.mitems:
segment = mat * segment
image.fillShapes(shapes, color, windingRule)
proc strokeShapes(
shapes: seq[seq[Segment]],
color: ColorRGBA,
@ -756,197 +1016,14 @@ proc strokeShapes(
if strokeShape.len > 0:
result.add(strokeShape)
proc computeBounds(shape: seq[Segment]): Rect =
var
xMin = float32.high
xMax = float32.low
yMin = float32.high
yMax = float32.low
for segment in shape:
xMin = min(xMin, min(segment.at.x, segment.to.x))
xMax = max(xMax, max(segment.at.x, segment.to.x))
yMin = min(yMin, min(segment.at.y, segment.to.y))
yMax = max(yMax, max(segment.at.y, segment.to.y))
xMin = floor(xMin)
xMax = ceil(xMax)
yMin = floor(yMin)
yMax = ceil(yMax)
result.x = xMin
result.y = yMin
result.w = xMax - xMin
result.h = yMax - yMin
proc fillShapes*(
image: Image,
shapes: seq[seq[Segment]],
color: ColorRGBA,
windingRule: WindingRule
) =
var sortedShapes = newSeq[seq[(Segment, bool)]](shapes.len)
for i, sorted in sortedShapes.mpairs:
for j, segment in shapes[i]:
if segment.at.y == segment.to.y or segment.at - segment.to == Vec2():
# Skip horizontal and zero-length
continue
var
segment = segment
winding = segment.at.y > segment.to.y
if winding:
swap(segment.at, segment.to)
sorted.add((segment, winding))
# Compute the bounds of each shape
var bounds = newSeq[Rect](shapes.len)
for i, shape in shapes:
bounds[i] = computeBounds(shape)
# Figure out the total bounds of all the shapes
var
minX = float32.high
minY = float32.high
maxY = float32.low
for bounds in bounds:
minX = min(minX, bounds.x)
minY = min(minY, bounds.y)
maxY = max(maxY, bounds.y + bounds.h)
# Rasterize only within the total bounds
let
startX = max(0, minX.int)
startY = max(0, miny.int)
stopY = min(image.height, maxY.int)
const
quality = 4
ep = 0.0001 * PI
proc scanLineHits(
shapes: seq[seq[(Segment, bool)]],
bounds: seq[Rect],
hits: var seq[(float32, bool)],
size: Vec2,
y: int,
shiftY: float32
) {.inline.} =
hits.setLen(0)
let
yLine = y.float32 + ep + shiftY
scanline = Line(a: vec2(-10000, yLine), b: vec2(10000, yLine))
for i, shape in shapes:
let bounds = bounds[i]
if bounds.y > y.float32 or bounds.y + bounds.h < y.float32:
continue
for (segment, winding) in shape:
# Lines often connect and we need them to not share starts and ends
var at: Vec2
if scanline.intersects(segment, at) and segment.to != at:
hits.add((at.x.clamp(0, size.x), winding))
hits.sort(proc(a, b: (float32, bool)): int = cmp(a[0], b[0]))
var
hits = newSeq[(float32, bool)]()
alphas = newSeq[float32](image.width)
for y in startY ..< stopY:
# Reset alphas for this row.
zeroMem(alphas[0].addr, alphas.len * 4)
# Do scan lines for this row.
for m in 0 ..< quality:
sortedShapes.scanLineHits(bounds, hits, image.wh, y, float32(m) / float32(quality))
if hits.len == 0:
continue
var
penFill = 0
curHit = 0
for x in startX ..< image.width:
var penEdge: float32
case windingRule
of wrNonZero:
penEdge = penFill.float32
of wrEvenOdd:
if penFill mod 2 == 0:
penEdge = 0.0
else:
penEdge = 1.0
while true:
if curHit >= hits.len or x != hits[curHit][0].int:
break
let
cover = hits[curHit][0] - x.float32
winding = hits[curHit][1]
if winding == false:
penFill += 1
penEdge += 1.0 - cover
else:
penFill -= 1
penEdge -= 1.0 - cover
inc curHit
alphas[x] += penEdge
for x in 0 ..< image.width:
let a = clamp(abs(alphas[x]) / float32(quality), 0.0, 1.0)
if a > 0:
var colorWithAlpha = color
colorWithAlpha.a = uint8(a * 255.0)
let rgba = image.getRgbaUnsafe(x, y)
image.setRgbaUnsafe(x, y, blendNormal(rgba, colorWithAlpha))
proc parseSomePath(path: SomePath): seq[seq[Segment]] =
when type(path) is string:
parsePath(path).commandsToShapes()
elif type(path) is Path:
path.commandsToShapes()
elif type(path) is seq[seq[Segment]]:
path
proc fillPath*(
image: Image,
path: SomePath,
color: ColorRGBA,
windingRule = wrNonZero
) {.inline.} =
image.fillShapes(parseSomePath(path), color, windingRule)
proc fillPath*(
image: Image,
path: SomePath,
color: ColorRGBA,
pos: Vec2,
windingRule = wrNonZero
) =
var shapes = parseSomePath(path)
for shape in shapes.mitems:
for segment in shape.mitems:
segment += pos
image.fillShapes(shapes, color, windingRule)
proc fillPath*(
image: Image,
path: SomePath,
color: ColorRGBA,
mat: Mat3,
windingRule = wrNonZero
) =
var shapes = parseSomePath(path)
for shape in shapes.mitems:
for segment in shape.mitems:
segment = mat * segment
image.fillShapes(shapes, color, windingRule)
proc strokePath*(
image: Image,
path: SomePath,
color: ColorRGBA,
strokeWidth: float32,
strokeWidth = 1.0,
windingRule = wrNonZero
) =
var strokeShapes = strokeShapes(
let strokeShapes = strokeShapes(
parseSomePath(path),
color,
strokeWidth,
@ -958,7 +1035,7 @@ proc strokePath*(
image: Image,
path: SomePath,
color: ColorRGBA,
strokeWidth: float32,
strokeWidth = 1.0,
pos: Vec2,
windingRule = wrNonZero
) =
@ -977,7 +1054,7 @@ proc strokePath*(
image: Image,
path: SomePath,
color: ColorRGBA,
strokeWidth: float32,
strokeWidth = 1.0,
mat: Mat3,
windingRule = wrNonZero
) =

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 B

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB