| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // For documentation see https://github.com/flutter/engine/blob/main/lib/ui/painting.dart |
| part of ui; |
| |
| // ignore: unused_element, Used in Shader assert. |
| bool _offsetIsValid(Offset offset) { |
| assert(!offset.dx.isNaN && !offset.dy.isNaN, 'Offset argument contained a NaN value.'); |
| return true; |
| } |
| |
| // ignore: unused_element, Used in Shader assert. |
| bool _matrix4IsValid(Float32List matrix4) { |
| assert(matrix4.length == 16, 'Matrix4 must have 16 entries.'); |
| return true; |
| } |
| |
| void _validateColorStops(List<Color> colors, List<double>? colorStops) { |
| if (colorStops == null) { |
| if (colors.length != 2) { |
| throw ArgumentError('"colors" must have length 2 if "colorStops" is omitted.'); |
| } |
| } else { |
| if (colors.length != colorStops.length) { |
| throw ArgumentError('"colors" and "colorStops" arguments must have equal length.'); |
| } |
| } |
| } |
| |
| Color _scaleAlpha(Color a, double factor) { |
| return a.withAlpha(engine.clampInt((a.alpha * factor).round(), 0, 255)); |
| } |
| |
| class Color { |
| const Color(int value) : value = value & 0xFFFFFFFF; |
| const Color.fromARGB(int a, int r, int g, int b) |
| : value = (((a & 0xff) << 24) | |
| ((r & 0xff) << 16) | |
| ((g & 0xff) << 8) | |
| ((b & 0xff) << 0)) & |
| 0xFFFFFFFF; |
| const Color.fromRGBO(int r, int g, int b, double opacity) |
| : value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) | |
| ((r & 0xff) << 16) | |
| ((g & 0xff) << 8) | |
| ((b & 0xff) << 0)) & |
| 0xFFFFFFFF; |
| final int value; |
| int get alpha => (0xff000000 & value) >> 24; |
| double get opacity => alpha / 0xFF; |
| int get red => (0x00ff0000 & value) >> 16; |
| int get green => (0x0000ff00 & value) >> 8; |
| int get blue => (0x000000ff & value) >> 0; |
| Color withAlpha(int a) { |
| return Color.fromARGB(a, red, green, blue); |
| } |
| |
| Color withOpacity(double opacity) { |
| assert(opacity >= 0.0 && opacity <= 1.0); |
| return withAlpha((255.0 * opacity).round()); |
| } |
| |
| Color withRed(int r) { |
| return Color.fromARGB(alpha, r, green, blue); |
| } |
| |
| Color withGreen(int g) { |
| return Color.fromARGB(alpha, red, g, blue); |
| } |
| |
| Color withBlue(int b) { |
| return Color.fromARGB(alpha, red, green, b); |
| } |
| |
| // See <https://www.w3.org/TR/WCAG20/#relativeluminancedef> |
| static double _linearizeColorComponent(double component) { |
| if (component <= 0.03928) { |
| return component / 12.92; |
| } |
| return math.pow((component + 0.055) / 1.055, 2.4) as double; |
| } |
| |
| double computeLuminance() { |
| // See <https://www.w3.org/TR/WCAG20/#relativeluminancedef> |
| final double R = _linearizeColorComponent(red / 0xFF); |
| final double G = _linearizeColorComponent(green / 0xFF); |
| final double B = _linearizeColorComponent(blue / 0xFF); |
| return 0.2126 * R + 0.7152 * G + 0.0722 * B; |
| } |
| |
| static Color? lerp(Color? a, Color? b, double t) { |
| if (b == null) { |
| if (a == null) { |
| return null; |
| } else { |
| return _scaleAlpha(a, 1.0 - t); |
| } |
| } else { |
| if (a == null) { |
| return _scaleAlpha(b, t); |
| } else { |
| return Color.fromARGB( |
| engine.clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255), |
| engine.clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255), |
| engine.clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255), |
| engine.clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255), |
| ); |
| } |
| } |
| } |
| |
| static Color alphaBlend(Color foreground, Color background) { |
| final int alpha = foreground.alpha; |
| if (alpha == 0x00) { |
| // Foreground completely transparent. |
| return background; |
| } |
| final int invAlpha = 0xff - alpha; |
| int backAlpha = background.alpha; |
| if (backAlpha == 0xff) { |
| // Opaque background case |
| return Color.fromARGB( |
| 0xff, |
| (alpha * foreground.red + invAlpha * background.red) ~/ 0xff, |
| (alpha * foreground.green + invAlpha * background.green) ~/ 0xff, |
| (alpha * foreground.blue + invAlpha * background.blue) ~/ 0xff, |
| ); |
| } else { |
| // General case |
| backAlpha = (backAlpha * invAlpha) ~/ 0xff; |
| final int outAlpha = alpha + backAlpha; |
| assert(outAlpha != 0x00); |
| return Color.fromARGB( |
| outAlpha, |
| (foreground.red * alpha + background.red * backAlpha) ~/ outAlpha, |
| (foreground.green * alpha + background.green * backAlpha) ~/ outAlpha, |
| (foreground.blue * alpha + background.blue * backAlpha) ~/ outAlpha, |
| ); |
| } |
| } |
| |
| static int getAlphaFromOpacity(double opacity) { |
| return (clampDouble(opacity, 0.0, 1.0) * 255).round(); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is Color && other.value == value; |
| } |
| |
| @override |
| int get hashCode => value.hashCode; |
| |
| @override |
| String toString() { |
| return 'Color(0x${value.toRadixString(16).padLeft(8, '0')})'; |
| } |
| } |
| |
| enum StrokeCap { |
| butt, |
| round, |
| square, |
| } |
| |
| // These enum values must be kept in sync with SkPaint::Join. |
| enum StrokeJoin { |
| miter, |
| round, |
| bevel, |
| } |
| |
| enum PaintingStyle { |
| fill, |
| stroke, |
| } |
| |
| enum BlendMode { |
| // This list comes from Skia's SkXfermode.h and the values (order) should be |
| // kept in sync. |
| // See: https://skia.org/user/api/skpaint#SkXfermode |
| clear, |
| src, |
| dst, |
| srcOver, |
| dstOver, |
| srcIn, |
| dstIn, |
| srcOut, |
| dstOut, |
| srcATop, |
| dstATop, |
| xor, |
| plus, |
| modulate, |
| |
| // Following blend modes are defined in the CSS Compositing standard. |
| screen, // The last coeff mode. |
| overlay, |
| darken, |
| lighten, |
| colorDodge, |
| colorBurn, |
| hardLight, |
| softLight, |
| difference, |
| exclusion, |
| multiply, // The last separable mode. |
| hue, |
| saturation, |
| color, |
| luminosity, |
| } |
| |
| enum Clip { |
| none, |
| hardEdge, |
| antiAlias, |
| antiAliasWithSaveLayer, |
| } |
| |
| abstract class Paint { |
| factory Paint() => engine.renderer.createPaint(); |
| static bool enableDithering = false; |
| BlendMode get blendMode; |
| set blendMode(BlendMode value); |
| PaintingStyle get style; |
| set style(PaintingStyle value); |
| double get strokeWidth; |
| set strokeWidth(double value); |
| StrokeCap get strokeCap; |
| set strokeCap(StrokeCap value); |
| StrokeJoin get strokeJoin; |
| set strokeJoin(StrokeJoin value); |
| bool get isAntiAlias; |
| set isAntiAlias(bool value); |
| |
| Color get color; |
| set color(Color value); |
| bool get invertColors; |
| |
| set invertColors(bool value); |
| Shader? get shader; |
| set shader(Shader? value); |
| MaskFilter? get maskFilter; |
| set maskFilter(MaskFilter? value); |
| // TODO(ianh): verify that the image drawing methods actually respect this |
| FilterQuality get filterQuality; |
| set filterQuality(FilterQuality value); |
| ColorFilter? get colorFilter; |
| set colorFilter(ColorFilter? value); |
| |
| double get strokeMiterLimit; |
| set strokeMiterLimit(double value); |
| ImageFilter? get imageFilter; |
| set imageFilter(ImageFilter? value); |
| } |
| |
| abstract class Shader { |
| Shader._(); |
| |
| void dispose(); |
| |
| bool get debugDisposed; |
| } |
| |
| abstract class Gradient implements Shader { |
| factory Gradient.linear( |
| Offset from, |
| Offset to, |
| List<Color> colors, [ |
| List<double>? colorStops, |
| TileMode tileMode = TileMode.clamp, |
| Float64List? matrix4, |
| ]) { |
| final Float32List? matrix = matrix4 == null ? null : engine.toMatrix32(matrix4); |
| return engine.renderer.createLinearGradient( |
| from, |
| to, |
| colors, |
| colorStops, |
| tileMode, |
| matrix); |
| } |
| |
| factory Gradient.radial( |
| Offset center, |
| double radius, |
| List<Color> colors, [ |
| List<double>? colorStops, |
| TileMode tileMode = TileMode.clamp, |
| Float64List? matrix4, |
| Offset? focal, |
| double focalRadius = 0.0, |
| ]) { |
| _validateColorStops(colors, colorStops); |
| // If focal is null or focal radius is null, this should be treated as a regular radial gradient |
| // If focal == center and the focal radius is 0.0, it's still a regular radial gradient |
| final Float32List? matrix32 = matrix4 != null ? engine.toMatrix32(matrix4) : null; |
| if (focal == null || (focal == center && focalRadius == 0.0)) { |
| return engine.renderer.createRadialGradient( |
| center, radius, colors, colorStops, tileMode, matrix32); |
| } else { |
| assert(center != Offset.zero || |
| focal != Offset.zero); // will result in exception(s) in Skia side |
| return engine.renderer.createConicalGradient( |
| focal, focalRadius, center, radius, colors, colorStops, tileMode, matrix32); |
| } |
| } |
| factory Gradient.sweep( |
| Offset center, |
| List<Color> colors, [ |
| List<double>? colorStops, |
| TileMode tileMode = TileMode.clamp, |
| double startAngle = 0.0, |
| double endAngle = math.pi * 2, |
| Float64List? matrix4, |
| ]) => engine.renderer.createSweepGradient( |
| center, |
| colors, |
| colorStops, |
| tileMode, |
| startAngle, |
| endAngle, |
| matrix4 != null ? engine.toMatrix32(matrix4) : null); |
| } |
| |
| typedef ImageEventCallback = void Function(Image image); |
| |
| abstract class Image { |
| static ImageEventCallback? onCreate; |
| static ImageEventCallback? onDispose; |
| |
| int get width; |
| int get height; |
| Future<ByteData?> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}); |
| void dispose(); |
| bool get debugDisposed; |
| |
| Image clone() => this; |
| |
| bool isCloneOf(Image other) => other == this; |
| |
| List<StackTrace>? debugGetOpenHandleStackTraces() => null; |
| |
| ColorSpace get colorSpace => ColorSpace.sRGB; |
| |
| @override |
| String toString() => '[$width\u00D7$height]'; |
| } |
| |
| class ColorFilter implements ImageFilter { |
| const factory ColorFilter.mode(Color color, BlendMode blendMode) = engine.EngineColorFilter.mode; |
| const factory ColorFilter.matrix(List<double> matrix) = engine.EngineColorFilter.matrix; |
| const factory ColorFilter.linearToSrgbGamma() = engine.EngineColorFilter.linearToSrgbGamma; |
| const factory ColorFilter.srgbToLinearGamma() = engine.EngineColorFilter.srgbToLinearGamma; |
| } |
| |
| // These enum values must be kept in sync with SkBlurStyle. |
| enum BlurStyle { |
| // These mirror SkBlurStyle and must be kept in sync. |
| normal, |
| solid, |
| outer, |
| inner, |
| } |
| |
| class MaskFilter { |
| const MaskFilter.blur( |
| this._style, |
| this._sigma, |
| ); |
| |
| final BlurStyle _style; |
| final double _sigma; |
| double get webOnlySigma => _sigma; |
| BlurStyle get webOnlyBlurStyle => _style; |
| |
| @override |
| bool operator ==(Object other) { |
| return other is MaskFilter |
| && other._style == _style |
| && other._sigma == _sigma; |
| } |
| |
| @override |
| int get hashCode => Object.hash(_style, _sigma); |
| |
| @override |
| String toString() => 'MaskFilter.blur($_style, ${_sigma.toStringAsFixed(1)})'; |
| } |
| |
| // This needs to be kept in sync with the "_FilterQuality" enum in skwasm's canvas.cpp |
| enum FilterQuality { |
| none, |
| low, |
| medium, |
| high, |
| } |
| |
| class ImageFilter { |
| factory ImageFilter.blur({ |
| double sigmaX = 0.0, |
| double sigmaY = 0.0, |
| TileMode tileMode = TileMode.clamp |
| }) => engine.renderer.createBlurImageFilter( |
| sigmaX: sigmaX, |
| sigmaY: sigmaY, |
| tileMode: tileMode |
| ); |
| |
| factory ImageFilter.dilate({ double radiusX = 0.0, double radiusY = 0.0 }) => |
| engine.renderer.createDilateImageFilter(radiusX: radiusX, radiusY: radiusY); |
| |
| factory ImageFilter.erode({ double radiusX = 0.0, double radiusY = 0.0 }) => |
| engine.renderer.createErodeImageFilter(radiusX: radiusX, radiusY: radiusY); |
| |
| factory ImageFilter.matrix(Float64List matrix4, {FilterQuality filterQuality = FilterQuality.low}) { |
| if (matrix4.length != 16) { |
| throw ArgumentError('"matrix4" must have 16 entries.'); |
| } |
| return engine.renderer.createMatrixImageFilter(matrix4, filterQuality: filterQuality); |
| } |
| |
| factory ImageFilter.compose({required ImageFilter outer, required ImageFilter inner}) => |
| engine.renderer.composeImageFilters(outer: outer, inner: inner); |
| } |
| |
| enum ColorSpace { |
| sRGB, |
| extendedSRGB, |
| } |
| |
| // This must be kept in sync with the `ImageByteFormat` enum in Skwasm's surface.cpp. |
| enum ImageByteFormat { |
| rawRgba, |
| rawStraightRgba, |
| rawUnmodified, |
| png, |
| } |
| |
| // This must be kept in sync with the `PixelFormat` enum in Skwasm's image.cpp. |
| enum PixelFormat { |
| rgba8888, |
| bgra8888, |
| rgbaFloat32, |
| } |
| |
| typedef ImageDecoderCallback = void Function(Image result); |
| |
| abstract class FrameInfo { |
| FrameInfo._(); |
| Duration get duration => Duration(milliseconds: _durationMillis); |
| int get _durationMillis => 0; |
| Image get image; |
| } |
| |
| class Codec { |
| Codec._(); |
| int get frameCount => 0; |
| int get repetitionCount => 0; |
| Future<FrameInfo> getNextFrame() { |
| return engine.futurize<FrameInfo>(_getNextFrame); |
| } |
| |
| String? _getNextFrame(engine.Callback<FrameInfo> callback) => null; |
| void dispose() {} |
| } |
| |
| Future<Codec> instantiateImageCodec( |
| Uint8List list, { |
| int? targetWidth, |
| int? targetHeight, |
| bool allowUpscaling = true, |
| }) => engine.renderer.instantiateImageCodec( |
| list, |
| targetWidth: targetWidth, |
| targetHeight: targetHeight, |
| allowUpscaling: allowUpscaling); |
| |
| Future<Codec> instantiateImageCodecFromBuffer( |
| ImmutableBuffer buffer, { |
| int? targetWidth, |
| int? targetHeight, |
| bool allowUpscaling = true, |
| }) => engine.renderer.instantiateImageCodec( |
| buffer._list!, |
| targetWidth: targetWidth, |
| targetHeight: targetHeight, |
| allowUpscaling: allowUpscaling); |
| |
| Future<Codec> instantiateImageCodecWithSize( |
| ImmutableBuffer buffer, { |
| TargetImageSizeCallback? getTargetSize, |
| }) async { |
| if (getTargetSize == null) { |
| return engine.renderer.instantiateImageCodec(buffer._list!); |
| } else { |
| final Codec codec = await engine.renderer.instantiateImageCodec(buffer._list!); |
| try { |
| final FrameInfo info = await codec.getNextFrame(); |
| try { |
| final int width = info.image.width; |
| final int height = info.image.height; |
| final TargetImageSize targetSize = getTargetSize(width, height); |
| return engine.renderer.instantiateImageCodec(buffer._list!, |
| targetWidth: targetSize.width, targetHeight: targetSize.height, allowUpscaling: false); |
| } finally { |
| info.image.dispose(); |
| } |
| } finally { |
| codec.dispose(); |
| } |
| } |
| } |
| |
| typedef TargetImageSizeCallback = TargetImageSize Function(int intrinsicWidth, int intrinsicHeight); |
| |
| class TargetImageSize { |
| const TargetImageSize({this.width, this.height}) |
| : assert(width == null || width > 0), |
| assert(height == null || height > 0); |
| |
| final int? width; |
| final int? height; |
| } |
| |
| Future<Codec> webOnlyInstantiateImageCodecFromUrl(Uri uri, |
| {engine.WebOnlyImageCodecChunkCallback? chunkCallback}) => |
| engine.renderer.instantiateImageCodecFromUrl(uri, chunkCallback: chunkCallback); |
| |
| void decodeImageFromList(Uint8List list, ImageDecoderCallback callback) { |
| _decodeImageFromListAsync(list, callback); |
| } |
| |
| Future<void> _decodeImageFromListAsync(Uint8List list, ImageDecoderCallback callback) async { |
| final Codec codec = await instantiateImageCodec(list); |
| final FrameInfo frameInfo = await codec.getNextFrame(); |
| callback(frameInfo.image); |
| } |
| |
| // Encodes the input pixels into a BMP file that supports transparency. |
| // |
| // The `pixels` should be the scanlined raw pixels, 4 bytes per pixel, from left |
| // to right, then from top to down. The order of the 4 bytes of pixels is |
| // decided by `format`. |
| Future<Codec> createBmp( |
| Uint8List pixels, |
| int width, |
| int height, |
| int rowBytes, |
| PixelFormat format, |
| ) { |
| late bool swapRedBlue; |
| switch (format) { |
| case PixelFormat.bgra8888: |
| swapRedBlue = true; |
| case PixelFormat.rgba8888: |
| swapRedBlue = false; |
| case PixelFormat.rgbaFloat32: |
| throw UnimplementedError('RGB conversion from rgbaFloat32 data is not implemented'); |
| } |
| |
| // See https://en.wikipedia.org/wiki/BMP_file_format for format examples. |
| // The header is in the 108-byte BITMAPV4HEADER format, or as called by |
| // Chromium, WindowsV4. Do not use the 56-byte or 52-byte Adobe formats, since |
| // they're not supported. |
| const int dibSize = 0x6C /* 108: BITMAPV4HEADER */; |
| const int headerSize = dibSize + 0x0E; |
| final int bufferSize = headerSize + (width * height * 4); |
| final ByteData bmpData = ByteData(bufferSize); |
| // 'BM' header |
| bmpData.setUint16(0x00, 0x424D); |
| // Size of data |
| bmpData.setUint32(0x02, bufferSize, Endian.little); |
| // Offset where pixel array begins |
| bmpData.setUint32(0x0A, headerSize, Endian.little); |
| // Bytes in DIB header |
| bmpData.setUint32(0x0E, dibSize, Endian.little); |
| // Width |
| bmpData.setUint32(0x12, width, Endian.little); |
| // Height |
| bmpData.setUint32(0x16, height, Endian.little); |
| // Color panes (always 1) |
| bmpData.setUint16(0x1A, 0x01, Endian.little); |
| // bpp: 32 |
| bmpData.setUint16(0x1C, 32, Endian.little); |
| // Compression method is BITFIELDS to enable bit fields |
| bmpData.setUint32(0x1E, 3, Endian.little); |
| // Raw bitmap data size |
| bmpData.setUint32(0x22, width * height, Endian.little); |
| // Print DPI width |
| bmpData.setUint32(0x26, width, Endian.little); |
| // Print DPI height |
| bmpData.setUint32(0x2A, height, Endian.little); |
| // Colors in the palette |
| bmpData.setUint32(0x2E, 0x00, Endian.little); |
| // Important colors |
| bmpData.setUint32(0x32, 0x00, Endian.little); |
| // Bitmask R |
| bmpData.setUint32(0x36, swapRedBlue ? 0x00FF0000 : 0x000000FF, Endian.little); |
| // Bitmask G |
| bmpData.setUint32(0x3A, 0x0000FF00, Endian.little); |
| // Bitmask B |
| bmpData.setUint32(0x3E, swapRedBlue ? 0x000000FF : 0x00FF0000, Endian.little); |
| // Bitmask A |
| bmpData.setUint32(0x42, 0xFF000000, Endian.little); |
| |
| int destinationByte = headerSize; |
| final Uint32List combinedPixels = Uint32List.sublistView(pixels); |
| // BMP is scanlined from bottom to top. Rearrange here. |
| for (int rowCount = height - 1; rowCount >= 0; rowCount -= 1) { |
| int sourcePixel = rowCount * rowBytes; |
| for (int colCount = 0; colCount < width; colCount += 1) { |
| bmpData.setUint32(destinationByte, combinedPixels[sourcePixel], Endian.little); |
| destinationByte += 4; |
| sourcePixel += 1; |
| } |
| } |
| |
| return instantiateImageCodec( |
| bmpData.buffer.asUint8List(), |
| ); |
| } |
| |
| void decodeImageFromPixels( |
| Uint8List pixels, |
| int width, |
| int height, |
| PixelFormat format, |
| ImageDecoderCallback callback, { |
| int? rowBytes, |
| int? targetWidth, |
| int? targetHeight, |
| bool allowUpscaling = true, |
| }) => engine.renderer.decodeImageFromPixels( |
| pixels, |
| width, |
| height, |
| format, |
| callback, |
| rowBytes: rowBytes, |
| targetWidth: targetWidth, |
| targetHeight: targetHeight, |
| allowUpscaling: allowUpscaling); |
| |
| class Shadow { |
| const Shadow({ |
| this.color = const Color(_kColorDefault), |
| this.offset = Offset.zero, |
| this.blurRadius = 0.0, |
| }) : assert(blurRadius >= 0.0, 'Text shadow blur radius should be non-negative.'); |
| |
| static const int _kColorDefault = 0xFF000000; |
| final Color color; |
| final Offset offset; |
| final double blurRadius; |
| // See SkBlurMask::ConvertRadiusToSigma(). |
| // <https://github.com/google/skia/blob/bb5b77db51d2e149ee66db284903572a5aac09be/src/effects/SkBlurMask.cpp#L23> |
| static double convertRadiusToSigma(double radius) { |
| return radius > 0 ? radius * 0.57735 + 0.5 : 0; |
| } |
| |
| double get blurSigma => convertRadiusToSigma(blurRadius); |
| Paint toPaint() { |
| return Paint() |
| ..color = color |
| ..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma); |
| } |
| |
| Shadow scale(double factor) { |
| return Shadow( |
| color: color, |
| offset: offset * factor, |
| blurRadius: blurRadius * factor, |
| ); |
| } |
| |
| static Shadow? lerp(Shadow? a, Shadow? b, double t) { |
| if (b == null) { |
| if (a == null) { |
| return null; |
| } else { |
| return a.scale(1.0 - t); |
| } |
| } else { |
| if (a == null) { |
| return b.scale(t); |
| } else { |
| return Shadow( |
| color: Color.lerp(a.color, b.color, t)!, |
| offset: Offset.lerp(a.offset, b.offset, t)!, |
| blurRadius: _lerpDouble(a.blurRadius, b.blurRadius, t), |
| ); |
| } |
| } |
| } |
| |
| static List<Shadow>? lerpList(List<Shadow>? a, List<Shadow>? b, double t) { |
| if (a == null && b == null) { |
| return null; |
| } |
| a ??= <Shadow>[]; |
| b ??= <Shadow>[]; |
| final List<Shadow> result = <Shadow>[]; |
| final int commonLength = math.min(a.length, b.length); |
| for (int i = 0; i < commonLength; i += 1) { |
| result.add(Shadow.lerp(a[i], b[i], t)!); |
| } |
| for (int i = commonLength; i < a.length; i += 1) { |
| result.add(a[i].scale(1.0 - t)); |
| } |
| for (int i = commonLength; i < b.length; i += 1) { |
| result.add(b[i].scale(t)); |
| } |
| return result; |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| return other is Shadow && |
| other.color == color && |
| other.offset == offset && |
| other.blurRadius == blurRadius; |
| } |
| |
| @override |
| int get hashCode => Object.hash(color, offset, blurRadius); |
| |
| @override |
| String toString() => 'TextShadow($color, $offset, $blurRadius)'; |
| } |
| |
| abstract class ImageShader implements Shader { |
| factory ImageShader( |
| Image image, |
| TileMode tmx, |
| TileMode tmy, |
| Float64List matrix4, { |
| FilterQuality? filterQuality, |
| }) => engine.renderer.createImageShader( |
| image, |
| tmx, |
| tmy, |
| matrix4, |
| filterQuality |
| ); |
| |
| @override |
| void dispose(); |
| |
| @override |
| bool get debugDisposed; |
| } |
| |
| class ImmutableBuffer { |
| ImmutableBuffer._(this._length); |
| static Future<ImmutableBuffer> fromUint8List(Uint8List list) async { |
| final ImmutableBuffer instance = ImmutableBuffer._(list.length); |
| instance._list = list; |
| return instance; |
| } |
| |
| static Future<ImmutableBuffer> fromAsset(String assetKey) async { |
| throw UnsupportedError('ImmutableBuffer.fromAsset is not supported on the web.'); |
| } |
| |
| static Future<ImmutableBuffer> fromFilePath(String path) async { |
| throw UnsupportedError('ImmutableBuffer.fromFilePath is not supported on the web.'); |
| } |
| |
| Uint8List? _list; |
| |
| int get length => _length; |
| final int _length; |
| |
| bool get debugDisposed { |
| late bool disposed; |
| assert(() { |
| disposed = _list == null; |
| return true; |
| }()); |
| return disposed; |
| } |
| void dispose() => _list = null; |
| } |
| |
| class ImageDescriptor { |
| // Not async because there's no expensive work to do here. |
| ImageDescriptor.raw( |
| ImmutableBuffer buffer, { |
| required int width, |
| required int height, |
| int? rowBytes, |
| required PixelFormat pixelFormat, |
| }) : _width = width, |
| _height = height, |
| _rowBytes = rowBytes, |
| _format = pixelFormat { |
| _data = buffer._list; |
| } |
| |
| ImageDescriptor._() |
| : _width = null, |
| _height = null, |
| _rowBytes = null, |
| _format = null; |
| |
| static Future<ImageDescriptor> encoded(ImmutableBuffer buffer) async { |
| final ImageDescriptor descriptor = ImageDescriptor._(); |
| descriptor._data = buffer._list; |
| return descriptor; |
| } |
| |
| Uint8List? _data; |
| final int? _width; |
| final int? _height; |
| final int? _rowBytes; |
| final PixelFormat? _format; |
| |
| Never _throw(String parameter) { |
| throw UnsupportedError('ImageDescriptor.$parameter is not supported on web.'); |
| } |
| |
| int get width => _width ?? _throw('width'); |
| int get height => _height ?? _throw('height'); |
| int get bytesPerPixel => |
| throw UnsupportedError('ImageDescriptor.bytesPerPixel is not supported on web.'); |
| void dispose() => _data = null; |
| Future<Codec> instantiateCodec({int? targetWidth, int? targetHeight}) async { |
| if (_data == null) { |
| throw StateError('Object is disposed'); |
| } |
| if (_width == null) { |
| return instantiateImageCodec( |
| _data!, |
| targetWidth: targetWidth, |
| targetHeight: targetHeight, |
| allowUpscaling: false, |
| ); |
| } |
| |
| return createBmp(_data!, width, height, _rowBytes ?? width, _format!); |
| } |
| } |
| |
| abstract class FragmentProgram { |
| static Future<FragmentProgram> fromAsset(String assetKey) { |
| return engine.renderer.createFragmentProgram(assetKey); |
| } |
| |
| FragmentShader fragmentShader(); |
| } |
| |
| abstract class FragmentShader implements Shader { |
| void setFloat(int index, double value); |
| |
| void setImageSampler(int index, Image image); |
| |
| @override |
| void dispose(); |
| |
| @override |
| bool get debugDisposed; |
| } |