blob: 5ff95df766913705a6f8c2433d79a7a505c04581 [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.
// @dart = 2.12
import 'dart:html' as html;
import 'dart:math' as 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.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 500, 250);
Future<void> matchPictureGolden(String goldenFile, CkPicture picture, { ui.Rect region = kDefaultRegion, bool write = false }) async {
final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher;
final LayerSceneBuilder sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPicture(ui.Offset.zero, picture);
dispatcher.rasterizer!.draw(sb.build().layerTree);
await matchGoldenFile(goldenFile, region: region, maxDiffRatePercent: 0.0, write: write);
}
void testMain() {
group('CkCanvas', () {
setUpCanvasKitTest();
test('renders using non-recording canvas if weak refs are supported',
() async {
expect(browserSupportsFinalizationRegistry, isTrue,
reason: 'This test specifically tests non-recording canvas, which '
'only works if FinalizationRegistry is available.');
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
expect(canvas.runtimeType, CkCanvas);
drawTestPicture(canvas);
await matchPictureGolden(
'canvaskit_picture.png', recorder.endRecording());
});
test('renders using a recording canvas if weak refs are not supported',
() async {
browserSupportsFinalizationRegistry = false;
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
expect(canvas, isA<RecordingCkCanvas>());
drawTestPicture(canvas);
final CkPicture originalPicture = recorder.endRecording();
await matchPictureGolden(
'canvaskit_picture.png', originalPicture);
final ByteData originalPixels =
await (await originalPicture.toImage(50, 50)).toByteData()
as ByteData;
// Test that a picture restored from a snapshot looks the same.
final CkPictureSnapshot? snapshot = canvas.pictureSnapshot;
expect(snapshot, isNotNull);
final SkPicture restoredSkPicture = snapshot!.toPicture();
expect(restoredSkPicture, isNotNull);
final CkPicture restoredPicture = CkPicture(
restoredSkPicture, ui.Rect.fromLTRB(0, 0, 50, 50), snapshot);
final ByteData restoredPixels =
await (await restoredPicture.toImage(50, 50)).toByteData()
as ByteData;
await matchPictureGolden(
'canvaskit_picture.png', restoredPicture);
expect(restoredPixels.buffer.asUint8List(),
originalPixels.buffer.asUint8List());
});
// Regression test for https://github.com/flutter/flutter/issues/51237
// Draws a grid of shadows at different offsets. Prior to directional
// light the shadows would shift depending on the offset. With directional
// light the cells in the grid must look identical.
test('uses directional shadows', () async {
const ui.Rect region = ui.Rect.fromLTRB(0, 0, 820, 420);
final CkPicture picture = paintPicture(region, (CkCanvas canvas) {
final CkPath shape = CkPath()
..addRect(const ui.Rect.fromLTRB(0, 0, 40, 40));
final CkPaint shapePaint = CkPaint()
..style = ui.PaintingStyle.stroke
..strokeWidth = 1
..color = const ui.Color(0xFF009900);
final CkPaint shadowBoundsPaint = CkPaint()
..style = ui.PaintingStyle.stroke
..strokeWidth = 1
..color = const ui.Color(0xFF000099);
canvas.translate(20, 20);
for (int row = 0; row < 5; row += 1) {
canvas.save();
for (int col = 0; col < 10; col += 1) {
final double elevation = 2 * (col % 5).toDouble();
canvas.drawShadow(shape, ui.Color(0xFFFF0000), elevation, true);
canvas.drawPath(shape, shapePaint);
final PhysicalShapeEngineLayer psl = PhysicalShapeEngineLayer(
elevation,
const ui.Color(0xFF000000),
const ui.Color(0xFF000000),
shape,
ui.Clip.antiAlias,
);
psl.preroll(
PrerollContext(
RasterCache(),
HtmlViewEmbedder(),
),
Matrix4.identity(),
);
canvas.drawRect(psl.paintBounds, shadowBoundsPaint);
final CkParagraphBuilder pb = CkParagraphBuilder(
CkParagraphStyle(),
);
pb.addText('$elevation');
final CkParagraph p = pb.build();
p.layout(const ui.ParagraphConstraints(width: 1000));
canvas.drawParagraph(p, ui.Offset(20 - p.maxIntrinsicWidth / 2, 20 - p.height / 2));
canvas.translate(80, 0);
}
canvas.restore();
canvas.translate(0, 80);
}
});
await matchPictureGolden('canvaskit_directional_shadows.png', picture, region: region);
});
test('computes shadow bounds correctly with parent transforms', () async {
const double rectSize = 50;
const double halfSize = rectSize / 2;
const double padding = 110;
const ui.Rect region = ui.Rect.fromLTRB(
0,
0,
(rectSize + padding) * 3 + padding,
(rectSize + padding) * 2 + padding,
);
late List<PhysicalShapeEngineLayer> physicalShapeLayers;
LayerTree buildTestScene({ required bool paintShadowBounds }) {
final Iterator<PhysicalShapeEngineLayer>? shadowBounds = paintShadowBounds
? physicalShapeLayers.iterator : null;
physicalShapeLayers = <PhysicalShapeEngineLayer>[];
final LayerSceneBuilder builder = LayerSceneBuilder();
builder.pushOffset(padding + halfSize, padding + halfSize);
final CkPath shape = CkPath()
..addRect(const ui.Rect.fromLTRB(-halfSize, -halfSize, halfSize, halfSize));
final CkPaint shadowBoundsPaint = CkPaint()
..style = ui.PaintingStyle.stroke
..strokeWidth = 1
..color = const ui.Color(0xFF000099);
for (int row = 0; row < 2; row += 1) {
for (int col = 0; col < 3; col += 1) {
builder.pushOffset(col * (rectSize + padding), row * (rectSize + padding));
builder.pushTransform(Float64List.fromList(Matrix4.rotationZ(row * math.pi / 4).storage));
final double scale = 1 / (1 + col);
builder.pushTransform(Float64List.fromList(Matrix4.diagonal3Values(scale, scale, 1).storage));
physicalShapeLayers.add(builder.pushPhysicalShape(
path: shape,
elevation: 6,
color: const ui.Color(0xFF009900),
shadowColor: const ui.Color(0xFF000000),
));
if (shadowBounds != null) {
shadowBounds.moveNext();
final ui.Rect bounds = shadowBounds.current.paintBounds;
builder.addPicture(ui.Offset.zero, paintPicture(region, (CkCanvas canvas) {
canvas.drawRect(bounds, shadowBoundsPaint);
}));
}
builder.pop();
builder.pop();
builder.pop();
builder.pop();
}
}
builder.pop();
return builder.build().layerTree;
}
// Render the scene once without painting the shadow bounds just to
// preroll the scene to compute the shadow bounds.
buildTestScene(paintShadowBounds: false).rootLayer.preroll(
PrerollContext(
RasterCache(),
HtmlViewEmbedder(),
),
Matrix4.identity(),
);
// Render again, this time with the shadow bounds.
final LayerTree layerTree = buildTestScene(paintShadowBounds: true);
final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher;
dispatcher.rasterizer!.draw(layerTree);
await matchGoldenFile('canvaskit_shadow_bounds.png', region: region);
});
test('text styles - default', () async {
await testTextStyle('default');
});
test('text styles - center aligned', () async {
await testTextStyle('center aligned', paragraphTextAlign: ui.TextAlign.center);
});
test('text styles - right aligned', () async {
await testTextStyle('right aligned', paragraphTextAlign: ui.TextAlign.right);
});
test('text styles - rtl', () async {
await testTextStyle('rtl', paragraphTextDirection: ui.TextDirection.rtl);
});
test('text styles - multiline', () async {
await testTextStyle('multiline', layoutWidth: 50);
});
test('text styles - max lines', () async {
await testTextStyle('max lines', paragraphMaxLines: 1, layoutWidth: 50);
});
test('text styles - ellipsis', () async {
await testTextStyle('ellipsis', paragraphMaxLines: 1, paragraphEllipsis: '...', layoutWidth: 60);
});
test('text styles - paragraph font family', () async {
await testTextStyle('paragraph font family', paragraphFontFamily: 'Ahem');
});
test('text styles - paragraph font size', () async {
await testTextStyle('paragraph font size', paragraphFontSize: 22);
});
// TODO(yjbanov): paragraphHeight seems to have no effect, but maybe I'm using it wrong.
// https://github.com/flutter/flutter/issues/74337
test('text styles - paragraph height', () async {
await testTextStyle('paragraph height', layoutWidth: 50, paragraphHeight: 1.5);
});
// TODO(yjbanov): paragraphTextHeightBehavior seems to have no effect. Unsure how to use it.
// https://github.com/flutter/flutter/issues/74337
test('text styles - paragraph text height behavior', () async {
await testTextStyle('paragraph text height behavior', layoutWidth: 50, paragraphHeight: 1.5, paragraphTextHeightBehavior: ui.TextHeightBehavior(
applyHeightToFirstAscent: false,
applyHeightToLastDescent: false,
));
});
// TODO(yjbanov): paragraph fontWeight doesn't seem to work.
// https://github.com/flutter/flutter/issues/74338
test('text styles - paragraph weight', () async {
await testTextStyle('paragraph weight', paragraphFontWeight: ui.FontWeight.w900);
});
// TODO(yjbanov): paragraph fontStyle doesn't seem to work.
// https://github.com/flutter/flutter/issues/74338
test('text style - paragraph font style', () async {
await testTextStyle(
'paragraph font style',
paragraphFontStyle: ui.FontStyle.italic,
);
});
// TODO(yjbanov): locales specified in paragraph styles don't work:
// https://github.com/flutter/flutter/issues/74687
// TODO(yjbanov): spaces are not rendered correctly:
// https://github.com/flutter/flutter/issues/74742
test('text styles - paragraph locale zh_CN', () async {
await testTextStyle('paragraph locale zh_CN', outerText: '次 化 刃 直 入 令', innerText: '', paragraphLocale: const ui.Locale('zh', 'CN'));
});
test('text styles - paragraph locale zh_TW', () async {
await testTextStyle('paragraph locale zh_TW', outerText: '次 化 刃 直 入 令', innerText: '', paragraphLocale: const ui.Locale('zh', 'TW'));
});
test('text styles - paragraph locale ja', () async {
await testTextStyle('paragraph locale ja', outerText: '次 化 刃 直 入 令', innerText: '', paragraphLocale: const ui.Locale('ja'));
});
test('text styles - paragraph locale ko', () async {
await testTextStyle('paragraph locale ko', outerText: '次 化 刃 直 入 令', innerText: '', paragraphLocale: const ui.Locale('ko'));
});
test('text styles - color', () async {
await testTextStyle('color', color: const ui.Color(0xFF009900));
});
test('text styles - decoration', () async {
await testTextStyle('decoration', decoration: ui.TextDecoration.underline);
});
test('text styles - decoration style', () async {
await testTextStyle('decoration style', decoration: ui.TextDecoration.underline, decorationStyle: ui.TextDecorationStyle.dashed);
});
test('text styles - decoration thickness', () async {
await testTextStyle('decoration thickness', decoration: ui.TextDecoration.underline, decorationThickness: 5.0);
});
test('text styles - font weight', () async {
await testTextStyle('font weight', fontWeight: ui.FontWeight.w900);
});
test('text styles - font style', () async {
await testTextStyle('font style', fontStyle: ui.FontStyle.italic);
});
// TODO(yjbanov): not sure how to test this.
test('text styles - baseline', () async {
await testTextStyle('baseline', textBaseline: ui.TextBaseline.ideographic);
});
test('text styles - font family', () async {
await testTextStyle('font family', fontFamily: 'Ahem');
});
test('text styles - non-existent font family', () async {
await testTextStyle('non-existent font family', fontFamily: 'DoesNotExist');
});
test('text styles - family fallback', () async {
await testTextStyle('family fallback', fontFamily: 'DoesNotExist', fontFamilyFallback: <String>['Ahem']);
});
test('text styles - font size', () async {
await testTextStyle('font size', fontSize: 24);
});
test('text styles - letter spacing', () async {
await testTextStyle('letter spacing', letterSpacing: 5);
});
test('text styles - word spacing', () async {
await testTextStyle('word spacing', innerText: 'Beautiful World!', wordSpacing: 25);
});
test('text styles - height', () async {
await testTextStyle('height', height: 2);
});
// TODO(yjbanov): locales specified in text styles don't work:
// https://github.com/flutter/flutter/issues/74687
// TODO(yjbanov): spaces are not rendered correctly:
// https://github.com/flutter/flutter/issues/74742
test('text styles - locale zh_CN', () async {
await testTextStyle('locale zh_CN', innerText: '次 化 刃 直 入 令', outerText: '', locale: const ui.Locale('zh', 'CN'));
});
test('text styles - locale zh_TW', () async {
await testTextStyle('locale zh_TW', innerText: '次 化 刃 直 入 令', outerText: '', locale: const ui.Locale('zh', 'TW'));
});
test('text styles - locale ja', () async {
await testTextStyle('locale ja', innerText: '次 化 刃 直 入 令', outerText: '', locale: const ui.Locale('ja'));
});
test('text styles - locale ko', () async {
await testTextStyle('locale ko', innerText: '次 化 刃 直 入 令', outerText: '', locale: const ui.Locale('ko'));
});
test('text styles - background', () async {
await testTextStyle('background', background: CkPaint()..color = const ui.Color(0xFF00FF00));
});
test('text styles - foreground', () async {
await testTextStyle('foreground', foreground: CkPaint()..color = const ui.Color(0xFF0000FF));
});
test('text styles - foreground and background', () async {
await testTextStyle(
'foreground and background',
foreground: CkPaint()..color = const ui.Color(0xFFFF5555),
background: CkPaint()..color = const ui.Color(0xFF007700),
);
});
test('text styles - background and color', () async {
await testTextStyle(
'background and color',
color: const ui.Color(0xFFFFFF00),
background: CkPaint()..color = const ui.Color(0xFF007700),
);
});
test('text styles - shadows', () async {
await testTextStyle('shadows', shadows: <ui.Shadow>[
ui.Shadow(
color: const ui.Color(0xFF999900),
offset: const ui.Offset(10, 10),
blurRadius: 5,
),
ui.Shadow(
color: const ui.Color(0xFF009999),
offset: const ui.Offset(-10, -10),
blurRadius: 10,
),
]);
});
test('text styles - old style figures', () async {
// TODO(yjbanov): we should not need to reset the fallbacks, see
// https://github.com/flutter/flutter/issues/74741
skiaFontCollection.debugResetFallbackFonts();
await testTextStyle(
'old style figures',
paragraphFontFamily: 'Roboto',
paragraphFontSize: 24,
outerText: '0 1 2 3 4 5 ',
innerText: '0 1 2 3 4 5',
fontFeatures: <ui.FontFeature>[const ui.FontFeature.oldstyleFigures()],
);
});
test('text styles - stylistic set 1', () async {
// TODO(yjbanov): we should not need to reset the fallbacks, see
// https://github.com/flutter/flutter/issues/74741
skiaFontCollection.debugResetFallbackFonts();
await testTextStyle(
'stylistic set 1',
paragraphFontFamily: 'Roboto',
paragraphFontSize: 24,
outerText: 'g',
innerText: 'g',
fontFeatures: <ui.FontFeature>[ui.FontFeature.stylisticSet(1)],
);
});
test('text styles - stylistic set 2', () async {
// TODO(yjbanov): we should not need to reset the fallbacks, see
// https://github.com/flutter/flutter/issues/74741
skiaFontCollection.debugResetFallbackFonts();
await testTextStyle(
'stylistic set 2',
paragraphFontFamily: 'Roboto',
paragraphFontSize: 24,
outerText: 'α',
innerText: 'α',
fontFeatures: <ui.FontFeature>[ui.FontFeature.stylisticSet(2)],
);
});
test('text styles - override font family', () async {
await testTextStyle(
'override font family',
paragraphFontFamily: 'Ahem',
fontFamily: 'Roboto',
);
});
test('text styles - override font size', () async {
await testTextStyle(
'override font size',
paragraphFontSize: 36,
fontSize: 18,
);
});
// TODO(yjbanov): paragraph fontWeight doesn't seem to work.
// https://github.com/flutter/flutter/issues/74338
test('text style - override font weight', () async {
await testTextStyle(
'override font weight',
paragraphFontWeight: ui.FontWeight.w900,
fontWeight: ui.FontWeight.normal,
);
});
// TODO(yjbanov): paragraph fontStyle doesn't seem to work.
// https://github.com/flutter/flutter/issues/74338
test('text style - override font style', () async {
await testTextStyle(
'override font style',
paragraphFontStyle: ui.FontStyle.italic,
fontStyle: ui.FontStyle.normal,
);
});
test('text style - foreground/background/color do not leak across paragraphs', () async {
const double testWidth = 440;
const double middle = testWidth / 2;
CkParagraph createTestParagraph({
ui.Color? color,
CkPaint? foreground,
CkPaint? background
}) {
final CkParagraphBuilder builder = CkParagraphBuilder(CkParagraphStyle());
builder.pushStyle(CkTextStyle(
fontSize: 16,
color: color,
foreground: foreground,
background: background,
));
final StringBuffer text = StringBuffer();
if (color == null && foreground == null && background == null) {
text.write('Default');
} else {
if (color != null) {
text.write('Color');
}
if (foreground != null) {
if (text.isNotEmpty) {
text.write('+');
}
text.write('Foreground');
}
if (background != null) {
if (text.isNotEmpty) {
text.write('+');
}
text.write('Background');
}
}
builder.addText(text.toString());
final CkParagraph paragraph = builder.build();
paragraph.layout(ui.ParagraphConstraints(width: testWidth));
return paragraph;
}
final List<ParagraphFactory> variations = <ParagraphFactory>[
() => createTestParagraph(),
() => createTestParagraph(color: ui.Color(0xFF009900)),
() => createTestParagraph(foreground: CkPaint()..color = ui.Color(0xFF990000)),
() => createTestParagraph(background: CkPaint()..color = ui.Color(0xFF7777FF)),
() => createTestParagraph(
color: ui.Color(0xFFFF00FF),
background: CkPaint()..color = ui.Color(0xFF0000FF),
),
() => createTestParagraph(
foreground: CkPaint()..color = ui.Color(0xFF00FFFF),
background: CkPaint()..color = ui.Color(0xFF0000FF),
),
];
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest);
canvas.translate(10, 10);
for (ParagraphFactory from in variations) {
for (ParagraphFactory to in variations) {
canvas.save();
final CkParagraph fromParagraph = from();
canvas.drawParagraph(fromParagraph, ui.Offset.zero);
final ui.Offset leftEnd = ui.Offset(fromParagraph.maxIntrinsicWidth + 10, fromParagraph.height / 2);
final ui.Offset rightEnd = ui.Offset(middle - 10, leftEnd.dy);
final ui.Offset tipOffset = ui.Offset(-5, -5);
canvas.drawLine(leftEnd, rightEnd, CkPaint());
canvas.drawLine(rightEnd, rightEnd + tipOffset, CkPaint());
canvas.drawLine(rightEnd, rightEnd + tipOffset.scale(1, -1), CkPaint());
canvas.translate(middle, 0);
canvas.drawParagraph(to(), ui.Offset.zero);
canvas.restore();
canvas.translate(0, 22);
}
}
final CkPicture picture = recorder.endRecording();
await matchPictureGolden(
'canvaskit_text_styles_do_not_leak.png',
picture,
region: ui.Rect.fromLTRB(0, 0, testWidth, 850),
);
});
// TODO: https://github.com/flutter/flutter/issues/60040
// TODO: https://github.com/flutter/flutter/issues/71520
}, skip: isIosSafari || isFirefox);
}
typedef ParagraphFactory = CkParagraph Function();
void drawTestPicture(CkCanvas canvas) {
canvas.clear(ui.Color(0xFFFFFFF));
canvas.translate(10, 10);
// Row 1
canvas.save();
canvas.save();
canvas.clipRect(
ui.Rect.fromLTRB(0, 0, 45, 45),
ui.ClipOp.intersect,
true,
);
canvas.clipRRect(
ui.RRect.fromLTRBR(5, 5, 50, 50, ui.Radius.circular(8)),
true,
);
canvas.clipPath(
CkPath()
..moveTo(5, 5)
..lineTo(25, 5)
..lineTo(45, 45)
..lineTo(5, 45)
..close(),
true,
);
canvas.drawColor(ui.Color.fromARGB(255, 100, 100, 0), ui.BlendMode.srcOver);
canvas.restore(); // remove clips
canvas.translate(60, 0);
canvas.drawCircle(
const ui.Offset(30, 25),
15,
CkPaint()..color = ui.Color(0xFF0000AA),
);
canvas.translate(60, 0);
canvas.drawArc(
ui.Rect.fromLTRB(10, 20, 50, 40),
math.pi / 4,
3 * math.pi / 2,
true,
CkPaint()..color = ui.Color(0xFF00AA00),
);
canvas.translate(60, 0);
canvas.drawImage(
generateTestImage(),
const ui.Offset(20, 20),
CkPaint(),
);
canvas.translate(60, 0);
final ui.RSTransform transform = ui.RSTransform.fromComponents(
rotation: 0,
scale: 1,
anchorX: 0,
anchorY: 0,
translateX: 0,
translateY: 0,
);
canvas.drawAtlasRaw(
CkPaint(),
generateTestImage(),
Float32List(4)
..[0] = transform.scos
..[1] = transform.ssin
..[2] = transform.tx + 20
..[3] = transform.ty + 20,
Float32List(4)
..[0] = 0
..[1] = 0
..[2] = 15
..[3] = 15,
Uint32List.fromList(<int>[0x00000000]),
ui.BlendMode.srcOver,
);
canvas.translate(60, 0);
canvas.drawDRRect(
ui.RRect.fromLTRBR(0, 0, 40, 30, ui.Radius.elliptical(16, 8)),
ui.RRect.fromLTRBR(10, 10, 30, 20, ui.Radius.elliptical(4, 8)),
CkPaint(),
);
canvas.translate(60, 0);
canvas.drawImageRect(
generateTestImage(),
ui.Rect.fromLTRB(0, 0, 15, 15),
ui.Rect.fromLTRB(10, 10, 40, 40),
CkPaint(),
);
canvas.translate(60, 0);
canvas.drawImageNine(
generateTestImage(),
ui.Rect.fromLTRB(5, 5, 15, 15),
ui.Rect.fromLTRB(10, 10, 50, 40),
CkPaint(),
);
canvas.restore();
// Row 2
canvas.translate(0, 60);
canvas.save();
canvas.drawLine(ui.Offset(0, 0), ui.Offset(40, 30), CkPaint());
canvas.translate(60, 0);
canvas.drawOval(
ui.Rect.fromLTRB(0, 0, 40, 30),
CkPaint(),
);
canvas.translate(60, 0);
canvas.save();
canvas.clipRect(ui.Rect.fromLTRB(0, 0, 50, 30), ui.ClipOp.intersect, true);
canvas.drawPaint(CkPaint()..color = ui.Color(0xFF6688AA));
canvas.restore();
canvas.translate(60, 0);
{
final CkPictureRecorder otherRecorder = CkPictureRecorder();
final CkCanvas otherCanvas =
otherRecorder.beginRecording(ui.Rect.fromLTRB(0, 0, 40, 20));
otherCanvas.drawCircle(
ui.Offset(30, 15),
10,
CkPaint()..color = ui.Color(0xFFAABBCC),
);
canvas.drawPicture(otherRecorder.endRecording());
}
canvas.translate(60, 0);
// TODO(yjbanov): CanvasKit.drawPoints is currently broken
// https://github.com/flutter/flutter/issues/71489
// But keeping this anyway as it's a good test-case that
// will ensure it's fixed when we have the fix.
canvas.drawPoints(
CkPaint()
..color = ui.Color(0xFF0000FF)
..strokeWidth = 5
..strokeCap = ui.StrokeCap.round,
ui.PointMode.polygon,
offsetListToFloat32List(<ui.Offset>[
ui.Offset(10, 10),
ui.Offset(20, 10),
ui.Offset(30, 20),
ui.Offset(40, 20)
]),
);
canvas.translate(60, 0);
canvas.drawRRect(
ui.RRect.fromLTRBR(0, 0, 40, 30, ui.Radius.circular(10)),
CkPaint(),
);
canvas.translate(60, 0);
canvas.drawRect(
ui.Rect.fromLTRB(0, 0, 40, 30),
CkPaint(),
);
canvas.translate(60, 0);
canvas.drawShadow(
CkPath()..addRect(ui.Rect.fromLTRB(0, 0, 40, 30)),
ui.Color(0xFF00FF00),
4,
true,
);
canvas.restore();
// Row 3
canvas.translate(0, 60);
canvas.save();
canvas.drawVertices(
CkVertices(
ui.VertexMode.triangleFan,
<ui.Offset>[
ui.Offset(10, 30),
ui.Offset(30, 50),
ui.Offset(10, 60),
],
),
ui.BlendMode.srcOver,
CkPaint(),
);
canvas.translate(60, 0);
final int restorePoint = canvas.save();
for (int i = 0; i < 5; i++) {
canvas.save();
canvas.translate(10, 10);
canvas.drawCircle(ui.Offset.zero, 5, CkPaint());
}
canvas.restoreToCount(restorePoint);
canvas.drawCircle(ui.Offset.zero, 7, CkPaint()..color = ui.Color(0xFFFF0000));
canvas.translate(60, 0);
canvas.drawLine(ui.Offset.zero, ui.Offset(30, 30), CkPaint());
canvas.save();
canvas.rotate(-math.pi / 8);
canvas.drawLine(ui.Offset.zero, ui.Offset(30, 30), CkPaint());
canvas.drawCircle(
ui.Offset(30, 30), 7, CkPaint()..color = ui.Color(0xFF00AA00));
canvas.restore();
canvas.translate(60, 0);
final CkPaint thickStroke = CkPaint()
..style = ui.PaintingStyle.stroke
..strokeWidth = 20;
final CkPaint semitransparent = CkPaint()..color = ui.Color(0x66000000);
canvas.saveLayer(kDefaultRegion, semitransparent);
canvas.drawLine(ui.Offset(10, 10), ui.Offset(50, 50), thickStroke);
canvas.drawLine(ui.Offset(50, 10), ui.Offset(10, 50), thickStroke);
canvas.restore();
canvas.translate(60, 0);
canvas.saveLayerWithoutBounds(semitransparent);
canvas.drawLine(ui.Offset(10, 10), ui.Offset(50, 50), thickStroke);
canvas.drawLine(ui.Offset(50, 10), ui.Offset(10, 50), thickStroke);
canvas.restore();
// To test saveLayerWithFilter we draw three circles with only the middle one
// blurred using the layer image filter.
canvas.translate(60, 0);
canvas.saveLayer(kDefaultRegion, CkPaint());
canvas.drawCircle(ui.Offset(30, 30), 10, CkPaint());
{
canvas.saveLayerWithFilter(
kDefaultRegion, ui.ImageFilter.blur(sigmaX: 5, sigmaY: 10));
canvas.drawCircle(ui.Offset(10, 10), 10, CkPaint());
canvas.drawCircle(ui.Offset(50, 50), 10, CkPaint());
canvas.restore();
}
canvas.restore();
canvas.translate(60, 0);
canvas.save();
canvas.translate(30, 30);
canvas.scale(2, 1.5);
canvas.drawCircle(ui.Offset.zero, 10, CkPaint());
canvas.restore();
canvas.translate(60, 0);
canvas.save();
canvas.translate(30, 30);
canvas.skew(2, 1.5);
canvas.drawRect(ui.Rect.fromLTRB(-10, -10, 10, 10), CkPaint());
canvas.restore();
canvas.restore();
// Row 4
canvas.translate(0, 60);
canvas.save();
canvas.save();
final Matrix4 matrix = Matrix4.identity();
matrix.translate(30, 30);
matrix.scale(2, 1.5);
canvas.transform(matrix.storage);
canvas.drawCircle(ui.Offset.zero, 10, CkPaint());
canvas.restore();
canvas.translate(60, 0);
final CkParagraphBuilder pb = CkParagraphBuilder(CkParagraphStyle(
fontFamily: 'Roboto',
fontStyle: ui.FontStyle.normal,
fontWeight: ui.FontWeight.normal,
fontSize: 18,
));
pb.pushStyle(CkTextStyle(
color: ui.Color(0xFF0000AA),
));
pb.addText('Hello');
pb.pop();
final CkParagraph p = pb.build();
p.layout(ui.ParagraphConstraints(width: 1000));
canvas.drawParagraph(
p,
ui.Offset(10, 20),
);
canvas.translate(60, 0);
canvas.drawPath(
CkPath()
..moveTo(30, 20)
..lineTo(50, 50)
..lineTo(10, 50)
..close(),
CkPaint()..color = ui.Color(0xFF0000AA),
);
canvas.restore();
}
CkImage generateTestImage() {
final html.CanvasElement canvas = html.CanvasElement()
..width = 20
..height = 20;
final html.CanvasRenderingContext2D ctx = canvas.context2D;
ctx.fillStyle = '#FF0000';
ctx.fillRect(0, 0, 10, 10);
ctx.fillStyle = '#00FF00';
ctx.fillRect(0, 10, 10, 10);
ctx.fillStyle = '#0000FF';
ctx.fillRect(10, 0, 10, 10);
ctx.fillStyle = '#FF00FF';
ctx.fillRect(10, 10, 10, 10);
final Uint8List imageData =
ctx.getImageData(0, 0, 20, 20).data.buffer.asUint8List();
final SkImage skImage = canvasKit.MakeImage(
SkImageInfo(
width: 20,
height: 20,
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
),
imageData,
4 * 20);
return CkImage(skImage);
}
/// A convenience function for testing paragraph and text styles.
///
/// Renders a paragraph with two pieces of text, [outerText] and [innerText].
/// [outerText] is added to the root of the paragraph where only paragraph
/// style applies. [innerText] is added under a text style with properties
/// set from the arguments to this method. Parameters with prefix "paragraph"
/// are applied to the paragraph style. Others are applied to the text style.
///
/// [name] is the name of the test used as the description on the golden as
/// well as in the golden file name. Avoid special characters. Spaces are OK;
/// they are replaced by "_" in the file name.
///
/// Set [write] to true to overwrite the golden file.
///
/// Use [layoutWidth] to customize the width of the paragraph constraints.
Future<void> testTextStyle(
// Test properties
String name, {
bool write = false,
double? layoutWidth,
// Top-level text where only paragraph style applies
String outerText = 'Hello ',
// Second-level text where paragraph and text styles both apply.
String innerText = 'World!',
// ParagraphStyle properties
ui.TextAlign? paragraphTextAlign,
ui.TextDirection? paragraphTextDirection,
int? paragraphMaxLines,
String? paragraphFontFamily,
double? paragraphFontSize,
double? paragraphHeight,
ui.TextHeightBehavior? paragraphTextHeightBehavior,
ui.FontWeight? paragraphFontWeight,
ui.FontStyle? paragraphFontStyle,
ui.StrutStyle? paragraphStrutStyle,
String? paragraphEllipsis,
ui.Locale? paragraphLocale,
// TextStyle properties
ui.Color? color,
ui.TextDecoration? decoration,
ui.Color? decorationColor,
ui.TextDecorationStyle? decorationStyle,
double? decorationThickness,
ui.FontWeight? fontWeight,
ui.FontStyle? fontStyle,
ui.TextBaseline? textBaseline,
String? fontFamily,
List<String>? fontFamilyFallback,
double? fontSize,
double? letterSpacing,
double? wordSpacing,
double? height,
ui.Locale? locale,
CkPaint? background,
CkPaint? foreground,
List<ui.Shadow>? shadows,
List<ui.FontFeature>? fontFeatures,
}) async {
late ui.Rect region;
CkPicture renderPicture() {
const double testWidth = 512;
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest);
canvas.translate(30, 10);
final CkParagraphBuilder descriptionBuilder = CkParagraphBuilder(CkParagraphStyle());
descriptionBuilder.addText(name);
final CkParagraph descriptionParagraph = descriptionBuilder.build();
descriptionParagraph.layout(ui.ParagraphConstraints(width: testWidth / 2 - 70));
final ui.Offset descriptionOffset = ui.Offset(testWidth / 2 + 30, 0);
canvas.drawParagraph(descriptionParagraph, descriptionOffset);
final CkParagraphBuilder pb = CkParagraphBuilder(CkParagraphStyle(
textAlign: paragraphTextAlign,
textDirection: paragraphTextDirection,
maxLines: paragraphMaxLines,
fontFamily: paragraphFontFamily,
fontSize: paragraphFontSize,
height: paragraphHeight,
textHeightBehavior: paragraphTextHeightBehavior,
fontWeight: ui.FontWeight.normal,
fontStyle: ui.FontStyle.normal,
strutStyle: paragraphStrutStyle,
ellipsis: paragraphEllipsis,
locale: paragraphLocale,
));
pb.addText(outerText);
pb.pushStyle(CkTextStyle(
color: color,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
fontWeight: fontWeight,
fontStyle: fontStyle,
textBaseline: textBaseline,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
fontSize: fontSize,
letterSpacing: letterSpacing,
wordSpacing: wordSpacing,
height: height,
locale: locale,
background: background,
foreground: foreground,
shadows: shadows,
fontFeatures: fontFeatures,
));
pb.addText(innerText);
pb.pop();
final CkParagraph p = pb.build();
p.layout(ui.ParagraphConstraints(width: layoutWidth ?? testWidth / 2));
canvas.drawParagraph(p, ui.Offset.zero);
canvas.drawPath(
CkPath()
..moveTo(-10, 0)
..lineTo(-20, 0)
..lineTo(-20, p.height)
..lineTo(-10, p.height),
CkPaint()
..style = ui.PaintingStyle.stroke
..strokeWidth = 1.0,
);
canvas.drawPath(
CkPath()
..moveTo(testWidth / 2 + 10, 0)
..lineTo(testWidth / 2 + 20, 0)
..lineTo(testWidth / 2 + 20, p.height)
..lineTo(testWidth / 2 + 10, p.height),
CkPaint()
..style = ui.PaintingStyle.stroke
..strokeWidth = 1.0,
);
const double padding = 20;
region = ui.Rect.fromLTRB(
0, 0, testWidth,
math.max(
descriptionOffset.dy + descriptionParagraph.height + padding,
p.height + padding,
),
);
return recorder.endRecording();
}
// Render once to trigger font downloads.
renderPicture();
// Wait for fonts to finish loading.
await notoDownloadQueue.downloader.debugWhenIdle();
// Render again for actual screenshotting.
final CkPicture picture = renderPicture();
await matchPictureGolden(
'canvaskit_text_styles_${name.replaceAll(' ', '_')}.png',
picture,
region: region,
write: write,
);
}