diff --git a/lib/src/blend.dart b/lib/src/blend.dart index a8f787c..20af209 100644 --- a/lib/src/blend.dart +++ b/lib/src/blend.dart @@ -3,369 +3,183 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:pxl/src/color.dart'; -/// Algorithms to use when combining colors during rendering. +/// Algorithms to use when painting on the canvas. /// -/// Blend modes are used to determine how the source color (the color being -/// drawn) is combined with the destination color (the color already present in -/// the buffer) when rendering. The result of blending is a new color that is -/// then drawn to the buffer. +/// 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. /// -/// ## Performance +/// 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. /// -/// These are algorithms for blending colors, which are typically represented as -/// RGBA values. Blending RGBA formatted colors is a relatively expensive -/// operation, as it requires multiple floating-point operations to compute the -/// result. The performance of blending can be improved by using SIMD -/// instructions to perform the calculations in parallel, but [blendVec4] is -/// required to support this feature (at the cost of compatibility and a higher -/// memory footprint). +/// ## Diagrams +/// +/// All of the example diagrams presented below use the same source and +/// destination images: +/// +/// | Source | Destination | +/// | -------- | ----------- | +/// | ![](https://github.com/user-attachments/assets/f1c20859-571b-4b0f-ae76-a501509be91e) | ![](https://github.com/user-attachments/assets/9106c638-fc15-410b-aa99-bfc990a5633c) | @immutable -abstract base mixin class BlendMode { - /// Drops both the source and destination images, leaving nothing. +abstract interface class BlendMode { + /// Destination pixels covered by the source are cleared to 0. /// - /// ```txt - /// output = (0, 0, 0, 0) - /// ``` - static const BlendMode clear = _ClearBlendMode(); + /// ![](https://github.com/user-attachments/assets/9e926257-27b9-454a-9a58-4f0818a361e1) + static const BlendMode clear = _PorterDuffBlendMode(0x0); - /// Replaces the destination image with the source image. + /// The source pixels replace the destination pixels. /// - /// ```txt - /// output = src - /// ``` - static const BlendMode src = _SrcBlendMode(); + /// ![](https://github.com/user-attachments/assets/f1c20859-571b-4b0f-ae76-a501509be91e) + static const BlendMode src = _PorterDuffBlendMode(0x1); - /// Replaces the source image with the destination image. + /// The source pixels are discarded, leaving the destination intact. /// - /// ```txt - /// output = dst - /// ``` - static const BlendMode dst = BlendMode.swapSrcDst(src); + /// ![](https://github.com/user-attachments/assets/9106c638-fc15-410b-aa99-bfc990a5633c) + static const BlendMode dst = _PorterDuffBlendMode(0x2); - /// Shows the source image, but only where the destination image is opaque. - /// - /// The destination image is not rendered, it is treated merely as a mask. The - /// color channels of the destination are ignored, only the opacity has an - /// effect. + /// The source pixels are drawn over the destination pixels. /// - /// ```txt - /// output = src * dst.alpha - /// ``` - static const BlendMode srcIn = _SrcInBlendMode(); + /// ![](https://github.com/user-attachments/assets/f8ec5abd-8ee9-4d1b-8049-89d99b9c7b86) + static const BlendMode srcOver = _PorterDuffBlendMode(0x3); - /// Shows the destination image, but only where the source image is opaque. + /// The source pixels are drawn behind the destination pixels. /// - /// The source image is not rendered, it is treated merely as a mask. The - /// color channels of the source are ignored, only the opacity has an effect. - /// - /// ```txt - /// output = dst * src.alpha - /// ``` - static const BlendMode dstIn = BlendMode.swapSrcDst(srcIn); + /// ![](https://github.com/user-attachments/assets/337834be-1376-4893-8b2e-b8316b92ba7c) + static const BlendMode dstOver = _PorterDuffBlendMode(0x4); - /// Shows the source image, but only where the destination image is - /// transparent. + /// Keeps the source pixels that cover the destination pixels. /// - /// The destination image is not rendered, it is treated merely as a mask. The - /// color channels of the destination are ignored, only the opacity has an - /// effect. + /// Discards the remaining source and destination pixels. /// - /// ```txt - /// output = src * (1 - dst.alpha) - /// ``` - static const BlendMode srcOut = _SrcOutBlendMode(); + /// ![](https://github.com/user-attachments/assets/dbe94ad9-c802-40bf-b771-876b147ee1d8) + static const BlendMode srcIn = _PorterDuffBlendMode(0x5); - /// Shows the destination image, but only where the source image is - /// transparent. + /// Keeps the destination pixels that cover source pixels. /// - /// The source image is not rendered, it is treated merely as a mask. The - /// color channels of the source are ignored, only the opacity has an effect. + /// Discards the remaining source and destination pixels. /// - /// ```txt - /// output = dst * (1 - src.alpha) - /// ``` - static const BlendMode dstOut = BlendMode.swapSrcDst(srcOut); + /// ![](https://github.com/user-attachments/assets/0ca174e6-fa5f-4418-906a-5f5ab45e92f0) + static const BlendMode dstIn = _PorterDuffBlendMode(0x6); - /// The default blend mode, which draws the source image over the destination. + /// Keeps the source pixels that do not cover destination pixels. /// - /// ```txt - /// output = src + dst * (1 - src.alpha) - /// ``` - static const BlendMode srcOver = _SrcOverBlendMode(); - - /// Draws the destination image over the source image. + /// - Discards source pixels that cover destination pixels. + /// - Discards all destination pixels. /// - /// ```txt - /// output = dst + src * (1 - dst.alpha) - /// ``` - static const BlendMode dstOver = BlendMode.swapSrcDst(srcOver); + /// ![](https://github.com/user-attachments/assets/a40a8184-727b-4b41-a952-192762da186c) + static const BlendMode srcOut = _PorterDuffBlendMode(0x7); - /// Draws the source image on top of the destination image, but only where the - /// destination image is opaque. + /// Keeps the destination pixels that are not covered by source pixels. /// - /// ```txt - /// output = src * dst.alpha + dst * (1 - src.alpha) - /// ``` - static const BlendMode srcATop = _SrcATopBlendMode(); + /// ![](https://github.com/user-attachments/assets/cc646ae8-afad-4b37-8e0d-44dc88cf0353) + static const BlendMode dstOut = _PorterDuffBlendMode(0x8); - /// Draws the destination image on top of the source image, but only where the - /// source image is opaque. + /// Discards the source pixels that do not cover destination pixels. /// - /// ```txt - /// output = dst * src.alpha + src * (1 - dst.alpha) - /// ``` - static const BlendMode dstATop = BlendMode.swapSrcDst(srcATop); + /// ![](https://github.com/user-attachments/assets/fbffcfa2-4aa6-4238-9096-e36146dc6c7b) + static const BlendMode srcAtop = _PorterDuffBlendMode(0x9); - /// Selects the darker of the source and destination colors. + /// Discards the destination pixels that are not covered by source pixels. /// - /// The opacity is computed similarly to [srcOver]. + /// Draws remaining destination pixels over source pixels. /// - /// ```txt - /// output = src.gray < dst.gray ? src : dst - /// ``` - static const BlendMode darken = _DarkenBlendMode(); + /// ![](https://github.com/user-attachments/assets/5352abf4-df5b-4db2-9242-faa1bdc45d51) + static const BlendMode dstAtop = _PorterDuffBlendMode(0xA); - /// Selects the lighter of the source and destination colors. + /// Discards the source and destination pixels where source pixels cover + /// destination pixels. /// - /// The opacity is computed similarly to [srcOver]. + /// Draws remaining source pixels. /// - /// ```txt - /// output = src.gray > dst.gray ? src : dst - /// ``` - static const BlendMode lighten = _LightenBlendMode(); + /// ![](https://github.com/user-attachments/assets/41e8a5f4-2474-4ed8-bfb3-3c1290ca297b) + static const BlendMode xor = _PorterDuffBlendMode(0xB); - /// Multiplies the source and destination colors. + /// Adds the source pixels to the destination pixels and saturates the result. /// - /// ```txt - /// output = src * dst - /// ``` - static const BlendMode multiply = _MultiplyBlendMode(); + /// ![](https://github.com/user-attachments/assets/f7ccf76a-d0f7-4b0b-bf7f-ccb702c90da1) + static const BlendMode plus = _PorterDuffBlendMode(0xC); - /// Multiplies the source and destination colors and inverts the result. - /// - /// ```txt - /// output = 1 - (1 - src) * (1 - dst) - /// ``` - static const BlendMode screen = _ScreenBlendMode(); + /// Multiply the color components of the source and destination pixels. + static const BlendMode modulate = _PorterDuffBlendMode(0xD); - /// Apply a bitwise xor operator to the source and destination. + /// Adds the source and destination pixels, then subtracts the source pixels + /// multiplied by the destination. /// - /// This leaves transparency where the source and destination overlap. - /// - /// ```txt - /// output = src * (1 - dst.alpha) + dst * (1 - src.alpha) - /// ``` - static const BlendMode xor = _XorBlendMode(); - - // TODO: Add Plus, Modulate. - // See also: https://github.com/flutter/engine/blob/619df47725ccd10054d2cce03c3ae2320a3d59c8/impeller/entity/contents/filters/blend_filter_contents.h#L15-L31. - - /// @nodoc - const BlendMode(); + /// ![](https://github.com/user-attachments/assets/01d30185-35a9-402f-afe1-c1c99707a835) + static const BlendMode screen = _PorterDuffBlendMode(0xE); - /// A wrapper that swaps the source and destination colors before blending. - /// - /// This wrapper converts a blend mode such as [BlendMode.srcOver] into - /// [BlendMode.dstOver] by swapping the source and destination colors before - /// blending without writing a new blend mode and duplicating the logic. - /// - /// ## Usage - /// - /// ```dart - /// final class _SrcOver extends BlendMode { /* ... */ } - /// const BlendMode srcOver = _SrcOver(); - /// const BlendMode dstOver = BlendMode.swapSrcDst(srcOver); - /// ``` - @literal - const factory BlendMode.swapSrcDst(BlendMode mode) = _SwapBlendMode; + /// Subtract the smaller value from the bigger value for each channel. + static const BlendMode difference = _PorterDuffBlendMode(0xF); - /// Blends the source color [src] with the destination color [dst]. - /// - /// The source color is the color being drawn, and the destination color is - /// the color already present in the buffer. The result of blending is a new - /// color that is typically drawn to the buffer in place of the destination - /// color. - /// - /// ## Performance - /// - /// Blending RGBA formatted colors is a relatively expensive operation, as it - /// requires multiple floating-point operations to compute the result. The - /// performance of blending can be improved by using SIMD instructions to - /// perform the calculations in parallel, but [blendVec4] is required to - /// support this feature. - RgbaColor blendRgba(RgbaColor src, RgbaColor dst) { - return blendVec4(src.vec4, dst.vec4).rgba; - } - - /// Blends the source color [src] with the destination color [dst]. - /// - /// The source color is the color being drawn, and the destination color is - /// the color already present in the buffer. The result of blending is a new - /// color that is typically drawn to the buffer in place of the destination - /// color. - /// - /// ## Performance - /// - /// Blending vectorized colors is a relatively inexpensive operation, as it - /// requires only a few floating-point operations to compute the result. The - /// performance of blending is improved by using SIMD instructions to perform - /// the calculations in parallel where supported. - Vec4Color blendVec4(Vec4Color src, Vec4Color dst); - - /// Blends the source color [src] with the destination color [dst]. - /// - /// The source color is the color being drawn, and the destination color is - /// the color already present in the buffer. The result of blending is a new - /// color that is typically drawn to the buffer in place of the destination - /// color. - /// - /// ## Performance - /// - /// This method attempts to dispatch to the most efficient blending method - /// based on the destination color format; if the destination color is in RGBA - /// format, [blendRgba] is called, otherwise [blendVec4] is called. - Color blendMixed(Color src, Color dst) { - return switch (dst) { - final RgbaColor _ => blendRgba(src.rgba, dst), - final Vec4Color _ => blendVec4(src.vec4, dst), - _ => blendVec4(src.vec4, dst.vec4), - }; - } -} - -final class _SwapBlendMode extends BlendMode { - const _SwapBlendMode(this._delegate); - final BlendMode _delegate; - - @override - RgbaColor blendRgba(RgbaColor src, RgbaColor dst) { - return _delegate.blendRgba(dst, src); - } - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - return _delegate.blendVec4(dst, src); - } -} - -final class _ClearBlendMode extends BlendMode { - const _ClearBlendMode(); - - @override - RgbaColor blendRgba(RgbaColor src, RgbaColor dst) { - return RgbaColor.transparentBlack; - } - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - return Vec4Color.transparentBlack; - } -} - -final class _SrcBlendMode extends BlendMode { - const _SrcBlendMode(); - - @override - RgbaColor blendRgba(RgbaColor src, RgbaColor dst) { - return src; - } - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - return src; - } -} - -final class _SrcInBlendMode extends BlendMode { - const _SrcInBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - return Vec4Color(src.value.scale(dst.alpha)); - } -} - -final class _SrcOutBlendMode extends BlendMode { - const _SrcOutBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - return Vec4Color(dst.value.scale(1.0 - src.alpha)); - } -} - -final class _SrcOverBlendMode extends BlendMode { - const _SrcOverBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - return Vec4Color(src.value + dst.value.scale(1.0 - src.alpha)); - } -} - -final class _SrcATopBlendMode extends BlendMode { - const _SrcATopBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - final fs = dst.alpha; - final fd = 1.0 - src.alpha; - final out = src.value.scale(fs) + dst.value.scale(fd); - return Vec4Color(out); - } -} - -final class _DarkenBlendMode extends BlendMode { - const _DarkenBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - var out = src.value.min(dst.value); - out = out.withW(src.alpha + dst.alpha * (1.0 - src.alpha)); - return Vec4Color(out); - } -} - -final class _LightenBlendMode extends BlendMode { - const _LightenBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - var out = src.value.max(dst.value); - out = out.withW(src.alpha + dst.alpha * (1.0 - src.alpha)); - return Vec4Color(out); - } -} - -final class _MultiplyBlendMode extends BlendMode { - const _MultiplyBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - return Vec4Color(src.value * dst.value); - } -} - -final class _ScreenBlendMode extends BlendMode { - const _ScreenBlendMode(); - - @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - final srcVec = Float32x4.splat(1.0) - src.value; - final dstVec = Float32x4.splat(1.0) - dst.value; - final outVec = Float32x4.splat(1.0) - srcVec * dstVec; - return Vec4Color(outVec); - } + /// Returns the result of blending the source color [src] with the destination + /// color [dst]. + R blend>(R src, R dst); } -final class _XorBlendMode extends BlendMode { - const _XorBlendMode(); +const _porterDuffCoefficients = >[ + [0, 0, 0, 0, 0], // 0x00:Clear + [1, 0, 0, 0, 0], // 0x01:Source + [0, 0, 1, 0, 0], // 0x02:Destination + [0, 0, 1, 0, 0], // 0x02:Destination + [1, 0, 1, -1, 0], // 0x03:SourceOver + [1, 0, 1, -1, 0], // 0x03:SourceOver + [1, -1, 1, 0, 0], // 0x04:DestinationOver + [0, 1, 0, 0, 0], // 0x05:SourceIn + [0, 0, 0, 1, 0], // 0x06:DestinationIn + [0, 0, 0, 1, 0], // 0x07:SourceOut + [0, 0, 1, -1, 0], // 0x08:DestinationOut + [0, 1, 1, -1, 0], // 0x09:SourceAtop + [0, 1, 1, -1, 0], // 0x09:SourceAtop + [1, -1, 0, 1, 0], // 0x0A:DestinationAtop + [1, -1, -1, -1, 0], // 0x0B:Xor + [1, 0, 1, 0, 0], // 0x0C:Plus + [0, 0, 0, 0, 1], // 0x0D:Modulate + [0, 0, 1, 0, -1], // 0x0E:Screen + [1, -1, 0, 0, 0], // 0x0F:Difference +]; + +final _porterDuffSplats = () { + final memory = Float32x4List(_porterDuffCoefficients.length * 5); + for (var i = 0; i < _porterDuffCoefficients.length; i++) { + final c = _porterDuffCoefficients[i]; + memory[i * 5 + 0] = Float32x4.splat(c[0]); + memory[i * 5 + 1] = Float32x4.splat(c[1]); + memory[i * 5 + 2] = Float32x4.splat(c[2]); + memory[i * 5 + 3] = Float32x4.splat(c[3]); + memory[i * 5 + 4] = Float32x4.splat(c[4]); + } + return memory; +}(); + +final class _PorterDuffBlendMode implements BlendMode { + const _PorterDuffBlendMode(this._i); + final int _i; @override - Vec4Color blendVec4(Vec4Color src, Vec4Color dst) { - final fs = src.alpha; - final fd = dst.alpha; - final out = src.value.scale(1.0 - fd) + dst.value.scale(1.0 - fs); - return Vec4Color(out); + R blend>(R src, R dst) { + // Load the coefficients for the blend mode. + final [ + sc, + dc, + sa, + da, + c, + ] = Float32x4List.view(_porterDuffSplats.buffer, _i * 5, 5); + + // Convert the colors to Float32x4 if necessary. + final srcF32x4 = src.toRgbaF32().value; + final dstF32x4 = dst.toRgbaF32().value; + + // Perform the blend operation. + final r = sc * srcF32x4 + dc * dstF32x4 + sa * srcF32x4 + da * dstF32x4 + c; + return dst.normalizedCopyWith( + red: r.x, + green: r.y, + blue: r.z, + alpha: r.w, + ) as R; } } diff --git a/lib/src/buffer.dart b/lib/src/buffer.dart index 6822d1c..83747d3 100644 --- a/lib/src/buffer.dart +++ b/lib/src/buffer.dart @@ -1,194 +1,16 @@ -import 'dart:math' as math; -import 'dart:typed_data'; - -import 'package:lodim/lodim.dart'; -import 'package:meta/meta.dart'; -import 'package:pxl/src/blend.dart'; import 'package:pxl/src/color.dart'; -part 'buffer/clipped.dart'; -part 'buffer/empty.dart'; -part 'buffer/mapped.dart'; -part 'buffer/pixels.dart'; -part 'buffer/rgba.dart'; -part 'buffer/scaled.dart'; -part 'buffer/vec4.dart'; - -/// Returns `height` if it is not `null`, otherwise calculates the height. -/// -/// [length], [width], and [height] is checked to be non-negative and valid. -int _checkAndInferHeight({ - required int length, - required int width, - int? height, -}) { - RangeError.checkNotNegative(width, 'width'); - if (height == null) { - height = width != 0 ? length ~/ width : 0; - } else { - RangeError.checkNotNegative(height, 'height'); - } - if (length != width * height) { - throw ArgumentError( - 'The length of the pixel data must be equal to the product of the ' - 'width and height.', - ); - } - return height; -} - -/// A 2-dimensional view of a pixel-buffer like object with a fixed [width] and -/// [height]. -/// -/// Buffer is analgous to [Iterable] but for 2-dimensional pixel data, and -/// typically exists in order to provide a common interface for reading -/// pixel data from different sources, or providing a common interface for -/// compatibility. -/// -/// See [Pixels] for a mutable concrete implementation of this interface. -abstract base mixin class Buffer { - /// Creates an empty buffer with zero width and height. - /// - /// This buffer is immutable and has no pixels. - const factory Buffer.empty() = _EmptyBuffer; - - /// The width of the buffer, in pixels. +/// TODO: Implement Buffer. +abstract final class Buffer { + /// TODO: Replace stub. int get width; - /// The height of the buffer, in pixels. + /// TODO: Replace stub. int get height; - /// The total number of pixels in the buffer. + /// TODO: Replace stub. int get length; - /// Returns the pixel at [index] in the buffer without bound checking. - /// - /// ## Safety - /// - /// {@template pxl:unsafe} - /// This method is intended for use in performance-sensitive code where the - /// parameters are known to be within bounds, otherwise it may result in - /// undefined behavior, including memory corruption and data loss. - /// {@endtemplate} - /// - /// Prefer [operator []] for most operations. + /// TODO: Replace stub. T uncheckedGet(int index); - - /// Returns the pixel at [index] in the buffer. - /// - /// The [index] must be in the range `0 ≤ index < length`. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - T operator [](int index) { - RangeError.checkValueInInterval(index, 0, length - 1, 'index'); - return uncheckedGet(index); - } - - /// Returns the index of the pixel at the given [position] in the buffer. - /// - /// ## Safety - /// - /// {@macro pxl:unsafe} - /// - /// Prefer using [indexAt] for most operations. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - int uncheckedIndexAt(Pos position) { - return position.y * width + position.x; - } - - /// Returns the index of the pixel at the given [position] in the buffer. - /// - /// The [position] must be within the bounds of the buffer. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @nonVirtual - int indexAt(Pos position) { - if (!toBoundsRect().contains(position)) { - throw RangeError('position is out of bounds: $position'); - } - return uncheckedIndexAt(position); - } - - /// Returns a view of the buffer clipped to the given [bounds]. - /// - /// If the buffer is entirely out of bounds, an empty buffer is returned. - /// - /// This is guaranteed to be a constant-time operation. - Buffer clip(Rect bounds) => _ClippedBuffer(this, bounds); - - /// Returns a view of the buffer scaled by the given factors. - /// - /// Both [widthBy] and [heightBy] must be positive integers. - Buffer scale({int widthBy = 1, int heightBy = 1}) { - if (widthBy < 1) { - throw ArgumentError.value( - widthBy, - 'widthBy', - 'must be greater than 0', - ); - } - if (heightBy < 1) { - throw ArgumentError.value( - heightBy, - 'heightBy', - 'must be greater than 0', - ); - } - return _ScaledBuffer(this, widthBy, heightBy); - } - - /// Returns a view of the buffer mapped to the given function. - /// - /// The [mapper] function is called for each pixel in the buffer. - /// - /// The returned buffer has the same dimensions as the original buffer. - Buffer map(R Function(T color) mapper) { - return _MappedBuffer(this, mapper); - } - - /// Returns the bounds of the buffer. - /// - /// If [clip] is provided, the bounds are clipped to the given rectangle. - /// - /// If the buffer is entirely out of bounds, an empty rectangle is returned. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @nonVirtual - Rect toBoundsRect([Rect? clip]) { - final bounds = Rect.fromLTWH(0, 0, width, height); - return clip == null ? bounds : bounds.intersect(clip); - } - - /// Converts and returns the buffer as a list of pixels in RGBA format. - /// - /// The returned list is a copy of the buffer and can be modified without - /// affecting the original. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @pragma('dart2js:index-bounds:trust') - @pragma('vm:unsafe:no-bounds-checks') - Uint32List toRgba8888List() { - final list = Uint32List(length); - for (var i = 0; i < length; i++) { - list[i] = this.uncheckedGet(i).rgba.value; - } - return list; - } - - /// Converts and returns the buffer as a list of pixels in Vec4 format. - /// - /// The returned list is a copy of the buffer and can be modified without - /// affecting the original. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @pragma('dart2js:index-bounds:trust') - @pragma('vm:unsafe:no-bounds-checks') - Float32x4List toVec4List() { - final list = Float32x4List(length); - for (var i = 0; i < length; i++) { - list[i] = this.uncheckedGet(i).vec4.value; - } - return list; - } } diff --git a/lib/src/buffer/clipped.dart b/lib/src/buffer/clipped.dart deleted file mode 100644 index 383530f..0000000 --- a/lib/src/buffer/clipped.dart +++ /dev/null @@ -1,31 +0,0 @@ -part of '../buffer.dart'; - -final class _ClippedBuffer with Buffer { - const _ClippedBuffer(this._buffer, this._bounds); - final Buffer _buffer; - final Rect _bounds; - - @override - int get length => _bounds.area; - - @override - int get width => _bounds.width; - - @override - int get height => _bounds.height; - - /// Remaps the given [index] to the underlying buffer. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @protected - int reIndex(int index) { - final x = index % width; - final y = index ~/ width; - return _buffer.uncheckedIndexAt(Pos(x, y) + _bounds.topLeft); - } - - @override - T uncheckedGet(int index) { - return _buffer.uncheckedGet(reIndex(index)); - } -} diff --git a/lib/src/buffer/empty.dart b/lib/src/buffer/empty.dart deleted file mode 100644 index 19f6d16..0000000 --- a/lib/src/buffer/empty.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of '../buffer.dart'; - -final class _EmptyBuffer with Buffer { - const _EmptyBuffer(); - - @override - int get width => 0; - - @override - int get height => 0; - - @override - int get length => 0; - - @override - T uncheckedGet(int index) { - throw StateError('Cannot get pixel from an empty buffer.'); - } -} diff --git a/lib/src/buffer/mapped.dart b/lib/src/buffer/mapped.dart deleted file mode 100644 index ae12d1e..0000000 --- a/lib/src/buffer/mapped.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of '../buffer.dart'; - -final class _MappedBuffer with Buffer { - const _MappedBuffer(this._buffer, this._map); - final Buffer _buffer; - final T Function(S) _map; - - @override - int get length => _buffer.length; - - @override - int get width => _buffer.width; - - @override - int get height => _buffer.height; - - @override - T uncheckedGet(int index) => _map(_buffer.uncheckedGet(index)); -} diff --git a/lib/src/buffer/pixels.dart b/lib/src/buffer/pixels.dart deleted file mode 100644 index e38eacc..0000000 --- a/lib/src/buffer/pixels.dart +++ /dev/null @@ -1,128 +0,0 @@ -part of '../buffer.dart'; - -/// A 2-dimensional buffer of pixels with a fixed [width] and [height]. -/// -/// Pixels is analgous to [List] but for 2-dimensional pixel data, and exists -/// in order to provide a common interface for reading and writing pixel data -/// from different sources, or providing a common interface for compatibility. -/// -/// In order to ensure performance and correctness, the buffer does not support -/// custom implementations, and is instead provided as a series of `final` -/// classes that implement the interface; see [Buffer] for an extensible -/// read-only version of this interface. -abstract final class Pixels with Buffer { - const Pixels._(); - - /// Sets the pixel at [index] in the buffer to the given [color] without - /// bounds checking. - /// - /// ## Safety - /// - /// {@macro pxl:unsafe} - /// - /// Prefer [operator []=] for most operations. - void uncheckedSet(int index, Color color); - - /// Sets the pixel at [index] in the buffer to the given [color]. - /// - /// The [index] must be in the range `0 ≤ index < length`. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @nonVirtual - void operator []=(int index, Color color) { - RangeError.checkValueInInterval(index, 0, length - 1, 'index'); - uncheckedSet(index, color); - } - - /// Fills the buffer with the given [color]. - /// - /// ## Blending - /// - /// The default color blending mode is [BlendMode.src], which replaces the - /// destination color with the source color without any blending operation - /// (i.e. opacity is ignored); this is a fast operation but not always - /// desirable; see [BlendMode] for alternatives. - /// - /// ## Clipping - /// - /// If [dstClip] is provided, the buffer is filled only within the given - /// rectangle. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - static void fill( - Pixels dst, - Color color, { - Rect? dstClip, - BlendMode blend = BlendMode.src, - }) { - dstClip = dst.toBoundsRect(dstClip); - - // Fill the buffer with the given color, staying within bounds. - for (var y = dstClip.top; y < dstClip.bottom; y++) { - final rowOffset = y * dst.width; - - for (var x = dstClip.left; x < dstClip.right; x++) { - final index = rowOffset + x; - - // Blend the source pixel with the destination pixel. - final dstColor = dst.uncheckedGet(index); - final result = blend.blendMixed(color, dstColor); - - // Set the pixel in the destination buffer. - dst.uncheckedSet(index, result); - } - } - } - - /// Copies the pixels from the source buffer to the destination buffer. - /// - /// ## Blending - /// - /// The default color blending mode is [BlendMode.src], which replaces the - /// destination color with the source color without any blending operation - /// (i.e. opacity is ignored); this is a fast operation but not always - /// desirable; see [BlendMode] for alternatives. - /// - /// ## Clipping - /// - /// - If [srcClip] is provided, only the pixels within the given rectangle are - /// copied. - /// - If [dstClip] is provided, the pixels are clampped to the given - /// rectangle. - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - static void copy( - Buffer src, - Pixels dst, { - Rect? srcClip, - Rect? dstClip, - BlendMode blend = BlendMode.src, - }) { - // Clip the source buffer to the given rectangle if its provided. - if (srcClip != null) { - src = src.clip(srcClip); - } - - dstClip = dst.toBoundsRect(dstClip); - - // The dimensions are not necessarily the same, so we need to translate the - // coordinates to the destination buffer, and clamp if we are out of bounds. - for (var y = 0; y < math.min(src.height, dst.height); y++) { - final srcRowOffset = y * src.width; - final dstRowOffset = (y + dstClip.top) * dst.width; - - for (var x = 0; x < math.min(src.width, dst.width); x++) { - final srcIndex = srcRowOffset + x; - final dstIndex = dstRowOffset + x + dstClip.left; - - // Blend the source pixel with the destination pixel. - final srcColor = src.uncheckedGet(srcIndex); - final dstColor = dst.uncheckedGet(dstIndex); - final result = blend.blendMixed(srcColor, dstColor); - - // Set the pixel in the destination buffer. - dst.uncheckedSet(dstIndex, result); - } - } - } -} diff --git a/lib/src/buffer/rgba.dart b/lib/src/buffer/rgba.dart deleted file mode 100644 index 2b3cba8..0000000 --- a/lib/src/buffer/rgba.dart +++ /dev/null @@ -1,116 +0,0 @@ -part of '../buffer.dart'; - -/// A 2-dimensional buffer of RGBA pixels with a fixed [width] and [height]. -final class RgbaPixels extends Pixels { - /// Creates a new buffer of RGBA pixels with the given [width] and [height]. - /// - /// If [fill] is provided, the buffer is filled with the given color. - factory RgbaPixels(int width, int height, [Color? fill]) { - height = _checkAndInferHeight( - length: width * height, - width: width, - height: height, - ); - final buffer = Uint32List(width * height); - if (fill != null) { - buffer.fillRange(0, buffer.length, fill.rgba.value); - } - return RgbaPixels._(buffer, width, height); - } - - /// {@template pxl:Buffer:from} - /// Creates a new buffer from another buffer. - /// - /// If [clip] is provided, a subregion of the buffer is copied. - /// {@endtemplate} - factory RgbaPixels.from( - Pixels other, { - Rect? clip, - }) { - final RgbaPixels buffer; - if (clip == null) { - buffer = RgbaPixels(other.width, other.height); - } else { - buffer = RgbaPixels(clip.width, clip.height); - } - Pixels.copy(other, buffer, srcClip: clip); - return buffer; - } - - /// Creates a new buffer from a sequence of RGBA colors from the given - /// [colors]. - /// - /// The [width] and [height] must be non-negative. - /// - /// If [height] is omitted, it is calculated as the number of colors divided - /// by the given [width]. - factory RgbaPixels.fromColors( - Iterable colors, { - required int width, - int? height, - }) { - height = _checkAndInferHeight( - length: colors.length, - width: width, - height: height, - ); - final buffer = Uint32List(width * height); - buffer.setAll(0, colors.map((c) => c.rgba.value)); - return RgbaPixels._(buffer, width, height); - } - - /// Creates a new buffer from a list of RGBA pixels from the given [pixels]. - /// - /// Each pixel is interpreted as a 32-bit integer in the format `0xAARRGGBB`. - /// - /// The [width] and [height] must be non-negative. - /// - /// If [height] is omitted, it is calculated as the number of pixels divided - /// by the given [width]. - factory RgbaPixels.fromList( - List pixels, { - required int width, - int? height, - }) { - height = _checkAndInferHeight( - length: pixels.length, - width: width, - height: height, - ); - return RgbaPixels._(Uint32List.fromList(pixels), width, height); - } - - const RgbaPixels._( - this._buffer, - this.width, - this.height, - ) : assert(_buffer.length == width * height, 'Invalid buffer size.'), - super._(); - - final Uint32List _buffer; - - @override - final int width; - - @override - final int height; - - @override - int get length => _buffer.length; - - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @pragma('dart2js:index-bounds:trust') - @pragma('vm:unsafe:no-bounds-checks') - @override - RgbaColor uncheckedGet(int index) => RgbaColor(_buffer[index]); - - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @pragma('dart2js:index-bounds:trust') - @pragma('vm:unsafe:no-bounds-checks') - @override - void uncheckedSet(int index, Color color) { - _buffer[index] = color.rgba.value; - } -} diff --git a/lib/src/buffer/scaled.dart b/lib/src/buffer/scaled.dart deleted file mode 100644 index 2437699..0000000 --- a/lib/src/buffer/scaled.dart +++ /dev/null @@ -1,26 +0,0 @@ -part of '../buffer.dart'; - -final class _ScaledBuffer with Buffer { - const _ScaledBuffer(this._buffer, this._scaleWidthBy, this._scaleHeightBy); - final Buffer _buffer; - final int _scaleWidthBy; - final int _scaleHeightBy; - - @override - int get length => _buffer.length * _scaleWidthBy * _scaleHeightBy; - - @override - int get width => _buffer.width * _scaleWidthBy; - - @override - int get height => _buffer.height * _scaleHeightBy; - - @override - T uncheckedGet(int index) { - final x = index % width; - final y = index ~/ width; - final bx = x ~/ _scaleWidthBy; - final by = y ~/ _scaleHeightBy; - return _buffer.uncheckedGet(bx + by * _buffer.width); - } -} diff --git a/lib/src/buffer/vec4.dart b/lib/src/buffer/vec4.dart deleted file mode 100644 index 1c48905..0000000 --- a/lib/src/buffer/vec4.dart +++ /dev/null @@ -1,115 +0,0 @@ -part of '../buffer.dart'; - -/// A 2-dimensional buffer of vectorized pixels with a fixed [width] and -/// [height]. -final class Vec4Pixels extends Pixels { - /// Creates a new buffer of vectorized pixels with the given [width] and - /// [height]. - /// - /// If [fill] is provided, the buffer is filled with the given color. - factory Vec4Pixels(int width, int height, [Color? fill]) { - height = _checkAndInferHeight( - length: width * height, - width: width, - height: height, - ); - final buffer = Float32x4List(width * height); - if (fill != null) { - buffer.fillRange(0, buffer.length, fill.vec4.value); - } - return Vec4Pixels._(buffer, width, height); - } - - /// {@macro pxl:Buffer:from} - factory Vec4Pixels.from( - Pixels other, { - Rect? clip, - }) { - final Vec4Pixels buffer; - if (clip == null) { - buffer = Vec4Pixels(other.width, other.height); - } else { - buffer = Vec4Pixels(clip.width, clip.height); - } - Pixels.copy(other, buffer, srcClip: clip); - return buffer; - } - - /// Creates a new buffer from a sequence of vectorized pixels from the given - /// [colors]. - /// - /// The [width] and [height] must be non-negative. - /// - /// If [height] is omitted, it is calculated as the number of colors divided - /// by the given [width]. - factory Vec4Pixels.fromColors( - Iterable colors, { - required int width, - int? height, - }) { - height = _checkAndInferHeight( - length: colors.length, - width: width, - height: height, - ); - final buffer = Float32x4List(width * height); - buffer.setAll(0, colors.map((c) => c.vec4.value)); - return Vec4Pixels._(buffer, width, height); - } - - /// Creates a new buffer from a list of vectorized pixels from the given - /// [pixels]. - /// - /// Each pixel is interpreted as a 128-bit vector in the format - /// `(r, g, b, a)`. - /// - /// The [width] and [height] must be non-negative. - /// - /// If [height] is omitted, it is calculated as the number of pixels divided - /// by the given [width]. - factory Vec4Pixels.fromList( - List pixels, { - required int width, - int? height, - }) { - height = _checkAndInferHeight( - length: pixels.length, - width: width, - height: height, - ); - return Vec4Pixels._(Float32x4List.fromList(pixels), width, height); - } - - const Vec4Pixels._( - this._buffer, - this.width, - this.height, - ) : assert(_buffer.length == width * height, 'Invalid buffer size.'), - super._(); - final Float32x4List _buffer; - - @override - final int width; - - @override - final int height; - - @override - int get length => _buffer.length; - - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @pragma('dart2js:index-bounds:trust') - @pragma('vm:unsafe:no-bounds-checks') - @override - Vec4Color uncheckedGet(int index) => Vec4Color(_buffer[index]); - - @pragma('dart2js:tryInline') - @pragma('vm:prefer-inline') - @pragma('dart2js:index-bounds:trust') - @pragma('vm:unsafe:no-bounds-checks') - @override - void uncheckedSet(int index, Color color) { - _buffer[index] = color.vec4.value; - } -} diff --git a/lib/src/color.dart b/lib/src/color.dart index ea3667d..aebe30f 100644 --- a/lib/src/color.dart +++ b/lib/src/color.dart @@ -1,106 +1,17 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; +import 'package:pxl/src/utils.dart'; -part 'color/const.dart'; +part 'color/base.dart'; +part 'color/format.dart'; +part 'color/rgb.dart'; +part 'color/rgb_8.dart'; part 'color/rgba.dart'; -part 'color/vec4.dart'; +part 'color/rgba_8.dart'; +part 'color/rgba_f32.dart'; +part 'color/rgba_f64.dart'; +part 'color/sized.dart'; +part 'color/zero.dart'; -/// An immutable color with discrete red, green, blue, and opacity channels. -/// -/// Color is an abstract class that models the concept of a color in a 2D image -/// with multiple channels. The color can be represented in different ways, such -/// as RGBA, ARGB, or even as a vector of floating-point values. -/// -/// ## Performance -/// -/// Where possible, APIs are encouraged to expose the most specific color type -/// that is needed, such as [RgbaColor] or [Vec4Color], particularly as a return -/// type, and to avoid storing colors as [Color] instances to prevent boxing. -/// -/// ```dart -/// // Good: Expose the most specific color type. -/// RgbaColor get color => _color; -/// -/// // Bad: Store the color as a generic type. -/// Color get color => _color; -/// ``` -@immutable -abstract final class Color { - /// Creates a new color with the given 64-bit floating-point components. - /// - /// The [red], [green], and [blue] components are in the range 0.0-1.0, and - /// the [alpha] component is optional and defaults to fully opaque; each - /// component is clamped to the valid range if necessary. - /// - /// ## Performance - /// - /// This is a special constructor that allows declaring a wide color gamut - /// color as a constant, which is not currently supported by Dart, and is - /// intended for use where a `const` constructor is required but not possible - /// otherwise. - /// - /// ## Precision - /// - /// There is a loss of precision (64-bit to 32-bit) when converted to either - /// [RgbaColor] or [Vec4Color]: - /// - /// ```dart - /// const color = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.4); - /// final vec4 = color.asVec4(); - /// print(vec4.red); // 0.10000000149011612 - /// print(vec4.green); // 0.20000000298023224 - /// print(vec4.blue); // 0.30000001192092896 - /// print(vec4.alpha); // 0.4000000059604645 - /// ``` - @literal - const factory Color.fromRGBAFloats( - @mustBeConst double red, - @mustBeConst double green, - @mustBeConst double blue, [ - @mustBeConst double alpha, - ]) = _ConstVec4Color; - - /// The color [transparent black][]: fully transparent with no channel - /// components. - /// - /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black - static const Color transparentBlack = _TransparentBlack(); - - const Color._(); - - /// Whether the color is grayscale. - bool get isGrayscale; - - /// Whether the color is fully opaque. - bool get isOpaque; - - /// Whether the color is fully transparent. - bool get isTransparent; - - /// The gray value of the color from 0.0 to 1.0 based on a luminance formula. - /// - /// Luminance is a weighted sum of the red, green, and blue channels, which - /// approximates the perceived brightness of the color (where green is the - /// most dominant color and blue is the least dominant color). - double get gray; - - /// The grayscale version of the color. - Color get grayscale; - - /// The color as a 32-bit RGBA fixed-point color value. - /// - /// This operation is a copy of a 32-bit RGBA color value with no conversion - /// when the source color is already in the RGBA format, otherwise the color - /// is converted to the RGBA format before being returned, which may involve - /// a loss of precision. - RgbaColor get rgba; - - /// The color as a 32-bit ARGB floating-point color vector. - /// - /// This operation is a copy of a 4D floating-point color vector with no - /// conversion when the source color is already in the vector format, - /// otherwise the color is converted to the vector format before being - /// returned, which may involve a loss of precision. - Vec4Color get vec4; -} +int _floatToU8(double value) => (value * 255).round(); diff --git a/lib/src/color/base.dart b/lib/src/color/base.dart new file mode 100644 index 0000000..f1b2dd9 --- /dev/null +++ b/lib/src/color/base.dart @@ -0,0 +1,79 @@ +part of '../color.dart'; + +/// An immutable color without any specific channels or components. +@immutable +sealed class Color { + /// The zero color, or [transparent black][]. + /// + /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black + static const Color zero = _ZeroColor(); + + /// @nodoc + const Color(); + + @override + @mustBeOverridden + bool operator ==(Object other); + + @override + @mustBeOverridden + int get hashCode; + + @override + @mustBeOverridden + String toString(); + + /// The opacity of this color as a percentage in the range 0.0-1.0. + /// + /// A value of 0.0 is fully transparent, and a value of 1.0 is fully opaque. + /// + /// See also: + /// - [isOpaque], which returns `true` if the opacity is 1.0. + /// - [isTransparent], which returns `true` if the opacity is 0.0. + double get opacity; + + /// Whether this color is fully opaque. + /// + /// A color is fully opaque if its [opacity] is 1.0. + @nonVirtual + bool get isOpaque => opacity == 1.0; + + /// Whether this color is fully transparent. + /// + /// A color is fully transparent if its [opacity] is 0.0. + @nonVirtual + bool get isTransparent => opacity == 0.0; + + /// Whether this color is the zero color for its type. + /// + /// The zero color is a color where all channels are zero, and the opacity is + /// 0.0. + bool get isZero; + + /// Whether this color is fully grayscale. + bool get isGrayscale; + + /// Converts this color to a grayscale color. + Color toGrayscale(); + + /// Converts this color to an [Rgb8] color. + Rgb8 toRgb8(); + + /// Converts this color to an [Rgba8] color. + /// + /// [opacity] is the opacity of the new color, from 0.0 to 1.0; if omitted + /// [Color.opacity] is used. + Rgba8 toRgba8({double opacity}); + + /// Converts this color to an [RgbaF32] color. + /// + /// [opacity] is the opacity of the new color, from 0.0 to 1.0; if omitted + /// [Color.opacity] is used. + RgbaF32 toRgbaF32({double opacity}); + + /// Converts this color to an [RgbaF64] color. + /// + /// [opacity] is the opacity of the new color, from 0.0 to 1.0; if omitted + /// [Color.opacity] is used. + RgbaF64 toRgbaF64({double opacity}); +} diff --git a/lib/src/color/const.dart b/lib/src/color/const.dart deleted file mode 100644 index 1d33362..0000000 --- a/lib/src/color/const.dart +++ /dev/null @@ -1,96 +0,0 @@ -part of '../color.dart'; - -final class _ConstVec4Color extends Color { - const _ConstVec4Color( - double red, - double green, - double blue, [ - double alpha = 1.0, - ]) - // coverage:ignore-start - : _red = red < 0.0 - ? 0.0 - : red > 1.0 - ? 1.0 - : red, - _green = green < 0.0 - ? 0.0 - : green > 1.0 - ? 1.0 - : green, - _blue = blue < 0.0 - ? 0.0 - : blue > 1.0 - ? 1.0 - : blue, - _alpha = alpha < 0.0 - ? 0.0 - : alpha > 1.0 - ? 1.0 - : alpha, - super._(); - // coverage:ignore-end - - final double _red; - final double _green; - final double _blue; - final double _alpha; - - @override - double get gray => _red * 0.33 + _green * 0.59 + _blue * 0.11; - - @override - bool get isGrayscale => _red == _green && _green == _blue; - - @override - Color get grayscale => Vec4Color.splat(gray, _alpha); - - @override - bool get isOpaque => _alpha == 1.0; - - @override - bool get isTransparent => _alpha == 0.0; - - @override - RgbaColor get rgba => vec4.rgba; - - @override - Vec4Color get vec4 { - return Vec4Color.fromRGBA(_red, _green, _blue, _alpha); - } - - @override - String toString() { - return 'Color.fromRGBAFloats($_red, $_green, $_blue, $_alpha)'; - } -} - -final class _TransparentBlack extends Color { - const _TransparentBlack() : super._(); - - @override - double get gray => 0.0; - - @override - bool get isGrayscale => true; - - @override - Color get grayscale => this; - - @override - bool get isOpaque => false; - - @override - bool get isTransparent => true; - - @override - RgbaColor get rgba => RgbaColor.transparentBlack; - - @override - Vec4Color get vec4 => Vec4Color.transparentBlack; - - @override - String toString() { - return 'Color.transparentBlack'; - } -} diff --git a/lib/src/color/format.dart b/lib/src/color/format.dart new file mode 100644 index 0000000..e13b054 --- /dev/null +++ b/lib/src/color/format.dart @@ -0,0 +1,37 @@ +part of '../color.dart'; + +/// A base class for color formats, which are codec-like factory classes for +/// sized colors. +/// +/// Color formats facilitate the reading and writing of colors from and to +/// buffers (e.g. images, files, etc.) by providing a common interface for +/// creating colors from raw data and converting colors to raw data. +abstract base mixin class ColorFormat { + /// @nodoc + const ColorFormat(); + + /// The number of bytes required to store a color in this format. + int get lengthInBytes; + + /// Encodes and writes a single pixel to the given [buffer] at the given + /// [offset]. + void setPixel(ByteData buffer, int offset, Color color); + + /// Reads and decodes a single pixel from the given [buffer] at the given + /// [offset]. + T getPixel(ByteData buffer, int offset); + + /// Allocates a new buffer of the given [length] for colors in this format. + /// + /// If [fill] is provided, the buffer is filled with the given value, + /// otherwise the the zero value for the format is used. + TypedData allocate(int length, [Color? fill]) { + final buffer = ByteData(length * lengthInBytes); + if (fill != null) { + for (var i = 0; i < length; i++) { + setPixel(buffer, i * lengthInBytes, fill); + } + } + return buffer; + } +} diff --git a/lib/src/color/rgb.dart b/lib/src/color/rgb.dart new file mode 100644 index 0000000..6c06633 --- /dev/null +++ b/lib/src/color/rgb.dart @@ -0,0 +1,46 @@ +part of '../color.dart'; + +/// An immutable color with discrete red, green, and blue channels. +sealed class Rgb extends Color { + /// @nodoc + const Rgb(); + + @override + double get opacity => 1.0; + + @override + bool get isGrayscale => red == green && green == blue; + + /// Creates a copy of this color with the given channels replaced. + /// + /// The new color will have the same type as this color. + Rgb normalizedCopyWith({ + double? red, + double? green, + double? blue, + }); + + /// The red channel of this color. + /// + /// The range of this channel depends on the implementation. + T get red; + + /// The red channel of this color normalized to the range 0.0-1.0. + double get normalizedRed; + + /// The green channel of this color. + /// + /// The range of this channel depends on the implementation. + T get green; + + /// The green channel of this color normalized to the range 0.0-1.0. + double get normalizedGreen; + + /// The blue channel of this color. + /// + /// The range of this channel depends on the implementation. + T get blue; + + /// The blue channel of this color normalized to the range 0.0-1.0. + double get normalizedBlue; +} diff --git a/lib/src/color/rgb_8.dart b/lib/src/color/rgb_8.dart new file mode 100644 index 0000000..ee28c58 --- /dev/null +++ b/lib/src/color/rgb_8.dart @@ -0,0 +1,210 @@ +part of '../color.dart'; + +/// 32-bit RGB colors with 8-bit unsigned integer channels. +const ColorFormat rgb8 = _Rgb8ColorFormat(); + +/// An 32-bit RGB color with 8-bit unsigned integer channels. +/// +/// The red, green, and blue channels are in the range 0-255. +/// +/// ```txt +/// +--------+--------+-------+ +/// | Blue | Green | Red | +/// +--------+--------+-------+ +/// | 23-16 | 15-08 | 00-07 | +/// +--------+--------+-------+ +/// ``` +final class Rgb8 extends Rgb with _Rgb8Mixin implements SizedColor { + /// The zero, or fully [transparent black][], color. + /// + /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black + static const zero = Rgb8(0); + + /// Creates a new color from the given 24-bit RGB [value]. + /// + /// The bottom 24 bits of the [value] are used to represent the color in the + /// format `0xRRGGBB`. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color. + /// Rgb8(0xFF0000); + /// + /// // Creates a full green color. + /// Rgb8(0x00FF00); + /// + /// // Creates a full blue color. + /// Rgb8(0x0000FF); + /// ``` + @literal + const Rgb8(int value) : value = value & 0xFFFFFF; + + /// Creates a new color from the given [red], [green], and [blue] channels. + /// + /// The [red], [green], and [blue] channels are clamped to the range 0-255. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color. + /// Rgb8.fromRGB(255, 0, 0); + /// + /// // Creates a full green color. + /// Rgb8.fromRGB(0, 255, 0); + /// + /// // Creates a full blue color. + /// Rgb8.fromRGB(0, 0, 255); + /// ``` + const Rgb8.fromRGB( + int red, + int green, + int blue, + ) : value = ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF); + + /// Creates a new color where all channels have the same [value]. + /// + /// The [value] is clamped to the range 0-255. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full white color. + /// Rgb8.splat(255); + /// + /// /// Creates a mid-gray color. + /// Rgb8.splat(128); + /// + /// // Creates a full black color. + /// Rgb8.splat(0); + /// ``` + const Rgb8.splat(int value) : this.fromRGB(value, value, value); + + /// The 24-bit RGB value of this color. + @override + final int value; + + @override + bool operator ==(Object other) { + if (other is Color && other.isOpaque) { + return other.toRgb8().value == value; + } + return false; + } + + @override + int get hashCode => value.hashCode; + + @override + bool get isZero => value == 0; + + @override + Rgb8 toGrayscale() { + final gray = (red * 0.33 + green * 0.59 + blue * 0.11).round(); + return Rgb8.splat(gray); + } + + @override + Rgb8 normalizedCopyWith({ + double? red, + double? green, + double? blue, + }) { + return Rgb8.fromRGB( + _floatToU8(red ?? normalizedRed), + _floatToU8(green ?? normalizedGreen), + _floatToU8(blue ?? normalizedBlue), + ); + } + + @override + Rgb8 toRgb8() => this; + + @override + Rgba8 toRgba8({double opacity = 1.0}) { + return Rgba8(value | (opacity * 0xFF ~/ 1) << 24); + } + + @override + RgbaF32 toRgbaF32({double opacity = 1.0}) { + return RgbaF32.fromRGBA( + normalizedRed, + normalizedGreen, + normalizedBlue, + opacity, + ); + } + + @override + RgbaF64 toRgbaF64({double opacity = 1.0}) { + return RgbaF64.fromRGBA( + normalizedRed, + normalizedGreen, + normalizedBlue, + opacity, + ); + } + + @override + String toString() { + return 'Rgb8(0x${value.toRadixString(16).padLeft(6, '0')})'; + } +} + +base mixin _Rgb8Mixin implements Rgb { + /// The raw integer value of this color. + int get value; + + /// The red channel of this color. + /// + /// The range of this channel is 0-255. + @override + int get red => (value >> 16) & 0xFF; + + @override + double get normalizedRed => red / 255; + + /// The green channel of this color. + /// + /// The range of this channel is 0-255. + @override + int get green => (value >> 8) & 0xFF; + + @override + double get normalizedGreen => green / 255; + + /// The blue channel of this color. + /// + /// The range of this channel is 0-255. + @override + int get blue => value & 0xFF; + + @override + double get normalizedBlue => blue / 255; +} + +final class _Rgb8ColorFormat with ColorFormat { + const _Rgb8ColorFormat(); + + @override + int get lengthInBytes => Uint8List.bytesPerElement * 3; + + @override + Rgb8 getPixel(ByteData buffer, int offset) { + return Rgb8.fromRGB( + buffer.getUint8(offset), + buffer.getUint8(offset + 1), + buffer.getUint8(offset + 2), + ); + } + + @override + @pragma('dart2js:tryInline') + @pragma('vm:prefer-inline') + void setPixel(ByteData buffer, int offset, Color color) { + final pixel = color.toRgb8(); + buffer.setUint8(offset, pixel.red); + buffer.setUint8(offset + 1, pixel.green); + buffer.setUint8(offset + 2, pixel.blue); + } +} diff --git a/lib/src/color/rgba.dart b/lib/src/color/rgba.dart index b83b8c6..b2298b7 100644 --- a/lib/src/color/rgba.dart +++ b/lib/src/color/rgba.dart @@ -1,181 +1,32 @@ part of '../color.dart'; -/// An immutable 32-bit RGBA fixed-point color value. -/// -/// On devices where wide color gamut is not supported, or when advanced blends -/// are not required, the 32-bit RGBA color value is a common and efficient way -/// to represent colors in a 2D image; the color is represented as a 32-bit -/// integer with the format `0xAARRGGBB`. -/// -/// ```txt -/// +--------+--------+--------+-------+ -/// | Alpha | Blue | Green | Red | -/// +--------+--------+--------+-------+ -/// | 31-24 | 23-16 | 15-08 | 00-07 | -/// +--------+--------+--------+-------+ -/// ``` -final class RgbaColor extends Color { - /// [Transparent black][]: fully transparent with no channel components. - /// - /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black - /// - /// Often used as the default color for transparent regions. - static const transparentBlack = RgbaColor(0x00000000); - - /// Creates a new color from the given 32-bit RGBA [value]. - /// - /// The bottom 32 bits of the [value] are used to represent the color in the - /// format `0xAARRGGBB`. - /// - /// ## Example - /// - /// ```dart - /// // Creates a fully opaque green color. - /// RgbaColor(0xFF00FF00); - /// - /// // Creates a 50% transparent red color. - /// RgbaColor(0x80FF0000); - /// ``` - const RgbaColor(int value) : this._(value & 0xFFFFFFFF); - - /// Creates a new color from the given 8-bit channel components. - /// - /// Each component is brought into the range 0-255. - /// - /// ## Example - /// - /// ```dart - /// // Creates a fully opaque green color. - /// RgbaColor.fromARGB(0x00, 0xFF, 0x00, 0xFF); - /// - /// // Creates a 50% transparent red color. - /// RgbaColor.fromARGB(0xFF, 0x00, 0x00, 0x80); - /// ``` - const RgbaColor.fromRGBA( - int red, - int green, - int blue, - int alpha, - ) : this._( - /******/ ((alpha & 0xFF) << 24) | - /**/ ((red & 0xFF) << 16) | - /**/ ((green & 0xFF) << 8) | - /**/ ((blue & 0xFF) << 0) & 0xFFFFFFFF, - ); - - /// Creates a new color from the given color channels and [opacity]. - /// - /// The [red], [green], and [blue] channels are integer values in the range - /// 0-255, and the [opacity] channel is a double value in the range 0.0-1.0; - /// each component is brought into the range 0-255 and opacity is defaulted to - /// fully opaque. - const RgbaColor.fromRGBO( - int red, - int green, - int blue, [ - double opacity = 1.0, - ]) : this.fromRGBA(red, green, blue, opacity * 0xFF ~/ 1); - - /// Creates a new color using [value] as each color channel and [opacity]. - /// - /// The [value] is an integer in the range 0-255, and the [opacity] is a - /// double value in the range 0.0-1.0; each component is brought into the - /// range 0-255 and opacity is defaulted to fully opaque. - const RgbaColor.splat( - int value, [ - double opacity = 1.0, - ]) : this.fromRGBO(value, value, value, opacity); - - const RgbaColor._(this.value) : super._(); +/// An immutable color with discrete red, green, blue, and alpha channels. +sealed class Rgba extends Rgb { + /// @nodoc + const Rgba(); - /// 32-bit RGBA color value. + /// Creates a copy of this color with the given channels replaced. /// - /// The color value is in the format `0xAARRGGBB`. - final int value; - - @override - bool operator ==(Object other) => other is RgbaColor && other.value == value; - + /// The new color will have the same type as this color. @override - int get hashCode => value.hashCode; + Rgb normalizedCopyWith({ + double? red, + double? green, + double? blue, + double? alpha, + }); - /// The opacity of the color, in the range 0.0-1.0. - double get opacity => alpha / 255.0; - - @override - bool get isGrayscale => red == green && green == blue; - - @override - RgbaColor get grayscale => RgbaColor.splat((gray * 255.0).round(), opacity); - - @override - double get gray => (red * 0.33 + green * 0.59 + blue * 0.11) / 255.0; - - @override - bool get isOpaque => alpha == 0xFF; - - @override - bool get isTransparent => alpha == 0x00; - - /// The alpha channel component of the color. - /// - /// The alpha channel is an integer value in the range 0-255. - int get alpha => (value >> 24) & 0xFF; - - /// The red channel component of the color. - /// - /// The red channel is an integer value in the range 0-255. - int get red => (value >> 16) & 0xFF; - - /// The green channel component of the color. + /// The alpha channel of this color. /// - /// The green channel is an integer value in the range 0-255. - int get green => (value >> 8) & 0xFF; + /// The range of this channel depends on the implementation. + T get alpha; - /// The blue channel component of the color. + /// The opacity of this color normalized to the range 0.0-1.0. /// - /// The blue channel is an integer value in the range 0-255. - int get blue => (value >> 0) & 0xFF; - - /// Returns a copy of the color with the given channel components replaced. - RgbaColor copyWith({ - int? alpha, - int? red, - int? green, - int? blue, - }) { - var color = value; - if (alpha != null) { - color = (color & 0x00FFFFFF) | ((alpha & 0xFF) << 24); - } - if (red != null) { - color = (color & 0xFF00FFFF) | ((red & 0xFF) << 16); - } - if (green != null) { - color = (color & 0xFFFF00FF) | ((green & 0xFF) << 8); - } - if (blue != null) { - color = (color & 0xFFFFFF00) | ((blue & 0xFF) << 0); - } - return RgbaColor._(color); - } - - @override - RgbaColor get rgba => this; - - @override - Vec4Color get vec4 { - final rgba = Float32x4( - red.toDouble(), - green.toDouble(), - blue.toDouble(), - alpha.toDouble(), - ); - return Vec4Color(rgba.scale(1.0 / 255.0)); - } + /// This value is the same as [opacity]. + double get normalizedAlpha; @override - String toString() { - return 'RgbaColor(0x${value.toRadixString(16).padLeft(8, '0')})'; - } + @nonVirtual + double get opacity => normalizedAlpha; } diff --git a/lib/src/color/rgba_8.dart b/lib/src/color/rgba_8.dart new file mode 100644 index 0000000..8da4d18 --- /dev/null +++ b/lib/src/color/rgba_8.dart @@ -0,0 +1,222 @@ +part of '../color.dart'; + +/// An 32-bit RGBA color with 8-bit unsigned integer channels. +const ColorFormat rgba8 = _Rgba8ColorFormat(); + +/// An 32-bit RGBA color with 8-bit unsigned integer channels. +/// +/// The red, green, blue, and alpha channels are in the range 0-255. +/// +/// ```txt +/// +--------+--------+--------+-------+ +/// | Alpha | Blue | Green | Red | +/// +--------+--------+--------+-------+ +/// | 31-24 | 23-16 | 15-08 | 00-07 | +/// +--------+--------+--------+-------+ +/// ``` +final class Rgba8 extends Rgba with _Rgb8Mixin implements SizedColor { + // The zero, or fully [transparent black][], color. + /// + /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black + static const zero = Rgba8(0); + + /// Creates a new color from the given 32-bit RGBA [value]. + /// + /// The bottom 32 bits of the [value] are used to represent the color in the + /// format `0xAARRGGBB`. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color with full opacity. + /// Rgba8(0xFFFF0000); + /// + /// // Creates a full green color with full opacity. + /// Rgba8(0xFF00FF00); + /// + /// // Creates a full blue color with full opacity. + /// Rgba8(0xFF0000FF); + /// ``` + @literal + const Rgba8(int value) : value = value & 0xFFFFFFFF; + + /// Creates a new color from the given [red], [green], [blue], and [alpha] + /// channels. + /// + /// The [red], [green], [blue], and [alpha] channels are clamped to the range + /// 0-255. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color with full opacity. + /// Rgba8.fromRGBA(255, 0, 0, 255); + /// + /// // Creates a full green color with half opacity. + /// Rgba8.fromRGBA(0, 255, 0, 128); + /// + /// // Creates a full blue color with no opacity. + /// Rgba8.fromRGBA(0, 0, 255, 0); + /// ``` + const Rgba8.fromRGBA( + int red, + int green, + int blue, [ + int alpha = 0xFF, + ]) : value = ((alpha & 0xFF) << 24) | + ((red & 0xFF) << 16) | + ((green & 0xFF) << 8) | + (blue & 0xFF); + + /// Creates a new color from the given [red], [green], [blue], and [opacity] + /// + /// The [red], [green], and [blue] channels are clamped to the range 0-255. + /// + /// The [opacity] is in the range 0.0-1.0, and defaults to fully opaque. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color with full opacity. + /// Rgba8.fromRGBO(255, 0, 0); + /// + /// // Creates a full green color with half opacity. + /// Rgba8.fromRGBO(0, 255, 0, 0.5); + /// + /// // Creates a full blue color with no opacity. + /// Rgba8.fromRGBO(0, 0, 255, 0.0); + /// ``` + const Rgba8.fromRGBO( + int red, + int green, + int blue, [ + double opacity = 1.0, + ]) : this.fromRGBA(red, green, blue, opacity * 0xFF ~/ 1); + + /// Creates a new color where all color channels have the same [value]. + /// + /// The [value] is clamped to the range 0-255. + /// + /// [opacity] is defaulted to fully opaque. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full white color. + /// Rgba8.splat(255); + /// + /// // Creates a mid-gray color. + /// Rgba8.splat(128); + /// + /// // Creates a full black color. + /// Rgba8.splat(0); + /// ``` + const Rgba8.splat( + int value, [ + int alpha = 0xFF, + ]) : this.fromRGBA(value, value, value, alpha); + + /// The 32-bit RGBA value of this color. + @override + final int value; + + @override + bool operator ==(Object other) { + if (other is Color && opacity == other.opacity) { + return other.toRgba8().value == value; + } + return false; + } + + @override + int get hashCode => value.hashCode; + + @override + bool get isZero => value == 0; + + /// The alpha channel of this color. + /// + /// The range of this channel is 0-255. + @override + int get alpha => (value >> 24) & 0xFF; + + @override + double get normalizedAlpha => alpha / 255; + + @override + Rgba8 toGrayscale() { + final gray = (red * 0.33 + green * 0.59 + blue * 0.11).round(); + return Rgba8.splat(gray, alpha); + } + + @override + Rgba8 normalizedCopyWith({ + double? red, + double? green, + double? blue, + double? alpha, + }) { + return Rgba8.fromRGBA( + _floatToU8(red ?? normalizedRed), + _floatToU8(green ?? normalizedGreen), + _floatToU8(blue ?? normalizedBlue), + _floatToU8(alpha ?? normalizedAlpha), + ); + } + + @override + Rgb8 toRgb8() => Rgb8(value); + + @override + Rgba8 toRgba8({double? opacity}) { + if (opacity == null) { + return this; + } + return Rgba8(value | (opacity * 0xFF ~/ 1) << 24); + } + + @override + RgbaF32 toRgbaF32({double? opacity}) { + return RgbaF32.fromRGBA( + normalizedRed, + normalizedGreen, + normalizedBlue, + opacity ?? this.opacity, + ); + } + + @override + RgbaF64 toRgbaF64({double? opacity}) { + return RgbaF64.fromRGBA( + normalizedRed, + normalizedGreen, + normalizedBlue, + opacity ?? this.opacity, + ); + } + + @override + String toString() { + return 'Rgb8(0x${value.toRadixString(16).padLeft(8, '0')})'; + } +} + +final class _Rgba8ColorFormat with ColorFormat { + const _Rgba8ColorFormat(); + + @override + int get lengthInBytes => Uint32List.bytesPerElement; + + @override + Rgba8 getPixel(ByteData buffer, int offset) { + return Rgba8(buffer.getUint32(offset)); + } + + @override + @pragma('dart2js:tryInline') + @pragma('vm:prefer-inline') + void setPixel(ByteData buffer, int offset, Color color) { + final pixel = color.toRgba8(); + buffer.setUint32(offset, pixel.value); + } +} diff --git a/lib/src/color/rgba_f32.dart b/lib/src/color/rgba_f32.dart new file mode 100644 index 0000000..0e06b92 --- /dev/null +++ b/lib/src/color/rgba_f32.dart @@ -0,0 +1,245 @@ +part of '../color.dart'; + +/// 128-bit RGBA colors with 4 32-bit floating-point channels. +const ColorFormat rgbaF32 = _RgbaF32ColorFormat(); + +/// A 128-bit RGBA color with 4 32-bit floating-point channels. +/// +/// The red, green, blue, and alpha channels are in the range 0.0-1.0. +/// +/// ```txt +/// +--------+--------+--------+--------+ +/// | Alpha | Blue | Green | Red | +/// +--------+--------+--------+--------+ +/// | 96-127| 64-95 | 32-63 | 00-31 | +/// +--------+--------+--------+--------+ +/// ``` +/// +/// ## Limitations +/// +/// Due to [platform limitations][31487], `const` constructors are not +/// currently supported. +/// +/// [31487]: https://github.com/dart-lang/sdk/issues/31487 +/// +/// As a workaround, see [RgbaF64]. +/// +/// ## Example +/// +/// ```dart +/// // Creates a full red color with full opacity. +/// RgbaF32.fromRGBA(1.0, 0.0, 0.0); +/// +/// // Creates a full green color with half opacity. +/// RgbaF32.fromRGBA(0.0, 1.0, 0.0, 0.5); +/// +/// // Creates a full blue color with no opacity. +/// RgbaF32.fromRGBA(0.0, 0.0, 1.0, 0.0); +/// ``` +final class RgbaF32 extends Rgba implements SizedColor { + // The zero, or fully [transparent black][], color. + /// + /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black + static final zero = RgbaF32(float32x4Zero); + + /// Creates a new color from the given 128-bit RGBA [value]. + /// + /// The lanes of the [value] are clamped to the range 0.0-1.0, and map to: + /// - [Float32x4.x] -> [red] + /// - [Float32x4.y] -> [green] + /// - [Float32x4.z] -> [blue] + /// - [Float32x4.w] -> [alpha] + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color with full opacity. + /// RgbaF32(Float32x4(1.0, 0.0, 0.0, 1.0)); + /// + /// // Creates a full green color with half opacity. + /// RgbaF32(Float32x4(0.0, 1.0, 0.0, 0.5)); + /// + /// // Creates a full blue color with no opacity. + /// RgbaF32(Float32x4(0.0, 0.0, 1.0, 0.0)); + /// ``` + RgbaF32( + Float32x4 value, + ) : value = value.clamp(Float32x4.zero(), float32x4One); + + /// Creates a new color from the given [red], [green], [blue], and [alpha] + /// channels. + /// + /// Each channel is clamped to the range 0.0-1.0, and alpha defaults to 1.0. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color with full opacity. + /// RgbaF32.fromRGBA(1.0, 0.0, 0.0); + /// + /// // Creates a full green color with half opacity. + /// RgbaF32.fromRGBA(0.0, 1.0, 0.0, 0.5); + /// + /// // Creates a full blue color with no opacity. + /// RgbaF32.fromRGBA(0.0, 0.0, 1.0, 0.0); + /// ``` + RgbaF32.fromRGBA( + double red, + double green, + double blue, [ + double alpha = 1.0, + ]) : this(Float32x4(red, green, blue, alpha)); + + /// Creates a new color where all channels have the same [value]. + /// + /// The [value] is clamped to the range 0.0-1.0, and alpha defaults to 1.0. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full white color. + /// RgbaF32.splat(1.0); + /// + /// // Creates a mid-gray color. + /// RgbaF32.splat(0.5); + /// + /// // Creates a full black color. + /// RgbaF32.splat(0.0); + /// ``` + RgbaF32.splat( + double value, [ + double alpha = 1.0, + ]) : this.fromRGBA(value, value, value, alpha); + + /// The 128-bit RGBA value of this color. + final Float32x4 value; + + @override + bool get isZero => value.equal(float32x4Zero).signMask == Int32x4.wwww; + + @override + bool operator ==(Object other) { + if (other is Color && opacity == other.opacity) { + final result = value.equal(other.toRgbaF32().value); + return result.signMask == Int32x4.wwxx; + } + return false; + } + + @override + int get hashCode => Object.hash(value.x, value.y, value.z, value.w); + + @override + double get red => value.x; + + @override + double get green => value.y; + + @override + double get blue => value.z; + + @override + double get alpha => value.w; + + @override + double get normalizedRed => red; + + @override + double get normalizedGreen => green; + + @override + double get normalizedBlue => blue; + + @override + double get normalizedAlpha => alpha; + + @override + RgbaF32 toGrayscale() { + final gray = red * 0.33 + green * 0.59 + blue * 0.11; + return RgbaF32.splat(gray, alpha); + } + + @override + RgbaF32 normalizedCopyWith({ + double? red, + double? green, + double? blue, + double? alpha, + }) { + return RgbaF32( + Float32x4( + red ?? this.red, + green ?? this.green, + blue ?? this.blue, + alpha ?? this.alpha, + ), + ); + } + + @override + Rgb8 toRgb8() { + final out = value.scale(255); + return Rgb8.fromRGB(out.x.round(), out.y.round(), out.z.round()); + } + + @override + Rgba8 toRgba8({double? opacity}) { + final out = value.scale(255); + return Rgba8.fromRGBA( + out.x.round(), + out.y.round(), + out.z.round(), + opacity == null ? out.w.round() : (opacity * 0xFF ~/ 1), + ); + } + + @override + RgbaF32 toRgbaF32({double? opacity}) { + return opacity == null ? this : RgbaF32(value.withW(opacity)); + } + + @override + RgbaF64 toRgbaF64({double? opacity}) { + return RgbaF64.fromRGBA( + red, + green, + blue, + opacity ?? this.opacity, + ); + } + + @override + String toString() { + return 'RgbaF32.fromRGBA($red, $green, $blue, $alpha)'; + } +} + +final class _RgbaF32ColorFormat with ColorFormat { + const _RgbaF32ColorFormat(); + + @override + int get lengthInBytes => Float32x4List.bytesPerElement; + + @override + RgbaF32 getPixel(ByteData buffer, int offset) { + return RgbaF32( + Float32x4( + buffer.getFloat32(offset), + buffer.getFloat32(offset + 4), + buffer.getFloat32(offset + 8), + buffer.getFloat32(offset + 12), + ), + ); + } + + @override + @pragma('dart2js:tryInline') + @pragma('vm:prefer-inline') + void setPixel(ByteData buffer, int offset, Color color) { + final pixel = color.toRgbaF32(); + buffer.setFloat32(offset, pixel.red); + buffer.setFloat32(offset + 4, pixel.green); + buffer.setFloat32(offset + 8, pixel.blue); + buffer.setFloat32(offset + 12, pixel.alpha); + } +} diff --git a/lib/src/color/rgba_f64.dart b/lib/src/color/rgba_f64.dart new file mode 100644 index 0000000..ab0641c --- /dev/null +++ b/lib/src/color/rgba_f64.dart @@ -0,0 +1,214 @@ +part of '../color.dart'; + +/// 256-bit floating-point color with red, green, blue, and alpha channels. +const ColorFormat rgbaF64 = _RgbaF64ColorFormat(); + +/// A 256-bit floating-point color with red, green, blue, and alpha channels. +/// +/// The red, green, and blue channels are in the range 0.0-1.0. +/// +/// ```txt +/// +--------+--------+--------+--------+ +/// | Alpha | Blue | Green | Red | +/// +--------+--------+--------+--------+ +/// | 192-255| 128-191| 64-127 | 00-63 | +/// +--------+--------+--------+--------+ +/// ``` +/// +/// ## Example +/// +/// ```dart +/// // Creates a full red color with full opacity. +/// RgbaF64.fromRGBA(1.0, 0.0, 0.0); +/// +/// // Creates a full green color with half opacity. +/// RgbaF64.fromRGBA(0.0, 1.0, 0.0, 0.5); +/// +/// // Creates a full blue color with no opacity. +/// RgbaF64.fromRGBA(0.0, 0.0, 1.0, 0.0); +/// ``` +final class RgbaF64 extends Rgba implements SizedColor { + /// The zero, or fully [transparent black][], color. + /// + /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black + static const zero = RgbaF64.splat(0.0); + + /// Creates a new color from the given [red], [green], [blue], and [alpha] + /// channels. + /// + /// Each channel is clamped to the range 0.0-1.0, and alpha defaults to 1.0. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full red color with full opacity. + /// RgbaF64.fromRGBA(1.0, 0.0, 0.0); + /// + /// // Creates a full green color with half opacity. + /// RgbaF64.fromRGBA(0.0, 1.0, 0.0, 0.5); + /// + /// // Creates a full blue color with no opacity. + /// RgbaF64.fromRGBA(0.0, 0.0, 1.0, 0.0); + /// ``` + const RgbaF64.fromRGBA( + this.red, + this.green, + this.blue, [ + this.alpha = 1.0, + ]); + + /// Creates a new color where all channels have the same [value]. + /// + /// The [value] is clamped to the range 0.0-1.0, and alpha defaults to 1.0. + /// + /// ## Example + /// + /// ```dart + /// // Creates a full white color. + /// RgbaF64.splat(1.0); + /// + /// // Creates a mid-gray color. + /// RgbaF64.splat(0.5); + /// + /// // Creates a full black color. + /// RgbaF64.splat(0.0); + /// ``` + const RgbaF64.splat( + double value, [ + double alpha = 1.0, + ]) : this.fromRGBA(value, value, value, alpha); + + @override + bool get isZero => red == 0.0 && green == 0.0 && blue == 0.0 && alpha == 0.0; + + /// The red channel of this color. + @override + final double red; + + @override + double get normalizedRed => red; + + /// The green channel of this color. + @override + final double green; + + @override + double get normalizedGreen => green; + + /// The blue channel of this color. + @override + final double blue; + + @override + double get normalizedBlue => blue; + + /// The alpha channel of this color. + @override + final double alpha; + + @override + double get normalizedAlpha => alpha; + + @override + bool operator ==(Object other) { + if (other is Color && opacity == other.opacity) { + final otherF64 = other.toRgbaF64(); + return otherF64.red == red && + otherF64.green == green && + otherF64.blue == blue && + otherF64.alpha == alpha; + } + return false; + } + + @override + int get hashCode => Object.hash(red, green, blue, alpha); + + @override + RgbaF64 toGrayscale() { + final gray = red * 0.33 + green * 0.59 + blue * 0.11; + return RgbaF64.splat(gray, alpha); + } + + @override + RgbaF64 normalizedCopyWith({ + double? red, + double? green, + double? blue, + double? alpha, + }) { + return RgbaF64.fromRGBA( + red ?? this.red, + green ?? this.green, + blue ?? this.blue, + alpha ?? this.alpha, + ); + } + + @override + Rgb8 toRgb8() { + return Rgb8.fromRGB( + (red * 255).round(), + (green * 255).round(), + (blue * 255).round(), + ); + } + + @override + Rgba8 toRgba8({double? opacity}) { + return Rgba8.fromRGBA( + (red * 255).round(), + (green * 255).round(), + (blue * 255).round(), + (opacity ?? this.opacity * 255).round(), + ); + } + + @override + RgbaF32 toRgbaF32({double? opacity}) { + return RgbaF32.fromRGBA( + red, + green, + blue, + opacity ?? this.opacity, + ); + } + + @override + RgbaF64 toRgbaF64({double? opacity}) { + return opacity == null ? this : RgbaF64.fromRGBA(red, green, blue, opacity); + } + + @override + String toString() { + return 'RgbaF64.fromRGBA($red, $green, $blue, $alpha)'; + } +} + +final class _RgbaF64ColorFormat with ColorFormat { + const _RgbaF64ColorFormat(); + + @override + int get lengthInBytes => Float64List.bytesPerElement * 4; + + @override + RgbaF64 getPixel(ByteData buffer, int offset) { + return RgbaF64.fromRGBA( + buffer.getFloat64(offset), + buffer.getFloat64(offset + 8), + buffer.getFloat64(offset + 16), + buffer.getFloat64(offset + 24), + ); + } + + @override + @pragma('dart2js:tryInline') + @pragma('vm:prefer-inline') + void setPixel(ByteData buffer, int offset, Color color) { + final pixel = color.toRgbaF64(); + buffer.setFloat64(offset, pixel.red); + buffer.setFloat64(offset + 8, pixel.green); + buffer.setFloat64(offset + 16, pixel.blue); + buffer.setFloat64(offset + 24, pixel.alpha); + } +} diff --git a/lib/src/color/sized.dart b/lib/src/color/sized.dart new file mode 100644 index 0000000..9e3ce09 --- /dev/null +++ b/lib/src/color/sized.dart @@ -0,0 +1,4 @@ +part of '../color.dart'; + +/// A marker interface for colors that have a fixed size. +sealed class SizedColor implements Color {} diff --git a/lib/src/color/vec4.dart b/lib/src/color/vec4.dart deleted file mode 100644 index 8d7dcee..0000000 --- a/lib/src/color/vec4.dart +++ /dev/null @@ -1,152 +0,0 @@ -part of '../color.dart'; - -/// An immutable 32-bit ARGB floating-point color vector. -/// -/// On devices where wide color gamut is supported, or when advanced blends are -/// required, the 4D floating-point color vector is a common and efficient way -/// to represent colors in a 2D image; the color is represented as a 4D vector -/// with the format `[r, g, b, a]` where each component is in the range 0.0-1.0. -/// -/// The color vector is useful for advanced blending operations, such as -/// compositing, where the color components are combined using floating-point -/// arithmetic to produce a final color value; the color vector is also useful -/// for representing colors in a wide color gamut, where the color components -/// are not limited to the range 0-255. -/// -/// ## Limitations -/// -/// Due to [platform limitations][31487], `const` constructors are not -/// currently supported. -/// -/// [31487]: https://github.com/dart-lang/sdk/issues/31487 -/// -/// As a workaround, see [Color.fromRGBAFloats]. -final class Vec4Color extends Color { - /// [Transparent black][]: fully transparent with no channel components. - /// - /// [transparent black]: https://drafts.csswg.org/css-color/#transparent-black - /// - /// Often used as the default color for transparent regions. - static final transparentBlack = Vec4Color(Float32x4.zero()); - - /// Creates a new color from the given 4D floating-point [value]. - /// - /// Each component is clamped to the range 0.0-1.0. - Vec4Color(Float32x4 value) : this._(_clamp(value)); - - /// Creates a new color from the given channel components. - /// - /// Each component is clamped to the range 0.0-1.0, and the [alpha] channel is - /// defaulted to fully opaque. - Vec4Color.fromRGBA( - double red, - double green, - double blue, [ - double alpha = 1.0, - ]) : this(Float32x4(red, green, blue, alpha)); - - /// Creates a new color using [value] as each color channel and [opacity]. - /// - /// The [value] is an integer in the range 0-255, and the [opacity] is a - /// double value in the range 0.0-1.0; each component is clamped to the valid - /// range and opacity is defaulted to fully opaque. - Vec4Color.splat( - double value, [ - double opacity = 1.0, - ]) : this.fromRGBA(value, value, value, opacity); - - const Vec4Color._(this.value) : super._(); - - /// 4D floating-point color vector. - /// - /// The color vector is in the format `[r, g, b, a]` where each component is - /// in the range 0.0-1.0. - final Float32x4 value; - - static Float32x4 _clamp(Float32x4 vec4) { - return vec4.clamp(Float32x4.zero(), Float32x4.splat(1.0)); - } - - @override - double get gray => red * 0.33 + green * 0.59 + blue * 0.11; - - @override - bool get isGrayscale => value.x == value.y && value.y == value.z; - - @override - Vec4Color get grayscale => Vec4Color.splat(gray, alpha); - - @override - bool get isOpaque => value.w == 1.0; - - @override - bool get isTransparent => value.w == 0.0; - - /// The red channel component of the color. - /// - /// The red channel is a floating-point value in the range 0.0-1.0. - double get red => value.x; - - /// The green channel component of the color. - /// - /// The green channel is a floating-point value in the range 0.0-1.0. - double get green => value.y; - - /// The blue channel component of the color. - /// - /// The blue channel is a floating-point value in the range 0.0-1.0. - double get blue => value.z; - - /// The alpha channel component of the color. - /// - /// The alpha channel is a floating-point value in the range 0.0-1.0. - double get alpha => value.w; - - @override - bool operator ==(Object other) { - if (other is! Vec4Color) { - return false; - } - final result = value.equal(other.value); - return result.signMask == Int32x4.wwxx; - } - - @override - int get hashCode => Object.hash(value.x, value.y, value.z, value.w); - - /// Returns a copy of the color with the given channel components replaced. - /// - /// The components are clamped to the range 0.0-1.0. - Vec4Color copyWith({ - double? red, - double? green, - double? blue, - double? alpha, - }) { - return Vec4Color.fromRGBA( - red ?? this.red, - green ?? this.green, - blue ?? this.blue, - alpha ?? this.alpha, - ); - } - - @override - RgbaColor get rgba { - final rgba = value.scale(255.0); - return RgbaColor.fromRGBA( - rgba.x.round(), - rgba.y.round(), - rgba.z.round(), - rgba.w.round(), - ); - } - - @override - Vec4Color get vec4 => this; - - @override - String toString() { - return 'Vec4Color.fromRGBA(${value.x}, ${value.y}, ${value.z}, ${value.w})'; - } -} diff --git a/lib/src/color/zero.dart b/lib/src/color/zero.dart new file mode 100644 index 0000000..2e56c3d --- /dev/null +++ b/lib/src/color/zero.dart @@ -0,0 +1,44 @@ +part of '../color.dart'; + +final class _ZeroColor extends Color { + const _ZeroColor(); + + @override + double get opacity => 0.0; + + @override + bool operator ==(Object other) => other is Color && other.isZero; + + @override + int get hashCode => 0; + + @override + bool get isZero => true; + + @override + bool get isGrayscale => true; + + @override + Color toGrayscale() => this; + + @override + Rgb8 toRgb8() => Rgb8.zero; + + @override + Rgba8 toRgba8({double opacity = 0.0}) { + return opacity == 0.0 ? Rgba8.zero : Rgba8.splat(0, opacity * 0xFF ~/ 1); + } + + @override + RgbaF32 toRgbaF32({double opacity = 0.0}) { + return opacity == 0.0 ? RgbaF32.zero : RgbaF32.splat(0.0, opacity); + } + + @override + RgbaF64 toRgbaF64({double opacity = 0.0}) { + return opacity == 0.0 ? RgbaF64.zero : RgbaF64.splat(0.0, opacity); + } + + @override + String toString() => 'Color.zero'; +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart new file mode 100644 index 0000000..2643782 --- /dev/null +++ b/lib/src/utils.dart @@ -0,0 +1,16 @@ +import 'dart:typed_data'; + +/// A 32-bit floating-point vector with each lane set to `0.0`. +/// +/// This is equivalent to `Float32x4.zero()`. +final float32x4Zero = Float32x4.zero(); + +/// A 32-bit floating-point vector with each lane set to `1.0`. +/// +/// This is equivalent to `Float32x4.splat(1.0)`. +final float32x4One = Float32x4.splat(1.0); + +/// A 32-bit floating-point vector with each lane set to `-1.0`. +/// +/// This is equivalent to `Float32x4.splat(-1.0)`. +final float32x4NegativeOne = Float32x4.splat(-1.0); diff --git a/test/blend_test.dart b/test/blend_test.dart deleted file mode 100644 index 24d731a..0000000 --- a/test/blend_test.dart +++ /dev/null @@ -1,305 +0,0 @@ -// Make the tests shuw implicit (default) arguments. -// ignore_for_file: avoid_redundant_argument_values - -import 'package:pxl/src/blend.dart'; -import 'package:pxl/src/color.dart'; - -import 'src/prelude.dart'; - -void main() { - group('clear: output = (0, 0, 0, 0)', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(1.0, 1.0, 1.0); - final dst = Vec4Color.fromRGBA(0.5, 0.5, 0.5); - final out = BlendMode.clear.blendVec4(src, dst); - check(out).equals(Vec4Color.transparentBlack); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBO(255, 255, 255); - final dst = RgbaColor.fromRGBO(128, 128, 128); - final out = BlendMode.clear.blendRgba(src, dst); - check(out).equals(RgbaColor.transparentBlack); - }); - }); - - group('src: output = src', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(1.0, 1.0, 1.0); - final dst = Vec4Color.fromRGBA(0.5, 0.5, 0.5); - final out = BlendMode.src.blendVec4(src, dst); - check(out).equals(src); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBO(255, 255, 255); - final dst = RgbaColor.fromRGBO(128, 128, 128); - final out = BlendMode.src.blendRgba(src, dst); - check(out).equals(src); - }); - }); - - group('dst: output = dst', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(1.0, 1.0, 1.0); - final dst = Vec4Color.fromRGBA(0.5, 0.5, 0.5); - final out = BlendMode.dst.blendVec4(src, dst); - check(out).equals(dst); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBO(255, 255, 255); - final dst = RgbaColor.fromRGBO(128, 128, 128); - final out = BlendMode.dst.blendRgba(src, dst); - check(out).equals(dst); - }); - }); - - group('srcIn: output = src * dst.alpha', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(1.0, 1.0, 1.0, 1.0); - final dst = Vec4Color.fromRGBA(0.5, 0.5, 0.5, 0.5); - final out = BlendMode.srcIn.blendVec4(src, dst); - check(out).equals(Vec4Color.fromRGBA(0.5, 0.5, 0.5, 0.5)); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(255, 255, 255, 255); - final dst = RgbaColor.fromRGBA(128, 128, 128, 128); - final out = BlendMode.srcIn.blendRgba(src, dst); - check(out).equals(RgbaColor.fromRGBA(128, 128, 128, 128)); - }); - }); - - group('dstIn: output = dst * src.alpha', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(1.0, 1.0, 1.0, 0.5); - final dst = Vec4Color.fromRGBA(0.5, 0.5, 0.5, 1.0); - final out = BlendMode.dstIn.blendVec4(src, dst); - check(out).equals(Vec4Color.fromRGBA(0.25, 0.25, 0.25, 0.5)); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(255, 255, 255, 128); - final dst = RgbaColor.fromRGBA(128, 128, 128, 255); - final out = BlendMode.dstIn.blendRgba(src, dst); - check(out).equals(RgbaColor.fromRGBA(64, 64, 64, 128)); - }); - }); - - group('srcOut: output = src * (1 - dst.alpha)', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(1.0, 1.0, 1.0, 0.5); - final dst = Vec4Color.fromRGBA(0.5, 0.5, 0.5, 1.0); - final out = BlendMode.srcOut.blendVec4(src, dst); - check(out).equals(Vec4Color.fromRGBA(0.25, 0.25, 0.25, 0.5)); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(255, 255, 255, 128); - final dst = RgbaColor.fromRGBA(128, 128, 128, 255); - final out = BlendMode.srcOut.blendRgba(src, dst); - check(out).equals(RgbaColor.fromRGBA(64, 64, 64, 127)); - }); - }); - - group('dstOut: output = dst * (1 - src.alpha)', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(1.0, 1.0, 1.0, 1.0); - final dst = Vec4Color.fromRGBA(0.5, 0.5, 0.5, 0.5); - final out = BlendMode.dstOut.blendVec4(src, dst); - check(out).equals(Vec4Color.fromRGBA(0.5, 0.5, 0.5, 0.5)); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(255, 255, 255, 255); - final dst = RgbaColor.fromRGBA(128, 128, 128, 128); - final out = BlendMode.dstOut.blendRgba(src, dst); - check(out).equals(RgbaColor.fromRGBA(127, 127, 127, 127)); - }); - }); - - group('srcOver: src + dst * (1 - src.alpha)', () { - test('vec4', () { - final blue50 = Vec4Color.fromRGBA(0.0, 0.0, 1.0, 0.5); - final red50 = Vec4Color.fromRGBA(1.0, 0.0, 0.0, 0.5); - final out = BlendMode.srcOver.blendVec4(blue50, red50); - check(out).equals(Vec4Color.fromRGBA(0.5, 0.0, 1.0, 0.75)); - }); - - test('rgba', () { - final blue50 = RgbaColor.fromRGBA(0, 0, 255, 128); - final red50 = RgbaColor.fromRGBA(255, 0, 0, 128); - final out = BlendMode.srcOver.blendRgba(blue50, red50); - check(out).equals(RgbaColor.fromRGBA(127, 0, 255, 192)); - }); - }); - - group('dstOver: dst + src * (1 - dst.alpha)', () { - test('vec4', () { - final blue50 = Vec4Color.fromRGBA(0.0, 0.0, 1.0, 0.5); - final red50 = Vec4Color.fromRGBA(1.0, 0.0, 0.0, 0.5); - final out = BlendMode.dstOver.blendVec4(blue50, red50); - check(out).equals(Vec4Color.fromRGBA(1.0, 0.0, 0.5, 0.75)); - }); - - test('rgba', () { - final blue50 = RgbaColor.fromRGBA(0, 0, 255, 128); - final red50 = RgbaColor.fromRGBA(255, 0, 0, 128); - final out = BlendMode.dstOver.blendRgba(blue50, red50); - check(out).equals(RgbaColor.fromRGBA(255, 0, 127, 192)); - }); - }); - - group('srcATop: src * dst.alpha + dst * (1 - src.alpha)', () { - test('vec4', () { - final blue50 = Vec4Color.fromRGBA(0.0, 0.0, 1.0, 0.5); - final red50 = Vec4Color.fromRGBA(1.0, 0.0, 0.0, 0.5); - final out = BlendMode.srcATop.blendVec4(blue50, red50); - check(out).equals(Vec4Color.fromRGBA(0.5, 0.0, 0.5, 0.5)); - }); - - test('rgba', () { - final blue50 = RgbaColor.fromRGBA(0, 0, 255, 128); - final red50 = RgbaColor.fromRGBA(255, 0, 0, 128); - final out = BlendMode.srcATop.blendRgba(blue50, red50); - check(out).equals(RgbaColor.fromRGBA(127, 0, 128, 128)); - }); - }); - - group('dstATop: dst * src.alpha + src * (1 - dst.alpha)', () { - test('vec4', () { - final blue50 = Vec4Color.fromRGBA(0.0, 0.0, 1.0, 0.5); - final red50 = Vec4Color.fromRGBA(1.0, 0.0, 0.0, 0.5); - final out = BlendMode.dstATop.blendVec4(blue50, red50); - check(out).equals(Vec4Color.fromRGBA(0.5, 0.0, 0.5, 0.5)); - }); - - test('rgba', () { - final blue50 = RgbaColor.fromRGBA(0, 0, 255, 128); - final red50 = RgbaColor.fromRGBA(255, 0, 0, 128); - final out = BlendMode.dstATop.blendRgba(blue50, red50); - check(out).equals(RgbaColor.fromRGBA(128, 0, 127, 128)); - }); - }); - - group('darken: output = min(src, dst)', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - final dst = Vec4Color.fromRGBA(0.3, 0.2, 0.1, 0.4); - final out = BlendMode.darken.blendVec4(src, dst); - check(out) - ..red.isCloseTo(0.1, 0.001) - ..green.isCloseTo(0.2, 0.001) - ..blue.isCloseTo(0.1, 0.001) - ..alpha.isCloseTo(0.64, 0.001); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(25, 50, 75, 100); - final dst = RgbaColor.fromRGBA(75, 50, 25, 100); - final out = BlendMode.darken.blendRgba(src, dst); - check(out) - ..red.equals(25) - ..green.equals(50) - ..blue.equals(25) - ..alpha.equals(161); - }); - }); - - group('lighten: output = max(src, dst)', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - final dst = Vec4Color.fromRGBA(0.3, 0.2, 0.1, 0.4); - final out = BlendMode.lighten.blendVec4(src, dst); - check(out) - ..red.isCloseTo(0.3, 0.001) - ..green.isCloseTo(0.2, 0.001) - ..blue.isCloseTo(0.3, 0.001) - ..alpha.isCloseTo(0.64, 0.001); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(25, 50, 75, 100); - final dst = RgbaColor.fromRGBA(75, 50, 25, 100); - final out = BlendMode.lighten.blendRgba(src, dst); - check(out) - ..red.equals(75) - ..green.equals(50) - ..blue.equals(75) - ..alpha.equals(161); - }); - }); - - group('multipy: output = src * dst', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - final dst = Vec4Color.fromRGBA(0.3, 0.2, 0.1, 0.4); - final out = BlendMode.multiply.blendVec4(src, dst); - check(out) - ..red.isCloseTo(0.03, 0.001) - ..green.isCloseTo(0.04, 0.001) - ..blue.isCloseTo(0.03, 0.001) - ..alpha.isCloseTo(0.16, 0.001); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(25, 50, 75, 100); - final dst = RgbaColor.fromRGBA(75, 50, 25, 100); - final out = BlendMode.multiply.blendRgba(src, dst); - check(out) - ..red.equals(7) - ..green.equals(10) - ..blue.equals(7) - ..alpha.equals(39); - }); - }); - - group('screen: output = 1 - (1 - src) * (1 - dst)', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - final dst = Vec4Color.fromRGBA(0.3, 0.2, 0.1, 0.4); - final out = BlendMode.screen.blendVec4(src, dst); - check(out) - ..red.isCloseTo(0.37, 0.001) - ..green.isCloseTo(0.36, 0.001) - ..blue.isCloseTo(0.37, 0.001) - ..alpha.isCloseTo(0.64, 0.001); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(25, 50, 75, 100); - final dst = RgbaColor.fromRGBA(75, 50, 25, 100); - final out = BlendMode.screen.blendRgba(src, dst); - check(out) - ..red.equals(93) - ..green.equals(90) - ..blue.equals(93) - ..alpha.equals(161); - }); - }); - - group('xor: output = src * (1 - dst) + dst * (1 - src)', () { - test('vec4', () { - final src = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - final dst = Vec4Color.fromRGBA(0.3, 0.2, 0.1, 0.4); - final out = BlendMode.xor.blendVec4(src, dst); - check(out) - ..red.isCloseTo(0.24, 0.001) - ..green.isCloseTo(0.24, 0.001) - ..blue.isCloseTo(0.24, 0.001) - ..alpha.isCloseTo(0.48, 0.001); - }); - - test('rgba', () { - final src = RgbaColor.fromRGBA(25, 50, 75, 100); - final dst = RgbaColor.fromRGBA(75, 50, 25, 100); - final out = BlendMode.xor.blendRgba(src, dst); - check(out) - ..red.equals(61) - ..green.equals(61) - ..blue.equals(61) - ..alpha.equals(122); - }); - }); -} diff --git a/test/buffer/base_test.dart b/test/buffer/base_test.dart deleted file mode 100644 index e69de29..0000000 diff --git a/test/buffer_test.dart b/test/buffer_test.dart deleted file mode 100644 index d2d0692..0000000 --- a/test/buffer_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:pxl/pxl.dart'; - -import 'src/prelude.dart'; - -void main() { - group('Buffer.empty', () { - test('width is 0', () { - check(Buffer.empty()).width.equals(0); - }); - - test('height is 0', () { - check(Buffer.empty()).height.equals(0); - }); - - test('length is 0', () { - check(Buffer.empty()).length.equals(0); - }); - - test('operator [] throws', () { - final buffer = Buffer.empty(); - check(() => buffer[0]).throws(); - }); - - test('uncheckedGet throws', () { - final buffer = Buffer.empty(); - check(() => buffer.uncheckedGet(0)).throws(); - }); - }); -} diff --git a/test/color/const_test.dart b/test/color/const_test.dart deleted file mode 100644 index b254454..0000000 --- a/test/color/const_test.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:pxl/pxl.dart' show Color, RgbaColor, Vec4Color; - -import '../src/prelude.dart'; - -void main() { - test('converts floats to integers', () { - const color = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.4); - check(color.rgba) - ..red.equals((0.1 * 255).round()) - ..green.equals((0.2 * 255).round()) - ..blue.equals((0.3 * 255).round()) - ..alpha.equals((0.4 * 255).round()); - }); - - test('converts floats to floats', () { - const color = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.4); - check(color.vec4) - ..red.isCloseTo(0.1, 0.0001) - ..green.isCloseTo(0.2, 0.0001) - ..blue.isCloseTo(0.3, 0.0001) - ..alpha.isCloseTo(0.4, 0.0001); - }); - - test('clamps red to 0.0 ... 1.0', () { - const color1 = Color.fromRGBAFloats(-0.1, 0.2, 0.3, 0.4); - check(color1.vec4).red.equals(0.0); - - const color2 = Color.fromRGBAFloats(1.1, 0.2, 0.3, 0.4); - check(color2.vec4).red.equals(1.0); - }); - - test('clamps green to 0.0 ... 1.0', () { - const color1 = Color.fromRGBAFloats(0.1, -0.2, 0.3, 0.4); - check(color1.vec4).green.equals(0.0); - - const color2 = Color.fromRGBAFloats(0.1, 1.1, 0.3, 0.4); - check(color2.vec4).green.equals(1.0); - }); - - test('clamps blue to 0.0 ... 1.0', () { - const color1 = Color.fromRGBAFloats(0.1, 0.2, -0.3, 0.4); - check(color1.vec4).blue.equals(0.0); - - const color2 = Color.fromRGBAFloats(0.1, 0.2, 1.1, 0.4); - check(color2.vec4).blue.equals(1.0); - }); - - test('clamps alpha to 0.0 ... 1.0', () { - const color1 = Color.fromRGBAFloats(0.1, 0.2, 0.3, -0.4); - check(color1.vec4).alpha.equals(0.0); - - const color2 = Color.fromRGBAFloats(0.1, 0.2, 0.3, 1.1); - check(color2.vec4).alpha.equals(1.0); - }); - - test('gray is 0.33/0.59/0.11', () { - const color1 = Color.fromRGBAFloats(1.0, 0.0, 0.0); - check(color1.gray).isCloseTo(0.33, 0.0001); - - const color2 = Color.fromRGBAFloats(0.0, 1.0, 0.0); - check(color2.gray).isCloseTo(0.59, 0.0001); - - const color3 = Color.fromRGBAFloats(0.0, 0.0, 1.0); - check(color3.gray).isCloseTo(0.11, 0.0001); - }); - - test('grayscale', () { - const color = Color.fromRGBAFloats(0.1, 0.2, 0.3); - check(color.grayscale).equals(Vec4Color.splat(color.gray)); - }); - - test('isGrayscale', () { - const color1 = Color.fromRGBAFloats(0.5, 0.5, 0.5); - check(color1.isGrayscale).isTrue(); - - const color2 = Color.fromRGBAFloats(0.1, 0.3, 0.5); - check(color2.isGrayscale).isFalse(); - }); - - test('isOpaque', () { - const color1 = Color.fromRGBAFloats(0.1, 0.2, 0.3); - check(color1.isOpaque).isTrue(); - - const color2 = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.0); - check(color2.isOpaque).isFalse(); - - const color3 = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.5); - check(color3.isOpaque).isFalse(); - }); - - test('isTransparent', () { - const color1 = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.0); - check(color1.isTransparent).isTrue(); - - const color2 = Color.fromRGBAFloats(0.1, 0.2, 0.3); - check(color2.isTransparent).isFalse(); - - const color3 = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.5); - check(color3.isTransparent).isFalse(); - }); - - test('toString', () { - const color = Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.4); - check( - color.toString(), - ).equals('Color.fromRGBAFloats(0.1, 0.2, 0.3, 0.4)'); - }); - - group('Color.transparentBlack', () { - test('gray is 0.0', () { - check(Color.transparentBlack.gray).equals(0.0); - }); - - test('isGrayscale', () { - check(Color.transparentBlack.isGrayscale).isTrue(); - }); - - test('grayscale is self', () { - check( - Color.transparentBlack.grayscale, - ).identicalTo(Color.transparentBlack); - }); - - test('isOpaque', () { - check(Color.transparentBlack.isOpaque).isFalse(); - }); - - test('isTransparent', () { - check(Color.transparentBlack.isTransparent).isTrue(); - }); - - test('rgba is RgbaColor.transparentBlack', () { - check( - Color.transparentBlack.rgba, - ).identicalTo(RgbaColor.transparentBlack); - }); - - test('vec4 is Vec4Color.transparentBlack', () { - check( - Color.transparentBlack.vec4, - ).identicalTo(Vec4Color.transparentBlack); - }); - - test('toString', () { - check(Color.transparentBlack.toString()).equals('Color.transparentBlack'); - }); - }); -} diff --git a/test/color/rgba_test.dart b/test/color/rgba_test.dart deleted file mode 100644 index 9d44e9b..0000000 --- a/test/color/rgba_test.dart +++ /dev/null @@ -1,249 +0,0 @@ -// Makes the test file easier to read. - -import 'package:pxl/pxl.dart' show RgbaColor; - -import '../src/prelude.dart'; - -void main() { - group('.fromARGB', () { - test('alpha < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBA(0, 0, 0, -1); - check(color).alpha.equals(255); - }); - - test('alpha 0 to 255 is retained', () { - for (var i = 0; i <= 255; i++) { - final color = RgbaColor.fromRGBA(0, 0, 0, i); - check(color).alpha.equals(i); - } - }); - - test('alpha > 255 is broght into range with %', () { - const color = RgbaColor.transparentBlack; - check(color).alpha.equals(0); - }); - - test('red < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBA(-1, 0, 0, 0); - check(color).red.equals(255); - }); - - test('red 0 to 255 is retained', () { - for (var i = 0; i <= 255; i++) { - final color = RgbaColor.fromRGBA(i, 0, 0, 0); - check(color).red.equals(i); - } - }); - - test('red > 255 is brought into range with %', () { - const color = RgbaColor.transparentBlack; - check(color).red.equals(0); - }); - - test('green < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBA(0, -1, 0, 0); - check(color).green.equals(255); - }); - - test('green 0 to 255 is retained', () { - for (var i = 0; i <= 255; i++) { - final color = RgbaColor.fromRGBA(0, i, 0, 0); - check(color).green.equals(i); - } - }); - - test('green > 255 is brought into range with %', () { - const color = RgbaColor.transparentBlack; - check(color).green.equals(0); - }); - - test('blue < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBA(0, 0, -1, 0); - check(color).blue.equals(255); - }); - - test('blue 0 to 255 is retained', () { - for (var i = 0; i <= 255; i++) { - final color = RgbaColor.fromRGBA(0, 0, i, 0); - check(color).blue.equals(i); - } - }); - - test('blue > 255 is brought into range with %', () { - const color = RgbaColor.transparentBlack; - check(color).blue.equals(0); - }); - }); - - group('.fromRGBO', () { - test('opacity < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBO(0, 0, 0, -1); - check(color).alpha.equals(1); - }); - - test('opacity 0 to 1 is retained', () { - for (var i = 0.0; i <= 1.0; i += 0.1) { - final color = RgbaColor.fromRGBO(0, 0, 0, i); - check(color).alpha.equals((i * 255).floor()); - } - }); - - test('opacity > 1 is brought into range with %', () { - const color = RgbaColor.fromRGBO(0, 0, 0, 2); - check(color).alpha.equals(254); - }); - - test('red < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBO(-1, 0, 0, 0); - check(color).red.equals(255); - }); - - test('red 0 to 255 is retained', () { - for (var i = 0; i <= 255; i++) { - final color = RgbaColor.fromRGBO(i, 0, 0, 0); - check(color).red.equals(i); - } - }); - - test('red > 255 is brought into range with %', () { - const color = RgbaColor.transparentBlack; - check(color).red.equals(0); - }); - - test('green < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBO(0, -1, 0, 0); - check(color).green.equals(255); - }); - - test('green 0 to 255 is retained', () { - for (var i = 0; i <= 255; i++) { - final color = RgbaColor.fromRGBO(0, i, 0, 0); - check(color).green.equals(i); - } - }); - - test('green > 255 is brought into range with %', () { - const color = RgbaColor.transparentBlack; - check(color).green.equals(0); - }); - - test('blue < 0 is brought into range with %', () { - const color = RgbaColor.fromRGBO(0, 0, -1, 0); - check(color).blue.equals(255); - }); - - test('blue 0 to 255 is retained', () { - for (var i = 0; i <= 255; i++) { - final color = RgbaColor.fromRGBO(0, 0, i, 0); - check(color).blue.equals(i); - } - }); - - test('blue > 255 is brought into range with %', () { - const color = RgbaColor.transparentBlack; - check(color).blue.equals(0); - }); - }); - - group('.splat', () { - test('creates a color with the same value for all colors', () { - const color = RgbaColor.splat(0x80); - check(color) - ..red.equals(0x80) - ..green.equals(0x80) - ..blue.equals(0x80) - ..alpha.equals(0xFF); - }); - - test('creates a color with the same value for all colors and alpha', () { - const color = RgbaColor.splat(0x80, 0.25); - check(color) - ..red.equals(0x80) - ..green.equals(0x80) - ..blue.equals(0x80) - ..alpha.equals(63); - }); - }); - - test('.value', () { - const color = RgbaColor(0x12345678); - check(color).value.equals(0x12345678); - }); - - test('==, hashCode, toString', () { - const color = RgbaColor(0x12345678); - check(color).equals(RgbaColor(0x12345678)); - check(color.hashCode).equals(RgbaColor(0x12345678).hashCode); - check(color.toString()).equals('RgbaColor(0x12345678)'); - }); - - group('copyWith', () { - test('creates a copy with the same value', () { - const color = RgbaColor(0x12345678); - check(color.copyWith()).equals(color); - }); - - test('creates a copy with the same value and new alpha', () { - const color = RgbaColor(0x12345678); - check(color.copyWith(alpha: 0x9A)).equals(RgbaColor(0x9A345678)); - }); - - test('creates a copy with the same value and new red', () { - const color = RgbaColor(0x12345678); - check(color.copyWith(red: 0x9A)).equals(RgbaColor(0x129A5678)); - }); - - test('creates a copy with the same value and new green', () { - const color = RgbaColor(0x12345678); - check(color.copyWith(green: 0x9A)).equals(RgbaColor(0x12349A78)); - }); - - test('creates a copy with the same value and new blue', () { - const color = RgbaColor(0x12345678); - check(color.copyWith(blue: 0x9A)).equals(RgbaColor(0x1234569A)); - }); - }); - - test('asRgba is a NOP', () { - const color = RgbaColor(0x12345678); - check(color.rgba).identicalTo(color); - }); - - test('asVec4', () { - const color = RgbaColor(0x12345678); - check(color.vec4) - ..red.isCloseTo(color.red / 255, 0.0001) - ..green.isCloseTo(color.green / 255, 0.0001) - ..blue.isCloseTo(color.blue / 255, 0.0001) - ..alpha.isCloseTo(color.alpha / 255, 0.0001); - }); - - test('opacity', () { - const color = RgbaColor(0x12345678); - check(color.opacity).isCloseTo(color.alpha / 255, 0.0001); - }); - - test('isGrayscale', () { - const color1 = RgbaColor(0xFF808080); - check(color1.isGrayscale).isTrue(); - - const color2 = RgbaColor(0x12345678); - check(color2.isGrayscale).isFalse(); - }); - - test('isOpaque', () { - const color1 = RgbaColor(0x12345678); - check(color1.isOpaque).isFalse(); - - const color2 = RgbaColor(0xFF123456); - check(color2.isOpaque).isTrue(); - }); - - test('isTransparent', () { - const color1 = RgbaColor(0x12345678); - check(color1.isTransparent).isFalse(); - - const color2 = RgbaColor(0x00123456); - check(color2.isTransparent).isTrue(); - }); -} diff --git a/test/color/vec4_test.dart b/test/color/vec4_test.dart deleted file mode 100644 index a8331bc..0000000 --- a/test/color/vec4_test.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'package:pxl/pxl.dart' show Vec4Color; - -import '../src/prelude.dart'; - -void main() { - group('.fromRGBA', () { - test('red < 0 is clamped to 0', () { - final color = Vec4Color.fromRGBA(-0.1, 0.0, 0.0, 0.0); - check(color).red.equals(0.0); - }); - - test('red 0 to 1 is retained', () { - for (var i = 0.0; i <= 1.0; i += 0.1) { - final color = Vec4Color.fromRGBA(i, 0.0, 0.0, 0.0); - check(color).red.isCloseTo(i, 0.0001); - } - }); - - test('red > 1 is clamped to 1', () { - final color = Vec4Color.fromRGBA(1.1, 0.0, 0.0, 0.0); - check(color).red.equals(1.0); - }); - - test('green < 0 is clamped to 0', () { - final color = Vec4Color.fromRGBA(0.0, -0.1, 0.0, 0.0); - check(color).green.equals(0.0); - }); - - test('green 0 to 1 is retained', () { - for (var i = 0.0; i <= 1.0; i += 0.1) { - final color = Vec4Color.fromRGBA(0.0, i, 0.0, 0.0); - check(color).green.isCloseTo(i, 0.0001); - } - }); - - test('green > 1 is clamped to 1', () { - final color = Vec4Color.fromRGBA(0.0, 1.1, 0.0, 0.0); - check(color).green.equals(1.0); - }); - - test('blue < 0 is clamped to 0', () { - final color = Vec4Color.fromRGBA(0.0, 0.0, -0.1, 0.0); - check(color).blue.equals(0.0); - }); - - test('blue 0 to 1 is retained', () { - for (var i = 0.0; i <= 1.0; i += 0.1) { - final color = Vec4Color.fromRGBA(0.0, 0.0, i, 0.0); - check(color).blue.isCloseTo(i, 0.0001); - } - }); - - test('blue > 1 is clamped to 1', () { - final color = Vec4Color.fromRGBA(0.0, 0.0, 1.1, 0.0); - check(color).blue.equals(1.0); - }); - - test('alpha < 0 is clamped to 0', () { - final color = Vec4Color.fromRGBA(0.0, 0.0, 0.0, -0.1); - check(color).alpha.equals(0.0); - }); - - test('alpha 0 to 1 is retained', () { - for (var i = 0.0; i <= 1.0; i += 0.1) { - final color = Vec4Color.fromRGBA(0.0, 0.0, 0.0, i); - check(color).alpha.isCloseTo(i, 0.0001); - } - }); - - test('alpha > 1 is clamped to 1', () { - final color = Vec4Color.fromRGBA(0.0, 0.0, 0.0, 1.1); - check(color).alpha.equals(1.0); - }); - }); - - group('.splat', () { - test('creates a color with the same value for all colors', () { - final color = Vec4Color.splat(0.5); - check(color) - ..red.isCloseTo(0.5, 0.0001) - ..green.isCloseTo(0.5, 0.0001) - ..blue.isCloseTo(0.5, 0.0001) - ..alpha.equals(1.0); - }); - - test('creates a color with the same value for all colors and alpha', () { - final color = Vec4Color.splat(0.5, 0.25); - check(color) - ..red.isCloseTo(0.5, 0.0001) - ..green.isCloseTo(0.5, 0.0001) - ..blue.isCloseTo(0.5, 0.0001) - ..alpha.isCloseTo(0.25, 0.0001); - }); - }); - - test('.value is a Float32x4', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check(color.value) - ..has((p) => p.x, 'x').isCloseTo(0.1, 0.0001) - ..has((p) => p.y, 'y').isCloseTo(0.2, 0.0001) - ..has((p) => p.z, 'z').isCloseTo(0.3, 0.0001) - ..has((p) => p.w, 'w').isCloseTo(0.4, 0.0001); - }); - - test('operator==, hashCode, toString', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check(color).equals(Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4)); - check( - color.hashCode, - ).equals(Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4).hashCode); - check(color.toString()).startsWith('Vec4Color.fromRGBA('); - }); - - group('copyWith', () { - test('creates a copy with the same value', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check(color.copyWith()).equals(color); - }); - - test('creates a copy with the same value and new red', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check( - color.copyWith(red: 0.5), - ).equals(Vec4Color.fromRGBA(0.5, 0.2, 0.3, 0.4)); - }); - - test('creates a copy with the same value and new green', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check( - color.copyWith(green: 0.5), - ).equals(Vec4Color.fromRGBA(0.1, 0.5, 0.3, 0.4)); - }); - - test('creates a copy with the same value and new blue', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check( - color.copyWith(blue: 0.5), - ).equals(Vec4Color.fromRGBA(0.1, 0.2, 0.5, 0.4)); - }); - - test('creates a copy with the same value and new alpha', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check( - color.copyWith(alpha: 0.5), - ).equals(Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.5)); - }); - }); - - test('rgba', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check(color.rgba) - ..red.equals((0.1 * 255).round()) - ..green.equals((0.2 * 255).round()) - ..blue.equals((0.3 * 255).round()) - ..alpha.equals((0.4 * 255).round()); - }); - - test('vec4 is a NOP', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.4); - check(color.vec4).identicalTo(color); - }); - - group('gray', () { - test('is 0.33 of the red channel', () { - final color = Vec4Color.fromRGBA(1.0, 0.0, 0.0); - check(color.gray).isCloseTo(0.33, 0.0001); - }); - - test('is 0.59 of the green channel', () { - final color = Vec4Color.fromRGBA(0.0, 1.0, 0.0); - check(color.gray).isCloseTo(0.59, 0.0001); - }); - - test('is 0.11 of the blue channel', () { - final color = Vec4Color.fromRGBA(0.0, 0.0, 1.0); - check(color.gray).isCloseTo(0.11, 0.0001); - }); - - test('.grayscale', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3); - check(color.grayscale).equals(Vec4Color.splat(color.gray)); - }); - }); - - group('isGrayscale', () { - test('is true for gray colors', () { - final color = Vec4Color.fromRGBA(0.5, 0.5, 0.5); - check(color.isGrayscale).isTrue(); - }); - - test('is false for non-gray colors', () { - final color = Vec4Color.fromRGBA(0.1, 0.3, 0.5); - check(color.isGrayscale).isFalse(); - }); - }); - - group('isOpaque', () { - test('is true for opaque colors', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3); - check(color.isOpaque).isTrue(); - }); - - test('is false for transparent colors', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.0); - check(color.isOpaque).isFalse(); - }); - - test('is false for translucent colors', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.5); - check(color.isOpaque).isFalse(); - }); - }); - - group('isTransparent', () { - test('is true for transparent colors', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.0); - check(color.isTransparent).isTrue(); - }); - - test('is false for opaque colors', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3); - check(color.isTransparent).isFalse(); - }); - - test('is false for translucent colors', () { - final color = Vec4Color.fromRGBA(0.1, 0.2, 0.3, 0.5); - check(color.isTransparent).isFalse(); - }); - }); -} diff --git a/test/pixels_test.dart b/test/pixels_test.dart deleted file mode 100644 index b603baa..0000000 --- a/test/pixels_test.dart +++ /dev/null @@ -1,441 +0,0 @@ -import 'package:pxl/pxl.dart'; - -import 'src/prelude.dart'; - -void main() { - group('RgbaPixels', () { - const red = RgbaColor.fromRGBO(0xFF, 0x00, 0x00); - const green = RgbaColor.fromRGBO(0x00, 0xFF, 0x00); - const blue = RgbaColor.fromRGBO(0x00, 0x00, 0xFF); - - test('width must be non-negative', () { - check(() => RgbaPixels(0, 1)).returnsNormally(); - check(() => RgbaPixels(1, 1)).returnsNormally(); - check(() => RgbaPixels(-1, 1)).throws(); - }); - - test('height must be non-negative', () { - check(() => RgbaPixels(1, 0)).returnsNormally(); - check(() => RgbaPixels(1, 1)).returnsNormally(); - check(() => RgbaPixels(1, -1)).throws(); - }); - - test('creates a transparent black buffer', () { - final buffer = RgbaPixels(3, 2); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toRgba8888List()).every((p) { - return p.equals(RgbaColor.transparentBlack.value); - }); - }); - - test('creates a filled buffer', () { - final buffer = RgbaPixels(3, 2, red); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toRgba8888List()).every((p) { - return p.equals(red.value); - }); - }); - - group('.indexAt', () { - test('out of bounds', () { - final buffer = RgbaPixels(3, 2); - check(() => buffer.indexAt(Pos(3, 0))).throws(); - check(() => buffer.indexAt(Pos(0, 2))).throws(); - check(() => buffer.indexAt(Pos(3, 2))).throws(); - check(() => buffer.indexAt(Pos(-1, 0))).throws(); - }); - - test('in bounds', () { - final buffer = RgbaPixels(3, 2); - check(buffer.indexAt(Pos(0, 0))).equals(0); - check(buffer.indexAt(Pos(1, 0))).equals(1); - check(buffer.indexAt(Pos(2, 0))).equals(2); - check(buffer.indexAt(Pos(0, 1))).equals(3); - check(buffer.indexAt(Pos(1, 1))).equals(4); - check(buffer.indexAt(Pos(2, 1))).equals(5); - }); - }); - - group('.from', () { - test('copies a buffer', () { - final source = RgbaPixels(3, 2, red); - final buffer = RgbaPixels.from(source); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toRgba8888List()).every((p) { - return p.equals(red.value); - }); - }); - - test('copies a clipped buffer', () { - final source = RgbaPixels(3, 2, red); - final buffer = RgbaPixels.from(source, clip: Rect.fromLTWH(1, 1, 2, 1)); - check(buffer.width).equals(2); - check(buffer.height).equals(1); - check(buffer.length).equals(2); - check(buffer.toRgba8888List()).every((p) { - return p.equals(red.value); - }); - }); - }); - - group('.fromColors', () { - test('width must be non-negative', () { - check( - () => RgbaPixels.fromColors([], width: 0, height: 0), - ).returnsNormally(); - check( - () => RgbaPixels.fromColors( - [RgbaColor.transparentBlack], - width: 1, - height: 1, - ), - ).returnsNormally(); - check( - () => RgbaPixels.fromColors( - [RgbaColor.transparentBlack], - width: -1, - height: 1, - ), - ).throws(); - }); - - test('height must be non-negative', () { - check( - () => RgbaPixels.fromColors([], width: 0, height: 0), - ).returnsNormally(); - check( - () => RgbaPixels.fromColors( - [RgbaColor.transparentBlack], - width: 1, - height: 1, - ), - ).returnsNormally(); - check( - () => RgbaPixels.fromColors( - [RgbaColor.transparentBlack], - width: 1, - height: -1, - ), - ).throws(); - }); - - test('the width and height must equal the length', () { - check( - () => RgbaPixels.fromColors( - [RgbaColor.transparentBlack], - width: 1, - height: 1, - ), - ).returnsNormally(); - check( - () => RgbaPixels.fromColors( - [RgbaColor.transparentBlack, RgbaColor.transparentBlack], - width: 1, - height: 1, - ), - ).throws(); - }); - - test('can create a buffer from a list of colors', () { - final buffer = RgbaPixels.fromColors( - [red, green, blue, red, green, blue], - width: 3, - height: 2, - ); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toRgba8888List()).deepEquals([ - red.value, - green.value, - blue.value, - red.value, - green.value, - blue.value, - ]); - }); - }); - - group('.fromList', () { - test('width must be non-negative', () { - check( - () => RgbaPixels.fromList([], width: 0), - ).returnsNormally(); - check( - () => RgbaPixels.fromList([red.value], width: 1), - ).returnsNormally(); - check( - () => RgbaPixels.fromList([red.value], width: -1), - ).throws(); - }); - - test('height must be non-negative', () { - check( - () => RgbaPixels.fromList([], width: 0), - ).returnsNormally(); - check( - () => RgbaPixels.fromList([red.value], width: 1), - ).returnsNormally(); - check( - () => RgbaPixels.fromList([red.value], width: 1, height: -1), - ).throws(); - }); - - test('the length must be a multiple of the width', () { - check( - () => RgbaPixels.fromList([red.value], width: 1), - ).returnsNormally(); - check( - () => RgbaPixels.fromList( - [ - red.value, - green.value, - blue.value, - ], - width: 2, - ), - ).throws(); - }); - - test('can create a buffer from a list of pixels', () { - final buffer = RgbaPixels.fromList( - [ - red.value, - green.value, - blue.value, - red.value, - green.value, - blue.value, - ], - width: 3, - ); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toRgba8888List()).deepEquals([ - red.value, - green.value, - blue.value, - red.value, - green.value, - blue.value, - ]); - }); - }); - }); - - group('Vec4Pixels', () { - final red = Vec4Color.fromRGBA(1.0, 0.0, 0.0); - final green = Vec4Color.fromRGBA(0.0, 1.0, 0.0); - final blue = Vec4Color.fromRGBA(0.0, 0.0, 1.0); - - test('width must be non-negative', () { - check(() => Vec4Pixels(0, 1)).returnsNormally(); - check(() => Vec4Pixels(1, 1)).returnsNormally(); - check(() => Vec4Pixels(-1, 1)).throws(); - }); - - test('height must be non-negative', () { - check(() => Vec4Pixels(1, 0)).returnsNormally(); - check(() => Vec4Pixels(1, 1)).returnsNormally(); - check(() => Vec4Pixels(1, -1)).throws(); - }); - - test('creates a transparent black buffer', () { - final buffer = Vec4Pixels(3, 2); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toVec4List()).every((p) { - return p - .has(Vec4Color.new, 'Vec4Color') - .equals(Vec4Color.transparentBlack); - }); - }); - - test('creates a filled buffer', () { - final buffer = Vec4Pixels(3, 2, red); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toVec4List()).every((p) { - return p.has(Vec4Color.new, 'Vec4Color').equals(red); - }); - }); - - group('.from', () { - test('copies a buffer', () { - final source = Vec4Pixels(3, 2, red); - final buffer = Vec4Pixels.from(source); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toVec4List()).every((p) { - return p.has(Vec4Color.new, 'Vec4Color').equals(red); - }); - }); - - test('copies a clipped buffer', () { - final source = Vec4Pixels(3, 2, red); - final buffer = Vec4Pixels.from(source, clip: Rect.fromLTWH(1, 1, 2, 1)); - check(buffer.width).equals(2); - check(buffer.height).equals(1); - check(buffer.length).equals(2); - check(buffer.toVec4List()).every((p) { - return p.has(Vec4Color.new, 'Vec4Color').equals(red); - }); - }); - }); - - group('.fromColors', () { - test('width must be non-negative', () { - check( - () => Vec4Pixels.fromColors([], width: 0, height: 0), - ).returnsNormally(); - check( - () => Vec4Pixels.fromColors( - [Vec4Color.transparentBlack], - width: 1, - height: 1, - ), - ).returnsNormally(); - check( - () => Vec4Pixels.fromColors( - [Vec4Color.transparentBlack], - width: -1, - height: 1, - ), - ).throws(); - }); - - test('height must be non-negative', () { - check( - () => Vec4Pixels.fromColors([], width: 0, height: 0), - ).returnsNormally(); - check( - () => Vec4Pixels.fromColors( - [Vec4Color.transparentBlack], - width: 1, - height: 1, - ), - ).returnsNormally(); - check( - () => Vec4Pixels.fromColors( - [Vec4Color.transparentBlack], - width: 1, - height: -1, - ), - ).throws(); - }); - - test('the width and height must equal the length', () { - check( - () => Vec4Pixels.fromColors( - [Vec4Color.transparentBlack], - width: 1, - height: 1, - ), - ).returnsNormally(); - check( - () => Vec4Pixels.fromColors( - [Vec4Color.transparentBlack, Vec4Color.transparentBlack], - width: 1, - height: 1, - ), - ).throws(); - }); - - test('can create a buffer from a list of colors', () { - final buffer = Vec4Pixels.fromColors( - [red, green, blue, red, green, blue], - width: 3, - height: 2, - ); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toVec4List().map(Vec4Color.new)).deepEquals([ - red, - green, - blue, - red, - green, - blue, - ]); - }); - }); - - group('.fromList', () { - test('width must be non-negative', () { - check( - () => Vec4Pixels.fromList([], width: 0), - ).returnsNormally(); - check( - () => Vec4Pixels.fromList([red.value], width: 1), - ).returnsNormally(); - check( - () => Vec4Pixels.fromList([red.value], width: -1), - ).throws(); - }); - - test('height must be non-negative', () { - check( - () => Vec4Pixels.fromList([], width: 0), - ).returnsNormally(); - check( - () => Vec4Pixels.fromList([red.value], width: 1), - ).returnsNormally(); - check( - () => Vec4Pixels.fromList([red.value], width: 1, height: -1), - ).throws(); - }); - - test('the length must be a multiple of the width', () { - check( - () => Vec4Pixels.fromList([red.value], width: 1), - ).returnsNormally(); - check( - () => Vec4Pixels.fromList( - [ - red.value, - green.value, - blue.value, - ], - width: 2, - ), - ).throws(); - }); - - test('can create a buffer from a list of pixels', () { - final buffer = Vec4Pixels.fromList( - [ - red.value, - green.value, - blue.value, - red.value, - green.value, - blue.value, - ], - width: 3, - ); - check(buffer.width).equals(3); - check(buffer.height).equals(2); - check(buffer.length).equals(6); - check(buffer.toVec4List().map(Vec4Color.new)).deepEquals([ - red, - green, - blue, - red, - green, - blue, - ]); - }); - }); - }); -} diff --git a/test/src/prelude.dart b/test/src/prelude.dart index 27db314..3e985c7 100644 --- a/test/src/prelude.dart +++ b/test/src/prelude.dart @@ -1,47 +1,2 @@ -import 'package:checks/checks.dart'; -import 'package:pxl/pxl.dart'; - export 'package:checks/checks.dart'; export 'package:test/test.dart' show TestOn, group, setUp, tearDown, test; - -extension BufferSubject on Subject> { - /// Width of the buffer. - Subject get width => has((p) => p.width, 'width'); - - /// Height of the buffer. - Subject get height => has((p) => p.height, 'height'); - - /// Length of the buffer. - Subject get length => has((p) => p.length, 'length'); -} - -extension RgbaColorSubject on Subject { - /// Returns the red channel component of the color. - Subject get red => has((p) => p.red, 'red'); - - /// Returns the green channel component of the color. - Subject get green => has((p) => p.green, 'green'); - - /// Returns the blue channel component of the color. - Subject get blue => has((p) => p.blue, 'blue'); - - /// Returns the alpha channel component of the color. - Subject get alpha => has((p) => p.alpha, 'alpha'); - - /// Returns the value of the color. - Subject get value => has((p) => p.value, 'value'); -} - -extension Vec4ColorSubject on Subject { - /// Returns the red channel component of the color. - Subject get red => has((p) => p.red, 'red'); - - /// Returns the green channel component of the color. - Subject get green => has((p) => p.green, 'green'); - - /// Returns the blue channel component of the color. - Subject get blue => has((p) => p.blue, 'blue'); - - /// Returns the alpha channel component of the color. - Subject get alpha => has((p) => p.alpha, 'alpha'); -}