blob: 6a1fea063ef34eb3ab8e76d63e825e0ffdf8278d [file] [log] [blame]
// 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.
import 'dart:js_interop';
import 'dart:math';
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
import '../common/matchers.dart';
import 'common.dart';
import 'test_data.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('CanvasKit API', () {
setUpCanvasKitTest();
_blendModeTests();
_paintStyleTests();
_strokeCapTests();
_strokeJoinTests();
_blurStyleTests();
_tileModeTests();
_fillTypeTests();
_pathOpTests();
_clipOpTests();
_pointModeTests();
_vertexModeTests();
_imageTests();
_shaderTests();
_paintTests();
_maskFilterTests();
_colorFilterTests();
_imageFilterTests();
_mallocTests();
_sharedColorTests();
_toSkPointTests();
_toSkColorStopsTests();
_toSkMatrixFromFloat32Tests();
_toSkM44FromFloat32Tests();
_matrix4x4CompositionTests();
_toSkRectTests();
_skVerticesTests();
_pictureTests();
group('SkParagraph', () {
_paragraphTests();
});
group('SkPath', () {
_pathTests();
});
group('SkCanvas', () {
_canvasTests();
});
group('SkParagraph', () {
_textStyleTests();
});
});
}
void _blendModeTests() {
test('blend mode mapping is correct', () {
expect(canvasKit.BlendMode.Clear.value, ui.BlendMode.clear.index);
expect(canvasKit.BlendMode.Src.value, ui.BlendMode.src.index);
expect(canvasKit.BlendMode.Dst.value, ui.BlendMode.dst.index);
expect(canvasKit.BlendMode.SrcOver.value, ui.BlendMode.srcOver.index);
expect(canvasKit.BlendMode.DstOver.value, ui.BlendMode.dstOver.index);
expect(canvasKit.BlendMode.SrcIn.value, ui.BlendMode.srcIn.index);
expect(canvasKit.BlendMode.DstIn.value, ui.BlendMode.dstIn.index);
expect(canvasKit.BlendMode.SrcOut.value, ui.BlendMode.srcOut.index);
expect(canvasKit.BlendMode.DstOut.value, ui.BlendMode.dstOut.index);
expect(canvasKit.BlendMode.SrcATop.value, ui.BlendMode.srcATop.index);
expect(canvasKit.BlendMode.DstATop.value, ui.BlendMode.dstATop.index);
expect(canvasKit.BlendMode.Xor.value, ui.BlendMode.xor.index);
expect(canvasKit.BlendMode.Plus.value, ui.BlendMode.plus.index);
expect(canvasKit.BlendMode.Modulate.value, ui.BlendMode.modulate.index);
expect(canvasKit.BlendMode.Screen.value, ui.BlendMode.screen.index);
expect(canvasKit.BlendMode.Overlay.value, ui.BlendMode.overlay.index);
expect(canvasKit.BlendMode.Darken.value, ui.BlendMode.darken.index);
expect(canvasKit.BlendMode.Lighten.value, ui.BlendMode.lighten.index);
expect(canvasKit.BlendMode.ColorDodge.value, ui.BlendMode.colorDodge.index);
expect(canvasKit.BlendMode.ColorBurn.value, ui.BlendMode.colorBurn.index);
expect(canvasKit.BlendMode.HardLight.value, ui.BlendMode.hardLight.index);
expect(canvasKit.BlendMode.SoftLight.value, ui.BlendMode.softLight.index);
expect(canvasKit.BlendMode.Difference.value, ui.BlendMode.difference.index);
expect(canvasKit.BlendMode.Exclusion.value, ui.BlendMode.exclusion.index);
expect(canvasKit.BlendMode.Multiply.value, ui.BlendMode.multiply.index);
expect(canvasKit.BlendMode.Hue.value, ui.BlendMode.hue.index);
expect(canvasKit.BlendMode.Saturation.value, ui.BlendMode.saturation.index);
expect(canvasKit.BlendMode.Color.value, ui.BlendMode.color.index);
expect(canvasKit.BlendMode.Luminosity.value, ui.BlendMode.luminosity.index);
});
test('ui.BlendMode converts to SkBlendMode', () {
for (final ui.BlendMode blendMode in ui.BlendMode.values) {
expect(toSkBlendMode(blendMode).value, blendMode.index);
}
});
}
void _paintStyleTests() {
test('paint style mapping is correct', () {
expect(canvasKit.PaintStyle.Fill.value, ui.PaintingStyle.fill.index);
expect(canvasKit.PaintStyle.Stroke.value, ui.PaintingStyle.stroke.index);
});
test('ui.PaintingStyle converts to SkPaintStyle', () {
for (final ui.PaintingStyle style in ui.PaintingStyle.values) {
expect(toSkPaintStyle(style).value, style.index);
}
});
}
void _strokeCapTests() {
test('stroke cap mapping is correct', () {
expect(canvasKit.StrokeCap.Butt.value, ui.StrokeCap.butt.index);
expect(canvasKit.StrokeCap.Round.value, ui.StrokeCap.round.index);
expect(canvasKit.StrokeCap.Square.value, ui.StrokeCap.square.index);
});
test('ui.StrokeCap converts to SkStrokeCap', () {
for (final ui.StrokeCap cap in ui.StrokeCap.values) {
expect(toSkStrokeCap(cap).value, cap.index);
}
});
}
void _strokeJoinTests() {
test('stroke cap mapping is correct', () {
expect(canvasKit.StrokeJoin.Miter.value, ui.StrokeJoin.miter.index);
expect(canvasKit.StrokeJoin.Round.value, ui.StrokeJoin.round.index);
expect(canvasKit.StrokeJoin.Bevel.value, ui.StrokeJoin.bevel.index);
});
test('ui.StrokeJoin converts to SkStrokeJoin', () {
for (final ui.StrokeJoin join in ui.StrokeJoin.values) {
expect(toSkStrokeJoin(join).value, join.index);
}
});
}
void _blurStyleTests() {
test('blur style mapping is correct', () {
expect(canvasKit.BlurStyle.Normal.value, ui.BlurStyle.normal.index);
expect(canvasKit.BlurStyle.Solid.value, ui.BlurStyle.solid.index);
expect(canvasKit.BlurStyle.Outer.value, ui.BlurStyle.outer.index);
expect(canvasKit.BlurStyle.Inner.value, ui.BlurStyle.inner.index);
});
test('ui.BlurStyle converts to SkBlurStyle', () {
for (final ui.BlurStyle style in ui.BlurStyle.values) {
expect(toSkBlurStyle(style).value, style.index);
}
});
}
void _tileModeTests() {
test('tile mode mapping is correct', () {
expect(canvasKit.TileMode.Clamp.value, ui.TileMode.clamp.index);
expect(canvasKit.TileMode.Repeat.value, ui.TileMode.repeated.index);
expect(canvasKit.TileMode.Mirror.value, ui.TileMode.mirror.index);
});
test('ui.TileMode converts to SkTileMode', () {
for (final ui.TileMode mode in ui.TileMode.values) {
expect(toSkTileMode(mode).value, mode.index);
}
});
}
void _fillTypeTests() {
test('fill type mapping is correct', () {
expect(canvasKit.FillType.Winding.value, ui.PathFillType.nonZero.index);
expect(canvasKit.FillType.EvenOdd.value, ui.PathFillType.evenOdd.index);
});
test('ui.PathFillType converts to SkFillType', () {
for (final ui.PathFillType type in ui.PathFillType.values) {
expect(toSkFillType(type).value, type.index);
}
});
}
void _pathOpTests() {
test('path op mapping is correct', () {
expect(
canvasKit.PathOp.Difference.value, ui.PathOperation.difference.index);
expect(canvasKit.PathOp.Intersect.value, ui.PathOperation.intersect.index);
expect(canvasKit.PathOp.Union.value, ui.PathOperation.union.index);
expect(canvasKit.PathOp.XOR.value, ui.PathOperation.xor.index);
expect(canvasKit.PathOp.ReverseDifference.value,
ui.PathOperation.reverseDifference.index);
});
test('ui.PathOperation converts to SkPathOp', () {
for (final ui.PathOperation op in ui.PathOperation.values) {
expect(toSkPathOp(op).value, op.index);
}
});
test('Path.combine test', () {
final ui.Path path1 = ui.Path();
expect(path1, isA<CkPath>());
path1.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
path1.addOval(const ui.Rect.fromLTRB(10, 10, 100, 100));
final ui.Path path2 = ui.Path();
expect(path2, isA<CkPath>());
path2.addRect(const ui.Rect.fromLTRB(5, 5, 15, 15));
path2.addOval(const ui.Rect.fromLTRB(15, 15, 105, 105));
final ui.Path union = ui.Path.combine(ui.PathOperation.union, path1, path2);
expect(union, isA<CkPath>());
expect(union.getBounds(), const ui.Rect.fromLTRB(0, 0, 105, 105));
// Smoke-test other operations.
for (final ui.PathOperation operation in ui.PathOperation.values) {
final ui.Path combined = ui.Path.combine(operation, path1, path2);
expect(combined, isA<CkPath>());
}
});
}
void _clipOpTests() {
test('clip op mapping is correct', () {
expect(canvasKit.ClipOp.Difference.value, ui.ClipOp.difference.index);
expect(canvasKit.ClipOp.Intersect.value, ui.ClipOp.intersect.index);
});
test('ui.ClipOp converts to SkClipOp', () {
for (final ui.ClipOp op in ui.ClipOp.values) {
expect(toSkClipOp(op).value, op.index);
}
});
}
void _pointModeTests() {
test('point mode mapping is correct', () {
expect(canvasKit.PointMode.Points.value, ui.PointMode.points.index);
expect(canvasKit.PointMode.Lines.value, ui.PointMode.lines.index);
expect(canvasKit.PointMode.Polygon.value, ui.PointMode.polygon.index);
});
test('ui.PointMode converts to SkPointMode', () {
for (final ui.PointMode op in ui.PointMode.values) {
expect(toSkPointMode(op).value, op.index);
}
});
}
void _vertexModeTests() {
test('vertex mode mapping is correct', () {
expect(canvasKit.VertexMode.Triangles.value, ui.VertexMode.triangles.index);
expect(canvasKit.VertexMode.TrianglesStrip.value,
ui.VertexMode.triangleStrip.index);
expect(canvasKit.VertexMode.TriangleFan.value,
ui.VertexMode.triangleFan.index);
});
test('ui.VertexMode converts to SkVertexMode', () {
for (final ui.VertexMode op in ui.VertexMode.values) {
expect(toSkVertexMode(op).value, op.index);
}
});
}
void _imageTests() {
test('MakeAnimatedImageFromEncoded makes a non-animated image', () {
final SkAnimatedImage nonAnimated =
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
expect(nonAnimated.getFrameCount(), 1);
expect(nonAnimated.getRepetitionCount(), 0);
expect(nonAnimated.width(), 1);
expect(nonAnimated.height(), 1);
final SkImage frame = nonAnimated.makeImageAtCurrentFrame();
expect(frame.width(), 1);
expect(frame.height(), 1);
expect(nonAnimated.decodeNextFrame(), -1);
expect(
frame.makeShaderOptions(
canvasKit.TileMode.Repeat,
canvasKit.TileMode.Mirror,
canvasKit.FilterMode.Linear,
canvasKit.MipmapMode.Nearest,
toSkMatrixFromFloat32(Matrix4.identity().storage),
),
isNotNull,
);
});
test('MakeAnimatedImageFromEncoded makes an animated image', () {
final SkAnimatedImage animated =
canvasKit.MakeAnimatedImageFromEncoded(kAnimatedGif)!;
expect(animated.getFrameCount(), 3);
expect(animated.getRepetitionCount(), -1); // animates forever
expect(animated.width(), 1);
expect(animated.height(), 1);
for (int i = 0; i < 100; i++) {
final SkImage frame = animated.makeImageAtCurrentFrame();
expect(frame.width(), 1);
expect(frame.height(), 1);
expect(animated.decodeNextFrame(), 100);
}
});
}
void _shaderTests() {
test('MakeLinearGradient', () {
expect(_makeTestShader(), isNotNull);
});
test('MakeRadialGradient', () {
expect(
canvasKit.Shader.MakeRadialGradient(
Float32List.fromList(<double>[1, 1]),
10.0,
Uint32List.fromList(<int>[0xff000000, 0xffffffff]),
Float32List.fromList(<double>[0, 1]),
canvasKit.TileMode.Repeat,
toSkMatrixFromFloat32(Matrix4.identity().storage),
0,
),
isNotNull);
});
test('MakeTwoPointConicalGradient', () {
expect(
canvasKit.Shader.MakeTwoPointConicalGradient(
Float32List.fromList(<double>[1, 1]),
10.0,
Float32List.fromList(<double>[1, 1]),
10.0,
Uint32List.fromList(<int>[0xff000000, 0xffffffff]),
Float32List.fromList(<double>[0, 1]),
canvasKit.TileMode.Repeat,
toSkMatrixFromFloat32(Matrix4.identity().storage),
0,
),
isNotNull);
});
test('RuntimeEffect', () {
const String kSkSlProgram = r'''
half4 main(vec2 fragCoord) {
return vec4(1.0, 0.0, 0.0, 1.0);
}
''';
final SkRuntimeEffect? effect = MakeRuntimeEffect(kSkSlProgram);
expect(effect, isNotNull);
const String kInvalidSkSlProgram = '';
// Invalid SkSL returns null.
final SkRuntimeEffect? invalidEffect = MakeRuntimeEffect(kInvalidSkSlProgram);
expect(invalidEffect, isNull);
final SkShader? shader = effect!.makeShader(<double>[]);
expect(shader, isNotNull);
// mismatched uniforms returns null.
final SkShader? invalidShader = effect.makeShader(<double>[1]);
expect(invalidShader, isNull);
const String kSkSlProgramWithUniforms = r'''
uniform vec4 u_color;
half4 main(vec2 fragCoord) {
return u_color;
}
''';
final SkShader? shaderWithUniform = MakeRuntimeEffect(kSkSlProgramWithUniforms)
!.makeShader(<double>[1.0, 0.0, 0.0, 1.0]);
expect(shaderWithUniform, isNotNull);
});
}
SkShader _makeTestShader() {
return canvasKit.Shader.MakeLinearGradient(
Float32List.fromList(<double>[0, 0]),
Float32List.fromList(<double>[1, 1]),
Uint32List.fromList(<int>[0xff0000ff]),
Float32List.fromList(<double>[0, 1]),
canvasKit.TileMode.Repeat,
null,
);
}
void _paintTests() {
test('can make SkPaint', () async {
final SkPaint paint = SkPaint();
paint.setBlendMode(canvasKit.BlendMode.SrcOut);
paint.setStyle(canvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(3.0);
paint.setStrokeCap(canvasKit.StrokeCap.Round);
paint.setStrokeJoin(canvasKit.StrokeJoin.Bevel);
paint.setAntiAlias(true);
paint.setColorInt(0x00FFCCAA);
paint.setShader(_makeTestShader());
paint.setMaskFilter(canvasKit.MaskFilter.MakeBlur(
canvasKit.BlurStyle.Outer,
2.0,
true,
));
paint.setColorFilter(canvasKit.ColorFilter.MakeLinearToSRGBGamma());
paint.setStrokeMiter(1.4);
paint.setImageFilter(canvasKit.ImageFilter.MakeBlur(
1,
2,
canvasKit.TileMode.Repeat,
null,
));
});
}
void _maskFilterTests() {
test('MaskFilter.MakeBlur', () {
expect(
canvasKit.MaskFilter.MakeBlur(
canvasKit.BlurStyle.Outer,
5.0,
false,
),
isNotNull);
});
test('MaskFilter.MakeBlur with 0 sigma returns null', () {
expect(
canvasKit.MaskFilter.MakeBlur(canvasKit.BlurStyle.Normal, 0.0, false),
isNull);
});
test('MaskFilter.MakeBlur with NaN sigma returns null', () {
expect(
canvasKit.MaskFilter.MakeBlur(
canvasKit.BlurStyle.Normal, double.nan, false),
isNull);
});
}
void _colorFilterTests() {
test('MakeBlend', () {
expect(
canvasKit.ColorFilter.MakeBlend(
Float32List.fromList(<double>[0, 0, 0, 1]),
canvasKit.BlendMode.SrcATop,
),
isNotNull,
);
});
test('MakeMatrix', () {
expect(
canvasKit.ColorFilter.MakeMatrix(
Float32List(20),
),
isNotNull,
);
});
test('MakeSRGBToLinearGamma', () {
expect(
canvasKit.ColorFilter.MakeSRGBToLinearGamma(),
isNotNull,
);
});
test('MakeLinearToSRGBGamma', () {
expect(
canvasKit.ColorFilter.MakeLinearToSRGBGamma(),
isNotNull,
);
});
}
void _imageFilterTests() {
test('MakeBlur', () {
expect(
canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
isNotNull,
);
});
test('toSkFilterOptions', () {
for (final ui.FilterQuality filterQuality in ui.FilterQuality.values) {
expect(toSkFilterOptions(filterQuality), isNotNull);
}
});
test('MakeMatrixTransform', () {
expect(
canvasKit.ImageFilter.MakeMatrixTransform(
toSkMatrixFromFloat32(Matrix4.identity().storage),
toSkFilterOptions(ui.FilterQuality.medium),
null,
),
isNotNull,
);
});
test('MakeColorFilter', () {
expect(
canvasKit.ImageFilter.MakeColorFilter(
canvasKit.ColorFilter.MakeLinearToSRGBGamma(),
null,
),
isNotNull,
);
});
test('MakeCompose', () {
expect(
canvasKit.ImageFilter.MakeCompose(
canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
),
isNotNull,
);
});
}
void _mallocTests() {
test('$SkFloat32List', () {
final List<SkFloat32List> lists = <SkFloat32List>[];
for (int size = 0; size < 1000; size++) {
final SkFloat32List skList = mallocFloat32List(4);
expect(skList, isNotNull);
expect(skList.toTypedArray(), hasLength(4));
lists.add(skList);
}
for (final SkFloat32List skList in lists) {
// toTypedArray() still works.
expect(() => skList.toTypedArray(), returnsNormally);
free(skList);
// toTypedArray() throws after free.
expect(() => skList.toTypedArray(), throwsA(isA<Error>()));
}
});
test('$SkUint32List', () {
final List<SkUint32List> lists = <SkUint32List>[];
for (int size = 0; size < 1000; size++) {
final SkUint32List skList = mallocUint32List(4);
expect(skList, isNotNull);
expect(skList.toTypedArray(), hasLength(4));
lists.add(skList);
}
for (final SkUint32List skList in lists) {
// toTypedArray() still works.
expect(() => skList.toTypedArray(), returnsNormally);
free(skList);
// toTypedArray() throws after free.
expect(() => skList.toTypedArray(), throwsA(isA<Error>()));
}
});
}
void _sharedColorTests() {
test('toSharedSkColor1', () {
expect(
toSharedSkColor1(const ui.Color(0xAABBCCDD)),
Float32List(4)
..[0] = 0xBB / 255.0
..[1] = 0xCC / 255.0
..[2] = 0xDD / 255.0
..[3] = 0xAA / 255.0,
);
});
test('toSharedSkColor2', () {
expect(
toSharedSkColor2(const ui.Color(0xAABBCCDD)),
Float32List(4)
..[0] = 0xBB / 255.0
..[1] = 0xCC / 255.0
..[2] = 0xDD / 255.0
..[3] = 0xAA / 255.0,
);
});
test('toSharedSkColor3', () {
expect(
toSharedSkColor3(const ui.Color(0xAABBCCDD)),
Float32List(4)
..[0] = 0xBB / 255.0
..[1] = 0xCC / 255.0
..[2] = 0xDD / 255.0
..[3] = 0xAA / 255.0,
);
});
}
void _toSkPointTests() {
test('toSkPoint', () {
expect(
toSkPoint(const ui.Offset(4, 5)),
Float32List(2)
..[0] = 4.0
..[1] = 5.0,
);
});
}
void _toSkColorStopsTests() {
test('toSkColorStops default', () {
expect(
toSkColorStops(null),
Float32List(2)
..[0] = 0
..[1] = 1,
);
});
test('toSkColorStops custom', () {
expect(
toSkColorStops(<double>[1, 2, 3, 4]),
Float32List(4)
..[0] = 1
..[1] = 2
..[2] = 3
..[3] = 4,
);
});
}
void _toSkMatrixFromFloat32Tests() {
test('toSkMatrixFromFloat32', () {
final Matrix4 matrix = Matrix4.identity()
..translate(1, 2, 3)
..rotateZ(4);
expect(
toSkMatrixFromFloat32(matrix.storage),
Float32List.fromList(<double>[
-0.6536436080932617,
0.756802499294281,
1,
-0.756802499294281,
-0.6536436080932617,
2,
-0.0,
0,
1,
]));
});
}
void _toSkM44FromFloat32Tests() {
test('toSkM44FromFloat32', () {
final Matrix4 matrix = Matrix4.identity()
..translate(1, 2, 3)
..rotateZ(4);
expect(
toSkM44FromFloat32(matrix.storage),
Float32List.fromList(<double>[
-0.6536436080932617,
0.756802499294281,
0,
1,
-0.756802499294281,
-0.6536436080932617,
0,
2,
0,
0,
1,
3,
0,
0,
0,
1,
]));
});
}
typedef CanvasCallback = void Function(ui.Canvas canvas);
Future<ui.Image> toImage(CanvasCallback callback, int width, int height) {
final ui.PictureRecorder recorder = ui.PictureRecorder();
final ui.Canvas canvas = ui.Canvas(
recorder, ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble()));
callback(canvas);
final ui.Picture picture = recorder.endRecording();
return picture.toImage(width, height);
}
/// @returns true When the images are reasonably similar.
/// @todo Make the search actually fuzzy to a certain degree.
Future<bool> fuzzyCompareImages(ui.Image golden, ui.Image img) async {
if (golden.width != img.width || golden.height != img.height) {
return false;
}
int getPixel(ByteData data, int x, int y) =>
data.getUint32((x + y * golden.width) * 4);
final ByteData goldenData = (await golden.toByteData())!;
final ByteData imgData = (await img.toByteData())!;
for (int y = 0; y < golden.height; y++) {
for (int x = 0; x < golden.width; x++) {
if (getPixel(goldenData, x, y) != getPixel(imgData, x, y)) {
return false;
}
}
}
return true;
}
void _matrix4x4CompositionTests() {
test('compose4x4MatrixInCanvas', () async {
const double rotateAroundX = pi / 6; // 30 degrees
const double rotateAroundY = pi / 9; // 20 degrees
const int width = 150;
const int height = 150;
const ui.Color black = ui.Color.fromARGB(255, 0, 0, 0);
const ui.Color green = ui.Color.fromARGB(255, 0, 255, 0);
void paint(ui.Canvas canvas, CanvasCallback rotate) {
canvas.translate(width * 0.5, height * 0.5);
rotate(canvas);
const double width3 = width / 3.0;
const double width5 = width / 5.0;
const double width10 = width / 10.0;
canvas.drawRect(const ui.Rect.fromLTRB(-width3, -width3, width3, width3),
ui.Paint()..color = green);
canvas.drawRect(
const ui.Rect.fromLTRB(-width5, -width5, -width10, width5),
ui.Paint()..color = black);
canvas.drawRect(
const ui.Rect.fromLTRB(-width5, -width5, width5, -width10),
ui.Paint()..color = black);
}
final ui.Image incrementalMatrixImage = await toImage((ui.Canvas canvas) {
paint(canvas, (ui.Canvas canvas) {
final Matrix4 matrix = Matrix4.identity();
matrix.setEntry(3, 2, 0.001);
canvas.transform(matrix.toFloat64());
matrix.setRotationX(rotateAroundX);
canvas.transform(matrix.toFloat64());
matrix.setRotationY(rotateAroundY);
canvas.transform(matrix.toFloat64());
});
}, width, height);
final ui.Image combinedMatrixImage = await toImage((ui.Canvas canvas) {
paint(canvas, (ui.Canvas canvas) {
final Matrix4 matrix = Matrix4.identity();
matrix.setEntry(3, 2, 0.001);
matrix.rotate(kUnitX, rotateAroundX);
matrix.rotate(kUnitY, rotateAroundY);
canvas.transform(matrix.toFloat64());
});
}, width, height);
final bool areEqual =
await fuzzyCompareImages(incrementalMatrixImage, combinedMatrixImage);
expect(areEqual, true);
});
}
void _toSkRectTests() {
test('toSkRect', () {
expect(toSkRect(const ui.Rect.fromLTRB(1, 2, 3, 4)), <double>[1, 2, 3, 4]);
});
test('fromSkRect', () {
expect(fromSkRect(Float32List.fromList(<double>[1, 2, 3, 4])),
const ui.Rect.fromLTRB(1, 2, 3, 4));
});
test('toSkRRect', () {
expect(
toSkRRect(ui.RRect.fromLTRBAndCorners(
1,
2,
3,
4,
topLeft: const ui.Radius.elliptical(5, 6),
topRight: const ui.Radius.elliptical(7, 8),
bottomRight: const ui.Radius.elliptical(9, 10),
bottomLeft: const ui.Radius.elliptical(11, 12),
)),
<double>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
);
});
}
SkPath _testClosedSkPath() {
return SkPath()
..moveTo(10, 10)
..lineTo(20, 10)
..lineTo(20, 20)
..lineTo(10, 20)
..close();
}
void _pathTests() {
late SkPath path;
setUp(() {
path = SkPath();
});
test('setFillType', () {
path.setFillType(canvasKit.FillType.Winding);
});
test('addArc', () {
path.addArc(
toSkRect(const ui.Rect.fromLTRB(10, 20, 30, 40)),
1,
5,
);
});
test('addOval', () {
path.addOval(
toSkRect(const ui.Rect.fromLTRB(10, 20, 30, 40)),
false,
1,
);
});
test('addPath', () {
path.addPath(_testClosedSkPath(), 1, 0, 0, 0, 1, 0, 0, 0, 0, false);
});
test('addPoly', () {
final SkFloat32List encodedPoints = toMallocedSkPoints(const <ui.Offset>[
ui.Offset.zero,
ui.Offset(10, 10),
]);
path.addPoly(encodedPoints.toTypedArray(), true);
free(encodedPoints);
});
test('addRRect', () {
final ui.RRect rrect = ui.RRect.fromRectAndRadius(
const ui.Rect.fromLTRB(10, 10, 20, 20),
const ui.Radius.circular(3),
);
path.addRRect(
toSkRRect(rrect),
false,
);
});
test('addRect', () {
path.addRect(toSkRect(const ui.Rect.fromLTRB(1, 2, 3, 4)));
});
test('arcTo', () {
path.arcToOval(
toSkRect(const ui.Rect.fromLTRB(1, 2, 3, 4)),
5,
40,
false,
);
});
test('overloaded arcTo (used for arcToPoint)', () {
path.arcToRotated(
1,
2,
3,
false,
true,
4,
5,
);
});
test('close', () {
_testClosedSkPath();
});
test('conicTo', () {
path.conicTo(1, 2, 3, 4, 5);
});
test('contains', () {
final SkPath testPath = _testClosedSkPath();
expect(testPath.contains(15, 15), isTrue);
expect(testPath.contains(100, 100), isFalse);
});
test('cubicTo', () {
path.cubicTo(1, 2, 3, 4, 5, 6);
});
test('getBounds', () {
final SkPath testPath = _testClosedSkPath();
final ui.Rect bounds = fromSkRect(testPath.getBounds());
expect(bounds, const ui.Rect.fromLTRB(10, 10, 20, 20));
});
test('lineTo', () {
path.lineTo(10, 10);
});
test('moveTo', () {
path.moveTo(10, 10);
});
test('quadTo', () {
path.quadTo(10, 10, 20, 20);
});
test('rArcTo', () {
path.rArcTo(
10,
20,
30,
false,
true,
40,
50,
);
});
test('rConicTo', () {
path.rConicTo(1, 2, 3, 4, 5);
});
test('rCubicTo', () {
path.rCubicTo(1, 2, 3, 4, 5, 6);
});
test('rLineTo', () {
path.rLineTo(10, 10);
});
test('rMoveTo', () {
path.rMoveTo(10, 10);
});
test('rQuadTo', () {
path.rQuadTo(10, 10, 20, 20);
});
test('reset', () {
final SkPath testPath = _testClosedSkPath();
expect(fromSkRect(testPath.getBounds()),
const ui.Rect.fromLTRB(10, 10, 20, 20));
testPath.reset();
expect(fromSkRect(testPath.getBounds()), ui.Rect.zero);
});
test('toSVGString', () {
expect(
_testClosedSkPath().toSVGString(), 'M10 10L20 10L20 20L10 20L10 10Z');
});
test('isEmpty', () {
expect(SkPath().isEmpty(), isTrue);
expect(_testClosedSkPath().isEmpty(), isFalse);
});
test('copy', () {
final SkPath original = _testClosedSkPath();
final SkPath copy = original.copy();
expect(fromSkRect(original.getBounds()), fromSkRect(copy.getBounds()));
});
test('transform', () {
path = _testClosedSkPath();
path.transform(2, 0, 10, 0, 2, 10, 0, 0, 0);
final ui.Rect transformedBounds = fromSkRect(path.getBounds());
expect(transformedBounds, const ui.Rect.fromLTRB(30, 30, 50, 50));
});
test('SkContourMeasureIter/SkContourMeasure', () {
final SkContourMeasureIter iter =
SkContourMeasureIter(_testClosedSkPath(), false, 1.0);
final SkContourMeasure measure1 = iter.next()!;
expect(measure1.length(), 40);
expect(measure1.getPosTan(5), Float32List.fromList(<double>[15, 10, 1, 0]));
expect(
measure1.getPosTan(15), Float32List.fromList(<double>[20, 15, 0, 1]));
expect(measure1.isClosed(), isTrue);
// Starting with a box path:
//
// 10 20
// 10 +-----------+
// | |
// | |
// | |
// | |
// | |
// 20 +-----------+
//
// Cut out the top-right quadrant:
//
// 10 15 20
// 10 +-----+=====+
// | â•‘+++++â•‘
// | â•‘+++++â•‘
// | +=====+ 15
// | |
// | |
// 20 +-----------+
final SkPath segment = measure1.getSegment(5, 15, true);
expect(fromSkRect(segment.getBounds()),
const ui.Rect.fromLTRB(15, 10, 20, 15));
final SkContourMeasure? measure2 = iter.next();
expect(measure2, isNull);
});
test('SkPath.toCmds and CanvasKit.Path.MakeFromCmds', () {
const ui.Rect rect = ui.Rect.fromLTRB(0, 0, 10, 10);
final SkPath path = SkPath();
path.addRect(toSkRect(rect));
expect(path.toCmds(), <num>[
0, 0, 0, // moveTo
1, 10, 0, // lineTo
1, 10, 10, // lineTo
1, 0, 10, // lineTo
5, // close
]);
final SkPath copy = canvasKit.Path.MakeFromCmds(path.toCmds());
expect(fromSkRect(copy.getBounds()), rect);
});
}
SkVertices _testVertices() {
return canvasKit.MakeVertices(
canvasKit.VertexMode.Triangles,
Float32List.fromList(<double>[0, 0, 10, 10, 0, 20]),
Float32List.fromList(<double>[0, 0, 10, 10, 0, 20]),
Uint32List.fromList(<int>[0xffff0000, 0xff00ff00, 0xff0000ff]),
Uint16List.fromList(<int>[0, 1, 2]),
);
}
void _skVerticesTests() {
test('SkVertices', () {
expect(_testVertices(), isNotNull);
});
}
void _pictureTests() {
late SkPicture picture;
setUp(() {
final SkPictureRecorder recorder = SkPictureRecorder();
final SkCanvas canvas = recorder.beginRecording(toSkRect(ui.Rect.largest));
canvas.drawRect(toSkRect(const ui.Rect.fromLTRB(20, 30, 40, 50)),
SkPaint()..setColorInt(0xffff00ff));
picture = recorder.finishRecordingAsPicture();
});
test('cullRect', () {
expect(
fromSkRect(picture.cullRect()), const ui.Rect.fromLTRB(20, 30, 40, 50));
});
test('approximateBytesUsed', () {
expect(picture.approximateBytesUsed() > 0, isTrue);
});
}
void _canvasTests() {
late SkPictureRecorder recorder;
late SkCanvas canvas;
setUp(() {
recorder = SkPictureRecorder();
canvas = recorder
.beginRecording(toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)));
});
tearDown(() {
expect(recorder.finishRecordingAsPicture(), isNotNull);
});
test('save/getSaveCount/restore/restoreToCount', () {
expect(canvas.save(), 1);
expect(canvas.save(), 2);
expect(canvas.save(), 3);
expect(canvas.save(), 4);
expect(canvas.getSaveCount(), 5);
canvas.restoreToCount(2);
expect(canvas.getSaveCount(), 2);
canvas.restore();
expect(canvas.getSaveCount(), 1);
});
test('saveLayer', () {
canvas.saveLayer(
SkPaint(),
toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)),
null,
null,
);
});
test('saveLayer without bounds', () {
canvas.saveLayer(SkPaint(), null, null, null);
});
test('saveLayer with filter', () {
canvas.saveLayer(
SkPaint(),
toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)),
canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
0,
);
});
test('clear', () {
canvas.clear(Float32List.fromList(<double>[0, 0, 0, 0]));
});
test('clipPath', () {
canvas.clipPath(
SkPath()
..moveTo(10.9, 10.9)
..lineTo(19.1, 10.9)
..lineTo(19.1, 19.1)
..lineTo(10.9, 19.1),
canvasKit.ClipOp.Intersect,
true,
);
expect(canvas.getDeviceClipBounds(), <int>[10, 10, 20, 20]);
});
test('clipRRect', () {
canvas.clipRRect(
Float32List.fromList(<double>[0.9, 0.9, 99.1, 99.1, 1, 2, 3, 4, 5, 6, 7, 8]),
canvasKit.ClipOp.Intersect,
true,
);
expect(canvas.getDeviceClipBounds(), <int>[0, 0, 100, 100]);
});
test('clipRect', () {
canvas.clipRect(
Float32List.fromList(<double>[0.9, 0.9, 99.1, 99.1]),
canvasKit.ClipOp.Intersect,
true,
);
expect(canvas.getDeviceClipBounds(), <int>[0, 0, 100, 100]);
});
test('drawArc', () {
canvas.drawArc(
Float32List.fromList(<double>[0, 0, 100, 50]),
0,
100,
true,
SkPaint(),
);
});
test('drawAtlas', () {
final SkAnimatedImage image =
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
canvas.drawAtlas(
image.makeImageAtCurrentFrame(),
Float32List.fromList(<double>[0, 0, 1, 1]),
Float32List.fromList(<double>[1, 0, 2, 3]),
SkPaint(),
canvasKit.BlendMode.SrcOver,
Uint32List.fromList(<int>[0xff000000, 0xffffffff]),
);
});
test('drawCircle', () {
canvas.drawCircle(1, 2, 3, SkPaint());
});
test('drawColorInt', () {
canvas.drawColorInt(0xFFFFFFFF, canvasKit.BlendMode.SoftLight);
});
test('drawDRRect', () {
canvas.drawDRRect(
Float32List.fromList(<double>[0, 0, 100, 100, 1, 2, 3, 4, 5, 6, 7, 8]),
Float32List.fromList(<double>[20, 20, 80, 80, 1, 2, 3, 4, 5, 6, 7, 8]),
SkPaint(),
);
});
test('drawImageOptions', () {
final SkAnimatedImage image =
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
canvas.drawImageOptions(
image.makeImageAtCurrentFrame(),
10,
20,
canvasKit.FilterMode.Linear,
canvasKit.MipmapMode.None,
SkPaint(),
);
});
test('drawImageCubic', () {
final SkAnimatedImage image =
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
canvas.drawImageCubic(
image.makeImageAtCurrentFrame(),
10,
20,
0.3,
0.3,
SkPaint(),
);
});
test('drawImageRectOptions', () {
final SkAnimatedImage image =
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
canvas.drawImageRectOptions(
image.makeImageAtCurrentFrame(),
Float32List.fromList(<double>[0, 0, 1, 1]),
Float32List.fromList(<double>[0, 0, 1, 1]),
canvasKit.FilterMode.Linear,
canvasKit.MipmapMode.None,
SkPaint(),
);
});
test('drawImageRectCubic', () {
final SkAnimatedImage image =
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
canvas.drawImageRectCubic(
image.makeImageAtCurrentFrame(),
Float32List.fromList(<double>[0, 0, 1, 1]),
Float32List.fromList(<double>[0, 0, 1, 1]),
0.3,
0.3,
SkPaint(),
);
});
test('drawImageNine', () {
final SkAnimatedImage image =
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
canvas.drawImageNine(
image.makeImageAtCurrentFrame(),
Float32List.fromList(<double>[0, 0, 1, 1]),
Float32List.fromList(<double>[0, 0, 1, 1]),
canvasKit.FilterMode.Linear,
SkPaint(),
);
});
test('drawLine', () {
canvas.drawLine(0, 1, 2, 3, SkPaint());
});
test('drawOval', () {
canvas.drawOval(Float32List.fromList(<double>[0, 0, 1, 1]), SkPaint());
});
test('drawPaint', () {
canvas.drawPaint(SkPaint());
});
test('drawPath', () {
canvas.drawPath(
_testClosedSkPath(),
SkPaint(),
);
});
test('drawPoints', () {
canvas.drawPoints(
canvasKit.PointMode.Lines,
Float32List.fromList(<double>[0, 0, 10, 10, 0, 10]),
SkPaint(),
);
});
test('drawRRect', () {
canvas.drawRRect(
Float32List.fromList(<double>[0, 0, 100, 100, 1, 2, 3, 4, 5, 6, 7, 8]),
SkPaint(),
);
});
test('drawRect', () {
canvas.drawRect(
Float32List.fromList(<double>[0, 0, 100, 100]),
SkPaint(),
);
});
test('drawShadow', () {
for (final int flags in const <int>[0x01, 0x00]) {
const double devicePixelRatio = 2.0;
const double elevation = 4.0;
const double ambientAlpha = 0.039;
const double spotAlpha = 0.25;
final SkPath path = _testClosedSkPath();
final ui.Rect bounds = fromSkRect(path.getBounds());
final double shadowX = (bounds.left + bounds.right) / 2.0;
final double shadowY = bounds.top - 600.0;
const ui.Color color = ui.Color(0xAABBCCDD);
final ui.Color inAmbient =
color.withAlpha((color.alpha * ambientAlpha).round());
final ui.Color inSpot =
color.withAlpha((color.alpha * spotAlpha).round());
final SkTonalColors inTonalColors = SkTonalColors(
ambient: makeFreshSkColor(inAmbient),
spot: makeFreshSkColor(inSpot),
);
final SkTonalColors tonalColors =
canvasKit.computeTonalColors(inTonalColors);
canvas.drawShadow(
path,
Float32List(3)..[2] = devicePixelRatio * elevation,
Float32List(3)
..[0] = shadowX
..[1] = shadowY
..[2] = devicePixelRatio * kLightHeight,
devicePixelRatio * kLightRadius,
tonalColors.ambient,
tonalColors.spot,
flags.toDouble(),
);
}
});
test('drawVertices', () {
canvas.drawVertices(
_testVertices(),
canvasKit.BlendMode.SrcOver,
SkPaint(),
);
});
test('rotate', () {
canvas.rotate(90, 10, 20);
expect(canvas.getLocalToDevice(), <double>[
0, -1, 0, 30, // tx = 10 - (-20) == 30
1, 0, 0, 10, // ty = 20 - 10 == 10
0, 0, 1, 0,
0, 0, 0, 1,
]);
});
test('scale', () {
canvas.scale(2, 3);
expect(canvas.getLocalToDevice(), <double>[
2, 0, 0, 0,
0, 3, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
});
test('skew', () {
canvas.skew(4, 5);
expect(canvas.getLocalToDevice(), <double>[
1, 4, 0, 0,
5, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
});
test('concat', () {
canvas.concat(toSkM44FromFloat32(Matrix4.identity().storage));
expect(canvas.getLocalToDevice(), <double>[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
canvas.concat(Float32List.fromList(<double>[
11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44,
]));
expect(canvas.getLocalToDevice(), <double>[
11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44,
]);
});
test('translate', () {
canvas.translate(4, 5);
expect(canvas.getLocalToDevice(), <double>[
1, 0, 0, 4,
0, 1, 0, 5,
0, 0, 1, 0,
0, 0, 0, 1,
]);
});
test('drawPicture', () {
final SkPictureRecorder otherRecorder = SkPictureRecorder();
final SkCanvas otherCanvas = otherRecorder
.beginRecording(Float32List.fromList(<double>[0, 0, 100, 100]));
otherCanvas.drawLine(0, 0, 10, 10, SkPaint());
canvas.drawPicture(otherRecorder.finishRecordingAsPicture());
});
test('drawParagraph', () {
final CkParagraphBuilder builder = CkParagraphBuilder(
CkParagraphStyle(),
);
builder.addText('Hello');
final CkParagraph paragraph = builder.build();
paragraph.layout(const ui.ParagraphConstraints(width: 100));
canvas.drawParagraph(
paragraph.skiaObject,
10,
20,
);
});
test('Paragraph converts caret position to charactor position', () {
final CkParagraphBuilder builder = CkParagraphBuilder(
CkParagraphStyle(),
);
builder.addText('Hello there');
final CkParagraph paragraph = builder.build();
paragraph.layout(const ui.ParagraphConstraints(width: 100));
ui.TextRange range = paragraph.getWordBoundary(const ui.TextPosition(offset: 5, affinity: ui.TextAffinity.upstream));
expect(range.start, 0);
expect(range.end, 5);
range = paragraph.getWordBoundary(const ui.TextPosition(offset: 5));
expect(range.start, 5);
expect(range.end, 6);
});
test('Paragraph dispose', () {
final CkParagraphBuilder builder = CkParagraphBuilder(
CkParagraphStyle(),
);
builder.addText('Hello');
final CkParagraph paragraph = builder.build();
paragraph.dispose();
expect(paragraph.debugDisposed, true);
});
test('toImage.toByteData', () async {
final SkPictureRecorder otherRecorder = SkPictureRecorder();
final SkCanvas otherCanvas = otherRecorder
.beginRecording(Float32List.fromList(<double>[0, 0, 1, 1]));
otherCanvas.drawRect(
Float32List.fromList(<double>[0, 0, 1, 1]),
SkPaint()..setColorInt(0xAAFFFFFF),
);
final CkPicture picture =
CkPicture(otherRecorder.finishRecordingAsPicture());
final CkImage image = await picture.toImage(1, 1) as CkImage;
final ByteData rawData =
await image.toByteData();
expect(rawData.lengthInBytes, greaterThan(0));
expect(
rawData.buffer.asUint32List(),
<int>[0xAAAAAAAA],
);
final ByteData rawStraightData =
await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba);
expect(rawStraightData.lengthInBytes, greaterThan(0));
expect(
rawStraightData.buffer.asUint32List(),
<int>[0xAAFFFFFF],
);
final ByteData pngData =
await image.toByteData(format: ui.ImageByteFormat.png);
expect(pngData.lengthInBytes, greaterThan(0));
});
}
void _textStyleTests() {
test('SkTextDecorationStyle mapping is correct', () {
expect(canvasKit.DecorationStyle.Solid.value,
ui.TextDecorationStyle.solid.index);
expect(canvasKit.DecorationStyle.Double.value,
ui.TextDecorationStyle.double.index);
expect(canvasKit.DecorationStyle.Dotted.value,
ui.TextDecorationStyle.dotted.index);
expect(canvasKit.DecorationStyle.Dashed.value,
ui.TextDecorationStyle.dashed.index);
expect(canvasKit.DecorationStyle.Wavy.value,
ui.TextDecorationStyle.wavy.index);
});
test('ui.TextDecorationStyle converts to SkTextDecorationStyle', () {
for (final ui.TextDecorationStyle decorationStyle
in ui.TextDecorationStyle.values) {
expect(toSkTextDecorationStyle(decorationStyle).value,
decorationStyle.index);
}
});
test('SkTextBaseline mapping is correct', () {
expect(canvasKit.TextBaseline.Alphabetic.value,
ui.TextBaseline.alphabetic.index);
expect(canvasKit.TextBaseline.Ideographic.value,
ui.TextBaseline.ideographic.index);
});
test('ui.TextBaseline converts to SkTextBaseline', () {
for (final ui.TextBaseline textBaseline in ui.TextBaseline.values) {
expect(toSkTextBaseline(textBaseline).value, textBaseline.index);
}
});
test('SkPlaceholderAlignment mapping is correct', () {
expect(canvasKit.PlaceholderAlignment.Baseline.value,
ui.PlaceholderAlignment.baseline.index);
expect(canvasKit.PlaceholderAlignment.AboveBaseline.value,
ui.PlaceholderAlignment.aboveBaseline.index);
expect(canvasKit.PlaceholderAlignment.BelowBaseline.value,
ui.PlaceholderAlignment.belowBaseline.index);
expect(canvasKit.PlaceholderAlignment.Top.value,
ui.PlaceholderAlignment.top.index);
expect(canvasKit.PlaceholderAlignment.Bottom.value,
ui.PlaceholderAlignment.bottom.index);
expect(canvasKit.PlaceholderAlignment.Middle.value,
ui.PlaceholderAlignment.middle.index);
});
test('ui.PlaceholderAlignment converts to SkPlaceholderAlignment', () {
for (final ui.PlaceholderAlignment placeholderAlignment
in ui.PlaceholderAlignment.values) {
expect(toSkPlaceholderAlignment(placeholderAlignment).value,
placeholderAlignment.index);
}
});
}
void _paragraphTests() {
// This test is just a kitchen sink that blasts CanvasKit with all paragraph
// properties all at once, making sure CanvasKit doesn't choke on anything.
// In particular, this tests that our JS bindings are correct, such as that
// arguments are of acceptable types and passed in the correct order.
test('kitchensink', () async {
final SkParagraphStyleProperties props = SkParagraphStyleProperties();
props.textAlign = canvasKit.TextAlign.Left;
props.textDirection = canvasKit.TextDirection.RTL;
props.heightMultiplier = 3;
props.textHeightBehavior = canvasKit.TextHeightBehavior.All;
props.maxLines = 4;
props.ellipsis = '___';
props.textStyle = SkTextStyleProperties()
..backgroundColor = Float32List.fromList(<double>[0.2, 0, 0, 0.5])
..color = Float32List.fromList(<double>[0, 1, 0, 1])
..foregroundColor = Float32List.fromList(<double>[1, 0, 1, 1])
..decoration = 0x2
..decorationThickness = 2.0
..decorationColor = Float32List.fromList(<double>[13, 14, 15, 16])
..decorationStyle = canvasKit.DecorationStyle.Dotted
..textBaseline = canvasKit.TextBaseline.Ideographic
..fontSize = 48
..letterSpacing = 5
..wordSpacing = 10
..heightMultiplier = 1.3
..halfLeading = true
..locale = 'en_CA'
..fontFamilies = <String>['Roboto', 'serif']
..fontStyle = (SkFontStyle()
..slant = canvasKit.FontSlant.Upright
..weight = canvasKit.FontWeight.Normal)
..shadows = <SkTextShadow>[]
..fontFeatures = <SkFontFeature>[
SkFontFeature()
..name = 'pnum'
..value = 1,
SkFontFeature()
..name = 'tnum'
..value = 1,
]
;
props.strutStyle = SkStrutStyleProperties()
..fontFamilies = <String>['Roboto', 'Noto']
..fontStyle = (SkFontStyle()
..slant = canvasKit.FontSlant.Italic
..weight = canvasKit.FontWeight.Bold)
..fontSize = 72
..heightMultiplier = 1.5
..halfLeading = false
..leading = 0
..strutEnabled = true
..forceStrutHeight = false;
final SkParagraphStyle paragraphStyle = canvasKit.ParagraphStyle(props);
final SkParagraphBuilder builder = canvasKit.ParagraphBuilder.MakeFromFontCollection(
paragraphStyle,
CanvasKitRenderer.instance.fontCollection.skFontCollection,
);
builder.addText('Hello');
builder.addPlaceholder(
50,
25,
canvasKit.PlaceholderAlignment.Middle,
canvasKit.TextBaseline.Ideographic,
4.0,
);
builder.pushStyle(canvasKit.TextStyle(SkTextStyleProperties()
..color = Float32List.fromList(<double>[1, 0, 0, 1])
..fontSize = 24
..fontFamilies = <String>['Roboto', 'serif']
));
builder.addText('World');
builder.pop();
builder.pushPaintStyle(
canvasKit.TextStyle(SkTextStyleProperties()
..color = Float32List.fromList(<double>[1, 0, 0, 1])
..fontSize = 60
..fontFamilies = <String>['Roboto', 'serif']
),
SkPaint()..setColorInt(0xFF0000FF),
SkPaint()..setColorInt(0xFFFF0000),
);
builder.addText('!');
builder.pop();
builder.pushStyle(
canvasKit.TextStyle(SkTextStyleProperties()..halfLeading = true));
builder.pop();
if (canvasKit.ParagraphBuilder.RequiresClientICU()) {
injectClientICU(builder);
}
final SkParagraph paragraph = builder.build();
paragraph.layout(500);
final DomCanvasElement canvas = createDomCanvasElement(
width: 400,
height: 160,
);
domDocument.body!.append(canvas);
// TODO(yjbanov): WebGL screenshot tests do not work on Firefox - https://github.com/flutter/flutter/issues/109265
if (!isFirefox) {
final SkSurface surface = canvasKit.MakeWebGLCanvasSurface(canvas);
final SkCanvas skCanvas = surface.getCanvas();
skCanvas.drawColorInt(0xFFCCCCCC, toSkBlendMode(ui.BlendMode.srcOver));
skCanvas.drawParagraph(paragraph, 20, 20);
skCanvas.drawRect(
Float32List.fromList(<double>[20, 20, 20 + paragraph.getMaxIntrinsicWidth(), 20 + paragraph.getHeight()]),
SkPaint()
..setStyle(toSkPaintStyle(ui.PaintingStyle.stroke))
..setStrokeWidth(1)
..setColorInt(0xFF00FF00),
);
surface.flush();
await matchGoldenFile(
'paragraph_kitchen_sink.png',
region: const ui.Rect.fromLTRB(0, 0, 400, 160),
);
}
void expectAlmost(double actual, double expected) {
expect(actual, within<double>(distance: actual / 100, from: expected));
}
expectAlmost(paragraph.getAlphabeticBaseline(), 85.5);
expect(paragraph.didExceedMaxLines(), isFalse);
expectAlmost(paragraph.getHeight(), 108);
expectAlmost(paragraph.getIdeographicBaseline(), 108);
expectAlmost(paragraph.getLongestLine(), 263);
expectAlmost(paragraph.getMaxIntrinsicWidth(), 263);
expectAlmost(paragraph.getMinIntrinsicWidth(), 135);
expectAlmost(paragraph.getMaxWidth(), 500);
final SkRectWithDirection rectWithDirection =
paragraph.getRectsForRange(
1,
3,
canvasKit.RectHeightStyle.Tight,
canvasKit.RectWidthStyle.Max).single;
expect(
rectWithDirection.rect,
hasLength(4),
);
expect(paragraph.getRectsForPlaceholders(), hasLength(1));
expect(paragraph.getLineMetrics(), hasLength(1));
final SkLineMetrics lineMetrics =
paragraph.getLineMetrics().single;
expectAlmost(lineMetrics.ascent, 55.6);
expectAlmost(lineMetrics.descent, 14.8);
expect(lineMetrics.isHardBreak, isTrue);
expectAlmost(lineMetrics.baseline, 85.5);
expectAlmost(lineMetrics.height, 108);
expectAlmost(lineMetrics.left, 2.5);
expectAlmost(lineMetrics.width, 263);
expect(lineMetrics.lineNumber, 0);
expect(
paragraph.getGlyphPositionAtCoordinate(5, 5).affinity,
canvasKit.Affinity.Upstream,
);
// "Hello"
for (int i = 0; i < 5; i++) {
expect(paragraph.getWordBoundary(i.toDouble()).start, 0);
expect(paragraph.getWordBoundary(i.toDouble()).end, 5);
}
// Placeholder
expect(paragraph.getWordBoundary(5).start, 5);
expect(paragraph.getWordBoundary(5).end, 6);
// "World"
for (int i = 6; i < 11; i++) {
expect(paragraph.getWordBoundary(i.toDouble()).start, 6);
expect(paragraph.getWordBoundary(i.toDouble()).end, 11);
}
// "!"
expect(paragraph.getWordBoundary(11).start, 11);
expect(paragraph.getWordBoundary(11).end, 12);
paragraph.delete();
});
test('RectHeightStyle', () {
final SkParagraphStyleProperties props = SkParagraphStyleProperties();
props.heightMultiplier = 3;
props.textAlign = canvasKit.TextAlign.Start;
props.textDirection = canvasKit.TextDirection.LTR;
props.textStyle = SkTextStyleProperties()
..fontSize = 25
..fontFamilies = <String>['Roboto']
..fontStyle = (SkFontStyle()..weight = canvasKit.FontWeight.Normal);
props.strutStyle = SkStrutStyleProperties()
..strutEnabled = true
..forceStrutHeight = true
..fontSize = 25
..fontFamilies = <String>['Roboto']
..heightMultiplier = 3
..fontStyle = (SkFontStyle()..weight = canvasKit.FontWeight.Normal);
final SkParagraphStyle paragraphStyle = canvasKit.ParagraphStyle(props);
final SkParagraphBuilder builder =
canvasKit.ParagraphBuilder.MakeFromFontCollection(
paragraphStyle,
CanvasKitRenderer.instance.fontCollection.skFontCollection,
);
builder.addText('hello');
if (canvasKit.ParagraphBuilder.RequiresClientICU()) {
injectClientICU(builder);
}
final SkParagraph paragraph = builder.build();
paragraph.layout(500);
final List<SkRectWithDirection> rects = paragraph.getRectsForRange(
0,
1,
canvasKit.RectHeightStyle.Strut,
canvasKit.RectWidthStyle.Tight,
);
expect(rects.length, 1);
final SkRectWithDirection rect = rects.first;
expect(rect.rect, <double>[0, 0, 13.770000457763672, 75]);
});
test('TextHeightBehavior', () {
expect(
toSkTextHeightBehavior(const ui.TextHeightBehavior()),
canvasKit.TextHeightBehavior.All,
);
expect(
toSkTextHeightBehavior(const ui.TextHeightBehavior(
applyHeightToFirstAscent: false,
)),
canvasKit.TextHeightBehavior.DisableFirstAscent,
);
expect(
toSkTextHeightBehavior(const ui.TextHeightBehavior(
applyHeightToLastDescent: false,
)),
canvasKit.TextHeightBehavior.DisableLastDescent,
);
expect(
toSkTextHeightBehavior(const ui.TextHeightBehavior(
applyHeightToFirstAscent: false,
applyHeightToLastDescent: false,
)),
canvasKit.TextHeightBehavior.DisableAll,
);
});
test('MakeOnScreenGLSurface test', () {
final DomCanvasElement canvas = createDomCanvasElement(
width: 100,
height: 100,
);
final WebGLContext gl = canvas.getGlContext(webGLVersion);
final int sampleCount = gl.getParameter(gl.samples);
final int stencilBits = gl.getParameter(gl.stencilBits);
final double glContext = canvasKit.GetWebGLContext(
canvas,
SkWebGLContextOptions(
antialias: 0,
majorVersion: webGLVersion.toDouble(),
),
);
final SkGrContext grContext = canvasKit.MakeGrContext(glContext);
final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface(
grContext,
100,
100,
SkColorSpaceSRGB,
sampleCount,
stencilBits
);
expect(skSurface, isNotNull);
}, skip: isFirefox); // Intended: Headless firefox has no webgl support https://github.com/flutter/flutter/issues/109265
test('MakeRenderTarget test', () {
final DomCanvasElement canvas = createDomCanvasElement(
width: 100,
height: 100,
);
final int glContext = canvasKit.GetWebGLContext(
canvas,
SkWebGLContextOptions(
antialias: 0,
majorVersion: webGLVersion.toDouble(),
),
).toInt();
final SkGrContext grContext = canvasKit.MakeGrContext(glContext.toDouble());
final SkSurface? surface = canvasKit.MakeRenderTarget(grContext, 1, 1);
expect(surface, isNotNull);
}, skip: isFirefox); // Intended: Headless firefox has no webgl support https://github.com/flutter/flutter/issues/109265
group('getCanvasKitJsFileNames', () {
JSAny? oldV8BreakIterator = v8BreakIterator;
JSAny? oldIntlSegmenter = intlSegmenter;
setUp(() {
oldV8BreakIterator = v8BreakIterator;
oldIntlSegmenter = intlSegmenter;
});
tearDown(() {
v8BreakIterator = oldV8BreakIterator;
intlSegmenter = oldIntlSegmenter;
debugResetBrowserSupportsImageDecoder();
});
test('in Chromium-based browsers', () {
v8BreakIterator = Object().toJSBox; // Any non-null value.
intlSegmenter = Object().toJSBox; // Any non-null value.
browserSupportsImageDecoder = true;
expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>[
'chromium/canvaskit.js',
'canvaskit.js',
]);
});
test('in older versions of Chromium-based browsers', () {
v8BreakIterator = Object().toJSBox; // Any non-null value.
intlSegmenter = null; // Older versions of Chromium didn't have the Intl.Segmenter API.
browserSupportsImageDecoder = true;
expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['canvaskit.js']);
});
test('in other browsers', () {
intlSegmenter = Object().toJSBox; // Any non-null value.
v8BreakIterator = null;
browserSupportsImageDecoder = true;
expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['canvaskit.js']);
v8BreakIterator = Object().toJSBox;
browserSupportsImageDecoder = false;
// TODO(mdebbar): we don't check image codecs for now.
// https://github.com/flutter/flutter/issues/122331
expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['chromium/canvaskit.js', 'canvaskit.js']);
v8BreakIterator = null;
browserSupportsImageDecoder = false;
expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['canvaskit.js']);
});
});
test('respects actual location of canvaskit files', () {
expect(
canvasKitWasmModuleUrl('canvaskit.wasm', 'https://example.com/'),
'https://example.com/canvaskit.wasm',
);
expect(
canvasKitWasmModuleUrl('canvaskit.wasm', 'http://localhost:1234/'),
'http://localhost:1234/canvaskit.wasm',
);
expect(
canvasKitWasmModuleUrl('canvaskit.wasm', 'http://localhost:1234/foo/'),
'http://localhost:1234/foo/canvaskit.wasm',
);
});
test('SkObjectFinalizationRegistry', () {
// There's no reliable way to test the actual functionality of
// FinalizationRegistry because it depends on GC, which cannot be controlled,
// So the test simply tests that a FinalizationRegistry can be constructed
// and its `register` method can be called.
final DomFinalizationRegistry registry = createDomFinalizationRegistry((String arg) {}.toJS);
registry.register(Object(), Object());
});
}
@JS('window.Intl.v8BreakIterator')
external JSAny? get v8BreakIterator;
@JS('window.Intl.v8BreakIterator')
external set v8BreakIterator(JSAny? x);
@JS('window.Intl.Segmenter')
external JSAny? get intlSegmenter;
@JS('window.Intl.Segmenter')
external set intlSegmenter(JSAny? x);