Simplify draw, bring in blends.

This commit is contained in:
treeform 2020-11-20 21:50:13 -08:00
parent c1cfbe0336
commit 344063ddbc
4 changed files with 323 additions and 179 deletions

View file

@ -1,9 +1,9 @@
## Public interface to you library.
import pixie/images, pixie/masks, pixie/paths, pixie/common,
import pixie/images, pixie/masks, pixie/paths, pixie/common, pixie/blends,
pixie/fileformats/bmp, pixie/fileformats/png, flatty/binny
export images, masks, paths, PixieError
export images, masks, paths, PixieError, blends
type
FileFormat* = enum

282
src/pixie/blends.nim Normal file
View file

@ -0,0 +1,282 @@
## Blending modes.
import chroma, math, algorithm
type BlendMode* = enum
Normal
Darken
Multiply
LinearBurn
ColorBurn
Lighten
Screen
LinearDodge
ColorDodge
Overlay
SoftLight
HardLight
Difference
Exclusion
Hue
Saturation
Color
Luminosity
Mask ## Special blend mode that is used for masking
Copy ## Special that does not blend but copies the pixels from target.
SubtractMask ## Inverse mask
proc parseBlendMode*(s: string): BlendMode =
case s:
of "NORMAL": Normal
of "DARKEN": Darken
of "MULTIPLY": Multiply
of "LINEAR_BURN": LinearBurn
of "COLOR_BURN": ColorBurn
of "LIGHTEN": Lighten
of "SCREEN": Screen
of "LINEAR_DODGE": LinearDodge
of "COLOR_DODGE": ColorDodge
of "OVERLAY": Overlay
of "SOFT_LIGHT": SoftLight
of "HARD_LIGHT": HardLight
of "DIFFERENCE": Difference
of "EXCLUSION": Exclusion
of "HUE": Hue
of "SATURATION": Saturation
of "COLOR": Color
of "LUMINOSITY": Luminosity
of "MASK": Mask
of "COPY": Copy
else: Normal
proc `+`*(a, b: Color): Color {.inline.} =
result.r = a.r + b.r
result.g = a.g + b.g
result.b = a.b + b.b
result.a = a.a + b.a
proc `+`*(c: Color, v: float32): Color {.inline.} =
result.r = c.r + v
result.g = c.g + v
result.b = c.b + v
result.a = c.a + v
proc `+`*(v: float32, c: Color): Color {.inline.} =
c + v
proc `*`*(c: Color, v: float32): Color {.inline.} =
result.r = c.r * v
result.g = c.g * v
result.b = c.b * v
result.a = c.a * v
proc `*`*(v: float32, target: Color): Color {.inline.} =
target * v
proc `/`*(c: Color, v: float32): Color {.inline.} =
result.r = c.r / v
result.g = c.g / v
result.b = c.b / v
result.a = c.a / v
proc `-`*(c: Color, v: float32): Color {.inline.} =
result.r = c.r - v
result.g = c.g - v
result.b = c.b - v
result.a = c.a - v
proc mix*(blendMode: BlendMode, target, blend: Color): Color =
if blendMode == Mask:
result.r = target.r
result.g = target.g
result.b = target.b
result.a = min(target.a, blend.a)
return
elif blendMode == SubtractMask:
result.r = target.r
result.g = target.g
result.b = target.b
result.a = target.a * (1 - blend.a)
return
elif blendMode == Copy:
result = target
return
proc multiply(Cb, Cs: float32): float32 =
Cb * Cs
proc screen(Cb, Cs: float32): float32 =
1 - (1 - Cb) * (1 - Cs)
proc hardLight(Cb, Cs: float32): float32 =
if Cs <= 0.5: multiply(Cb, 2 * Cs)
else: screen(Cb, 2 * Cs - 1)
# Here are 4 implementations of soft light, none of them are quite right.
# proc softLight(Cb, Cs: float32): float32 =
# ## W3C
# proc D(cb: float32): float32 =
# if Cb <= 0.25:
# ((16 * Cb - 12) * Cb + 4) * Cb
# else:
# sqrt(Cb)
# if Cs <= 0.5:
# return Cb - (1 - 2 * Cs) * Cb * (1 - Cb)
# else:
# return Cb + (2 * Cs - 1) * (D(Cb) - Cb)
# proc softLight(a, b: float32): float32 =
# ## Photoshop
# if b < 0.5:
# 2 * a * b + a ^ 2 * (1 - 2 * b)
# else:
# 2 * a * (1 - b) + sqrt(a) * (2 * b - 1)
proc softLight(a, b: float32): float32 =
## Pegtop
(1 - 2 * b) * a ^ 2 + 2 * b * a
# proc softLight(a, b: float32): float32 =
# ## Illusions.hu
# pow(a, pow(2, (2 * (0.5 - b))))
proc Lum(C: Color): float32 =
0.3 * C.r + 0.59 * C.g + 0.11 * C.b
proc ClipColor(C: Color): Color =
let
L = Lum(C)
n = min([C.r, C.g, C.b])
x = max([C.r, C.g, C.b])
var
C = C
if n < 0:
C = L + (((C - L) * L) / (L - n))
if x > 1:
C = L + (((C - L) * (1 - L)) / (x - L))
return C
proc SetLum(C: Color, l: float32): Color =
let
d = l - Lum(C)
result.r = C.r + d
result.g = C.g + d
result.b = C.b + d
return ClipColor(result)
proc Sat(C: Color): float32 =
max([C.r, C.g, C.b]) - min([C.r, C.g, C.b])
proc SetSat(C: Color, s: float32): Color =
var arr = [(C.r, 0), (C.g, 1), (C.b, 2)]
# TODO: Don't rely on sort.
arr.sort()
var
Cmin = arr[0][0]
Cmid = arr[1][0]
Cmax = arr[2][0]
if Cmax > Cmin:
Cmid = (((Cmid - Cmin) * s) / (Cmax - Cmin))
Cmax = s
else:
Cmid = 0
Cmax = 0
Cmin = 0
if arr[0][1] == 0:
result.r = Cmin
if arr[1][1] == 0:
result.r = Cmid
if arr[2][1] == 0:
result.r = Cmax
if arr[0][1] == 1:
result.g = Cmin
if arr[1][1] == 1:
result.g = Cmid
if arr[2][1] == 1:
result.g = Cmax
if arr[0][1] == 2:
result.b = Cmin
if arr[1][1] == 2:
result.b = Cmid
if arr[2][1] == 2:
result.b = Cmax
proc blendChannel(blendMode: BlendMode, Cb, Cs: float32): float32 =
result = case blendMode
of Normal: Cs
of Darken: min(Cb, Cs)
of Multiply: multiply(Cb, Cs)
of LinearBurn: Cb + Cs - 1
of ColorBurn:
if Cb == 1: 1.0
elif Cs == 0: 0.0
else: 1.0 - min(1, (1 - Cb) / Cs)
of Lighten: max(Cb, Cs)
of Screen: screen(Cb, Cs)
of LinearDodge: Cb + Cs
of ColorDodge:
if Cb == 0: 0.0
elif Cs == 1: 1.0
else: min(1, Cb / (1 - Cs))
of Overlay: hardLight(Cs, Cb)
of HardLight: hardLight(Cb, Cs)
of SoftLight: softLight(Cb, Cs)
of Difference: abs(Cb - Cs)
of Exclusion: Cb + Cs - 2 * Cb * Cs
else: 0.0
let Cb = target
let Cs = blend
var mixed: Color
if blendMode == Color:
mixed = SetLum(Cs, Lum(Cb))
elif blendMode == Luminosity:
mixed = SetLum(Cb, Lum(Cs))
elif blendMode == Hue:
mixed = SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb))
elif blendMode == Saturation:
mixed = SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb))
else:
mixed.r = blendMode.blendChannel(Cb.r, Cs.r)
mixed.g = blendMode.blendChannel(Cb.g, Cs.g)
mixed.b = blendMode.blendChannel(Cb.b, Cs.b)
let ab = Cb.a
let As = Cs.a
result.r = As * (1 - ab) * Cs.r + As * ab * mixed.r + (1 - As) * ab * Cb.r
result.g = As * (1 - ab) * Cs.g + As * ab * mixed.g + (1 - As) * ab * Cb.g
result.b = As * (1 - ab) * Cs.b + As * ab * mixed.b + (1 - As) * ab * Cb.b
result.a = (blend.a + target.a * (1.0 - blend.a))
result.r /= result.a
result.g /= result.a
result.b /= result.a
proc mix*(blendMode: BlendMode, dest, src: ColorRGBA): ColorRGBA {.inline.} =
return blendMode.mix(dest.color, src.color).rgba
# TODO: Fix fast paths
# if blendMode == Normal:
# # Fast pass
# # target * (1 - blend.a) + blend * blend.a
# if target.a == 0: return blend
# let blendAComp = 255 - blend.a
# result.r = ((target.r.uint16 * blendAComp + blend.r.uint16 * blend.a) div 255).uint8
# result.g = ((target.g.uint16 * blendAComp + blend.g.uint16 * blend.a) div 255).uint8
# result.b = ((target.b.uint16 * blendAComp + blend.b.uint16 * blend.a) div 255).uint8
# result.a = (blend.a.uint16 + (target.a.uint16 * blendAComp) div 255).uint8
# inc blendCount
# elif blendMode == Mask:
# result.r = target.r
# result.g = target.g
# result.b = target.b
# result.a = min(target.a, blend.a)
# elif blendMode == COPY:
# result = target
# else:
# return blendMode.mix(target.color, blend.color).rgba

View file

@ -1,4 +1,4 @@
import chroma, chroma/blends, vmath
import chroma, blends, vmath
type
Image* = ref object
@ -102,70 +102,6 @@ proc magnifyBy2*(image: Image, scale2x: int): Image =
proc magnifyBy2*(image: Image): Image =
image.magnifyBy2(2)
proc blitUnsafe*(destImage: Image, srcImage: Image, src, dest: Rect) =
## Blits rectangle from one image to the other image.
## * No bounds checking *
## Make sure that src and dest rect are in bounds.
## Make sure that channels for images are the same.
## Failure in the assumptions will case unsafe memory writes.
## Note: Does not do alpha or color mixing.
for y in 0 ..< int(dest.h):
let
srcIdx = int(src.x) + (int(src.y) + y) * srcImage.width
destIdx = int(dest.x) + (int(dest.y) + y) * destImage.width
copyMem(
destImage.data[destIdx].addr,
srcImage.data[srcIdx].addr,
int(dest.w) * 4
)
proc blit*(destImage: Image, srcImage: Image, src, dest: Rect) =
## Blits rectangle from one image to the other image.
## Note: Does not do alpha or color mixing.
doAssert src.w == dest.w and src.h == dest.h
doAssert src.x >= 0 and src.x + src.w <= srcImage.width.float32
doAssert src.y >= 0 and src.y + src.h <= srcImage.height.float32
# See if the image hits the bounds and needs to be adjusted.
var
src = src
dest = dest
if dest.x < 0:
dest.w += dest.x
src.x -= dest.x
src.w += dest.x
dest.x = 0
if dest.x + dest.w > destImage.width.float32:
let diff = destImage.width.float32 - (dest.x + dest.w)
dest.w += diff
src.w += diff
if dest.y < 0:
dest.h += dest.y
src.y -= dest.y
src.h += dest.y
dest.y = 0
if dest.y + dest.h > destImage.height.float32:
let diff = destImage.height.float32 - (dest.y + dest.h)
dest.h += diff
src.h += diff
# See if image is entirely outside the bounds:
if dest.x + dest.w < 0 or dest.x > destImage.width.float32:
return
if dest.y + dest.h < 0 or dest.y > destImage.height.float32:
return
blitUnsafe(destImage, srcImage, src, dest)
proc blit*(destImage: Image, srcImage: Image, pos: Vec2) =
## Blits rectangle from one image to the other image.
## Note: Does not do alpha or color mixing.
destImage.blit(
srcImage,
rect(0.0, 0.0, srcImage.width.float32, srcImage.height.float32),
rect(pos.x, pos.y, srcImage.width.float32, srcImage.height.float32)
)
func moduloMod(n, M: int): int {.inline.} =
## Computes "mathematical" modulo vs c modulo.
((n mod M) + M) mod M
@ -222,75 +158,6 @@ proc hasEffect*(blendMode: BlendMode, rgba: ColorRGBA): bool =
else:
rgba.a > 0
proc drawBlendIntegerPos*(
destImage, srcImage: Image, pos = vec2(0, 0), blendMode = Normal,
) =
## Fast draw of dest + fill using offset with color blending.
for y in 0 ..< srcImage.height:
for x in 0 ..< srcImage.width:
let
srcRgba = srcImage.getRgbaUnsafe(x, y)
if blendMode.hasEffect(srcRgba):
let
destRgba = destImage.getRgbaUnsafe(x + pos.x.int, y + pos.y.int)
rgba = blendMode.mix(destRgba, srcRgba)
# TODO: Make unsafe
destImage[x + pos.x.int, y + pos.y.int] = rgba
proc draw*(destImage: Image, srcImage: Image, mat: Mat4, blendMode = Normal) =
## Draws one image onto another using matrix with color blending.
var srcImage = srcImage
let
matInv = mat.inverse()
bounds = [
mat * vec3(-1, -1, 0),
mat * vec3(-1, float32 srcImage.height + 1, 0),
mat * vec3(float32 srcImage.width + 1, -1, 0),
mat * vec3(float32 srcImage.width + 1, float32 srcImage.height + 1, 0)
]
var
boundsX: array[4, float32]
boundsY: array[4, float32]
for i, v in bounds:
boundsX[i] = v.x
boundsY[i] = v.y
let
xStart = max(int min(boundsX), 0)
yStart = max(int min(boundsY), 0)
xEnd = min(int max(boundsX), destImage.width)
yEnd = min(int max(boundsY), destImage.height)
var
# compute movement vectors
start = matInv * vec3(0.5, 0.5, 0)
stepX = matInv * vec3(1.5, 0.5, 0) - start
stepY = matInv * vec3(0.5, 1.5, 0) - start
minFilterBy2 = max(stepX.length, stepY.length)
while minFilterBy2 > 2.0:
srcImage = srcImage.minifyBy2()
start /= 2
stepX /= 2
stepY /= 2
minFilterBy2 /= 2
# fill the bounding rectangle
for y in yStart ..< yEnd:
for x in xStart ..< xEnd:
let srcV = start + stepX * float32(x) + stepY * float32(y)
if srcImage.inside(int srcV.x.floor, int srcV.y.floor):
let
srcRgba = srcImage.getRgbaSmooth(srcV.x - 0.5, srcV.y - 0.5)
if blendMode.hasEffect(srcRgba):
let
destRgba = destImage.getRgbaUnsafe(x, y)
color = blendMode.mix(destRgba, srcRgba)
destImage.setRgbaUnsafe(x, y, color)
proc draw*(destImage: Image, srcImage: Image, pos = vec2(0, 0), blendMode = Normal) =
destImage.draw(srcImage, translate(vec3(pos.x, pos.y, 0)), blendMode)
func translate*(v: Vec2): Mat3 =
result[0, 0] = 1
result[1, 1] = 1
@ -298,8 +165,13 @@ func translate*(v: Vec2): Mat3 =
result[2, 1] = v.y
result[2, 2] = 1
proc copyDraw*(destImage: Image, srcImage: Image, mat: Mat3, blendMode = Normal): Image =
proc draw*(destImage: Image, srcImage: Image, mat: Mat3, blendMode = Normal): Image =
## Draws one image onto another using matrix with color blending.
# Todo: if matrix is simple integer translation -> fast pass
# Todo: if matrix is a simple flip -> fast path
# Todo: if blend mode is copy -> fast path
result = newImage(destImage.width, destImage.height)
for y in 0 ..< destImage.width:
for x in 0 ..< destImage.height:
@ -313,31 +185,5 @@ proc copyDraw*(destImage: Image, srcImage: Image, mat: Mat3, blendMode = Normal)
rgba = blendMode.mix(destRgba, srcRgba)
result.setRgbaUnsafe(x, y, rgba)
proc copyDraw*(destImage: Image, srcImage: Image, pos = vec2(0, 0), blendMode = Normal): Image =
destImage.copyDraw(srcImage, translate(-pos), blendMode)
proc inplaceDraw*(destImage: Image, srcImage: Image, mat: Mat3, blendMode = Normal) =
## Draws one image onto another using matrix with color blending.
for y in 0 ..< destImage.width:
for x in 0 ..< destImage.height:
let srcPos = mat * vec2(x.float32, y.float32)
let destRgba = destImage.getRgbaUnsafe(x, y)
var rgba = destRgba
var srcRgba = rgba(0, 0, 0, 0)
if srcImage.inside(srcPos.x.floor.int, srcPos.y.floor.int):
srcRgba = srcImage.getRgbaSmooth(srcPos.x - 0.5, srcPos.y - 0.5)
if blendMode.hasEffect(srcRgba):
rgba = blendMode.mix(destRgba, srcRgba)
destImage.setRgbaUnsafe(x, y, rgba)
proc inplaceDraw*(destImage: Image, srcImage: Image, pos = vec2(0, 0), blendMode = Normal) =
destImage.inplaceDraw(srcImage, translate(-pos), blendMode)
## Thoughts
## single draw function that takes a matrix
## if matrix is simple integer translation -> fast pass
## if matrix is a simple flip -> fast path
## if blend mode is copy -> fast path
##
## Helper function that takes x,y
## Helper function that takes x,y and rotation.
proc draw*(destImage: Image, srcImage: Image, pos = vec2(0, 0), blendMode = Normal): Image =
destImage.draw(srcImage, translate(-pos), blendMode)

View file

@ -1,40 +1,56 @@
import pixie, chroma, vmath, fidget/opengl/perf, pixie/fileformats/bmp
block:
var a = newImage(100, 100)
a.fill(rgba(255, 0, 0, 255))
var b = newImage(100, 100)
b.fill(rgba(0, 255, 0, 255))
a.inplaceDraw(b, pos=vec2(25, 25))
writeFile("tests/images/inplaceDraw.bmp", a.encodeBmp())
proc inPlaceDraw*(destImage: Image, srcImage: Image, mat: Mat3, blendMode = Normal) =
## Draws one image onto another using matrix with color blending.
for y in 0 ..< destImage.width:
for x in 0 ..< destImage.height:
let srcPos = mat * vec2(x.float32, y.float32)
let destRgba = destImage.getRgbaUnsafe(x, y)
var rgba = destRgba
var srcRgba = rgba(0, 0, 0, 0)
if srcImage.inside(srcPos.x.floor.int, srcPos.y.floor.int):
srcRgba = srcImage.getRgbaSmooth(srcPos.x - 0.5, srcPos.y - 0.5)
if blendMode.hasEffect(srcRgba):
rgba = blendMode.mix(destRgba, srcRgba)
destImage.setRgbaUnsafe(x, y, rgba)
proc inPlaceDraw*(destImage: Image, srcImage: Image, pos = vec2(0, 0), blendMode = Normal) =
destImage.inPlaceDraw(srcImage, translate(-pos), blendMode)
block:
var a = newImage(100, 100)
a.fill(rgba(255, 0, 0, 255))
var b = newImage(100, 100)
b.fill(rgba(0, 255, 0, 255))
var c = a.copyDraw(b, pos=vec2(25, 25))
a.inPlaceDraw(b, pos=vec2(25, 25))
writeFile("tests/images/inPlaceDraw.bmp", a.encodeBmp())
block:
var a = newImage(100, 100)
a.fill(rgba(255, 0, 0, 255))
var b = newImage(100, 100)
b.fill(rgba(0, 255, 0, 255))
var c = a.draw(b, pos=vec2(25, 25))
writeFile("tests/images/copyDraw.bmp", c.encodeBmp())
timeIt "inplaceDraw":
timeIt "inPlaceDraw":
var tmp = 0
for i in 0 ..< 100000:
for i in 0 ..< 1000:
var a = newImage(100, 100)
a.fill(rgba(255, 0, 0, 255))
var b = newImage(100, 100)
b.fill(rgba(0, 255, 0, 255))
a.inplaceDraw(b, pos=vec2(25, 25))
a.inPlaceDraw(b, pos=vec2(25, 25))
tmp += a.width * a.height
echo tmp
timeIt "copyDraw":
var tmp = 0
for i in 0 ..< 100000:
for i in 0 ..< 1000:
var a = newImage(100, 100)
a.fill(rgba(255, 0, 0, 255))
var b = newImage(100, 100)
b.fill(rgba(0, 255, 0, 255))
var c = a.copyDraw(b, pos=vec2(25, 25))
var c = a.draw(b, pos=vec2(25, 25))
tmp += c.width * c.height
echo tmp