Merge pull request #247 from guzba/master

paint has opacity
This commit is contained in:
treeform 2021-07-18 20:36:37 -07:00 committed by GitHub
commit 4d43941a4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 228 additions and 85 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -2,7 +2,6 @@
import math, pixie, sdl2, sdl2/gfx
const
rmask = uint32 0x000000ff
gmask = uint32 0x0000ff00
@ -23,7 +22,6 @@ var
mainTexture: TexturePtr
evt = sdl2.defaultEvent
proc display() =
## Called every frame by main while loop
@ -52,7 +50,8 @@ proc display() =
inc frameCount
var dataPtr = ctx.image.data[0].addr
mainSurface = createRGBSurfaceFrom(dataPtr, cint w, cint h, cint 32, cint 4*w, rmask, gmask, bmask, amask)
mainSurface = createRGBSurfaceFrom(dataPtr, cint w, cint h, cint 32, cint 4*w,
rmask, gmask, bmask, amask)
mainTexture = render.createTextureFromSurface(mainSurface)
destroy(mainSurface)
@ -62,7 +61,6 @@ proc display() =
render.present()
discard sdl2.init(INIT_EVERYTHING)
window = createWindow("SDL/Pixie", 100, 100, cint w, cint h, SDL_WINDOW_SHOWN)
render = createRenderer(window, -1, 0)

View file

@ -1,7 +1,6 @@
## Load and Save SVG files.
import cairo, chroma, pixie/common, pixie/images, strutils, vmath,
xmlparser, xmltree
import cairo, chroma, pixie/common, pixie/images, strutils, vmath, xmlparser, xmltree
include pixie/paths

View file

@ -23,6 +23,18 @@ proc gaussianKernel*(radius: int): seq[uint32] =
for i, f in floats:
result[i] = round(f * 255 * 1024).uint32
proc applyOpacity*(color: ColorRGBX, opacity: float32): ColorRGBX =
if opacity == 0:
rgbx(0, 0, 0, 0)
else:
let
x = round(opacity * 255).uint32
r = ((color.r * x) div 255).uint8
g = ((color.g * x) div 255).uint8
b = ((color.b * x) div 255).uint8
a = ((color.a * x) div 255).uint8
rgbx(r, g, b, a)
proc toStraightAlpha*(data: var seq[ColorRGBA | ColorRGBX]) =
## Converts an image from premultiplied alpha to straight alpha.
## This is expensive for large images.

View file

@ -1,4 +1,4 @@
import blends, chroma, common, images, vmath
import blends, chroma, common, images, internal, options, vmath
type
PaintKind* = enum
@ -21,6 +21,7 @@ type
gradientHandlePositions*: seq[Vec2] ## Gradient positions (image space).
gradientStops*: seq[ColorStop] ## Color stops (gradient space).
blendMode*: BlendMode ## Blend mode.
opacityOption: Option[float32]
ColorStop* = object
## Color stop on a gradient curve.
@ -41,37 +42,55 @@ converter parseSomePaint*(paint: SomePaint): Paint {.inline.} =
elif type(paint) is Paint:
paint
proc opacity*(paint: Paint): float32 =
## Paint opacity (applies with color or image opacity).
if paint.opacityOption.isSome:
paint.opacityOption.get()
else:
1
proc `opacity=`*(paint: var Paint, opacity: float32) =
## Set the paint opacity (applies with color or image opacity).
if opacity >= 0 and opacity <= 1:
paint.opacityOption = some(opacity)
else:
raise newException(PixieError, "Invalid opacity: " & $opacity)
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
return (d.y * (point.y - at.y) + d.x * (point.x - at.x)) / det
(d.y * (point.y - at.y) + d.x * (point.x - at.x)) / det
proc gradientPut(image: Image, x, y: int, a: float32, stops: seq[ColorStop]) =
## Put an gradient color based on the "a" - were are we related to a line.
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.
var index = -1
for i, stop in stops:
if stop.position < a:
if stop.position < t:
index = i
if stop.position > a:
if stop.position > t:
break
var color: Color
var color: ColorRGBX
if index == -1:
# first stop solid
color = stops[0].color.color
color = stops[0].color
elif index + 1 >= stops.len:
# last stop solid
color = stops[index].color.color
color = stops[index].color
else:
let
gs1 = stops[index]
gs2 = stops[index+1]
color = mix(
gs1.color.color,
gs2.color.color,
(a - gs1.position) / (gs2.position - gs1.position)
gs2 = stops[index + 1]
color = lerp(
gs1.color,
gs2.color,
(t - gs1.position) / (gs2.position - gs1.position)
)
if paint.opacity != 1:
color = color.applyOpacity(paint.opacity)
image.setRgbaUnsafe(x, y, color.rgba.rgbx())
proc fillGradientLinear*(image: Image, paint: Paint) =
@ -86,6 +105,9 @@ proc fillGradientLinear*(image: Image, paint: Paint) =
if paint.gradientStops.len == 0:
raise newException(PixieError, "Gradient must have at least 1 color stop")
if paint.opacity == 0:
return
let
at = paint.gradientHandlePositions[0]
to = paint.gradientHandlePositions[1]
@ -93,8 +115,8 @@ proc fillGradientLinear*(image: Image, paint: Paint) =
for x in 0 ..< image.width:
let
xy = vec2(x.float32, y.float32)
a = toLineSpace(at, to, xy)
image.gradientPut(x, y, a, paint.gradientStops)
t = toLineSpace(at, to, xy)
image.gradientPut(paint, x, y, t, paint.gradientStops)
proc fillGradientRadial*(image: Image, paint: Paint) =
## Fills a radial gradient.
@ -108,6 +130,9 @@ proc fillGradientRadial*(image: Image, paint: Paint) =
if paint.gradientStops.len == 0:
raise newException(PixieError, "Gradient must have at least 1 color stop")
if paint.opacity == 0:
return
let
center = paint.gradientHandlePositions[0]
edge = paint.gradientHandlePositions[1]
@ -124,8 +149,8 @@ proc fillGradientRadial*(image: Image, paint: Paint) =
for x in 0 ..< image.width:
let
xy = vec2(x.float32, y.float32)
b = (mat * xy).length()
image.gradientPut(x, y, b, paint.gradientStops)
t = (mat * xy).length()
image.gradientPut(paint, x, y, t, paint.gradientStops)
proc fillGradientAngular*(image: Image, paint: Paint) =
## Fills an angular gradient.
@ -139,6 +164,9 @@ proc fillGradientAngular*(image: Image, paint: Paint) =
if paint.gradientStops.len == 0:
raise newException(PixieError, "Gradient must have at least 1 color stop")
if paint.opacity == 0:
return
let
center = paint.gradientHandlePositions[0]
edge = paint.gradientHandlePositions[1]
@ -149,5 +177,5 @@ proc fillGradientAngular*(image: Image, paint: Paint) =
let
xy = vec2(x.float32, y.float32)
angle = normalize(xy - center).angle()
a = (angle + gradientAngle + PI/2).fixAngle() / 2 / PI + 0.5
image.gradientPut(x, y, a, paint.gradientStops)
t = (angle + gradientAngle + PI / 2).fixAngle() / 2 / PI + 0.5
image.gradientPut(paint, x, y, t, paint.gradientStops)

View file

@ -1,5 +1,4 @@
import blends, bumpy, chroma, common, images, masks, paints, pixie/internal,
strutils, vmath
import blends, bumpy, chroma, common, images, internal, masks, paints, strutils, vmath
when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
@ -1780,11 +1779,17 @@ proc fillPath*(
windingRule = wrNonZero
) =
## Fills a path.
if paint.opacity == 0:
return
if paint.kind == pkSolid:
if paint.color.a > 0 or paint.blendMode == bmOverwrite:
var shapes = parseSomePath(path, true, transform.pixelScale())
shapes.transform(transform)
image.fillShapes(shapes, paint.color, windingRule, paint.blendMode)
var color = paint.color
if paint.opacity != 1:
color = color.applyOpacity(paint.opacity)
image.fillShapes(shapes, color, windingRule, paint.blendMode)
return
let
@ -1793,19 +1798,28 @@ proc fillPath*(
mask.fillPath(path, transform, windingRule)
# Draw the image (maybe tiled) or gradients. Do this with opaque paint and
# and then apply the paint's opacity to the mask.
var paintOpaque = paint
paintOpaque.opacity = 1
case paint.kind:
of pkSolid:
discard # Handled above
of pkImage:
fill.draw(paint.image, paint.imageMat)
fill.draw(paintOpaque.image, paintOpaque.imageMat)
of pkImageTiled:
fill.drawTiled(paint.image, paint.imageMat)
fill.drawTiled(paintOpaque.image, paintOpaque.imageMat)
of pkGradientLinear:
fill.fillGradientLinear(paint)
fill.fillGradientLinear(paintOpaque)
of pkGradientRadial:
fill.fillGradientRadial(paint)
fill.fillGradientRadial(paintOpaque)
of pkGradientAngular:
fill.fillGradientAngular(paint)
fill.fillGradientAngular(paintOpaque)
if paint.opacity != 1:
mask.applyOpacity(paint.opacity)
fill.draw(mask)
image.draw(fill, blendMode = paint.blendMode)
@ -1845,6 +1859,9 @@ proc strokePath*(
dashes: seq[float32] = @[]
) =
## Strokes a path.
if paint.opacity == 0:
return
if paint.kind == pkSolid:
if paint.color.a > 0 or paint.blendMode == bmOverwrite:
var strokeShapes = strokeShapes(
@ -1856,7 +1873,10 @@ proc strokePath*(
dashes
)
strokeShapes.transform(transform)
image.fillShapes(strokeShapes, paint.color, wrNonZero, paint.blendMode)
var color = paint.color
if paint.opacity != 1:
color = color.applyOpacity(paint.opacity)
image.fillShapes(strokeShapes, color, wrNonZero, paint.blendMode)
return
let
@ -1873,19 +1893,28 @@ proc strokePath*(
dashes
)
# Draw the image (maybe tiled) or gradients. Do this with opaque paint and
# and then apply the paint's opacity to the mask.
var paintOpaque = paint
paintOpaque.opacity = 1
case paint.kind:
of pkSolid:
discard # Handled above
of pkImage:
fill.draw(paint.image, paint.imageMat)
fill.draw(paintOpaque.image, paintOpaque.imageMat)
of pkImageTiled:
fill.drawTiled(paint.image, paint.imageMat)
fill.drawTiled(paintOpaque.image, paintOpaque.imageMat)
of pkGradientLinear:
fill.fillGradientLinear(paint)
fill.fillGradientLinear(paintOpaque)
of pkGradientRadial:
fill.fillGradientRadial(paint)
fill.fillGradientRadial(paintOpaque)
of pkGradientAngular:
fill.fillGradientAngular(paint)
fill.fillGradientAngular(paintOpaque)
if paint.opacity != 1:
mask.applyOpacity(paint.opacity)
fill.draw(mask)
image.draw(fill, blendMode = paint.blendMode)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -592,7 +592,7 @@ block:
image = newImage(300, 227)
ctx = newContext(image)
rhino = readImage("tests/images/rhino.png")
ctx.drawImage(rhino, 33, 71, 104, 124, 21, 20, 87, 104);
ctx.drawImage(rhino, 33, 71, 104, 124, 21, 20, 87, 104)
image.writeFile("tests/images/context/draw_image_rhino.png")
block:
@ -600,7 +600,7 @@ block:
image = newImage(300, 227)
ctx = newContext(image)
rhino = readImage("tests/images/rhino.png")
ctx.drawImage(rhino, rect(33, 71, 104, 124), rect(21, 20, 87, 104));
ctx.drawImage(rhino, rect(33, 71, 104, 124), rect(21, 20, 87, 104))
image.writeFile("tests/images/context/draw_image_rhino2.png")
block:

View file

@ -31,6 +31,21 @@ block:
)
image.writeFile("tests/images/paths/paintImage.png")
block:
var paint = Paint(
kind: pkImage,
image: decodePng(readFile("tests/images/png/baboon.png")),
imageMat: scale(vec2(0.2, 0.2))
)
paint.opacity = 0.5
let image = newImage(100, 100)
image.fillPath(
heartShape,
paint
)
image.writeFile("tests/images/paths/paintImageOpacity.png")
block:
let image = newImage(100, 100)
image.fillPath(
@ -43,6 +58,21 @@ block:
)
image.writeFile("tests/images/paths/paintImageTiled.png")
block:
var paint = Paint(
kind: pkImageTiled,
image: decodePng(readFile("tests/images/png/baboon.png")),
imageMat: scale(vec2(0.02, 0.02))
)
paint.opacity = 0.5
let image = newImage(100, 100)
image.fillPath(
heartShape,
paint
)
image.writeFile("tests/images/paths/paintImageTiledOpacity.png")
block:
let image = newImage(100, 100)
image.fillPath(
@ -100,3 +130,26 @@ block:
)
image.writeFile("tests/images/paths/gradientAngular.png")
block:
var paint = Paint(
kind: pkGradientAngular,
gradientHandlePositions: @[
vec2(50, 50),
vec2(100, 50),
vec2(50, 100)
],
gradientStops: @[
ColorStop(color: rgba(255, 0, 0, 255), position: 0),
ColorStop(color: rgba(255, 0, 0, 40), position: 1.0),
]
)
paint.opacity = 0.5
let image = newImage(100, 100)
image.fillPath(
heartShape,
paint
)
image.writeFile("tests/images/paths/gradientAngularOpacity.png")

View file

@ -445,20 +445,20 @@ block:
# Draw shapes
for i in 0 .. 3:
for j in 0 .. 2:
ctx.beginPath();
let x = 25f + j.float32 * 50f; # x coordinate
let y = 25f + i.float32 * 50f; # y coordinate
let radius = 20f; # Arc radius
let startAngle = 0f; # Starting point on circle
let endAngle = PI + (PI * j.float32) / 2; # End point on circle
let counterclockwise = i mod 2 == 1; # Draw counterclockwise
ctx.beginPath()
let x = 25f + j.float32 * 50f # x coordinate
let y = 25f + i.float32 * 50f # y coordinate
let radius = 20f # Arc radius
let startAngle = 0f # Starting point on circle
let endAngle = PI + (PI * j.float32) / 2 # End point on circle
let counterclockwise = i mod 2 == 1 # Draw counterclockwise
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise)
if i > 1:
ctx.fill();
ctx.fill()
else:
ctx.stroke();
ctx.stroke()
surface.writeFile("tests/images/paths/arc.png")
@ -469,14 +469,14 @@ block:
surface.fill(rgba(255, 255, 255, 255))
let
p0 = vec2(230, 20 )
p1 = vec2(90, 130)
p2 = vec2(20, 20 )
p0 = vec2(230, 20)
p1 = vec2(90, 130)
p2 = vec2(20, 20)
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
ctx.arcTo(p1.x, p1.y, p2.x, p2.y, 50);
ctx.lineTo(p2.x, p2.y);
ctx.beginPath()
ctx.moveTo(p0.x, p0.y)
ctx.arcTo(p1.x, p1.y, p2.x, p2.y, 50)
ctx.lineTo(p2.x, p2.y)
ctx.stroke()
surface.writeFile("tests/images/paths/arcTo1.png")
@ -487,33 +487,33 @@ block:
ctx = newContext(surface)
surface.fill(rgba(255, 255, 255, 255))
# Tangential lines
ctx.beginPath();
ctx.strokeStyle = "gray";
ctx.moveTo(200, 20);
ctx.lineTo(200, 130);
ctx.lineTo(50, 20);
ctx.stroke();
ctx.beginPath()
ctx.strokeStyle = "gray"
ctx.moveTo(200, 20)
ctx.lineTo(200, 130)
ctx.lineTo(50, 20)
ctx.stroke()
# Arc
ctx.beginPath();
ctx.strokeStyle = "black";
ctx.lineWidth = 5;
ctx.moveTo(200, 20);
ctx.arcTo(200,130, 50,20, 40);
ctx.stroke();
ctx.beginPath()
ctx.strokeStyle = "black"
ctx.lineWidth = 5
ctx.moveTo(200, 20)
ctx.arcTo(200, 130, 50, 20, 40)
ctx.stroke()
# Start point
ctx.beginPath();
ctx.fillStyle = "blue";
ctx.arc(200, 20, 5, 0, 2 * PI);
ctx.fill();
ctx.beginPath()
ctx.fillStyle = "blue"
ctx.arc(200, 20, 5, 0, 2 * PI)
ctx.fill()
# Control points
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(200, 130, 5, 0, 2 * PI); # Control point one
ctx.arc(50, 20, 5, 0, 2 * PI); # Control point two
ctx.fill();
ctx.beginPath()
ctx.fillStyle = "red"
ctx.arc(200, 130, 5, 0, 2 * PI) # Control point one
ctx.arc(50, 20, 5, 0, 2 * PI) # Control point two
ctx.fill()
surface.writeFile("tests/images/paths/arcTo2.png")
@ -523,11 +523,11 @@ block:
ctx = newContext(surface)
surface.fill(rgba(255, 255, 255, 255))
ctx.beginPath();
ctx.moveTo(180, 90);
ctx.arcTo(180,130, 110,130, 130);
ctx.lineTo(110, 130);
ctx.stroke();
ctx.beginPath()
ctx.moveTo(180, 90)
ctx.arcTo(180, 130, 110, 130, 130)
ctx.lineTo(110, 130)
ctx.stroke()
surface.writeFile("tests/images/paths/arcTo3.png")
@ -568,3 +568,27 @@ block:
doAssert path.strokeOverlaps(vec2(40, 20))
doAssert path.strokeOverlaps(vec2(19.8, 30.2))
doAssert not path.strokeOverlaps(vec2(19.4, 30.6))
block:
var path: Path
path.circle(50, 50, 30)
var paint = Paint(kind: pkSolid, color: rgba(255, 0, 255, 255))
paint.opacity = 0.5
let image = newImage(100, 100)
image.fillPath(path, paint)
image.writeFile("tests/images/paths/opacityFill.png")
block:
var path: Path
path.circle(50, 50, 30)
var paint = Paint(kind: pkSolid, color: rgba(255, 0, 255, 255))
paint.opacity = 0.5
let image = newImage(100, 100)
image.strokePath(path, paint, strokeWidth = 10)
image.writeFile("tests/images/paths/opacityStroke.png")