From 1af7f5ca7c9ae03adcff19297ae484ad180f48d4 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Tue, 26 Jan 2021 18:20:57 -0600 Subject: [PATCH 01/10] simpler --- src/pixie/paths.nim | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index f02290c..130eebb 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -744,18 +744,17 @@ proc quickSort(a: var seq[(float32, bool)], inl, inr: int) = quickSort(a, inl, r) quickSort(a, l, inr) -proc computeBounds(shapes: seq[seq[(Segment, bool)]]): Rect = +proc computeBounds(segments: seq[(Segment, bool)]): Rect = var xMin = float32.high xMax = float32.low yMin = float32.high yMax = float32.low - for shape in shapes: - 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)) + for (segment, _) in segments: + 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) @@ -773,23 +772,23 @@ proc fillShapes( color: ColorRGBA, windingRule: WindingRule ) = - var sortedShapes = newSeq[seq[(Segment, bool)]](shapes.len) - for i, sorted in sortedShapes.mpairs: - for segment in shapes[i].segments: + var sortedSegments: seq[(Segment, bool)] + for shape in shapes: + for segment in shape.segments: if segment.at.y == segment.to.y: # Skip horizontal continue let winding = segment.at.y > segment.to.y if winding: var segment = segment swap(segment.at, segment.to) - sorted.add((segment, winding)) + sortedSegments.add((segment, winding)) else: - sorted.add((segment, winding)) + sortedSegments.add((segment, winding)) # Figure out the total bounds of all the shapes, # rasterize only within the total bounds let - bounds = computeBounds(sortedShapes) + bounds = computeBounds(sortedSegments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) stopY = min(image.height, (bounds.y + bounds.h).int) @@ -816,16 +815,15 @@ proc fillShapes( 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: - for (segment, winding) in shape: - if segment.at.y > yLine or segment.to.y < y.float32: - continue - 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 + for (segment, winding) in sortedSegments: + if segment.at.y > yLine or segment.to.y < y.float32: + continue + 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) From 7389c9257b744afcb0592d9bd370ebe5165d7669 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Tue, 26 Jan 2021 18:21:14 -0600 Subject: [PATCH 02/10] better name --- src/pixie/images.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 6435fe2..a727b6f 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -466,7 +466,7 @@ proc drawCorrect*(a, b: Image, mat: Mat3, blendMode: BlendMode) = proc drawUber( a, b: Image, p, dx, dy: Vec2, - segments: array[0..3, Segment], + perimeter: array[0..3, Segment], blendMode: BlendMode, smooth: bool ) = @@ -480,7 +480,7 @@ proc drawUber( a: vec2(-1000, y.float32 + yOffset), b: vec2(1000, y.float32 + yOffset) ) - for segment in segments: + for segment in perimeter: var at: Vec2 if scanline.intersects(segment, at) and segment.to != at: xMin = min(xMin, at.x.floor.int) @@ -520,7 +520,7 @@ proc draw*(a, b: Image, mat: Mat3, blendMode: BlendMode) = mat * vec2(b.width.float32, b.height.float32), mat * vec2(0, b.height.float32) ] - segments = [ + perimeter = [ segment(corners[0], corners[1]), segment(corners[1], corners[2]), segment(corners[2], corners[3]), @@ -551,7 +551,7 @@ proc draw*(a, b: Image, mat: Mat3, blendMode: BlendMode) = mat[2, 1].fractional == 0.0 ) - a.drawUber(b, p, dx, dy, segments, blendMode, smooth) + a.drawUber(b, p, dx, dy, perimeter, blendMode, smooth) proc draw*(a, b: Image, pos = vec2(0, 0), blendMode = bmNormal) {.inline.} = a.draw(b, translate(pos), blendMode) From c8b78800a688fe951988710d782211757e7243eb Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Tue, 26 Jan 2021 19:54:49 -0600 Subject: [PATCH 03/10] f --- src/pixie/paths.nim | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 130eebb..7c83217 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -878,10 +878,9 @@ proc fillShapes( let coverageMask1 = cast[M128i]([0xffffffff, 0, 0, 0]) # First 32 bits - coverageMask3 = mm_set1_epi32(cast[int32](0x000000ff)) # Only `r` + coverageMask2 = mm_set1_epi32(cast[int32](0x000000ff)) # Only `r` oddMask = mm_set1_epi16(cast[int16](0xff00)) div255 = mm_set1_epi16(cast[int16](0x8081)) - zero = mm_set1_epi32(0) v255 = mm_set1_epi32(255) vColor = mm_set1_epi32(cast[int32](color)) @@ -889,9 +888,11 @@ proc fillShapes( var coverage = mm_loadu_si128(coverages[x].addr) coverage = mm_and_si128(coverage, coverageMask1) - if mm_movemask_epi8(mm_cmpeq_epi16(coverage, zero)) != 0xffff: + let eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128()) + if mm_movemask_epi8(eqZero) != 0xffff: # If the coverages are not all zero var source = vColor + coverage = mm_slli_si128(coverage, 2) coverage = mm_shuffle_epi32(coverage, MM_SHUFFLE(1, 1, 0, 0)) @@ -908,7 +909,7 @@ proc fillShapes( coverage = mm_and_si128( mm_or_si128(mm_or_si128(a, b), mm_or_si128(c, d)), - coverageMask3 + coverageMask2 ) if mm_movemask_epi8(mm_cmpeq_epi32(coverage, v255)) != 0xffff: From 8bf68ec5cb4174a087d3835c71a0d355d8e09305 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Wed, 27 Jan 2021 02:07:21 -0600 Subject: [PATCH 04/10] nimsimd dep --- pixie.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixie.nimble b/pixie.nimble index 6d31984..fcb7418 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -10,5 +10,5 @@ requires "vmath >= 0.4.0" requires "chroma >= 0.2.1" requires "zippy >= 0.3.5" requires "flatty >= 0.1.3" -requires "nimsimd >= 0.4.8" +requires "nimsimd >= 1.0.0" requires "bumpy >= 1.0.1" From 4fad39c5bdeb27a5d15236abf6f7ce7286cdcce9 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Wed, 27 Jan 2021 02:45:07 -0600 Subject: [PATCH 05/10] rm forward declare --- src/pixie/images.nim | 107 +++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 55 deletions(-) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index a727b6f..8d9b400 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -227,9 +227,6 @@ proc toStraightAlpha*(image: Image) = when defined(release): {.pop.} -proc draw*(a, b: Image, mat: Mat3, blendMode = bmNormal) -proc draw*(a, b: Image, pos = vec2(0, 0), blendMode = bmNormal) {.inline.} - proc invert*(image: Image) = ## Inverts all of the colors and alpha. var i: int @@ -266,16 +263,6 @@ proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA {.inline.} = finalMix.toStraightAlpha() -proc resize*(srcImage: Image, width, height: int): Image = - result = newImage(width, height) - result.draw( - srcImage, - scale(vec2( - (width + 1).float / srcImage.width.float, - (height + 1).float / srcImage.height.float - )) - ) - proc blur*(image: Image, radius: float32) = ## Applies Gaussian blur to the image given a radius. let radius = round(radius).int @@ -374,47 +361,6 @@ proc blurAlpha*(image: Image, radius: float32) = alpha += c2.a.float32 * a image.setRgbaUnsafe(x, y, rgba(0, 0, 0, alpha.uint8)) -proc shift*(image: Image, offset: Vec2) = - ## Shifts the image by offset. - if offset != vec2(0, 0): - let copy = image.copy() # Copy to read from. - image.fill(rgba(0, 0, 0, 0)) # Reset this for being drawn to. - image.draw(copy, offset) # Draw copy into image. - -proc spread*(image: Image, spread: float32) = - ## Grows the image as a mask by spread. - let - copy = image.copy() - spread = round(spread).int - assert spread > 0 - for y in 0 ..< image.height: - for x in 0 ..< image.width: - var maxAlpha = 0.uint8 - block blurBox: - for bx in -spread .. spread: - for by in -spread .. spread: - let alpha = copy[x + bx, y + by].a - if alpha > maxAlpha: - maxAlpha = alpha - if maxAlpha == 255: - break blurBox - image[x, y] = rgba(0, 0, 0, maxAlpha) - -proc shadow*( - mask: Image, offset: Vec2, spread, blur: float32, color: ColorRGBA -): Image = - ## Create a shadow of the image with the offset, spread and blur. - var shadow = mask - if offset != vec2(0, 0): - shadow.shift(offset) - if spread > 0: - shadow.spread(spread) - if blur > 0: - shadow.blurAlpha(blur) - result = newImage(mask.width, mask.height) - result.fill(color) - result.draw(shadow, blendMode = bmMask) - proc applyOpacity*(image: Image, opacity: float32) = ## Multiplies alpha of the image by opacity. let op = (255 * opacity).uint32 @@ -510,7 +456,7 @@ proc drawUber( if a.width - xMax > 0: zeroMem(a.data[a.dataIndex(xMax, y)].addr, 4 * (a.width - xMax)) -proc draw*(a, b: Image, mat: Mat3, blendMode: BlendMode) = +proc draw*(a, b: Image, mat: Mat3, blendMode = bmNormal) = ## Draws one image onto another using matrix with color blending. let @@ -555,3 +501,54 @@ proc draw*(a, b: Image, mat: Mat3, blendMode: BlendMode) = proc draw*(a, b: Image, pos = vec2(0, 0), blendMode = bmNormal) {.inline.} = a.draw(b, translate(pos), blendMode) + +proc resize*(srcImage: Image, width, height: int): Image = + result = newImage(width, height) + result.draw( + srcImage, + scale(vec2( + (width + 1).float / srcImage.width.float, + (height + 1).float / srcImage.height.float + )) + ) + +proc shift*(image: Image, offset: Vec2) = + ## Shifts the image by offset. + if offset != vec2(0, 0): + let copy = image.copy() # Copy to read from. + image.fill(rgba(0, 0, 0, 0)) # Reset this for being drawn to. + image.draw(copy, offset) # Draw copy into image. + +proc spread*(image: Image, spread: float32) = + ## Grows the image as a mask by spread. + let + copy = image.copy() + spread = round(spread).int + assert spread > 0 + for y in 0 ..< image.height: + for x in 0 ..< image.width: + var maxAlpha = 0.uint8 + block blurBox: + for bx in -spread .. spread: + for by in -spread .. spread: + let alpha = copy[x + bx, y + by].a + if alpha > maxAlpha: + maxAlpha = alpha + if maxAlpha == 255: + break blurBox + image[x, y] = rgba(0, 0, 0, maxAlpha) + +proc shadow*( + mask: Image, offset: Vec2, spread, blur: float32, color: ColorRGBA +): Image = + ## Create a shadow of the image with the offset, spread and blur. + var shadow = mask + if offset != vec2(0, 0): + shadow.shift(offset) + if spread > 0: + shadow.spread(spread) + if blur > 0: + shadow.blurAlpha(blur) + result = newImage(mask.width, mask.height) + result.fill(color) + result.draw(shadow, blendMode = bmMask) From e85ab1f3fe01cf2470d3dabd1759d776c6f126c1 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Wed, 27 Jan 2021 02:46:00 -0600 Subject: [PATCH 06/10] f --- src/pixie.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pixie.nim b/src/pixie.nim index dd6a0c2..cbd19eb 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -44,7 +44,7 @@ proc writeFile*(image: Image, filePath: string) = let fileFormat = case splitFile(filePath).ext: of ".png": ffPng of ".bmp": ffBmp - of ".jpg",".jpeg": ffJpg + of ".jpg", ".jpeg": ffJpg else: raise newException(PixieError, "Unsupported image file extension") image.writeFile(filePath, fileformat) From 8668f355f101f1ef0646f25a80123832dbdb4593 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Wed, 27 Jan 2021 03:07:17 -0600 Subject: [PATCH 07/10] little speedups --- src/pixie/images.nim | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 8d9b400..6f399f0 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -364,20 +364,17 @@ proc blurAlpha*(image: Image, radius: float32) = proc applyOpacity*(image: Image, opacity: float32) = ## Multiplies alpha of the image by opacity. let op = (255 * opacity).uint32 - for i in 0 ..< image.data.len: - var rgba = image.data[i] + for rgba in image.data.mitems: rgba.a = ((rgba.a.uint32 * op) div 255).clamp(0, 255).uint8 - image.data[i] = rgba proc sharpOpacity*(image: Image) = ## Sharpens the opacity to extreme. ## A = 0 stays 0. Anything else turns into 255. - for i in 0 ..< image.data.len: - var rgba = image.data[i] + for rgba in image.data.mitems: if rgba.a == 0: - image.data[i] = rgba(0, 0, 0, 0) + rgba = rgba(0, 0, 0, 0) else: - image.data[i] = rgba(255, 255, 255, 255) + rgba = rgba(255, 255, 255, 255) proc drawCorrect*(a, b: Image, mat: Mat3, blendMode: BlendMode) = ## Draws one image onto another using matrix with color blending. @@ -509,7 +506,8 @@ proc resize*(srcImage: Image, width, height: int): Image = scale(vec2( (width + 1).float / srcImage.width.float, (height + 1).float / srcImage.height.float - )) + )), + bmOverwrite ) proc shift*(image: Image, offset: Vec2) = @@ -517,7 +515,7 @@ proc shift*(image: Image, offset: Vec2) = if offset != vec2(0, 0): let copy = image.copy() # Copy to read from. image.fill(rgba(0, 0, 0, 0)) # Reset this for being drawn to. - image.draw(copy, offset) # Draw copy into image. + image.draw(copy, offset, bmOverwrite) # Draw copy into image. proc spread*(image: Image, spread: float32) = ## Grows the image as a mask by spread. @@ -536,19 +534,18 @@ proc spread*(image: Image, spread: float32) = maxAlpha = alpha if maxAlpha == 255: break blurBox - image[x, y] = rgba(0, 0, 0, maxAlpha) + image.setRgbaUnsafe(x, y, rgba(0, 0, 0, maxAlpha)) proc shadow*( mask: Image, offset: Vec2, spread, blur: float32, color: ColorRGBA ): Image = ## Create a shadow of the image with the offset, spread and blur. - var shadow = mask if offset != vec2(0, 0): - shadow.shift(offset) + mask.shift(offset) if spread > 0: - shadow.spread(spread) + mask.spread(spread) if blur > 0: - shadow.blurAlpha(blur) + mask.blurAlpha(blur) result = newImage(mask.width, mask.height) result.fill(color) - result.draw(shadow, blendMode = bmMask) + result.draw(mask, blendMode = bmMask) From c4abba5d8a41f173ccab6d9d34c3c89d7d6a12e0 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Wed, 27 Jan 2021 03:11:37 -0600 Subject: [PATCH 08/10] - assert, + exception --- src/pixie/images.nim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 6f399f0..0172a8d 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -519,10 +519,14 @@ proc shift*(image: Image, offset: Vec2) = proc spread*(image: Image, spread: float32) = ## Grows the image as a mask by spread. + if spread == 0: + return + if spread < 0: + raise newException(PixieError, "Cannot apply negative spread") + let copy = image.copy() spread = round(spread).int - assert spread > 0 for y in 0 ..< image.height: for x in 0 ..< image.width: var maxAlpha = 0.uint8 From eb360887ab90c4ec6b852e9606eeb3eb3ba81ed5 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Wed, 27 Jan 2021 03:15:43 -0600 Subject: [PATCH 09/10] repeat into proc --- src/pixie/images.nim | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 0172a8d..d99d2cf 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -263,24 +263,27 @@ proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA {.inline.} = finalMix.toStraightAlpha() +proc gaussianLookup(radius: int): seq[float32] = + ## Compute lookup table for 1d Gaussian kernel. + result.setLen(radius * 2 + 1) + var total = 0.0 + for xb in -radius .. radius: + let + s = radius.float32 / 2.2 # 2.2 matches Figma. + x = xb.float32 + a = 1 / sqrt(2 * PI * s^2) * exp(-1 * x^2 / (2 * s^2)) + result[xb + radius] = a + total += a + for xb in -radius .. radius: + result[xb + radius] = result[xb + radius] / total + proc blur*(image: Image, radius: float32) = ## Applies Gaussian blur to the image given a radius. let radius = round(radius).int if radius == 0: return - # Compute lookup table for 1d Gaussian kernel. - var - lookup = newSeq[float](radius * 2 + 1) - total = 0.0 - for xb in -radius .. radius: - let s = radius.float32 / 2.2 # 2.2 matches Figma. - let x = xb.float32 - let a = 1 / sqrt(2 * PI * s^2) * exp(-1 * x^2 / (2 * s^2)) - lookup[xb + radius] = a - total += a - for xb in -radius .. radius: - lookup[xb + radius] /= total + let lookup = gaussianLookup(radius) # Blur in the X direction. var blurX = newImage(image.width, image.height) @@ -327,18 +330,7 @@ proc blurAlpha*(image: Image, radius: float32) = if radius == 0: return - # Compute lookup table for 1d Gaussian kernel. - var - lookup = newSeq[float](radius * 2 + 1) - total = 0.0 - for xb in -radius .. radius: - let s = radius.float32 / 2.2 # 2.2 matches Figma. - let x = xb.float32 - let a = 1 / sqrt(2 * PI * s^2) * exp(-1 * x^2 / (2 * s^2)) - lookup[xb + radius] = a - total += a - for xb in -radius .. radius: - lookup[xb + radius] /= total + let lookup = gaussianLookup(radius) # Blur in the X direction. var blurX = newImage(image.width, image.height) From bb7ec44fcebeb92a65770b94c91383232a0c132e Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Wed, 27 Jan 2021 03:47:03 -0600 Subject: [PATCH 10/10] store winding as int directly --- src/pixie/paths.nim | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 7c83217..e6b4bfd 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -724,7 +724,7 @@ iterator segments*(s: seq[Vec2]): Segment = for i in 0 ..< s.len - 1: yield(segment(s[i], s[i + 1])) -proc quickSort(a: var seq[(float32, bool)], inl, inr: int) = +proc quickSort(a: var seq[(float32, int16)], inl, inr: int) = var r = inr l = inl @@ -744,7 +744,7 @@ proc quickSort(a: var seq[(float32, bool)], inl, inr: int) = quickSort(a, inl, r) quickSort(a, l, inr) -proc computeBounds(segments: seq[(Segment, bool)]): Rect = +proc computeBounds(segments: seq[(Segment, int16)]): Rect = var xMin = float32.high xMax = float32.low @@ -772,18 +772,17 @@ proc fillShapes( color: ColorRGBA, windingRule: WindingRule ) = - var sortedSegments: seq[(Segment, bool)] + var sortedSegments: seq[(Segment, int16)] for shape in shapes: for segment in shape.segments: if segment.at.y == segment.to.y: # Skip horizontal continue - let winding = segment.at.y > segment.to.y - if winding: + if segment.at.y > segment.to.y: var segment = segment swap(segment.at, segment.to) - sortedSegments.add((segment, winding)) + sortedSegments.add((segment, -1.int16)) else: - sortedSegments.add((segment, winding)) + sortedSegments.add((segment, 1.int16)) # Figure out the total bounds of all the shapes, # rasterize only within the total bounds @@ -801,7 +800,7 @@ proc fillShapes( initialOffset = offset / 2 var - hits = newSeq[(float32, bool)](4) + hits = newSeq[(float32, int16)](4) coverages = newSeq[uint8](image.width) numHits: int @@ -868,7 +867,7 @@ proc fillShapes( for j in i ..< fillStart + fillLen: coverages[j] += sampleCoverage - count += (if winding: -1 else: 1) + count += winding x = at # Apply the coverage and blend