diff --git a/lib/pxl.dart b/lib/pxl.dart index 0ebb201..08f0071 100644 --- a/lib/pxl.dart +++ b/lib/pxl.dart @@ -1,3 +1,4 @@ +export 'package:pxl/src/blend.dart'; export 'package:pxl/src/buffer.dart'; export 'package:pxl/src/format.dart'; export 'package:pxl/src/geometry.dart'; diff --git a/lib/src/blend.dart b/lib/src/blend.dart index a4c5838..12a884e 100644 --- a/lib/src/blend.dart +++ b/lib/src/blend.dart @@ -1 +1,122 @@ -abstract interface class BlendMode {} +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import 'package:pxl/src/format.dart'; +import 'package:pxl/src/internal.dart'; + +part 'blend/porter_duff.dart'; + +/// Algorithms to use when painting on the canvas. +/// +/// When drawing a shape or image onto a canvas, different algorithms can be +/// used to blend the pixels. A custom [BlendMode] can be used to implement +/// custom blending algorithms, or one of the predefined [BlendMode]s can be +/// used. +/// +/// Each algorithm takes two colors as input, the _source_ color, which is the +/// color being drawn, and the _destination_ color, which is the color already +/// on the canvas. The algorithm then returns a new color that is the result of +/// blending the two colors. +/// +/// ## SIMD +/// +/// Some blend modes can optionally use [SIMD optimizations][] by setting the +/// `pxl.SIMD` Dart compilation environment variable: +/// +/// [SIMD optimizations]: https://en.wikipedia.org/wiki/SIMD +/// +/// ```sh +/// dart compile -Dpxl.SIMD=true +/// ``` +/// +/// This will use the [Float32x4] class to perform the blending calculations, +/// which can be faster than using scalar but should be tested for performance +/// and correctness. +/// +/// See for more details. +@immutable +abstract mixin class BlendMode { + /// Destination pixels covered by the source are cleared to 0. + static const BlendMode clear = PorterDuff( + PorterDuff.zero, + PorterDuff.zero, + ); + + /// Destination pixels are replaced by the source pixels. + static const BlendMode src = PorterDuff( + PorterDuff.one, + PorterDuff.zero, + ); + + /// Source pixels are replaced by the destination pixels. + static const BlendMode dst = PorterDuff( + PorterDuff.zero, + PorterDuff.one, + ); + + /// The source color is placed over the destination color. + static const BlendMode srcOver = PorterDuff( + PorterDuff.one, + PorterDuff.oneMinusSrc, + ); + + /// The destination color is placed over the source color. + static const BlendMode dstOver = PorterDuff( + PorterDuff.oneMinusDst, + PorterDuff.one, + ); + + /// The source that overlaps the destination replaces the destination. + static const BlendMode srcIn = PorterDuff( + PorterDuff.dst, + PorterDuff.zero, + ); + + /// The destination that overlaps the source replaces the source. + static const BlendMode dstIn = PorterDuff( + PorterDuff.zero, + PorterDuff.src, + ); + + /// The source that does not overlap the destination replaces the destination. + static const BlendMode srcOut = PorterDuff( + PorterDuff.oneMinusDst, + PorterDuff.zero, + ); + + /// The destination that does not overlap the source replaces the source. + static const BlendMode dstOut = PorterDuff( + PorterDuff.zero, + PorterDuff.oneMinusSrc, + ); + + /// The source that overlaps the destination is blended with the destination. + static const BlendMode srcAtop = PorterDuff( + PorterDuff.dst, + PorterDuff.oneMinusSrc, + ); + + /// The destination that overlaps the source is blended with the source. + static const BlendMode dstAtop = PorterDuff( + PorterDuff.oneMinusDst, + PorterDuff.src, + ); + + /// The non-overlapping regions of the source and destination are combined. + static const BlendMode xor = PorterDuff( + PorterDuff.oneMinusDst, + PorterDuff.oneMinusSrc, + ); + + /// The source and destination regions are added together. + static const BlendMode plus = PorterDuff( + PorterDuff.one, + PorterDuff.one, + ); + + /// Returns a function that blends two pixels together. + T Function(S src, T dst) getBlend( + PixelFormat srcFormat, + PixelFormat dstFormat, + ); +} diff --git a/lib/src/blend/porter_duff.dart b/lib/src/blend/porter_duff.dart new file mode 100644 index 0000000..e3228f8 --- /dev/null +++ b/lib/src/blend/porter_duff.dart @@ -0,0 +1,95 @@ +part of '../blend.dart'; + +/// A [BlendMode] that uses [Porter-Duff coefficients][1] to blend colors. +/// +/// [1]: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending +/// +/// A custom [BlendMode] can be created by providing custom coefficients to the +/// [PorterDuff] constructor: +/// +/// ```dart +/// // Creates a custom blend mode that always returns zero. +/// final customBlendMode = PorterDuff(PorterDuff.zero, PorterDuff.one); +/// ``` +final class PorterDuff with BlendMode { + /// Always returns zero (`0.0`). + static double zero(double srcAlpha, double dstAlpha) => 0; + + /// Always returns one (`1.0`). + static double one(double srcAlpha, double dstAlpha) => 1; + + /// Returns the source alpha channel (`srcAlpha`). + static double src(double srcAlpha, double dstAlpha) => srcAlpha; + + /// Returns the destination alpha channel (`dstAlpha`). + static double dst(double srcAlpha, double dstAlpha) => dstAlpha; + + /// Returns one minus the source alpha channel (`1 - srcAlpha`). + static double oneMinusSrc(double srcAlpha, double dstAlpha) => 1 - srcAlpha; + + /// Returns one minus the destination alpha channel (`1 - dstAlpha`). + static double oneMinusDst(double srcAlpha, double dstAlpha) => 1 - dstAlpha; + + /// Creates a blend mode with [Porter-Duff coefficients][1]. + /// + /// [1]: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending + /// + /// The [src] and [dst] functions are used to calculate the source and + /// destination coefficients, respectively. The result of blending the source + /// and destination colors is calculated as: + /// + /// ```dart + /// final srcAlpha = src(src.w, dst.w); + /// final dstAlpha = dst(src.w, dst.w); + /// return src * srcAlpha + dst * dstAlpha; + /// ``` + const PorterDuff(this._src, this._dst); + final double Function(double, double) _src; + final double Function(double, double) _dst; + + @override + T Function(S src, T dst) getBlend( + PixelFormat srcFormat, + PixelFormat dstFormat, + ) { + return (src, dst) { + final srcRgba = srcFormat.toFloatRgba(src); + final dstRgba = dstFormat.toFloatRgba(dst); + final result = _blendFloatRgba(srcRgba, dstRgba); + return dstFormat.fromFloatRgba(result); + }; + } + + Float32x4 _blendFloatRgba(Float32x4 src, Float32x4 dst) { + return useSimd + ? _blendFloat32x4SIMD(src, dst) + : _blendFloat32x4Scalar(src, dst); + } + + Float32x4 _blendFloat32x4Scalar(Float32x4 src, Float32x4 dst) { + final Float32x4( + x: sr, + y: sg, + z: sb, + w: sa, + ) = src; + final Float32x4( + x: dr, + y: dg, + z: db, + w: da, + ) = dst; + + final r = _src(sa, da) * sr + _dst(sa, da) * dr; + final g = _src(sa, da) * sg + _dst(sa, da) * dg; + final b = _src(sa, da) * sb + _dst(sa, da) * db; + final a = _src(sa, da) * sa + _dst(sa, da) * da; + return Float32x4(r, g, b, a); + } + + Float32x4 _blendFloat32x4SIMD(Float32x4 src, Float32x4 dst) { + final srcA = Float32x4.splat(_src(src.w, dst.w)); + final dstA = Float32x4.splat(_dst(src.w, dst.w)); + return src * srcA + dst * dstA; + } +} diff --git a/lib/src/buffer.dart b/lib/src/buffer.dart index 47cd5fc..e5fc1a4 100644 --- a/lib/src/buffer.dart +++ b/lib/src/buffer.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:pxl/src/blend.dart'; import 'package:pxl/src/format.dart'; import 'package:pxl/src/geometry.dart'; import 'package:pxl/src/internal.dart'; diff --git a/lib/src/buffer/pixels.dart b/lib/src/buffer/pixels.dart index dc7289a..7b5c119 100644 --- a/lib/src/buffer/pixels.dart +++ b/lib/src/buffer/pixels.dart @@ -7,7 +7,7 @@ part of '../buffer.dart'; /// but cannot be extended or implemented (similar to [TypedDataList]). /// /// In most cases either [IntPixels] or [FloatPixels] will be used directly. -abstract final class Pixels

with Buffer

{ +abstract final class Pixels with Buffer { /// @nodoc const Pixels({ required this.format, @@ -26,10 +26,10 @@ abstract final class Pixels

with Buffer

{ /// [1]: https://pub.dev/documentation/web/latest/web/ImageData/ImageData.html /// [2]: https://api.dart.dev/stable/3.5.1/dart-isolate/TransferableTypedData-class.html @override - TypedDataList

get data; + TypedDataList get data; @override - final PixelFormat format; + final PixelFormat format; @override final int width; @@ -41,12 +41,12 @@ abstract final class Pixels

with Buffer

{ @override @unsafeNoBoundsChecks - P getUnsafe(Pos pos) => data[_indexAtUnsafe(pos)]; + T getUnsafe(Pos pos) => data[_indexAtUnsafe(pos)]; /// Sets the pixel at the given position. /// /// If outside the bounds of the buffer, does nothing. - void set(Pos pos, P pixel) { + void set(Pos pos, T pixel) { if (contains(pos)) { setUnsafe(pos, pixel); } @@ -56,7 +56,7 @@ abstract final class Pixels

with Buffer

{ /// /// If outside the bounds of the buffer, the behavior is undefined. @unsafeNoBoundsChecks - void setUnsafe(Pos pos, P pixel) { + void setUnsafe(Pos pos, T pixel) { data[_indexAtUnsafe(pos)] = pixel; } @@ -115,7 +115,7 @@ abstract final class Pixels

with Buffer

{ /// pixels.fill(0xFFFFFFFF); /// pixels.fill(0x00000000, target: Rect.fromLTWH(1, 0, 1, 2)); /// ``` - void fill(P pixel, {Rect? target}) { + void fill(T pixel, {Rect? target}) { if (target != null) { target = target.intersect(bounds); } @@ -138,7 +138,7 @@ abstract final class Pixels

with Buffer

{ /// pixels.fillUnsafe(0x00000000, target: Rect.fromLTWH(1, 0, 1, 2)); /// ``` @unsafeNoBoundsChecks - void fillUnsafe(P pixel, {Rect? target}) { + void fillUnsafe(T pixel, {Rect? target}) { if (target == null) { return data.fillRange( 0, @@ -181,7 +181,7 @@ abstract final class Pixels

with Buffer

{ /// pixels.fillWith([0x00000000, 0xFFFFFFFF], target: Rect.fromLTWH(1, 0, 1, 2)); /// ``` void fillWith( - Iterable

pixels, { + Iterable pixels, { Rect? target, }) { if (target == null) { @@ -219,7 +219,7 @@ abstract final class Pixels

with Buffer

{ /// ``` @unsafeNoBoundsChecks void fillWithUnsafe( - Iterable

pixels, { + Iterable pixels, { Rect? target, }) { if (target == null) { @@ -238,14 +238,14 @@ abstract final class Pixels

with Buffer

{ } @override - Iterable

getRangeUnsafe(Pos start, Pos end) { + Iterable getRangeUnsafe(Pos start, Pos end) { final s = _indexAtUnsafe(start); final e = _indexAtUnsafe(end); return data.getRange(s, e + 1); } @override - Iterable

getRectUnsafe(Rect rect) { + Iterable getRectUnsafe(Rect rect) { if (rect.width == width) { return getRangeUnsafe(rect.topLeft, rect.bottomRight); } @@ -281,7 +281,7 @@ abstract final class Pixels

with Buffer

{ /// dst.copyFrom(src, source: Rect.fromLTWH(1, 0, 1, 2), target: Pos(1, 1)); /// ``` void copyFrom( - Buffer

from, { + Buffer from, { Rect? source, Pos? target, }) { @@ -338,11 +338,11 @@ abstract final class Pixels

with Buffer

{ /// ``` @unsafeNoBoundsChecks void copyFromUnsafe( - Buffer

from, { + Buffer from, { Rect? source, Pos? target, }) { - if (from is Pixels

) { + if (from is Pixels) { _copyFromUnsafeFast(from, source: source, target: target); } else { _copyFromUnsafeSlow(from, source: source, target: target); @@ -350,7 +350,7 @@ abstract final class Pixels

with Buffer

{ } void _copyFromUnsafeSlow( - Buffer

from, { + Buffer from, { Rect? source, Pos? target, }) { @@ -368,7 +368,7 @@ abstract final class Pixels

with Buffer

{ } void _copyFromUnsafeFast( - Pixels

from, { + Pixels from, { Rect? source, Pos? target, }) { @@ -394,6 +394,161 @@ abstract final class Pixels

with Buffer

{ dstIdx += width; } } + + /// Blits, or copies with blending, the pixel data from a source buffer to + /// `this` buffer. + /// + /// If a [source] rectangle is provided, only the pixels within that rectangle + /// are copied, and the rectangle will be clipped to the bounds of the source + /// buffer. If omitted, the entire source buffer will be copied. + /// + /// If a [target] position is provided, the top-left corner of the source + /// rectangle will be copied starting at that position. If omitted, the + /// top-left corner of the source rectangle will be copied to the top-left + /// corner of the `this` buffer. If there is not sufficient space in the + /// target buffer, the source rectangle will be clipped to fit `this`. + /// + /// If a [blend] mode is provided, the pixels will be blended using that mode. + /// + /// ## Example + /// + /// ```dart + /// final src = IntPixels(2, 2, data: Uint32List.fromList([ + /// 0xFFFFFFFF, 0x00000000, // + /// 0x00000000, 0xFFFFFFFF, // + /// ])); + /// + /// final dst = IntPixels(3, 3); + /// dst.blit(src); + /// dst.blit(src, source: Rect.fromLTWH(1, 0, 1, 2)); + /// dst.blit(src, target: Pos(1, 1)); + /// dst.blit(src, source: Rect.fromLTWH(1, 0, 1, 2), target: Pos(1, 1)); + /// ``` + void blit( + Buffer from, { + Rect? source, + Pos? target, + BlendMode blend = BlendMode.srcOver, + }) { + if (source == null) { + if (target == null) { + return blitUnsafe(from, blend: blend); + } + source = from.bounds; + } else { + source = source.intersect(from.bounds); + } + target ??= Pos.zero; + final clipped = Rect.fromTLBR( + target, + target + source.size, + ).intersect(bounds); + if (clipped.isEmpty) { + return; + } + source = Rect.fromLTWH( + source.top, + source.left, + clipped.width, + clipped.height, + ); + return blitUnsafe(from, source: source, target: target, blend: blend); + } + + /// Blits, or copies with blending, the pixel data from a source buffer to + /// `this` buffer. + /// + /// If a [source] rectangle is provided, only the pixels within that rectangle + /// are copied. If the rectangle is outside the bounds of the source buffer, + /// the behavior is undefined. + /// + /// If a [target] position is provided, the top-left corner of the source + /// rectangle will be copied starting at that position. If there is not + /// sufficient space in the target buffer, the behavior is undefined. + /// + /// If a [blend] mode is provided, the pixels will be blended using that mode. + /// + /// ## Example + /// + /// ```dart + /// final src = IntPixels(2, 2, data: Uint32List.fromList([ + /// 0xFFFFFFFF, 0x00000000, // + /// 0x00000000, 0xFFFFFFFF, // + /// ])); + /// + /// final dst = IntPixels(3, 3); + /// dst.blitUnsafe(src); + /// dst.blitUnsafe(src, source: Rect.fromLTWH(1, 0, 1, 2)); + /// dst.blitUnsafe(src, target: Pos(1, 1)); + /// dst.blitUnsafe(src, source: Rect.fromLTWH(1, 0, 1, 2), target: Pos(1, 1)); + /// ``` + void blitUnsafe( + Buffer from, { + Rect? source, + Pos? target, + BlendMode blend = BlendMode.srcOver, + }) { + target ??= Pos.zero; + final sRect = source ?? from.bounds; + final tRect = Rect.fromTLBR( + target, + target + sRect.size, + ).intersect(bounds); + if (tRect.isEmpty) { + return; + } + source = Rect.fromLTWH( + sRect.top, + sRect.left, + tRect.width, + tRect.height, + ); + final fn = blend.getBlend(from.format, format); + if (from is Pixels) { + _blitUnsafeFast(from, source: source, target: tRect, blend: fn); + } else { + _blitUnsafeSlow(from, source: source, target: tRect, blend: fn); + } + } + + void _blitUnsafeSlow( + Buffer from, { + required T Function(S src, T dst) blend, + required Rect source, + required Rect target, + }) { + final src = source.positions.iterator; + final dst = target.positions.iterator; + while (src.moveNext() && dst.moveNext()) { + setUnsafe( + dst.current, + blend( + from.getUnsafe(src.current), + getUnsafe(dst.current), + ), + ); + } + } + + void _blitUnsafeFast( + Pixels from, { + required T Function(S src, T dst) blend, + required Rect source, + required Rect target, + }) { + final src = from.data; + final dst = this.data; + var srcIdx = from._indexAtUnsafe(source.topLeft); + var dstIdx = _indexAtUnsafe(target.topLeft); + for (var y = source.top; y < source.bottom; y++) { + for (var x = source.left; x < source.right; x++) { + dst[dstIdx] = blend(src[srcIdx], dst[dstIdx]); + srcIdx++; + dstIdx++; + } + dstIdx += width - source.width; + } + } } final class _PixelsRectIterable extends Iterable { diff --git a/lib/src/format/rgba.dart b/lib/src/format/rgba.dart index 8a37462..a3c90e5 100644 --- a/lib/src/format/rgba.dart +++ b/lib/src/format/rgba.dart @@ -14,6 +14,31 @@ abstract final class Rgba extends PixelFormat { /// @nodoc const Rgba(); + /// Creates a new pixel with the given channel values. + /// + /// The [red], [green], [blue], and [alpha] values are optional and default to + /// [zero] if not provided + /// + /// ## Example + /// + /// ```dart + /// final pixel = abgr8888.create(red: 0xFF, alpha: 0x80); + /// ``` + P create({ + C? red, + C? green, + C? blue, + C? alpha, + }) { + return copyWith( + zero, + red: red, + green: green, + blue: blue, + alpha: alpha, + ); + } + @override P copyWith( P pixel, { diff --git a/test/blend_test.dart b/test/blend_test.dart new file mode 100644 index 0000000..b4f421c --- /dev/null +++ b/test/blend_test.dart @@ -0,0 +1,96 @@ +import 'package:pxl/pxl.dart'; + +import 'src/prelude.dart'; + +void main() { + test('BlendMode.clear', () { + final src = abgr8888.red; + final dst = abgr8888.green; + final blend = BlendMode.clear.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(abgr8888.zero); + }); + + test('BlendMode.src', () { + final src = abgr8888.red; + final dst = abgr8888.green; + final blend = BlendMode.src.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(src); + }); + + test('BlendMode.dst', () { + final src = abgr8888.red; + final dst = abgr8888.green; + final blend = BlendMode.dst.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(dst); + }); + + test('BlendMode.srcOver', () { + final src = abgr8888.create(red: 0x80, alpha: 0x80); + final dst = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.srcOver.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x803f00bf); + }); + + test('BlendMode.dstOver', () { + final dst = abgr8888.create(red: 0x80, alpha: 0x80); + final src = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.dstOver.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x803f00bf); + }); + + test('BlendMode.srcIn', () { + final src = abgr8888.create(red: 0x80, alpha: 0x80); + final dst = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.srcIn.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x40000040); + }); + + test('BlendMode.dstIn', () { + final dst = abgr8888.create(red: 0x80, alpha: 0x80); + final src = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.dstIn.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x40000040); + }); + + test('BlendMode.srcOut', () { + final src = abgr8888.create(red: 0x80, alpha: 0x80); + final dst = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.srcOut.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x3f00003f); + }); + + test('BlendMode.dstOut', () { + final dst = abgr8888.create(red: 0x80, alpha: 0x80); + final src = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.dstOut.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x3f00003f); + }); + + test('BlendMode.srcAtop', () { + final src = abgr8888.create(red: 0x80, alpha: 0x80); + final dst = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.srcAtop.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x403f0080); + }); + + test('BlendMode.dstAtop', () { + final dst = abgr8888.create(red: 0x80, alpha: 0x80); + final src = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.dstAtop.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x403f0080); + }); + + test('BlendMode.xor', () { + final src = abgr8888.create(red: 0x80, alpha: 0x80); + final dst = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.xor.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x3f3f007f); + }); + + test('BlendMode.plus', () { + final src = abgr8888.create(red: 0x80, alpha: 0x80); + final dst = abgr8888.create(green: 0x80, alpha: 0x80); + final blend = BlendMode.plus.getBlend(abgr8888, abgr8888); + check(blend(src, dst)).equals(0x808000ff); + }); +} diff --git a/test/pixels_test.dart b/test/pixels_test.dart index 2dad567..6ced836 100644 --- a/test/pixels_test.dart +++ b/test/pixels_test.dart @@ -434,107 +434,47 @@ void main() { ]); }); - // group('blit', () { - // test('copy in same format', () { - // final src = IntPixels( - // 2, - // 2, - // data: Uint32List.fromList([ - // abgr8888.red, abgr8888.green, // - // abgr8888.blue, abgr8888.cyan, // - // ]), - // ); - - // final dst = IntPixels(2, 2); - // Pixels.blit(src, dst); - - // check(dst.data).deepEquals(src.data); - // }); - - // test('copy in different format', () { - // final src = FloatPixels( - // 2, - // 2, - // data: Float32x4List.fromList([ - // floatRgba.red, floatRgba.green, // - // floatRgba.blue, floatRgba.cyan, // - // ]), - // ); - - // final dst = IntPixels(2, 2); - // Pixels.blit(src, dst); - - // check(dst.data).deepEquals([ - // abgr8888.red, abgr8888.green, // - // abgr8888.blue, abgr8888.cyan, // - // ]); - // }); - - // test('copy with blend', () { - // final src = IntPixels( - // 2, - // 2, - // data: Uint32List.fromList([ - // abgr8888.red, abgr8888.green, // - // abgr8888.blue, abgr8888.cyan, // - // ]), - // ); - - // final dst = IntPixels(2, 2, data: Uint32List.fromList([0, 0, 0, 0])); - // Pixels.blit(src, dst, blend: (s, d) => s + 1); - - // check(dst.data).deepEquals([ - // abgr8888.red + 1, abgr8888.green + 1, // - // abgr8888.blue + 1, abgr8888.cyan + 1, // - // ]); - // }); - - // test('copy to a smaller destination', () { - // final src = IntPixels( - // 2, - // 2, - // data: Uint32List.fromList([ - // abgr8888.red, abgr8888.green, // - // abgr8888.blue, abgr8888.cyan, // - // ]), - // ); - - // final dst = IntPixels(1, 1); - // Pixels.blit(src, dst); - - // check(dst.data).deepEquals([abgr8888.red]); - // }); - - // test('copy to a larger destination', () { - // final src = IntPixels( - // 1, - // 1, - // data: Uint32List.fromList([abgr8888.red]), - // ); - - // final dst = IntPixels(2, 2); - // Pixels.blit(src, dst); - - // check(dst.data).deepEquals([ - // abgr8888.red, abgr8888.zero, // - // abgr8888.zero, abgr8888.zero, // - // ]); - // }); - - // test('copy to an offset destination', () { - // final src = IntPixels( - // 1, - // 1, - // data: Uint32List.fromList([abgr8888.red]), - // ); - - // final dst = IntPixels(2, 2, data: Uint32List.fromList([0, 0, 0, 0])); - // Pixels.blit(src, dst, destination: Pos(1, 1)); - - // check(dst.data).deepEquals([ - // abgr8888.zero, abgr8888.zero, // - // abgr8888.zero, abgr8888.red, // - // ]); - // }); - // }); + test('blit (full, buffer -> pixels)', () { + final src = IntPixels( + 2, + 2, + data: Uint32List.fromList([ + abgr8888.copyWithNormalized(abgr8888.red, alpha: 0.5), + abgr8888.copyWithNormalized(abgr8888.green, alpha: 0.5), + abgr8888.copyWithNormalized(abgr8888.blue, alpha: 0.5), + abgr8888.copyWithNormalized(abgr8888.cyan, alpha: 0.5), + ]), + ); + final dst = IntPixels(2, 2); + dst.blit(src.map((p) => p)); + + check(dst.data).deepEquals([ + 0xff00007f, + 0x00ff007f, + 0x0000ff7f, + 0x00ffff7f, + ]); + }); + + test('blit (full, pixels -> pixels)', () { + final src = IntPixels( + 2, + 2, + data: Uint32List.fromList([ + abgr8888.copyWithNormalized(abgr8888.red, alpha: 0.5), + abgr8888.copyWithNormalized(abgr8888.green, alpha: 0.5), + abgr8888.copyWithNormalized(abgr8888.blue, alpha: 0.5), + abgr8888.copyWithNormalized(abgr8888.cyan, alpha: 0.5), + ]), + ); + final dst = IntPixels(2, 2); + dst.blit(src); + + check(dst.data).deepEquals([ + 0xff00007f, + 0x00ff007f, + 0x0000ff7f, + 0x00ffff7f, + ]); + }); }