| // 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, |
| ); |
| } |