diff --git a/cairotest.png b/cairotest.png new file mode 100644 index 0000000..0d2d187 Binary files /dev/null and b/cairotest.png differ diff --git a/screen.png b/screen.png new file mode 100644 index 0000000..8da519e Binary files /dev/null and b/screen.png differ diff --git a/src/pixie.nim b/src/pixie.nim index a7b5c0b..c650b14 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -4,7 +4,7 @@ import pixie/images, pixie/masks, pixie/paths, pixie/common, pixie/blends, pixie/fileformats/bmp, pixie/fileformats/png, pixie/fileformats/jpg, flatty/binny, os -export images, masks, paths, PixieError, blends +export images, masks, paths, common, blends type FileFormat* = enum diff --git a/src/pixie/blends.nim b/src/pixie/blends.nim index 31573e3..162809e 100644 --- a/src/pixie/blends.nim +++ b/src/pixie/blends.nim @@ -632,7 +632,10 @@ proc blendSaturation*(a, b: ColorRGBA): ColorRGBA = blendSaturation(a.color, b.color).rgba proc blendMask*(a, b: ColorRGBA): ColorRGBA = - blendMask(a.color, b.color).rgba + result.r = a.r + result.g = a.g + result.b = a.b + result.a = min(a.a, b.a) proc blendSubtractMask*(a, b: ColorRGBA): ColorRGBA = blendSubtractMask(a.color, b.color).rgba diff --git a/src/pixie/cairotest.nim b/src/pixie/cairotest.nim new file mode 100644 index 0000000..9905c41 --- /dev/null +++ b/src/pixie/cairotest.nim @@ -0,0 +1,28 @@ +import cairo, math, times + +var + surface = imageSurfaceCreate(FORMAT_ARGB32, 256, 256) + ctx = surface.create() + +let start = epochTime() + +ctx.setSourceRGB(0, 0, 1) +ctx.newPath() # current path is not consumed by ctx.clip() +ctx.rectangle(96, 96, 128, 128) +ctx.fill() + +ctx.setSourceRGB(0, 1, 0) +ctx.newPath() # current path is not consumed by ctx.clip() +ctx.rectangle(64, 64, 128, 128) +ctx.fill() + +for i in 0 .. 10000: + + ctx.setSourceRGB(1, 0, 0) + ctx.newPath() # current path is not consumed by ctx.clip() + ctx.rectangle(32, 32, 128, 128) + ctx.fill() + +echo epochTime() - start + +discard surface.writeToPng("cairotest.png") diff --git a/src/pixie/common.nim b/src/pixie/common.nim index e090fe6..9228e92 100644 --- a/src/pixie/common.nim +++ b/src/pixie/common.nim @@ -1,2 +1,34 @@ +import vmath + type PixieError* = object of ValueError ## Raised if an operation fails. + + Segment* = object + ## A math segment from point "at" to point "to" + at*: Vec2 + to*: Vec2 + +proc segment*(at, to: Vec2): Segment = + result.at = at + result.to = to + +proc intersects*(a, b: Segment, at: var Vec2): bool = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + var s1x, s1y, s2x, s2y: float32 + s1x = a.to.x - a.at.x + s1y = a.to.y - a.at.y + s2x = b.to.x - b.at.x + s2y = b.to.y - b.at.y + + var s, t: float32 + s = (-s1y * (a.at.x - b.at.x) + s1x * (a.at.y - b.at.y)) / + (-s2x * s1y + s1x * s2y) + t = (s2x * (a.at.y - b.at.y) - s2y * (a.at.x - b.at.x)) / + (-s2x * s1y + s1x * s2y) + + if s >= 0 and s < 1 and t >= 0 and t < 1: + at.x = a.at.x + (t * s1x) + at.y = a.at.y + (t * s1y) + return true + return false diff --git a/src/pixie/gputest.nim b/src/pixie/gputest.nim new file mode 100644 index 0000000..8b3b41d --- /dev/null +++ b/src/pixie/gputest.nim @@ -0,0 +1,57 @@ +import staticglfw, opengl, pixie, times + +if init() == 0: + raise newException(Exception, "Failed to Initialize GLFW") +windowHint(VISIBLE, false.cint) +var window = createWindow(512, 512, "GLFW3 WINDOW", nil, nil) +window.makeContextCurrent() +# This must be called to make any GL function work +loadExtensions() + + +let start = epochTime() + +# Draw red color screen. +glClearColor(1, 1, 1, 1) +glClear(GL_COLOR_BUFFER_BIT) + +glLoadIdentity() +glTranslatef(-0.25, -0.25, 0) +glBegin(GL_QUADS) +glColor3f(1.0, 0.0, 0.0) +glVertex2f(0.0, 0.0) +glVertex2f(1.0, 0.0) +glVertex2f(1.0, 1.0) +glVertex2f(0.0, 1.0) +glEnd() + +glTranslatef(-0.25, -0.25, 0) +glBegin(GL_QUADS) +glColor3f(0.0, 0.0, 1.0) +glVertex2f(0.0, 0.0) +glVertex2f(1.0, 0.0) +glVertex2f(1.0, 1.0) +glVertex2f(0.0, 1.0) +glEnd() + +glTranslatef(-0.25, -0.25, 0) +glBegin(GL_QUADS) +glColor3f(0.0, 1.0, 0.0) +glVertex2f(0.0, 0.0) +glVertex2f(1.0, 0.0) +glVertex2f(1.0, 1.0) +glVertex2f(0.0, 1.0) +glEnd() + +var screen = newImage(512, 512) +glReadPixels( + 0, 0, + 512, 512, + GL_RGBA, GL_UNSIGNED_BYTE, + screen.data[0].addr +) + +echo epochTime() - start + + +screen.writeFile("screen.png") diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 542f526..573e038 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -290,6 +290,7 @@ proc drawCorrect*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Image = stepX /= 2 stepY /= 2 minFilterBy2 /= 2 + matInv = matInv * scale(vec2(0.5, 0.5)) for y in 0 ..< a.height: for x in 0 ..< a.width: @@ -363,10 +364,11 @@ proc drawStepper*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Image = stepX /= 2 stepY /= 2 minFilterBy2 /= 2 + matInv = matInv * scale(vec2(0.5, 0.5)) template forBlend( mixer: proc(a, b: ColorRGBA): ColorRGBA, - getRgba: proc(a: Image, x, y: float32): ColorRGBA {.inline.}, + getRgbaFn: proc(a: Image, x, y: float32): ColorRGBA {.inline.}, ) = for y in 0 ..< a.height: var @@ -398,12 +400,11 @@ proc drawStepper*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Image = copyMem(result.getAddr(0, y), a.getAddr(0, y), 4*xMin) for x in xMin ..< xMax: - let srcV = start + stepX * float32(x) + stepY * float32(y) + let srcPos = start + stepX * float32(x) + stepY * float32(y) + #let srcPos = matInv * vec2(x.float32 + h, y.float32 + h) var rgba = a.getRgbaUnsafe(x, y) - # TODO maybe remove inside check? - if b.inside((srcV.x - h).int, (srcV.y - h).int): - let rgba2 = b.getRgba(srcV.x - h, srcV.y - h) - rgba = mixer(rgba, rgba2) + let rgba2 = b.getRgbaFn(srcPos.x - h, srcPos.y - h) + rgba = mixer(rgba, rgba2) result.setRgbaUnsafe(x, y, rgba) #for x in xMax ..< a.width: @@ -411,36 +412,38 @@ proc drawStepper*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Image = if a.width - xMax > 0: copyMem(result.getAddr(xMax, y), a.getAddr(xMax, y), 4*(a.width - xMax)) - proc getRgba(a: Image, x, y: float32): ColorRGBA {.inline.} = - a.getRgbaUnsafe(x.int, y.int) + proc getRgbaUnsafe(a: Image, x, y: float32): ColorRGBA {.inline.} = + a.getRgbaUnsafe(x.round.int, y.round.int) - # TODO check pos for fractional - if stepX.length == 1.0 and stepY.length == 1.0: + if stepX.length == 1.0 and stepY.length == 1.0 and + mat[2, 0].fractional == 0.0 and mat[2, 1].fractional == 0.0: + #echo "copy non-smooth" case blendMode - of bmNormal: forBlend(blendNormal, getRgba) - of bmDarken: forBlend(blendDarken, getRgba) - of bmMultiply: forBlend(blendMultiply, getRgba) - of bmLinearBurn: forBlend(blendLinearBurn, getRgba) - of bmColorBurn: forBlend(blendColorBurn, getRgba) - of bmLighten: forBlend(blendLighten, getRgba) - of bmScreen: forBlend(blendScreen, getRgba) - of bmLinearDodge: forBlend(blendLinearDodge, getRgba) - of bmColorDodge: forBlend(blendColorDodge, getRgba) - of bmOverlay: forBlend(blendOverlay, getRgba) - of bmSoftLight: forBlend(blendSoftLight, getRgba) - of bmHardLight: forBlend(blendHardLight, getRgba) - of bmDifference: forBlend(blendDifference, getRgba) - of bmExclusion: forBlend(blendExclusion, getRgba) - of bmHue: forBlend(blendHue, getRgba) - of bmSaturation: forBlend(blendSaturation, getRgba) - of bmColor: forBlend(blendColor, getRgba) - of bmLuminosity: forBlend(blendLuminosity, getRgba) - of bmMask: forBlend(blendMask, getRgba) - of bmOverwrite: forBlend(blendOverwrite, getRgba) - of bmSubtractMask: forBlend(blendSubtractMask, getRgba) - of bmIntersectMask: forBlend(blendIntersectMask, getRgba) - of bmExcludeMask: forBlend(blendExcludeMask, getRgba) + of bmNormal: forBlend(blendNormal, getRgbaUnsafe) + of bmDarken: forBlend(blendDarken, getRgbaUnsafe) + of bmMultiply: forBlend(blendMultiply, getRgbaUnsafe) + of bmLinearBurn: forBlend(blendLinearBurn, getRgbaUnsafe) + of bmColorBurn: forBlend(blendColorBurn, getRgbaUnsafe) + of bmLighten: forBlend(blendLighten, getRgbaUnsafe) + of bmScreen: forBlend(blendScreen, getRgbaUnsafe) + of bmLinearDodge: forBlend(blendLinearDodge, getRgbaUnsafe) + of bmColorDodge: forBlend(blendColorDodge, getRgbaUnsafe) + of bmOverlay: forBlend(blendOverlay, getRgbaUnsafe) + of bmSoftLight: forBlend(blendSoftLight, getRgbaUnsafe) + of bmHardLight: forBlend(blendHardLight, getRgbaUnsafe) + of bmDifference: forBlend(blendDifference, getRgbaUnsafe) + of bmExclusion: forBlend(blendExclusion, getRgbaUnsafe) + of bmHue: forBlend(blendHue, getRgbaUnsafe) + of bmSaturation: forBlend(blendSaturation, getRgbaUnsafe) + of bmColor: forBlend(blendColor, getRgbaUnsafe) + of bmLuminosity: forBlend(blendLuminosity, getRgbaUnsafe) + of bmMask: forBlend(blendMask, getRgbaUnsafe) + of bmOverwrite: forBlend(blendOverwrite, getRgbaUnsafe) + of bmSubtractMask: forBlend(blendSubtractMask, getRgbaUnsafe) + of bmIntersectMask: forBlend(blendIntersectMask, getRgbaUnsafe) + of bmExcludeMask: forBlend(blendExcludeMask, getRgbaUnsafe) else: + #echo "copy smooth" case blendMode of bmNormal: forBlend(blendNormal, getRgbaSmooth) of bmDarken: forBlend(blendDarken, getRgbaSmooth) @@ -479,13 +482,175 @@ proc draw*(a: Image, b: Image, mat: Mat3, blendMode = bmNormal): Image = # else: # return drawBlend(a, b, mat, blendMode) - # return drawCorrect(a, b, mat, blendMode) + #return drawCorrect(a, b, mat, blendMode) return drawStepper(a, b, mat, blendMode) proc draw*(a: Image, b: Image, pos = vec2(0, 0), blendMode = bmNormal): Image = a.draw(b, translate(pos), blendMode) +proc drawInPlace*(a: Image, b: Image, mat: Mat3, blendMode = bmNormal) = + ## Draws one image onto another using matrix with color blending. + + type Segment = object + ## A math segment from point "at" to point "to" + at*: Vec2 + to*: Vec2 + + proc segment(at, to: Vec2): Segment = + result.at = at + result.to = to + + proc intersects(a, b: Segment, at: var Vec2): bool = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + var s1x, s1y, s2x, s2y: float32 + s1x = a.to.x - a.at.x + s1y = a.to.y - a.at.y + s2x = b.to.x - b.at.x + s2y = b.to.y - b.at.y + + var s, t: float32 + s = (-s1y * (a.at.x - b.at.x) + s1x * (a.at.y - b.at.y)) / + (-s2x * s1y + s1x * s2y) + t = (s2x * (a.at.y - b.at.y) - s2y * (a.at.x - b.at.x)) / + (-s2x * s1y + s1x * s2y) + + if s >= 0 and s < 1 and t >= 0 and t < 1: + at.x = a.at.x + (t * s1x) + at.y = a.at.y + (t * s1y) + return true + return false + + var + matInv = mat.inverse() + # compute movement vectors + h = 0.5.float32 + start = matInv * vec2(0 + h, 0 + h) + stepX = matInv * vec2(1 + h, 0 + h) - start + stepY = matInv * vec2(0 + h, 1 + h) - start + minFilterBy2 = max(stepX.length, stepY.length) + b = b + + let corners = [ + mat * vec2(0, 0), + mat * vec2(b.width.float32, 0), + mat * vec2(b.width.float32, b.height.float32), + mat * vec2(0, b.height.float32) + ] + + let lines = [ + segment(corners[0], corners[1]), + segment(corners[1], corners[2]), + segment(corners[2], corners[3]), + segment(corners[3], corners[0]) + ] + + while minFilterBy2 > 2.0: + b = b.minifyBy2() + start /= 2 + stepX /= 2 + stepY /= 2 + minFilterBy2 /= 2 + matInv = matInv * scale(vec2(0.5, 0.5)) + + template forBlend( + mixer: proc(a, b: ColorRGBA): ColorRGBA, + getRgbaFn: proc(a: Image, x, y: float32): ColorRGBA {.inline.}, + ) = + for y in 0 ..< a.height: + var + xMin = 0 + xMax = 0 + hasIntersection = false + for yOffset in [0.float32, 1]: + var scanLine = segment( + vec2(-100000, y.float32 + yOffset), + vec2(10000, y.float32 + yOffset) + ) + for l in lines: + var at: Vec2 + if intersects(l, scanLine, at): + if hasIntersection: + xMin = min(xMin, at.x.floor.int) + xMax = max(xMax, at.x.ceil.int) + else: + hasIntersection = true + xMin = at.x.floor.int + xMax = at.x.ceil.int + + xMin = xMin.clamp(0, a.width) + xMax = xMax.clamp(0, a.width) + + + for x in xMin ..< xMax: + let srcPos = start + stepX * float32(x) + stepY * float32(y) + #let srcPos = matInv * vec2(x.float32 + h, y.float32 + h) + var rgba = a.getRgbaUnsafe(x, y) + let rgba2 = b.getRgbaFn(srcPos.x - h, srcPos.y - h) + rgba = mixer(rgba, rgba2) + a.setRgbaUnsafe(x, y, rgba) + + proc getRgbaUnsafe(a: Image, x, y: float32): ColorRGBA {.inline.} = + a.getRgbaUnsafe(x.round.int, y.round.int) + + if stepX.length == 1.0 and stepY.length == 1.0 and + mat[2, 0].fractional == 0.0 and mat[2, 1].fractional == 0.0: + #echo "inplace non-smooth" + case blendMode + of bmNormal: forBlend(blendNormal, getRgbaUnsafe) + of bmDarken: forBlend(blendDarken, getRgbaUnsafe) + of bmMultiply: forBlend(blendMultiply, getRgbaUnsafe) + of bmLinearBurn: forBlend(blendLinearBurn, getRgbaUnsafe) + of bmColorBurn: forBlend(blendColorBurn, getRgbaUnsafe) + of bmLighten: forBlend(blendLighten, getRgbaUnsafe) + of bmScreen: forBlend(blendScreen, getRgbaUnsafe) + of bmLinearDodge: forBlend(blendLinearDodge, getRgbaUnsafe) + of bmColorDodge: forBlend(blendColorDodge, getRgbaUnsafe) + of bmOverlay: forBlend(blendOverlay, getRgbaUnsafe) + of bmSoftLight: forBlend(blendSoftLight, getRgbaUnsafe) + of bmHardLight: forBlend(blendHardLight, getRgbaUnsafe) + of bmDifference: forBlend(blendDifference, getRgbaUnsafe) + of bmExclusion: forBlend(blendExclusion, getRgbaUnsafe) + of bmHue: forBlend(blendHue, getRgbaUnsafe) + of bmSaturation: forBlend(blendSaturation, getRgbaUnsafe) + of bmColor: forBlend(blendColor, getRgbaUnsafe) + of bmLuminosity: forBlend(blendLuminosity, getRgbaUnsafe) + of bmMask: forBlend(blendMask, getRgbaUnsafe) + of bmOverwrite: forBlend(blendOverwrite, getRgbaUnsafe) + of bmSubtractMask: forBlend(blendSubtractMask, getRgbaUnsafe) + of bmIntersectMask: forBlend(blendIntersectMask, getRgbaUnsafe) + of bmExcludeMask: forBlend(blendExcludeMask, getRgbaUnsafe) + else: + #echo "inplace smooth" + case blendMode + of bmNormal: forBlend(blendNormal, getRgbaSmooth) + of bmDarken: forBlend(blendDarken, getRgbaSmooth) + of bmMultiply: forBlend(blendMultiply, getRgbaSmooth) + of bmLinearBurn: forBlend(blendLinearBurn, getRgbaSmooth) + of bmColorBurn: forBlend(blendColorBurn, getRgbaSmooth) + of bmLighten: forBlend(blendLighten, getRgbaSmooth) + of bmScreen: forBlend(blendScreen, getRgbaSmooth) + of bmLinearDodge: forBlend(blendLinearDodge, getRgbaSmooth) + of bmColorDodge: forBlend(blendColorDodge, getRgbaSmooth) + of bmOverlay: forBlend(blendOverlay, getRgbaSmooth) + of bmSoftLight: forBlend(blendSoftLight, getRgbaSmooth) + of bmHardLight: forBlend(blendHardLight, getRgbaSmooth) + of bmDifference: forBlend(blendDifference, getRgbaSmooth) + of bmExclusion: forBlend(blendExclusion, getRgbaSmooth) + of bmHue: forBlend(blendHue, getRgbaSmooth) + of bmSaturation: forBlend(blendSaturation, getRgbaSmooth) + of bmColor: forBlend(blendColor, getRgbaSmooth) + of bmLuminosity: forBlend(blendLuminosity, getRgbaSmooth) + of bmMask: forBlend(blendMask, getRgbaSmooth) + of bmOverwrite: forBlend(blendOverwrite, getRgbaSmooth) + of bmSubtractMask: forBlend(blendSubtractMask, getRgbaSmooth) + of bmIntersectMask: forBlend(blendIntersectMask, getRgbaSmooth) + of bmExcludeMask: forBlend(blendExcludeMask, getRgbaSmooth) + +proc drawInPlace*(a: Image, b: Image, pos = vec2(0, 0), blendMode = bmNormal) = + a.drawInPlace(b, translate(pos), blendMode) + proc blur*(image: Image, radius: float32): Image = ## Applies Gaussian blur to the image given a radius. let radius = (radius).int diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index e12a174..c493832 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1,10 +1,6 @@ -import vmath, images, chroma, strutils, algorithm +import vmath, images, chroma, strutils, algorithm, common type - Segment* = object - ## A math segment from point "at" to point "to" - at*: Vec2 - to*: Vec2 PathCommandKind* = enum ## Type of path commands @@ -21,10 +17,6 @@ type at*: Vec2 commands*: seq[PathCommand] -proc segment(at, to: Vec2): Segment = - result.at = at - result.to = to - proc newPath*(): Path = result = Path() @@ -503,27 +495,6 @@ iterator zipwise*[T](s: seq[T]): (T, T) = if s.len > 0: yield(s[^1], s[0]) -proc intersects*(a, b: Segment, at: var Vec2): bool = - ## Checks if the a segment intersects b segment. - ## If it returns true, at will have point of intersection - var s1x, s1y, s2x, s2y: float32 - s1x = a.to.x - a.at.x - s1y = a.to.y - a.at.y - s2x = b.to.x - b.at.x - s2y = b.to.y - b.at.y - - var s, t: float32 - s = (-s1y * (a.at.x - b.at.x) + s1x * (a.at.y - b.at.y)) / - (-s2x * s1y + s1x * s2y) - t = (s2x * (a.at.y - b.at.y) - s2y * (a.at.x - b.at.x)) / - (-s2x * s1y + s1x * s2y) - - if s >= 0 and s < 1 and t >= 0 and t < 1: - at.x = a.at.x + (t * s1x) - at.y = a.at.y + (t * s1y) - return true - return false - proc strokePolygons*(ps: seq[seq[Vec2]], strokeWidthR, strokeWidthL: float32): seq[seq[Vec2]] = ## Converts simple polygons into stroked versions: # TODO: Stroke location, add caps and joins. diff --git a/tests/benchmark_draw.nim b/tests/benchmark_draw.nim index 8e5bd22..b7ade6b 100644 --- a/tests/benchmark_draw.nim +++ b/tests/benchmark_draw.nim @@ -77,6 +77,16 @@ timeIt "drawStepper bmNormal": c.writeFile("tests/images/bench.drawStepper.bmNormal.png") echo tmp +timeIt "drawInPlace bmNormal": + var tmp = 0 + for i in 0 ..< 1000: + var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) + a.drawInPlace(b, translate(vec2(25, 25)), bmNormal) + tmp += a.width * a.height + #a.writeFile("tests/images/bench.drawStepper.bmNormal.png") + echo tmp + # timeIt "drawBlend bmSaturation": # var tmp = 0 # var c: Image