diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 1b2919f..60977a2 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -976,12 +976,18 @@ proc shadow*( image: Image, offset: Vec2, spread, blur: float32, color: SomeColor ): Image {.raises: [PixieError].} = ## Create a shadow of the image with the offset, spread and blur. - let - mask = image.newMask() + let mask = image.newMask() + + var shifted: Mask + if offset == vec2(0, 0): + shifted = mask + else: shifted = newMask(mask.width, mask.height) - shifted.draw(mask, translate(offset), bmOverwrite) + shifted.draw(mask, translate(offset), bmOverwrite) + shifted.spread(spread) shifted.blur(blur) + result = newImage(shifted.width, shifted.height) result.fill(color) result.draw(shifted, blendMode = bmMask) diff --git a/src/pixie/paints.nim b/src/pixie/paints.nim index 9b6ee64..9b9bbaa 100644 --- a/src/pixie/paints.nim +++ b/src/pixie/paints.nim @@ -1,5 +1,8 @@ import blends, chroma, common, images, vmath +when defined(amd64) and not defined(pixieNoSimd): + import nimsimd/sse2 + type PaintKind* = enum pkSolid @@ -45,9 +48,7 @@ proc newPaint*(paint: Paint): Paint {.raises: [].} = result.gradientHandlePositions = paint.gradientHandlePositions result.gradientStops = paint.gradientStops -converter parseSomePaint*( - paint: SomePaint -): Paint {.inline.} = +converter parseSomePaint*(paint: SomePaint): Paint {.inline.} = ## Given SomePaint, parse it in different ways. when type(paint) is string: result = newPaint(pkSolid) @@ -67,19 +68,10 @@ converter parseSomePaint*( proc colorStop*(color: Color, position: float32): ColorStop = ColorStop(color: color, position: position) -proc toLineSpace(at, to, point: Vec2): float32 {.inline.} = - ## Convert position on to where it would fall on a line between at and to. - let - d = to - at - det = d.x * d.x + d.y * d.y - (d.y * (point.y - at.y) + d.x * (point.x - at.x)) / det - -proc gradientPut( - image: Image, paint: Paint, x, y: int, t: float32, stops: seq[ColorStop] -) = - ## Put an gradient color based on `t` - where are we related to a line. +proc gradientColor(paint: Paint, t: float32): ColorRGBX = + ## Get the gradient color based on `t` - where are we related to a line. var index = -1 - for i, stop in stops: + for i, stop in paint.gradientStops: if stop.position < t: index = i if stop.position > t: @@ -87,21 +79,21 @@ proc gradientPut( var color: Color if index == -1: # first stop solid - color = stops[0].color - elif index + 1 >= stops.len: + color = paint.gradientStops[0].color + elif index + 1 >= paint.gradientStops.len: # last stop solid - color = stops[index].color + color = paint.gradientStops[index].color else: let - gs1 = stops[index] - gs2 = stops[index + 1] + gs1 = paint.gradientStops[index] + gs2 = paint.gradientStops[index + 1] color = mix( gs1.color, gs2.color, (t - gs1.position) / (gs2.position - gs1.position) ) color.a *= paint.opacity - image.setRgbaUnsafe(x, y, color.rgbx()) + color.rgbx() proc fillGradientLinear(image: Image, paint: Paint) = ## Fills a linear gradient. @@ -115,15 +107,66 @@ proc fillGradientLinear(image: Image, paint: Paint) = if paint.opacity == 0: return + proc toLineSpace(at, to, point: Vec2): float32 {.inline.} = + ## Convert position on to where it would fall on a line between at and to. + let + d = to - at + det = d.x * d.x + d.y * d.y + (d.y * (point.y - at.y) + d.x * (point.x - at.x)) / det + let at = paint.gradientHandlePositions[0] to = paint.gradientHandlePositions[1] - for y in 0 ..< image.height: - for x in 0 ..< image.width: + + if at.y == to.y: # Horizontal gradient + var x: int + while x < image.width: + when defined(amd64) and not defined(pixieNoSimd): + if x + 4 <= image.width: + var colors: array[4, ColorRGBX] + for i in 0 ..< 4: + let + xy = vec2((x + i).float32, 0.float32) + t = toLineSpace(at, to, xy) + rgbx = paint.gradientColor(t) + colors[i] = rgbx + + let colorVec = cast[M128i](colors) + for y in 0 ..< image.height: + mm_storeu_si128(image.data[image.dataIndex(x, y)].addr, colorVec) + x += 4 + continue + let - xy = vec2(x.float32, y.float32) + xy = vec2(x.float32, 0.float32) t = toLineSpace(at, to, xy) - image.gradientPut(paint, x, y, t, paint.gradientStops) + rgbx = paint.gradientColor(t) + for y in 0 ..< image.height: + image.setRgbaUnsafe(x, y, rgbx) + inc x + + elif at.x == to.x: # Vertical gradient + for y in 0 ..< image.height: + let + xy = vec2(0.float32, y.float32) + t = toLineSpace(at, to, xy) + rgbx = paint.gradientColor(t) + var x: int + when defined(amd64) and not defined(pixieNoSimd): + let colorVec = mm_set1_epi32(cast[int32](rgbx)) + for _ in 0 ..< image.width div 4: + mm_storeu_si128(image.data[image.dataIndex(x, y)].addr, colorVec) + x += 4 + for x in x ..< image.width: + image.setRgbaUnsafe(x, y, rgbx) + + else: + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let + xy = vec2(x.float32, y.float32) + t = toLineSpace(at, to, xy) + image.setRgbaUnsafe(x, y, paint.gradientColor(t)) proc fillGradientRadial(image: Image, paint: Paint) = ## Fills a radial gradient. @@ -154,7 +197,7 @@ proc fillGradientRadial(image: Image, paint: Paint) = let xy = vec2(x.float32, y.float32) t = (mat * xy).length() - image.gradientPut(paint, x, y, t, paint.gradientStops) + image.setRgbaUnsafe(x, y, paint.gradientColor(t)) proc fillGradientAngular(image: Image, paint: Paint) = ## Fills an angular gradient. @@ -171,6 +214,7 @@ proc fillGradientAngular(image: Image, paint: Paint) = let center = paint.gradientHandlePositions[0] edge = paint.gradientHandlePositions[1] + f32PI = PI.float32 # TODO: make edge between start and end anti-aliased. let gradientAngle = normalize(edge - center).angle().fixAngle() for y in 0 ..< image.height: @@ -178,8 +222,8 @@ proc fillGradientAngular(image: Image, paint: Paint) = let xy = vec2(x.float32, y.float32) angle = normalize(xy - center).angle() - t = (angle + gradientAngle + PI / 2).fixAngle() / 2 / PI + 0.5 - image.gradientPut(paint, x, y, t, paint.gradientStops) + t = (angle + gradientAngle + f32PI / 2).fixAngle() / 2 / f32PI + 0.5.float32 + image.setRgbaUnsafe(x, y, paint.gradientColor(t)) proc fillGradient*(image: Image, paint: Paint) {.raises: [PixieError].} = ## Fills with the Paint gradient. diff --git a/tests/benchmark_images_draw.nim b/tests/benchmark_images_draw.nim index 34930be..3083c05 100644 --- a/tests/benchmark_images_draw.nim +++ b/tests/benchmark_images_draw.nim @@ -115,7 +115,7 @@ block: a = newImage(100, 100) b = newImage(50, 50) - timeIt "shadow": + timeIt "shadow (no offset)": b.fill(rgba(0, 0, 0, 255)) a.draw(b, translate(vec2(25, 25))) @@ -126,3 +126,20 @@ block: color = rgba(0, 0, 0, 255) ) keep(shadow) + +block: + let + a = newImage(100, 100) + b = newImage(50, 50) + + timeIt "shadow (with offset)": + b.fill(rgba(0, 0, 0, 255)) + a.draw(b, translate(vec2(25, 25))) + + let shadow = a.shadow( + offset = vec2(10, 10), + spread = 10, + blur = 10, + color = rgba(0, 0, 0, 255) + ) + keep(shadow) diff --git a/tests/benchmark_paints.nim b/tests/benchmark_paints.nim new file mode 100644 index 0000000..24e7d93 --- /dev/null +++ b/tests/benchmark_paints.nim @@ -0,0 +1,43 @@ +import benchy, pixie + +let image = newImage(1000, 1000) + +timeIt "GradientLinear vertical": + let paint = newPaint(pkGradientLinear) + paint.gradientHandlePositions = @[ + vec2(50, 0), + vec2(50, 1000), + ] + paint.gradientStops = @[ + ColorStop(color: color(1, 0, 0, 1), position: 0), + ColorStop(color: color(1, 0, 0, 0.15625), position: 1.0), + ] + image.fillGradient(paint) + +timeIt "GradientLinear horizontal": + let paint = newPaint(pkGradientLinear) + paint.gradientHandlePositions = @[ + vec2(0, 50), + vec2(1000, 50), + ] + paint.gradientStops = @[ + ColorStop(color: color(1, 0, 0, 1), position: 0), + ColorStop(color: color(1, 0, 0, 0.15625), position: 1.0), + ] + image.fillGradient(paint) + +# timeIt "GradientLinear radial": +# discard + +timeIt "GradientLinear angular": + let paint = newPaint(pkGradientAngular) + paint.gradientHandlePositions = @[ + vec2(500, 500), + vec2(1000, 500), + vec2(500, 1000) + ] + paint.gradientStops = @[ + ColorStop(color: color(1, 0, 0, 1), position: 0), + ColorStop(color: color(1, 0, 0, 0.15625), position: 1.0), + ] + image.fillGradient(paint) diff --git a/tests/paths/gradientLinear2.png b/tests/paths/gradientLinear2.png new file mode 100644 index 0000000..7acc3f8 Binary files /dev/null and b/tests/paths/gradientLinear2.png differ diff --git a/tests/test_paints.nim b/tests/test_paints.nim index 8761dbf..f206a08 100644 --- a/tests/test_paints.nim +++ b/tests/test_paints.nim @@ -69,6 +69,21 @@ block: image.fillPath(heartShape, paint) image.writeFile("tests/paths/gradientLinear.png") +block: + let paint = newPaint(pkGradientLinear) + paint.gradientHandlePositions = @[ + vec2(50, 0), + vec2(50, 100), + ] + paint.gradientStops = @[ + ColorStop(color: color(1, 0, 0, 1), position: 0), + ColorStop(color: color(1, 0, 0, 0.15625), position: 1.0), + ] + + let image = newImage(100, 100) + image.fillPath(heartShape, paint) + image.writeFile("tests/paths/gradientLinear2.png") + block: let paint = newPaint(pkGradientRadial) paint.gradientHandlePositions = @[