Add paints to pixie. (#127)

Add paints for: solid, image, image tiled, linear, radial and angular gradients.
This commit is contained in:
treeform 2021-02-24 21:49:21 -08:00 committed by GitHub
parent 91983aa812
commit c8c981b04e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 271 additions and 22 deletions

30
examples/paint.nim Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

BIN
tests/images/png/baboon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

109
tests/test_paints.nim Normal file
View 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")