diff --git a/examples/paint.nim b/examples/paint.nim new file mode 100644 index 0000000..6e7386e --- /dev/null +++ b/examples/paint.nim @@ -0,0 +1,30 @@ +import pixie + +let + image = newImage(200, 200) + +image.fill(rgba(255, 255, 255, 255)) +image.fillPath( + """ + M 20 60 + A 40 40 90 0 1 100 60 + A 40 40 90 0 1 180 60 + Q 180 120 100 180 + Q 20 120 20 60 + z + """, + Paint( + kind:pkGradientRadial, + gradientHandlePositions: @[ + vec2(100, 100), + vec2(200, 100), + vec2(100, 200) + ], + gradientStops: @[ + ColorStop(color:rgba(255, 0, 0, 255).color, position: 0), + ColorStop(color:rgba(255, 0, 0, 40).color, position: 1.0), + ] + ) +) + +image.writeFile("examples/paint.png") diff --git a/examples/paint.png b/examples/paint.png new file mode 100644 index 0000000..5489b30 Binary files /dev/null and b/examples/paint.png differ diff --git a/src/pixie.nim b/src/pixie.nim index f01bbc1..3b98c4b 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -1,9 +1,9 @@ import bumpy, chroma, flatty/binny, os, pixie/blends, pixie/common, pixie/fileformats/bmp, pixie/fileformats/jpg, pixie/fileformats/png, - pixie/fileformats/svg, pixie/gradients, pixie/images, pixie/masks, + pixie/fileformats/svg, pixie/paints, pixie/images, pixie/masks, pixie/paths, vmath -export blends, bumpy, chroma, common, gradients, images, masks, paths, vmath +export blends, bumpy, chroma, common, paints, images, masks, paths, vmath type FileFormat* = enum diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 5c27922..3281def 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -529,6 +529,25 @@ proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA = lerp(bottomMix, topMix, diffY) +proc getRgbaSmoothWrapped*(image: Image, x, y: float32): ColorRGBA = + let + minX = floor(x) + minY = floor(y) + diffX = x - minX + diffY = y - minY + x = minX.int + y = minY.int + + x0y0 = image[(x + 0) mod image.width, (y + 0) mod image.height] + x1y0 = image[(x + 1) mod image.width, (y + 0) mod image.height] + x0y1 = image[(x + 0) mod image.width, (y + 1) mod image.height] + x1y1 = image[(x + 1) mod image.width, (y + 1) mod image.height] + + bottomMix = lerp(x0y0, x1y0, diffX) + topMix = lerp(x0y1, x1y1, diffX) + + lerp(bottomMix, topMix, diffY) + proc drawCorrect(a, b: Image | Mask, mat = mat3(), blendMode = bmNormal) = ## Draws one image onto another using matrix with color blending. diff --git a/src/pixie/gradients.nim b/src/pixie/paints.nim similarity index 64% rename from src/pixie/gradients.nim rename to src/pixie/paints.nim index 95b23f5..01742c0 100644 --- a/src/pixie/gradients.nim +++ b/src/pixie/paints.nim @@ -1,9 +1,67 @@ -import chroma, common, images, vmath +import chroma, common, images, vmath, blends -type ColorStop* = object - ## Represents color on a gradient curve. - color*: Color - position*: float32 +type + PaintKind* = enum + pkSolid + pkImage + pkImageTiled + pkGradientLinear + pkGradientRadial + pkGradientAngular + + Paint* = ref object + kind*: PaintKind + color*: ColorRGBA + image*: Image + imageMat*: Mat3 + gradientHandlePositions*: seq[Vec2] + gradientStops*: seq[ColorStop] + blendMode*: BlendMode + + ColorStop* = object + ## Represents color on a gradient curve. + color*: Color + position*: float32 + +proc fillImage*( + dest: Image, + src: Image, + mat: Mat3 +) = + dest.draw( + src, + mat + ) + +proc fillImageTiled*( + dest: Image, + src: Image, + mat: Mat3 +) = + var + matInv = mat.inverse() + src = src + + block: # Shrink by 2 as needed + const h = 0.5.float32 + var + p = matInv * vec2(0 + h, 0 + h) + dx = matInv * vec2(1 + h, 0 + h) - p + dy = matInv * vec2(0 + h, 1 + h) - p + minFilterBy2 = max(dx.length, dy.length) + + while minFilterBy2 > 2: + src = src.minifyBy2() + dx /= 2 + dy /= 2 + minFilterBy2 /= 2 + matInv = matInv * scale(vec2(0.5, 0.5)) + + for y in 0 ..< dest.height: + for x in 0 ..< dest.width: + var srcPos = matInv * vec2(x.float32, y.float32) + let rgba = src.getRgbaSmoothWrapped(srcPos.x, srcPos.y) + dest.setRgbaUnsafe(x,y, rgba) proc toLineSpace(at, to, point: Vec2): float32 = ## Convert position on to where it would fall on a line between at and to. @@ -88,17 +146,3 @@ proc fillAngularGradient*( angle = normalize(xy - center).angle() a = (angle + gradientAngle + PI/2).fixAngle() / 2 / PI + 0.5 image.gradientPut(x, y, a, stops) - -proc fillDiamondGradient*( - image: Image, - center, edge, skew: Vec2, - stops: seq[ColorStop] -) = - # TODO: implement GRADIENT_DIAMOND, now will just do GRADIENT_RADIAL - let - distance = dist(center, edge) - for y in 0 ..< image.height: - for x in 0 ..< image.width: - let xy = vec2(x.float32, y.float32) - let a = (center - xy).length() / distance - image.gradientPut(x, y, a, stops) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index eaec192..94bc37a 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1,4 +1,4 @@ -import blends, bumpy, chroma, common, images, masks, strutils, vmath +import blends, bumpy, chroma, common, images, masks, strutils, vmath, paints when defined(amd64) and not defined(pixieNoSimd): import nimsimd/sse2 @@ -1424,6 +1424,53 @@ proc fillPath*( segment = transform * segment mask.fillShapes(shapes, color, windingRule) +proc fillPath*( + image: Image, + path: SomePath, + paint: Paint, + windingRule = wrNonZero, +) = + var mask = newMask(image.width, image.height) + var fill = newImage(image.width, image.height) + mask.fillPath(parseSomePath(path), windingRule) + + case paint.kind: + of pkSolid: + fill.fill(paint.color.toPremultipliedAlpha()) + of pkImage: + fill.fillImage( + paint.image, + paint.imageMat + ) + of pkImageTiled: + fill.fillImageTiled( + paint.image, + paint.imageMat + ) + of pkGradientLinear: + fill.fillLinearGradient( + paint.gradientHandlePositions[0], + paint.gradientHandlePositions[1], + paint.gradientStops + ) + of pkGradientRadial: + fill.fillRadialGradient( + paint.gradientHandlePositions[0], + paint.gradientHandlePositions[1], + paint.gradientHandlePositions[2], + paint.gradientStops + ) + of pkGradientAngular: + fill.fillAngularGradient( + paint.gradientHandlePositions[0], + paint.gradientHandlePositions[1], + paint.gradientHandlePositions[2], + paint.gradientStops + ) + + fill.draw(mask, blendMode = bmMask) + image.draw(fill, blendMode = paint.blendMode) + proc strokePath*( image: Image, path: SomePath, diff --git a/tests/images/paths/gradientAngular.png b/tests/images/paths/gradientAngular.png new file mode 100644 index 0000000..3af2207 Binary files /dev/null and b/tests/images/paths/gradientAngular.png differ diff --git a/tests/images/paths/gradientLinear.png b/tests/images/paths/gradientLinear.png new file mode 100644 index 0000000..a97db19 Binary files /dev/null and b/tests/images/paths/gradientLinear.png differ diff --git a/tests/images/paths/gradientRadial.png b/tests/images/paths/gradientRadial.png new file mode 100644 index 0000000..6ecfbb8 Binary files /dev/null and b/tests/images/paths/gradientRadial.png differ diff --git a/tests/images/paths/paintImage.png b/tests/images/paths/paintImage.png new file mode 100644 index 0000000..4e71264 Binary files /dev/null and b/tests/images/paths/paintImage.png differ diff --git a/tests/images/paths/paintImageTiled.png b/tests/images/paths/paintImageTiled.png new file mode 100644 index 0000000..31a7d0e Binary files /dev/null and b/tests/images/paths/paintImageTiled.png differ diff --git a/tests/images/paths/paintSolid.png b/tests/images/paths/paintSolid.png new file mode 100644 index 0000000..a5e0625 Binary files /dev/null and b/tests/images/paths/paintSolid.png differ diff --git a/tests/images/png/baboon.png b/tests/images/png/baboon.png new file mode 100644 index 0000000..a8556f2 Binary files /dev/null and b/tests/images/png/baboon.png differ diff --git a/tests/test_paints.nim b/tests/test_paints.nim new file mode 100644 index 0000000..7f2a049 --- /dev/null +++ b/tests/test_paints.nim @@ -0,0 +1,109 @@ +import chroma, pixie, pixie/fileformats/png, vmath + +const heartShape = """ + M 10,30 + A 20,20 0,0,1 50,30 + A 20,20 0,0,1 90,30 + Q 90,60 50,90 + Q 10,60 10,30 z + """ + +block: + let + image = newImage(100, 100) + image.fillPath( + heartShape, + Paint( + kind:pkSolid, + color:rgba(255, 0, 0, 255) + ) + ) + image.writeFile("tests/images/paths/paintSolid.png") + +block: + let + image = newImage(100, 100) + image.fillPath( + heartShape, + Paint( + kind:pkImage, + image:decodePng(readFile("tests/images/png/baboon.png")), + imageMat:scale(vec2(0.2, 0.2)) + ) + ) + image.writeFile("tests/images/paths/paintImage.png") + +block: + let + image = newImage(100, 100) + image.fillPath( + heartShape, + Paint( + kind:pkImageTiled, + image:decodePng(readFile("tests/images/png/baboon.png")), + imageMat:scale(vec2(0.02, 0.02)) + ) + ) + image.writeFile("tests/images/paths/paintImageTiled.png") + + +block: + let + image = newImage(100, 100) + image.fillPath( + heartShape, + Paint( + kind:pkGradientLinear, + gradientHandlePositions: @[ + vec2(0, 50), + vec2(100, 50), + ], + gradientStops: @[ + ColorStop(color:rgba(255, 0, 0, 255).color, position: 0), + ColorStop(color:rgba(255, 0, 0, 40).color, position: 1.0), + ] + ) + ) + image.writeFile("tests/images/paths/gradientLinear.png") + +block: + let + image = newImage(100, 100) + image.fillPath( + heartShape, + Paint( + kind:pkGradientRadial, + gradientHandlePositions: @[ + vec2(50, 50), + vec2(100, 50), + vec2(50, 100) + ], + gradientStops: @[ + ColorStop(color:rgba(255, 0, 0, 255).color, position: 0), + ColorStop(color:rgba(255, 0, 0, 40).color, position: 1.0), + ] + ) + ) + + image.writeFile("tests/images/paths/gradientRadial.png") + +block: + let + image = newImage(100, 100) + image.fillPath( + heartShape, + Paint( + kind:pkGradientAngular, + gradientHandlePositions: @[ + vec2(50, 50), + vec2(100, 50), + vec2(50, 100) + ], + gradientStops: @[ + ColorStop(color:rgba(255, 0, 0, 255).color, position: 0), + ColorStop(color:rgba(255, 0, 0, 40).color, position: 1.0), + ] + ) + ) + + image.writeFile("tests/images/paths/gradientAngular.png")