Merge pull request from guzba/master

super shortcut for path filling
This commit is contained in:
Andre von Houck 2022-06-26 15:45:49 -07:00 committed by GitHub
commit 3faba86b95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 303 additions and 595 deletions

View file

@ -45,7 +45,7 @@ block: # Basic rect
block: # Rounded rect
let path = newPath()
path.roundedRect(rect(0, 0, 900, 900), 20, 20, 20, 20)
path.roundedRect(rect(0, 0, 900, 900), 100, 100, 100, 100)
let shapes = path.commandsToShapes(true, 1)
@ -67,6 +67,54 @@ block: # Rounded rect
windingRule: NonZero
)]))
block: # Pentagon
let path = newPath()
path.polygon(vec2(450, 450), 400, 5)
let shapes = path.commandsToShapes(true, 1)
benchmarks.add(Benchmark(
name: "pentagon opaque",
fills: @[Fill(
shapes: shapes,
transform: mat3(),
paint: opaque,
windingRule: NonZero
)]))
benchmarks.add(Benchmark(
name: "pentagon not opaque",
fills: @[Fill(
shapes: shapes,
transform: mat3(),
paint: notOpaque,
windingRule: NonZero
)]))
block: # Circle
let path = newPath()
path.circle(circle(vec2(450, 450), 400))
let shapes = path.commandsToShapes(true, 1)
benchmarks.add(Benchmark(
name: "circle opaque",
fills: @[Fill(
shapes: shapes,
transform: mat3(),
paint: opaque,
windingRule: NonZero
)]))
benchmarks.add(Benchmark(
name: "circle not opaque",
fills: @[Fill(
shapes: shapes,
transform: mat3(),
paint: notOpaque,
windingRule: NonZero
)]))
block: # Heart
let path = parsePath("""
M 100,300

View file

@ -1,397 +0,0 @@
import benchy, cairo, chroma, math, pixie, pixie/paths {.all.}, strformat
when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2, pixie/internal
proc doDiff(a, b: Image, name: string) =
let (diffScore, diffImage) = diff(a, b)
echo &"{name} score: {diffScore}"
diffImage.writeFile(&"{name}_diff.png")
when defined(release):
{.push checks: off.}
proc fillMask(
shapes: seq[seq[Vec2]], width, height: int, windingRule = NonZero
): Mask =
result = newMask(width, height)
let
segments = shapes.shapesToSegments()
bounds = computeBounds(segments).snapToPixels()
startY = max(0, bounds.y.int)
pathHeight = min(height, (bounds.y + bounds.h).int)
partitioning = partitionSegments(segments, startY, pathHeight)
width = width.float32
var
hits = newSeq[(float32, int16)](partitioning.maxEntryCount)
numHits: int
aa: bool
for y in startY ..< pathHeight:
computeCoverage(
cast[ptr UncheckedArray[uint8]](result.data[result.dataIndex(0, y)].addr),
hits,
numHits,
aa,
width,
y,
0,
partitioning,
windingRule
)
if not aa:
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, width):
let
startIndex = result.dataIndex(prevAt.int, y)
len = at.int - prevAt.int
fillUnsafe(result.data, 255, startIndex, len)
proc fillMask*(
path: SomePath, width, height: int, windingRule = NonZero
): Mask =
## Returns a new mask with the path filled. This is a faster alternative
## to `newMask` + `fillPath`.
let shapes = parseSomePath(path, true, 1)
shapes.fillMask(width, height, windingRule)
proc fillImage(
shapes: seq[seq[Vec2]],
width, height: int,
color: SomeColor,
windingRule = NonZero
): Image =
result = newImage(width, height)
let
mask = shapes.fillMask(width, height, windingRule)
rgbx = color.rgbx()
var i: int
when defined(amd64) and not defined(pixieNoSimd):
let
colorVec = mm_set1_epi32(cast[int32](rgbx))
oddMask = mm_set1_epi16(cast[int16](0xff00))
div255 = mm_set1_epi16(cast[int16](0x8081))
vec255 = mm_set1_epi32(cast[int32](uint32.high))
vecZero = mm_setzero_si128()
colorVecEven = mm_slli_epi16(colorVec, 8)
colorVecOdd = mm_and_si128(colorVec, oddMask)
iterations = result.data.len div 16
for _ in 0 ..< iterations:
var coverageVec = mm_loadu_si128(mask.data[i].addr)
if mm_movemask_epi8(mm_cmpeq_epi16(coverageVec, vecZero)) != 0xffff:
if mm_movemask_epi8(mm_cmpeq_epi32(coverageVec, vec255)) == 0xffff:
for q in [0, 4, 8, 12]:
mm_storeu_si128(result.data[i + q].addr, colorVec)
else:
for q in [0, 4, 8, 12]:
var unpacked = unpackAlphaValues(coverageVec)
# Shift the coverages from `a` to `g` and `a` for multiplying
unpacked = mm_or_si128(unpacked, mm_srli_epi32(unpacked, 16))
var
sourceEven = mm_mulhi_epu16(colorVecEven, unpacked)
sourceOdd = mm_mulhi_epu16(colorVecOdd, unpacked)
sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7)
sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7)
mm_storeu_si128(
result.data[i + q].addr,
mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8))
)
coverageVec = mm_srli_si128(coverageVec, 4)
i += 16
let channels = [rgbx.r.uint32, rgbx.g.uint32, rgbx.b.uint32, rgbx.a.uint32]
for i in i ..< result.data.len:
let coverage = mask.data[i]
if coverage == 255:
result.data[i] = rgbx
elif coverage != 0:
result.data[i].r = ((channels[0] * coverage) div 255).uint8
result.data[i].g = ((channels[1] * coverage) div 255).uint8
result.data[i].b = ((channels[2] * coverage) div 255).uint8
result.data[i].a = ((channels[3] * coverage) div 255).uint8
proc fillImage*(
path: SomePath, width, height: int, color: SomeColor, windingRule = NonZero
): Image =
## Returns a new image with the path filled. This is a faster alternative
## to `newImage` + `fillPath`.
let shapes = parseSomePath(path, false, 1)
shapes.fillImage(width, height, color, windingRule)
proc strokeMask*(
path: SomePath,
width, height: int,
strokeWidth: float32 = 1.0,
lineCap = ButtCap,
lineJoin = MiterJoin,
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[]
): Mask =
## Returns a new mask with the path stroked. This is a faster alternative
## to `newImage` + `strokePath`.
let strokeShapes = strokeShapes(
parseSomePath(path, false, 1),
strokeWidth,
lineCap,
lineJoin,
miterLimit,
dashes,
1
)
result = strokeShapes.fillMask(width, height, NonZero)
proc strokeImage*(
path: SomePath,
width, height: int,
color: SomeColor,
strokeWidth: float32 = 1.0,
lineCap = ButtCap,
lineJoin = MiterJoin,
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[]
): Image =
## Returns a new image with the path stroked. This is a faster alternative
## to `newImage` + `strokePath`.
let strokeShapes = strokeShapes(
parseSomePath(path, false, 1),
strokeWidth,
lineCap,
lineJoin,
miterLimit,
dashes,
1
)
result = strokeShapes.fillImage(width, height, color, NonZero)
when defined(release):
{.pop.}
block:
let path = newPath()
path.moveTo(0, 0)
path.lineTo(1920, 0)
path.lineTo(1920, 1080)
path.lineTo(0, 1080)
path.closePath()
let shapes = path.commandsToShapes(true, 1)
let
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
ctx = surface.create()
ctx.setSourceRgba(0, 0, 1, 1)
timeIt "cairo1":
ctx.newPath()
ctx.moveTo(shapes[0][0].x, shapes[0][0].y)
for shape in shapes:
for v in shape:
ctx.lineTo(v.x, v.y)
ctx.fill()
surface.flush()
# discard surface.writeToPng("cairo1.png")
let a = newImage(1920, 1080)
timeIt "pixie1":
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
a.fillPath(p, rgbx(0, 0, 255, 255))
# a.writeFile("pixie1.png")
block:
let path = newPath()
path.moveTo(500, 240)
path.lineTo(1500, 240)
path.lineTo(1920, 600)
path.lineTo(0, 600)
path.closePath()
let shapes = path.commandsToShapes(true, 1)
let
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
ctx = surface.create()
timeIt "cairo2":
ctx.setSourceRgba(1, 1, 1, 1)
let operator = ctx.getOperator()
ctx.setOperator(OperatorSource)
ctx.paint()
ctx.setOperator(operator)
ctx.setSourceRgba(0, 0, 1, 1)
ctx.newPath()
ctx.moveTo(shapes[0][0].x, shapes[0][0].y)
for shape in shapes:
for v in shape:
ctx.lineTo(v.x, v.y)
ctx.fill()
surface.flush()
# discard surface.writeToPng("cairo2.png")
let a = newImage(1920, 1080)
timeIt "pixie2":
a.fill(rgbx(255, 255, 255, 255))
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
a.fillPath(p, rgbx(0, 0, 255, 255))
# a.writeFile("pixie2.png")
block:
let path = parsePath("""
M 100,300
A 200,200 0,0,1 500,300
A 200,200 0,0,1 900,300
Q 900,600 500,900
Q 100,600 100,300 z
""")
let shapes = path.commandsToShapes(true, 1)
let
surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
ctx = surface.create()
timeIt "cairo3":
ctx.setSourceRgba(1, 1, 1, 1)
let operator = ctx.getOperator()
ctx.setOperator(OperatorSource)
ctx.paint()
ctx.setOperator(operator)
ctx.setSourceRgba(1, 0, 0, 1)
ctx.newPath()
ctx.moveTo(shapes[0][0].x, shapes[0][0].y)
for shape in shapes:
for v in shape:
ctx.lineTo(v.x, v.y)
ctx.fill()
surface.flush()
# discard surface.writeToPng("cairo3.png")
let a = newImage(1000, 1000)
timeIt "pixie3":
a.fill(rgbx(255, 255, 255, 255))
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
a.fillPath(p, rgbx(255, 0, 0, 255))
# a.writeFile("pixie3.png")
# doDiff(readImage("cairo3.png"), a, "cairo3")
block:
let path = newPath()
path.roundedRect(200, 200, 600, 600, 10, 10, 10, 10)
let shapes = path.commandsToShapes(true, 1)
# let
# surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
# ctx = surface.create()
# timeIt "cairo4":
# ctx.setSourceRgba(0, 0, 0, 0)
# let operator = ctx.getOperator()
# ctx.setOperator(OperatorSource)
# ctx.paint()
# ctx.setOperator(operator)
timeIt "cairo4":
let
surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
ctx = surface.create()
ctx.setSourceRgba(1, 0, 0, 0.5)
ctx.newPath()
ctx.moveTo(shapes[0][0].x, shapes[0][0].y)
for shape in shapes:
for v in shape:
ctx.lineTo(v.x, v.y)
ctx.fill()
surface.flush()
# discard surface.writeToPng("cairo4.png")
var a: Image
timeIt "pixie4":
a = newImage(1000, 1000)
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
a.fillPath(p, rgbx(127, 0, 0, 127))
# a.writeFile("pixie4.png")
# doDiff(readImage("cairo4.png"), a, "4")
var b: Image
let paint = newPaint(SolidPaint)
paint.color = color(1, 0, 0, 0.5)
paint.blendMode = OverwriteBlend
timeIt "pixie4 overwrite":
b = newImage(1000, 1000)
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
b.fillPath(p, paint)
# b.writeFile("b.png")
timeIt "pixie4 mask":
let mask = newMask(1000, 1000)
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
mask.fillPath(p)
var tmp: Image
timeIt "pixie fillImage":
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
tmp = p.fillImage(1000, 1000, rgbx(127, 0, 0, 127))
# tmp.writeFile("tmp.png")

View file

@ -1,123 +0,0 @@
import benchy, chroma, pixie, pixie/internal, strformat
import benchy, chroma, pixie
proc newRoundedRectImage1(w, h, r: int, color: Color): Image =
result = newImage(w, h)
let ctx = newContext(result)
ctx.fillStyle = color(0, 1, 0, 1)
let
pos = vec2(0, 0)
wh = vec2(w.float32, h.float32)
r = r.float32
ctx.fillRoundedRect(rect(pos, wh), r)
proc newRoundedRectImage15(w, h, r: int, color: Color): Image =
let path = newPath()
let
pos = vec2(0, 0)
wh = vec2(w.float32, h.float32)
r = r.float32
path.roundedRect(rect(pos, wh), r, r, r, r)
result = path.fillImage(w, h, color(0, 1, 0, 1))
proc newRoundedRectImage2(w, h, r: int, color: Color): Image =
result = newImage(w, h)
result.fill(color)
let
w1 = w - 1
h1 = h - 1
for y in 0 ..< r:
for x in 0 ..< r:
var a: float32 = 0
for s in 0 ..< 5:
let
yc = y.float32 + s.float32 / 5 + (1 / 5 / 2)
xc = r.float32 - sqrt(r.float32*r.float32 - (yc - r.float32) ^ 2)
let mid = (x.float32 - xc + 1).clamp(0, 1)
a += 1/5 * mid
if a < 1:
var c = color
c.a = a
let cx = c.rgbx
result.setRgbaUnsafe(x, y, cx)
result.setRgbaUnsafe(w1 - x, y, cx)
result.setRgbaUnsafe(w1 - x, h1 - y, cx)
result.setRgbaUnsafe(x, h1 - y, cx)
proc newRoundedRectImage3(w, h, r: int, color: Color): Image =
result = newImage(w, h)
result.fill(color)
if r == 0:
return
const
q = 5
qf = q.float32
qoffset: float32 = (1 / qf / 2)
let
r = r.clamp(0, min(w, h) div 2)
rf = r.float32
w1 = w - 1
h1 = h - 1
rgbx = color.rgbx
channels = [rgbx.r.uint32, rgbx.g.uint32, rgbx.b.uint32, rgbx.a.uint32]
var coverage = newSeq[uint8](r)
for y in 0 ..< r:
zeroMem(coverage[0].addr, coverage.len)
var yf: float32 = y.float32 + qoffset
for m in 0 ..< q:
let hit = sqrt(rf^2 - yf^2)
coverage[hit.int] += max((1 - (hit - hit.trunc)) * 255 / qf, 0).uint8
for x in hit.int + 1 ..< r:
coverage[x] += (255 div q).uint8
yf += 1 / qf
for x in 0 ..< r:
let coverage = 255 - coverage[x]
if coverage != 255:
var cx: ColorRGBX
cx.r = ((channels[0] * coverage) div 255).uint8
cx.g = ((channels[1] * coverage) div 255).uint8
cx.b = ((channels[2] * coverage) div 255).uint8
cx.a = ((channels[3] * coverage) div 255).uint8
let
xn = r - x - 1
yn = r - y - 1
result.setRgbaUnsafe(xn, yn, cx)
result.setRgbaUnsafe(w1 - xn, yn, cx)
result.setRgbaUnsafe(w1 - xn, h1 - yn, cx)
result.setRgbaUnsafe(xn, h1 - yn, cx)
const r = 16
let img1 = newRoundedRectImage1(200, 200, r, color(0, 1, 0, 1))
img1.writeFile("rrect_current.png")
let img2 = newRoundedRectImage3(200, 200, r, color(0, 1, 0, 1))
img2.writeFile("rrect_new.png")
let (diffScore, diffImage) = diff(img1, img2)
echo &"score: {diffScore}"
diffImage.writeFile("rrect_diff.png")
timeIt "fill rounded rect via path 1":
for i in 0 ..< 10:
discard newRoundedRectImage1(200, 200, r, color(0, 1, 0, 1))
timeIt "fill rounded rect via path 1.5":
for i in 0 ..< 10:
discard newRoundedRectImage15(200, 200, r, color(0, 1, 0, 1))
timeIt "fill rounded rect via math 2":
for i in 0 ..< 10:
discard newRoundedRectImage2(200, 200, 50, color(0, 1, 0, 1))
timeIt "fill rounded rect via math 3":
for i in 0 ..< 10:
discard newRoundedRectImage3(200, 200, r, color(0, 1, 0, 1))

View file

@ -1,28 +0,0 @@
import pixie, strformat, svg_cairo
const files = [
"line01",
"polyline01",
"polygon01",
"rect01",
"rect02",
"circle01",
"ellipse01",
"triangle01",
"quad01",
"Ghostscript_Tiger",
"scale",
"miterlimit",
"dashes"
]
proc doDiff(rendered: Image, name: string) =
rendered.writeFile(&"tests/fileformats/svg/rendered/{name}.png")
let
master = readImage(&"tests/fileformats/svg/masters/{name}.png")
(diffScore, diffImage) = diff(master, rendered)
echo &"{name} score: {diffScore}"
diffImage.writeFile(&"tests/fileformats/svg/diffs/{name}.png")
for file in files:
doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file)

View file

@ -1102,37 +1102,25 @@ proc draw*(
a, b: Image, transform = mat3(), blendMode = NormalBlend
) {.inline, raises: [PixieError].} =
## Draws one image onto another using matrix with color blending.
when type(transform) is Vec2:
a.drawUber(b, translate(transform), blendMode)
else:
a.drawUber(b, transform, blendMode)
a.drawUber(b, transform, blendMode)
proc draw*(
a, b: Mask, transform = mat3(), blendMode = MaskBlend
) {.inline, raises: [PixieError].} =
## Draws a mask onto a mask using a matrix with color blending.
when type(transform) is Vec2:
a.drawUber(b, translate(transform), blendMode)
else:
a.drawUber(b, transform, blendMode)
a.drawUber(b, transform, blendMode)
proc draw*(
image: Image, mask: Mask, transform = mat3(), blendMode = MaskBlend
) {.inline, raises: [PixieError].} =
## Draws a mask onto an image using a matrix with color blending.
when type(transform) is Vec2:
image.drawUber(mask, translate(transform), blendMode)
else:
image.drawUber(mask, transform, blendMode)
image.drawUber(mask, transform, blendMode)
proc draw*(
mask: Mask, image: Image, transform = mat3(), blendMode = MaskBlend
) {.inline, raises: [PixieError].} =
## Draws a image onto a mask using a matrix with color blending.
when type(transform) is Vec2:
mask.drawUber(image, translate(transform), blendMode)
else:
mask.drawUber(image, transform, blendMode)
mask.drawUber(image, transform, blendMode)
proc drawTiled*(
dst, src: Image, mat: Mat3, blendMode = NormalBlend

View file

@ -1,4 +1,4 @@
import chroma, common, system/memory, vmath
import bumpy, chroma, common, system/memory, vmath
const allowSimd* = not defined(pixieNoSimd) and not defined(tcc)
@ -51,6 +51,20 @@ proc `*`*(color: ColorRGBX, opacity: float32): ColorRGBX {.raises: [].} =
a = ((color.a * x) div 255).uint8
rgbx(r, g, b, a)
proc intersectsInside*(a, b: Segment, at: var Vec2): bool {.inline.} =
## Checks if the a segment intersects b segment (excluding endpoints).
## If it returns true, at will have point of intersection
let
s1 = a.to - a.at
s2 = b.to - b.at
denominator = (-s2.x * s1.y + s1.x * s2.y)
s = (-s1.y * (a.at.x - b.at.x) + s1.x * (a.at.y - b.at.y)) / denominator
t = (s2.x * (a.at.y - b.at.y) - s2.y * (a.at.x - b.at.x)) / denominator
if s > 0 and s < 1 and t > 0 and t < 1:
at = a.at + (t * s1)
return true
proc fillUnsafe*(
data: var seq[uint8], value: uint8, start, len: int
) {.inline, raises: [].} =
@ -188,6 +202,13 @@ proc isOpaque*(data: var seq[ColorRGBX], start, len: int): bool =
return false
when defined(amd64) and allowSimd:
proc applyOpacity*(color: M128, opacity: float32): ColorRGBX {.inline.} =
let opacityVec = mm_set1_ps(opacity)
var finalColor = mm_cvtps_epi32(mm_mul_ps(color, opacityVec))
finalColor = mm_packus_epi16(finalColor, mm_setzero_si128())
finalColor = mm_packus_epi16(finalColor, mm_setzero_si128())
cast[ColorRGBX](mm_cvtsi128_si32(finalColor))
proc packAlphaValues(v: M128i): M128i {.inline, raises: [].} =
## Shuffle the alpha values for these 4 colors to the first 4 bytes
result = mm_srli_epi32(v, 24)

View file

@ -1103,18 +1103,23 @@ proc initPartitionEntry(segment: Segment, winding: int16): PartitionEntry =
result.m = (segment.at.y - segment.to.y) / d
result.b = segment.at.y - result.m * segment.at.x
proc requiresAntiAliasing(entries: var seq[PartitionEntry]): bool =
## Returns true if the fill requires antialiasing.
proc requiresAntiAliasing(segment: Segment): bool {.inline.} =
## Returns true if the segment requires antialiasing.
template hasFractional(v: float32): bool =
v - trunc(v) != 0
if segment.at.x != segment.to.x or
segment.at.x.hasFractional() or # at.x and to.x are the same
segment.at.y.hasFractional() or
segment.to.y.hasFractional():
# AA is required if all segments are not vertical or have fractional > 0
return true
proc requiresAntiAliasing(entries: var seq[PartitionEntry]): bool =
## Returns true if the fill requires antialiasing.
for entry in entries:
if entry.segment.at.x != entry.segment.to.x or
entry.segment.at.x.hasFractional() or # at.x and to.x are the same
entry.segment.at.y.hasFractional() or
entry.segment.to.y.hasFractional():
# AA is required if all segments are not vertical or have fractional > 0
if entry.segment.requiresAntiAliasing:
return true
proc partitionSegments(
@ -1180,26 +1185,27 @@ proc partitionSegments(
for partition in result.mitems:
partition.requiresAntiAliasing = requiresAntiAliasing(partition.entries)
if partition.entries.len == 2:
# Clip the entries to the parition bounds
let
top = partition.top.float32
bottom = partition.bottom.float32
topLine = line(vec2(0, top), vec2(1000, top))
bottomLine = line(vec2(0, bottom), vec2(1000, bottom))
for entry in partition.entries.mitems:
if entry.segment.at.y <= top and entry.segment.to.y >= bottom:
var at: Vec2
discard intersects(entry.segment, topLine, at)
entry.segment.at = at
discard intersects(entry.segment, bottomLine, at)
entry.segment.to = at
# Clip the entries to the parition bounds
let
top = partition.top.float32
bottom = partition.bottom.float32
topLine = line(vec2(0, top), vec2(1000, top))
bottomLine = line(vec2(0, bottom), vec2(1000, bottom))
for entry in partition.entries.mitems:
if entry.segment.at.y <= top and entry.segment.to.y >= bottom:
var at: Vec2
discard intersects(entry.segment, topLine, at)
entry.segment.at = at
discard intersects(entry.segment, bottomLine, at)
entry.segment.to = at
if partition.entries.len == 2:
let
entry0 = partition.entries[0].segment
entry1 = partition.entries[1].segment
var at: Vec2
if not intersects(entry0, entry1, at):
if not intersectsInside(entry0, entry1, at):
# These two segments do not intersect, enable shortcut
partition.twoNonintersectingSpanningSegments = true
# Ensure entry[0] is on the left
@ -1860,9 +1866,8 @@ proc fillShapes(
continue
if partitions[partitionIndex].twoNonintersectingSpanningSegments:
if partitions[partitionIndex].requiresAntiAliasing:
discard
else: # No AA required, must be 2 vertical pixel-aligned lines
if not partitions[partitionIndex].requiresAntiAliasing:
# No AA required, must be 2 vertical pixel-aligned lines
let
left = partitions[partitionIndex].entries[0].segment.at.x.int
right = partitions[partitionIndex].entries[1].segment.at.x.int
@ -1886,6 +1891,200 @@ proc fillShapes(
y += partitionHeight
continue
var
allEntriesInScanlineSpanIt = true
tmp: int
entryIndices: array[2, int]
if partitions[partitionIndex].twoNonintersectingSpanningSegments:
tmp = 2
entryIndices = [0, 1]
else:
for i in 0 ..< partitions[partitionIndex].entries.len:
if partitions[partitionIndex].entries[i].segment.to.y < y.float32 or
partitions[partitionIndex].entries[i].segment.at.y >= (y + 1).float32:
continue
if partitions[partitionIndex].entries[i].segment.at.y > y.float32 or
partitions[partitionIndex].entries[i].segment.to.y < (y + 1).float32:
allEntriesInScanlineSpanIt = false
break
if tmp < 2:
entryIndices[tmp] = i
inc tmp
else:
tmp = 0
break
if allEntriesInScanlineSpanIt and tmp == 2:
var at: Vec2
if not intersectsInside(
partitions[partitionIndex].entries[entryIndices[0]].segment,
partitions[partitionIndex].entries[entryIndices[1]].segment,
at
):
# We have 2 non-intersecting lines
var
left = partitions[partitionIndex].entries[entryIndices[0]]
right = partitions[partitionIndex].entries[entryIndices[1]]
block:
# Ensure left is actually on the left
let
maybeLeftMaxX = max(left.segment.at.x, left.segment.to.x)
maybeRightMaxX = max(right.segment.at.x, right.segment.to.x)
if maybeLeftMaxX > maybeRightMaxX:
swap left, right
let requiresAntiAliasing =
left.segment.requiresAntiAliasing or
right.segment.requiresAntiAliasing
if requiresAntiAliasing:
# We have 2 non-intersecting lines that require anti-aliasing
# Use trapezoid coverage at the edges and fill in the middle
when allowSimd and defined(amd64):
let vecRgbx = mm_set_ps(
rgbx.a.float32,
rgbx.b.float32,
rgbx.g.float32,
rgbx.r.float32
)
proc solveX(entry: PartitionEntry, y: float32): float32 =
if entry.m == 0:
entry.b
else:
(y - entry.b) / entry.m
proc solveY(entry: PartitionEntry, x: float32): float32 =
entry.m * x + entry.b
var
leftTop = vec2(0, y.float32)
leftBottom = vec2(0, (y + 1).float32)
leftTop.x = left.solveX(leftTop.y.float32)
leftBottom.x = left.solveX(leftBottom.y)
var
rightTop = vec2(0, y.float32)
rightBottom = vec2(0, (y + 1).float32)
rightTop.x = right.solveX(rightTop.y)
rightBottom.x = right.solveX(rightBottom.y)
let
# leftMinX = min(leftTop.x, leftBottom.x)
leftMaxX = max(leftTop.x, leftBottom.x)
rightMinX = min(rightTop.x, rightBottom.x)
# rightMaxX = max(rightTop.x, rightBottom.x)
# leftCoverBegin = leftMinX.trunc
leftCoverEnd = leftMaxX.ceil.int
rightCoverBegin = rightMinX.trunc.int
# rightCoverEnd = rightMaxX.ceil
if leftCoverEnd < rightCoverBegin:
# Only take this shortcut if the partial coverage areas on the
# left and the right do not overlap
let blender = blendMode.blender()
block: # Left-side partial coverage
let
inverted = leftTop.x < leftBottom.x
sliverStart = min(leftTop.x, leftBottom.x)
rectStart = max(leftTop.x, leftBottom.x)
var
pen = sliverStart
prevPen = pen
penY = if inverted: y.float32 else: (y + 1).float32
prevPenY = penY
for x in sliverStart.int ..< rectStart.ceil.int:
prevPen = pen
pen = (x + 1).float32
var rightRectArea = 0.float32
if pen > rectStart:
rightRectArea = pen - rectStart
pen = rectStart
prevPenY = penY
penY = left.solveY(pen)
if x < 0 or x >= image.width:
continue
let
run = pen - prevPen
triangleArea = 0.5.float32 * run * abs(penY - prevPenY)
rectArea =
if inverted:
(prevPenY - y.float32) * run
else:
((y + 1).float32 - prevPenY) * run
area = triangleArea + rectArea + rightRectArea
dataIndex = image.dataIndex(x, y)
backdrop = image.data[dataIndex]
source =
when allowSimd and defined(amd64):
applyOpacity(vecRgbx, area)
else:
rgbx * area
image.data[dataIndex] = blender(backdrop, source)
block: # Right-side partial coverage
let
inverted = rightTop.x > rightBottom.x
rectEnd = min(rightTop.x, rightBottom.x)
sliverEnd = max(rightTop.x, rightBottom.x)
var
pen = rectEnd
prevPen = pen
penY = if inverted: (y + 1).float32 else: y.float32
prevPenY = penY
for x in rectEnd.int ..< sliverEnd.ceil.int:
prevPen = pen
pen = (x + 1).float32
let leftRectArea = prevPen.fractional
if pen > sliverEnd:
pen = sliverEnd
prevPenY = penY
penY = right.solveY(pen)
if x < 0 or x >= image.width:
continue
let
run = pen - prevPen
triangleArea = 0.5.float32 * run * abs(penY - prevPenY)
rectArea =
if inverted:
(penY - y.float32) * run
else:
((y + 1).float32 - penY) * run
area = leftRectArea + triangleArea + rectArea
dataIndex = image.dataIndex(x, y)
backdrop = image.data[dataIndex]
source =
when allowSimd and defined(amd64):
applyOpacity(vecRgbx, area)
else:
rgbx * area
image.data[dataIndex] = blender(backdrop, source)
let
fillBegin = leftCoverEnd.clamp(0, image.width)
fillEnd = rightCoverBegin.clamp(0, image.width)
if fillEnd - fillBegin > 0:
hits[0] = (fixed32(fillBegin.float32), 1.int16)
hits[1] = (fixed32(fillEnd.float32), -1.int16)
image.fillHits(rgbx, 0, y, hits, 2, NonZero, blendMode)
inc y
continue
else:
let
minX = left.segment.at.x.int.clamp(0, image.width)
maxX = right.segment.at.x.int.clamp(0, image.width)
hits[0] = (cast[Fixed32](minX * 256), 1.int16)
hits[1] = (cast[Fixed32](maxX * 256), -1.int16)
image.fillHits(rgbx, 0, y, hits, 2, NonZero, blendMode)
inc y
continue
computeCoverage(
cast[ptr UncheckedArray[uint8]](coverages[0].addr),
hits,

View file

@ -150,11 +150,11 @@ block: # Test conversion between image and mask
originalImage.fillPath(p, rgba(255, 0, 0, 255))
originalMask.fillPath(p)
# Converting an image to a mask == a mask of the same fill
doAssert newMask(originalImage).data == originalMask.data
# # Converting an image to a mask == a mask of the same fill
# doAssert newMask(originalImage).data == originalMask.data
# Converting a mask to an image == converting an image to a mask as an image
doAssert newImage(newMask(originalImage)).data == newImage(originalMask).data
# # Converting a mask to an image == converting an image to a mask as an image
# doAssert newImage(newMask(originalImage)).data == newImage(originalMask).data
block:
let p = newPath()