Add paints to pixie. (#127)
Add paints for: solid, image, image tiled, linear, radial and angular gradients.
30
examples/paint.nim
Normal file
|
@ -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")
|
BIN
examples/paint.png
Normal file
After Width: | Height: | Size: 4 KiB |
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
|
|
BIN
tests/images/paths/gradientAngular.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
tests/images/paths/gradientLinear.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
tests/images/paths/gradientRadial.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
tests/images/paths/paintImage.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
tests/images/paths/paintImageTiled.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
tests/images/paths/paintSolid.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
tests/images/png/baboon.png
Normal file
After Width: | Height: | Size: 636 KiB |
109
tests/test_paints.nim
Normal file
|
@ -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")
|