Merge pull request #209 from guzba/master
svg improvements, emoji megatest, ctx blend test, remove seq[seq[Vec2]] from SomePath
|
@ -36,6 +36,13 @@ proc initCtx(): Ctx =
|
||||||
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
result = inherited
|
result = inherited
|
||||||
|
|
||||||
|
proc splitArgs(s: string): seq[string] =
|
||||||
|
# Handles (1,1) or (1 1) or (1, 1) or (1,1 2,2) etc
|
||||||
|
let tmp = s.replace(',', ' ').split(' ')
|
||||||
|
for entry in tmp:
|
||||||
|
if entry.len > 0:
|
||||||
|
result.add(entry)
|
||||||
|
|
||||||
var
|
var
|
||||||
fillRule = node.attr("fill-rule")
|
fillRule = node.attr("fill-rule")
|
||||||
fill = node.attr("fill")
|
fill = node.attr("fill")
|
||||||
|
@ -157,7 +164,7 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
if strokeDashArray == "":
|
if strokeDashArray == "":
|
||||||
discard
|
discard
|
||||||
else:
|
else:
|
||||||
var values = strokeDashArray.replace(',', ' ').split(' ')
|
var values = splitArgs(strokeDashArray)
|
||||||
for value in values:
|
for value in values:
|
||||||
result.strokeDashArray.add(parseFloat(value))
|
result.strokeDashArray.add(parseFloat(value))
|
||||||
|
|
||||||
|
@ -178,55 +185,46 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
remaining = remaining[index + 1 .. ^1]
|
remaining = remaining[index + 1 .. ^1]
|
||||||
|
|
||||||
if f.startsWith("matrix("):
|
if f.startsWith("matrix("):
|
||||||
let arr =
|
let arr = splitArgs(f[7 .. ^2])
|
||||||
if f.contains(","):
|
|
||||||
f[7 .. ^2].split(",")
|
|
||||||
else:
|
|
||||||
f[7 .. ^2].split(" ")
|
|
||||||
if arr.len != 6:
|
if arr.len != 6:
|
||||||
failInvalidTransform(transform)
|
failInvalidTransform(transform)
|
||||||
var m = mat3()
|
var m = mat3()
|
||||||
m[0, 0] = parseFloat(arr[0].strip())
|
m[0, 0] = parseFloat(arr[0])
|
||||||
m[0, 1] = parseFloat(arr[1].strip())
|
m[0, 1] = parseFloat(arr[1])
|
||||||
m[1, 0] = parseFloat(arr[2].strip())
|
m[1, 0] = parseFloat(arr[2])
|
||||||
m[1, 1] = parseFloat(arr[3].strip())
|
m[1, 1] = parseFloat(arr[3])
|
||||||
m[2, 0] = parseFloat(arr[4].strip())
|
m[2, 0] = parseFloat(arr[4])
|
||||||
m[2, 1] = parseFloat(arr[5].strip())
|
m[2, 1] = parseFloat(arr[5])
|
||||||
result.transform = result.transform * m
|
result.transform = result.transform * m
|
||||||
elif f.startsWith("translate("):
|
elif f.startsWith("translate("):
|
||||||
let
|
let
|
||||||
components = f[10 .. ^2].split(" ")
|
components = splitArgs(f[10 .. ^2])
|
||||||
tx = parseFloat(components[0].strip())
|
tx = parseFloat(components[0])
|
||||||
ty =
|
ty =
|
||||||
if components[1].len == 0:
|
if components.len == 1:
|
||||||
0.0
|
0.0
|
||||||
else:
|
else:
|
||||||
parseFloat(components[1].strip())
|
parseFloat(components[1])
|
||||||
result.transform = result.transform * translate(vec2(tx, ty))
|
result.transform = result.transform * translate(vec2(tx, ty))
|
||||||
elif f.startsWith("rotate("):
|
elif f.startsWith("rotate("):
|
||||||
let
|
let
|
||||||
values = f[7 .. ^2].split(" ")
|
values = splitArgs(f[7 .. ^2])
|
||||||
angle: float32 = parseFloat(values[0].strip()) * -PI / 180
|
angle: float32 = parseFloat(values[0]) * -PI / 180
|
||||||
var cx, cy: float32
|
var cx, cy: float32
|
||||||
if values.len > 1:
|
if values.len > 1:
|
||||||
cx = parseFloat(values[1].strip())
|
cx = parseFloat(values[1])
|
||||||
if values.len > 2:
|
if values.len > 2:
|
||||||
cy = parseFloat(values[2].strip())
|
cy = parseFloat(values[2])
|
||||||
let center = vec2(cx, cy)
|
let center = vec2(cx, cy)
|
||||||
result.transform = result.transform *
|
result.transform = result.transform *
|
||||||
translate(center) * rotate(angle) * translate(-center)
|
translate(center) * rotate(angle) * translate(-center)
|
||||||
elif f.startsWith("scale("):
|
elif f.startsWith("scale("):
|
||||||
let
|
let
|
||||||
values =
|
values = splitArgs(f[6 .. ^2])
|
||||||
if f.contains(","):
|
sx: float32 = parseFloat(values[0])
|
||||||
f[6 .. ^2].split(",")
|
|
||||||
else:
|
|
||||||
f[6 .. ^2].split(" ")
|
|
||||||
let
|
|
||||||
sx: float32 = parseFloat(values[0].strip())
|
|
||||||
sy: float32 =
|
sy: float32 =
|
||||||
if values.len > 1:
|
if values.len > 1:
|
||||||
parseFloat(values[1].strip())
|
parseFloat(values[1])
|
||||||
else:
|
else:
|
||||||
sx
|
sx
|
||||||
result.transform = result.transform * scale(vec2(sx, sy))
|
result.transform = result.transform * scale(vec2(sx, sy))
|
||||||
|
@ -242,6 +240,8 @@ proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} =
|
||||||
ctx.stroke,
|
ctx.stroke,
|
||||||
ctx.transform,
|
ctx.transform,
|
||||||
ctx.strokeWidth,
|
ctx.strokeWidth,
|
||||||
|
ctx.strokeLineCap,
|
||||||
|
ctx.strokeLineJoin,
|
||||||
miterLimit = ctx.strokeMiterLimit,
|
miterLimit = ctx.strokeMiterLimit,
|
||||||
dashes = ctx.strokeDashArray
|
dashes = ctx.strokeDashArray
|
||||||
)
|
)
|
||||||
|
@ -330,8 +330,11 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
ctx = decodeCtx(ctxStack[^1], node)
|
||||||
x = parseFloat(node.attrOrDefault("x", "0"))
|
x = parseFloat(node.attrOrDefault("x", "0"))
|
||||||
y = parseFloat(node.attrOrDefault("y", "0"))
|
y = parseFloat(node.attrOrDefault("y", "0"))
|
||||||
width = parseFloat(node.attr("width"))
|
width = parseFloat(node.attrOrDefault("width", "0"))
|
||||||
height = parseFloat(node.attr("height"))
|
height = parseFloat(node.attrOrDefault("height", "0"))
|
||||||
|
|
||||||
|
if width == 0 or height == 0:
|
||||||
|
return
|
||||||
|
|
||||||
var
|
var
|
||||||
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
|
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
|
||||||
|
|
|
@ -25,11 +25,11 @@ type
|
||||||
font*: Font
|
font*: Font
|
||||||
|
|
||||||
Arrangement* = ref object
|
Arrangement* = ref object
|
||||||
spans*: seq[(int, int)]
|
spans*: seq[(int, int)] ## The (start, stop) of the spans in the text.
|
||||||
fonts*: seq[Font]
|
fonts*: seq[Font] ## The font for each span.
|
||||||
runes*: seq[Rune]
|
runes*: seq[Rune] ## The runes of the text.
|
||||||
positions*: seq[Vec2]
|
positions*: seq[Vec2] ## The positions of the glyphs for each rune.
|
||||||
selectionRects*: seq[Rect]
|
selectionRects*: seq[Rect] ## The selection rects for each glyph.
|
||||||
|
|
||||||
HAlignMode* = enum
|
HAlignMode* = enum
|
||||||
haLeft
|
haLeft
|
||||||
|
@ -110,6 +110,7 @@ proc defaultLineHeight*(font: Font): float32 {.inline.} =
|
||||||
round(fontUnits * font.scale)
|
round(fontUnits * font.scale)
|
||||||
|
|
||||||
proc newSpan*(text: string, font: Font): Span =
|
proc newSpan*(text: string, font: Font): Span =
|
||||||
|
## Creates a span, associating a font with the text.
|
||||||
result = Span()
|
result = Span()
|
||||||
result.text = text
|
result.text = text
|
||||||
result.font = font
|
result.font = font
|
||||||
|
@ -369,6 +370,7 @@ proc typeset*(
|
||||||
typeset(@[newSpan(text, font)], bounds, hAlign, vAlign, wrap)
|
typeset(@[newSpan(text, font)], bounds, hAlign, vAlign, wrap)
|
||||||
|
|
||||||
proc computeBounds*(arrangement: Arrangement): Vec2 =
|
proc computeBounds*(arrangement: Arrangement): Vec2 =
|
||||||
|
## Computes the width and height of the arrangement in pixels.
|
||||||
if arrangement.runes.len > 0:
|
if arrangement.runes.len > 0:
|
||||||
for i in 0 ..< arrangement.runes.len:
|
for i in 0 ..< arrangement.runes.len:
|
||||||
if arrangement.runes[i] != LF:
|
if arrangement.runes[i] != LF:
|
||||||
|
@ -382,6 +384,7 @@ proc computeBounds*(font: Font, text: string): Vec2 {.inline.} =
|
||||||
font.typeset(text).computeBounds()
|
font.typeset(text).computeBounds()
|
||||||
|
|
||||||
proc computeBounds*(spans: seq[Span]): Vec2 {.inline.} =
|
proc computeBounds*(spans: seq[Span]): Vec2 {.inline.} =
|
||||||
|
## Computes the width and height of the spans in pixels.
|
||||||
typeset(spans).computeBounds()
|
typeset(spans).computeBounds()
|
||||||
|
|
||||||
proc parseOtf*(buf: string): Font =
|
proc parseOtf*(buf: string): Font =
|
||||||
|
|
|
@ -34,7 +34,7 @@ type
|
||||||
commands*: seq[PathCommand]
|
commands*: seq[PathCommand]
|
||||||
start, at: Vec2 # Maintained by moveTo, lineTo, etc. Used by arcTo.
|
start, at: Vec2 # Maintained by moveTo, lineTo, etc. Used by arcTo.
|
||||||
|
|
||||||
SomePath* = Path | string | seq[seq[Vec2]]
|
SomePath* = Path | string
|
||||||
|
|
||||||
const
|
const
|
||||||
epsilon = 0.0001 * PI ## Tiny value used for some computations.
|
epsilon = 0.0001 * PI ## Tiny value used for some computations.
|
||||||
|
@ -1164,14 +1164,6 @@ proc fillShapes(
|
||||||
stopY = min(image.height, (bounds.y + bounds.h).int)
|
stopY = min(image.height, (bounds.y + bounds.h).int)
|
||||||
blender = blendMode.blender()
|
blender = blendMode.blender()
|
||||||
|
|
||||||
when defined(amd64) and not defined(pixieNoSimd):
|
|
||||||
let
|
|
||||||
blenderSimd = blendMode.blenderSimd()
|
|
||||||
first32 = cast[M128i]([uint32.high, 0, 0, 0]) # First 32 bits
|
|
||||||
oddMask = mm_set1_epi16(cast[int16](0xff00))
|
|
||||||
div255 = mm_set1_epi16(cast[int16](0x8081))
|
|
||||||
vColor = mm_set1_epi32(cast[int32](rgbx))
|
|
||||||
|
|
||||||
var
|
var
|
||||||
coverages = newSeq[uint8](image.width)
|
coverages = newSeq[uint8](image.width)
|
||||||
hits = newSeq[(float32, int16)](4)
|
hits = newSeq[(float32, int16)](4)
|
||||||
|
@ -1190,51 +1182,58 @@ proc fillShapes(
|
||||||
# Apply the coverage and blend
|
# Apply the coverage and blend
|
||||||
var x = startX
|
var x = startX
|
||||||
when defined(amd64) and not defined(pixieNoSimd):
|
when defined(amd64) and not defined(pixieNoSimd):
|
||||||
# When supported, SIMD blend as much as possible
|
if blendMode.hasSimdBlender():
|
||||||
for _ in countup(x, image.width - 16, 4):
|
# When supported, SIMD blend as much as possible
|
||||||
var coverage = mm_loadu_si128(coverages[x].addr)
|
|
||||||
coverage = mm_and_si128(coverage, first32)
|
|
||||||
|
|
||||||
let
|
let
|
||||||
index = image.dataIndex(x, y)
|
blenderSimd = blendMode.blenderSimd()
|
||||||
eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128())
|
first32 = cast[M128i]([uint32.high, 0, 0, 0]) # First 32 bits
|
||||||
if mm_movemask_epi8(eqZero) != 0xffff:
|
oddMask = mm_set1_epi16(cast[int16](0xff00))
|
||||||
# If the coverages are not all zero
|
div255 = mm_set1_epi16(cast[int16](0x8081))
|
||||||
if mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff:
|
vColor = mm_set1_epi32(cast[int32](rgbx))
|
||||||
# Coverages are all 255
|
for _ in countup(x, image.width - 16, 4):
|
||||||
if rgbx.a == 255 and blendMode == bmNormal:
|
var coverage = mm_loadu_si128(coverages[x].addr)
|
||||||
mm_storeu_si128(image.data[index].addr, vColor)
|
coverage = mm_and_si128(coverage, first32)
|
||||||
|
|
||||||
|
let
|
||||||
|
index = image.dataIndex(x, y)
|
||||||
|
eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128())
|
||||||
|
if mm_movemask_epi8(eqZero) != 0xffff:
|
||||||
|
# If the coverages are not all zero
|
||||||
|
if mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff:
|
||||||
|
# Coverages are all 255
|
||||||
|
if rgbx.a == 255 and blendMode == bmNormal:
|
||||||
|
mm_storeu_si128(image.data[index].addr, vColor)
|
||||||
|
else:
|
||||||
|
let backdrop = mm_loadu_si128(image.data[index].addr)
|
||||||
|
mm_storeu_si128(
|
||||||
|
image.data[index].addr,
|
||||||
|
blenderSimd(backdrop, vColor)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
# Coverages are not all 255
|
||||||
|
coverage = unpackAlphaValues(coverage)
|
||||||
|
# Shift the coverages from `a` to `g` and `a` for multiplying
|
||||||
|
coverage = mm_or_si128(coverage, mm_srli_epi32(coverage, 16))
|
||||||
|
|
||||||
|
var
|
||||||
|
source = vColor
|
||||||
|
sourceEven = mm_slli_epi16(source, 8)
|
||||||
|
sourceOdd = mm_and_si128(source, oddMask)
|
||||||
|
|
||||||
|
sourceEven = mm_mulhi_epu16(sourceEven, coverage)
|
||||||
|
sourceOdd = mm_mulhi_epu16(sourceOdd, coverage)
|
||||||
|
|
||||||
|
sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7)
|
||||||
|
sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7)
|
||||||
|
|
||||||
|
source = mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8))
|
||||||
|
|
||||||
let backdrop = mm_loadu_si128(image.data[index].addr)
|
let backdrop = mm_loadu_si128(image.data[index].addr)
|
||||||
mm_storeu_si128(
|
mm_storeu_si128(
|
||||||
image.data[index].addr,
|
image.data[index].addr,
|
||||||
blenderSimd(backdrop, vColor)
|
blenderSimd(backdrop, source)
|
||||||
)
|
)
|
||||||
else:
|
x += 4
|
||||||
# Coverages are not all 255
|
|
||||||
coverage = unpackAlphaValues(coverage)
|
|
||||||
# Shift the coverages from `a` to `g` and `a` for multiplying
|
|
||||||
coverage = mm_or_si128(coverage, mm_srli_epi32(coverage, 16))
|
|
||||||
|
|
||||||
var
|
|
||||||
source = vColor
|
|
||||||
sourceEven = mm_slli_epi16(source, 8)
|
|
||||||
sourceOdd = mm_and_si128(source, oddMask)
|
|
||||||
|
|
||||||
sourceEven = mm_mulhi_epu16(sourceEven, coverage)
|
|
||||||
sourceOdd = mm_mulhi_epu16(sourceOdd, coverage)
|
|
||||||
|
|
||||||
sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7)
|
|
||||||
sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7)
|
|
||||||
|
|
||||||
source = mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8))
|
|
||||||
|
|
||||||
let backdrop = mm_loadu_si128(image.data[index].addr)
|
|
||||||
mm_storeu_si128(
|
|
||||||
image.data[index].addr,
|
|
||||||
blenderSimd(backdrop, source)
|
|
||||||
)
|
|
||||||
x += 4
|
|
||||||
|
|
||||||
while x < image.width:
|
while x < image.width:
|
||||||
if x + 8 <= coverages.len:
|
if x + 8 <= coverages.len:
|
||||||
|
@ -1328,11 +1327,11 @@ proc fillShapes(
|
||||||
inc x
|
inc x
|
||||||
|
|
||||||
proc miterLimitToAngle*(limit: float32): float32 =
|
proc miterLimitToAngle*(limit: float32): float32 =
|
||||||
## Converts milter-limit-ratio to miter-limit-angle.
|
## Converts miter-limit-ratio to miter-limit-angle.
|
||||||
arcsin(1 / limit) * 2
|
arcsin(1 / limit) * 2
|
||||||
|
|
||||||
proc angleToMiterLimit*(angle: float32): float32 =
|
proc angleToMiterLimit*(angle: float32): float32 =
|
||||||
## Converts miter-limit-angle to milter-limit-ratio.
|
## Converts miter-limit-angle to miter-limit-ratio.
|
||||||
1 / sin(angle / 2)
|
1 / sin(angle / 2)
|
||||||
|
|
||||||
proc strokeShapes(
|
proc strokeShapes(
|
||||||
|
@ -1528,11 +1527,7 @@ proc fillPath*(
|
||||||
mask = newMask(image.width, image.height)
|
mask = newMask(image.width, image.height)
|
||||||
fill = newImage(image.width, image.height)
|
fill = newImage(image.width, image.height)
|
||||||
|
|
||||||
mask.fillPath(
|
mask.fillPath(path, transform, windingRule)
|
||||||
parseSomePath(path, transform.pixelScale()),
|
|
||||||
transform,
|
|
||||||
windingRule
|
|
||||||
)
|
|
||||||
|
|
||||||
case paint.kind:
|
case paint.kind:
|
||||||
of pkSolid:
|
of pkSolid:
|
||||||
|
@ -1604,7 +1599,7 @@ proc strokePath*(
|
||||||
fill = newImage(image.width, image.height)
|
fill = newImage(image.width, image.height)
|
||||||
|
|
||||||
mask.strokePath(
|
mask.strokePath(
|
||||||
parseSomePath(path, transform.pixelScale()),
|
path,
|
||||||
transform,
|
transform,
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
lineCap,
|
lineCap,
|
||||||
|
|
59
tests/emoji_megatest.nim
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import cligen, os, pixie, pixie/fileformats/svg, strformat
|
||||||
|
|
||||||
|
# Clone https://github.com/twitter/twemoji
|
||||||
|
# Check out commit 59cb0eacce837d0f5de30223bd8f530e447f547a
|
||||||
|
|
||||||
|
# Clone https://github.com/hfg-gmuend/openmoji
|
||||||
|
# Check out commit c1f14ae0be29b20c7eed215d1e03df23b1c9a5d5
|
||||||
|
|
||||||
|
type EmojiSet = object
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
|
||||||
|
const
|
||||||
|
emojiSets = [
|
||||||
|
EmojiSet(name: "twemoji", path: "../twemoji/assets/svg/*"),
|
||||||
|
EmojiSet(name: "openmoji", path: "../openmoji/color/svg/*")
|
||||||
|
]
|
||||||
|
width = 32
|
||||||
|
height = 32
|
||||||
|
|
||||||
|
proc renderEmojiSet(index: int) =
|
||||||
|
let emojiSet = emojiSets[index]
|
||||||
|
|
||||||
|
var images: seq[(string, Image)]
|
||||||
|
|
||||||
|
for filePath in walkFiles(emojiSet.path):
|
||||||
|
let (_, name, _) = splitFile(filePath)
|
||||||
|
var image: Image
|
||||||
|
try:
|
||||||
|
image = decodeSvg(readFile(filePath), width, height)
|
||||||
|
except PixieError:
|
||||||
|
echo &"Failed decoding {name}"
|
||||||
|
image = newImage(width, height)
|
||||||
|
images.add((name, image))
|
||||||
|
|
||||||
|
let
|
||||||
|
columns = 40
|
||||||
|
rows = (images.len + columns - 1) div columns
|
||||||
|
rendered = newImage((width + 4) * columns, (height + 4) * rows)
|
||||||
|
|
||||||
|
for i in 0 ..< rows:
|
||||||
|
for j in 0 ..< max(images.len - i * columns, 0):
|
||||||
|
let (_, icon) = images[i * columns + j]
|
||||||
|
rendered.draw(
|
||||||
|
icon,
|
||||||
|
vec2(((width + 4) * j + 2).float32, ((height + 4) * i + 2).float32),
|
||||||
|
bmOverwrite
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered.writeFile(&"tests/images/svg/{emojiSet.name}.png")
|
||||||
|
|
||||||
|
proc main(index = -1) =
|
||||||
|
if index >= 0:
|
||||||
|
renderEmojiSet(index)
|
||||||
|
else:
|
||||||
|
for i in 0 ..< emojiSets.len:
|
||||||
|
renderEmojiSet(i)
|
||||||
|
|
||||||
|
dispatch(main)
|
BIN
tests/images/context/blendmode_1.png
Normal file
After Width: | Height: | Size: 880 B |
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 280 KiB |
Before Width: | Height: | Size: 636 KiB After Width: | Height: | Size: 642 KiB |
BIN
tests/images/svg/openmoji.png
Normal file
After Width: | Height: | Size: 3.3 MiB |
Before Width: | Height: | Size: 594 KiB After Width: | Height: | Size: 611 KiB |
BIN
tests/images/svg/twemoji.png
Normal file
After Width: | Height: | Size: 3.9 MiB |
|
@ -515,3 +515,19 @@ block:
|
||||||
drawDashedLine(@[12.float32, 3, 3]);
|
drawDashedLine(@[12.float32, 3, 3]);
|
||||||
|
|
||||||
image.writeFile("tests/images/context/setLineDash_1.png")
|
image.writeFile("tests/images/context/setLineDash_1.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(300, 150)
|
||||||
|
ctx = newContext(image)
|
||||||
|
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
|
||||||
|
var paint = Paint(kind: pkSolid, color: rgba(0, 0, 255, 255))
|
||||||
|
paint.blendMode = bmExclusion
|
||||||
|
|
||||||
|
ctx.fillStyle = paint
|
||||||
|
|
||||||
|
ctx.fillRect(10, 10, 100, 100)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/context/blendmode_1.png")
|
||||||
|
|