| // 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:async'; |
| import 'dart:convert'; |
| 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/fake_asset_manager.dart'; |
| import '../common/test_initialization.dart'; |
| import 'utils.dart'; |
| |
| const String kGlitchShaderSksl = r''' |
| { |
| "sksl": "// This SkSL shader is autogenerated by spirv-cross.\n\nfloat4 flutter_FragCoord;\n\nuniform vec2 uResolution;\nuniform float uTime;\nuniform shader uTex;\nuniform half2 uTex_size;\n\nvec4 oColor;\n\nvec2 FLT_flutter_local_FlutterFragCoord()\n{\n return flutter_FragCoord.xy;\n}\n\nfloat FLT_flutter_local_cubicPulse(float c, float w, inout float x)\n{\n x = abs(x - c);\n if (x > w)\n {\n return 0.0;\n }\n x /= w;\n return 1.0 - ((x * x) * (3.0 - (2.0 * x)));\n}\n\nfloat FLT_flutter_local_twoSin(inout float x)\n{\n x = (6.4899997711181640625 * x) - 0.64999997615814208984375;\n float t = ((-0.699999988079071044921875) * sin(6.80000019073486328125 * x)) + (1.39999997615814208984375 * sin(2.900000095367431640625 * x));\n t = (t / 4.099999904632568359375) + 0.5;\n return t;\n}\n\nfloat FLT_flutter_local_hash_1d(float v)\n{\n float u = 50.0 * sin(v * 3000.0);\n return (2.0 * fract((2.0 * u) * u)) - 1.0;\n}\n\nvoid FLT_main()\n{\n vec2 uv = vec2(FLT_flutter_local_FlutterFragCoord()) / uResolution;\n float param = 0.5;\n float param_1 = 0.0500000007450580596923828125;\n float param_2 = fract(uTime / 4.0);\n float _127 = FLT_flutter_local_cubicPulse(param, param_1, param_2);\n float t_2 = _127;\n float param_3 = fract(uTime / 5.0);\n float _134 = FLT_flutter_local_twoSin(param_3);\n float t_1 = _134;\n float glitchScale = mix(0.0, 8.0, t_1 + t_2);\n float aberrationSize = mix(0.0, 5.0, t_1 + t_2);\n float param_4 = uv.y;\n float h = FLT_flutter_local_hash_1d(param_4);\n float hs = sign(h);\n h = max(h, 0.0);\n h *= h;\n h = floor(h + float(0.5)) * hs;\n uv += (vec2(h * glitchScale, 0.0) / uResolution);\n vec2 redOffset = vec2(aberrationSize, 0.0) / uResolution;\n vec2 greenOffset = vec2(0.0) / uResolution;\n vec2 blueOffset = vec2(-aberrationSize, 0.0) / uResolution;\n vec2 redUv = uv + redOffset;\n vec2 greenUv = uv + greenOffset;\n vec2 blueUv = uv + blueOffset;\n vec2 ra = uTex.eval(uTex_size * redUv).xw;\n vec2 ga = uTex.eval(uTex_size * greenUv).yw;\n vec2 ba = uTex.eval(uTex_size * blueUv).zw;\n ra.x /= ra.y;\n ga.x /= ga.y;\n ba.x /= ba.y;\n float alpha = max(ra.y, max(ga.y, ba.y));\n oColor = vec4(ra.x, ga.x, ba.x, 1.0) * alpha;\n}\n\nhalf4 main(float2 iFragCoord)\n{\n flutter_FragCoord = float4(iFragCoord, 0, 0);\n FLT_main();\n return oColor;\n}\n", |
| "stage": 1, |
| "target_platform": 2, |
| "uniforms": [ |
| { |
| "array_elements": 0, |
| "bit_width": 32, |
| "columns": 1, |
| "location": 0, |
| "name": "uResolution", |
| "rows": 2, |
| "type": 10 |
| }, |
| { |
| "array_elements": 0, |
| "bit_width": 32, |
| "columns": 1, |
| "location": 1, |
| "name": "uTime", |
| "rows": 1, |
| "type": 10 |
| }, |
| { |
| "array_elements": 0, |
| "bit_width": 0, |
| "columns": 1, |
| "location": 2, |
| "name": "uTex", |
| "rows": 1, |
| "type": 12 |
| } |
| ] |
| } |
| '''; |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| Future<void> testMain() async { |
| setUpUnitTests( |
| setUpTestViewDimensions: false, |
| ); |
| |
| late FakeAssetScope assetScope; |
| setUp(() { |
| assetScope = fakeAssetManager.pushAssetScope(); |
| assetScope.setAsset( |
| 'glitch_shader', |
| ByteData.sublistView(utf8.encode(kGlitchShaderSksl)) |
| ); |
| }); |
| |
| tearDown(() { |
| fakeAssetManager.popAssetScope(assetScope); |
| }); |
| |
| const ui.Rect drawRegion = ui.Rect.fromLTWH(0, 0, 300, 300); |
| const ui.Rect imageRegion = ui.Rect.fromLTWH(0, 0, 150, 150); |
| |
| // Emits a set of rendering tests for an image |
| // `imageGenerator` should produce an image that is 150x150 pixels. |
| void emitImageTests(String name, Future<ui.Image> Function() imageGenerator) { |
| group(name, () { |
| late ui.Image image; |
| setUp(() async { |
| image = await imageGenerator(); |
| }); |
| |
| tearDown(() { |
| image.dispose(); |
| }); |
| |
| test('drawImage', () async { |
| final ui.Image image = await imageGenerator(); |
| |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); |
| canvas.drawImage(image, const ui.Offset(100, 100), ui.Paint()); |
| |
| await drawPictureUsingCurrentRenderer(recorder.endRecording()); |
| |
| await matchGoldenFile('${name}_canvas_drawImage.png', region: drawRegion); |
| }); |
| |
| test('drawImageRect', () async { |
| final ui.Image image = await imageGenerator(); |
| |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); |
| canvas.drawImageRect( |
| image, |
| const ui.Rect.fromLTRB(50, 50, 100, 100), |
| const ui.Rect.fromLTRB(100, 100, 150, 150), |
| ui.Paint() |
| ); |
| |
| await drawPictureUsingCurrentRenderer(recorder.endRecording()); |
| |
| await matchGoldenFile('${name}_canvas_drawImageRect.png', region: drawRegion); |
| }); |
| |
| test('drawImageNine', () async { |
| final ui.Image image = await imageGenerator(); |
| |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); |
| canvas.drawImageNine( |
| image, |
| const ui.Rect.fromLTRB(50, 50, 100, 100), |
| drawRegion, |
| ui.Paint() |
| ); |
| |
| await drawPictureUsingCurrentRenderer(recorder.endRecording()); |
| |
| await matchGoldenFile('${name}_canvas_drawImageNine.png', region: drawRegion); |
| }); |
| |
| test('image_shader_cubic_rotated', () async { |
| final ui.Image image = await imageGenerator(); |
| |
| final Float64List matrix = Matrix4.rotationZ(pi / 6).toFloat64(); |
| final ui.ImageShader shader = ui.ImageShader( |
| image, |
| ui.TileMode.repeated, |
| ui.TileMode.repeated, |
| matrix, |
| filterQuality: ui.FilterQuality.high, |
| ); |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); |
| canvas.drawOval( |
| const ui.Rect.fromLTRB(0, 50, 300, 250), |
| ui.Paint()..shader = shader |
| ); |
| |
| await drawPictureUsingCurrentRenderer(recorder.endRecording()); |
| await matchGoldenFile('${name}_image_shader_cubic_rotated.png', region: drawRegion); |
| }); |
| |
| test('fragment_shader_sampler', () async { |
| final ui.Image image = await imageGenerator(); |
| |
| final ui.FragmentProgram program = await renderer.createFragmentProgram('glitch_shader'); |
| final ui.FragmentShader shader = program.fragmentShader(); |
| |
| // Resolution |
| shader.setFloat(0, 300); |
| shader.setFloat(1, 300); |
| |
| // Time |
| shader.setFloat(2, 2); |
| |
| // Image |
| shader.setImageSampler(0, image); |
| |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); |
| canvas.drawCircle(const ui.Offset(150, 150), 100, ui.Paint()..shader = shader); |
| |
| await drawPictureUsingCurrentRenderer(recorder.endRecording()); |
| |
| await matchGoldenFile('${name}_fragment_shader_sampler.png', region: drawRegion); |
| }, skip: isHtml); // HTML doesn't support fragment shaders |
| |
| test('toByteData_rgba', () async { |
| final ui.Image image = await imageGenerator(); |
| |
| final ByteData? rgbaData = await image.toByteData(); |
| expect(rgbaData, isNotNull); |
| expect(rgbaData!.lengthInBytes, isNonZero); |
| }); |
| |
| test('toByteData_png', () async { |
| final ui.Image image = await imageGenerator(); |
| |
| final ByteData? pngData = await image.toByteData(format: ui.ImageByteFormat.png); |
| expect(pngData, isNotNull); |
| expect(pngData!.lengthInBytes, isNonZero); |
| }, skip: isHtml); // https://github.com/flutter/flutter/issues/126611 |
| }); |
| } |
| |
| emitImageTests('picture_toImage', () { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, imageRegion); |
| for (int y = 0; y < 15; y++) { |
| for (int x = 0; x < 15; x++) { |
| final ui.Offset center = ui.Offset(x * 10 + 5, y * 10 + 5); |
| final ui.Color color = ui.Color.fromRGBO( |
| (center.dx * 256 / 150).round(), |
| (center.dy * 256 / 150).round(), 0, 1); |
| canvas.drawCircle(center, 5, ui.Paint()..color = color); |
| } |
| } |
| return recorder.endRecording().toImage(150, 150); |
| }); |
| |
| Uint8List generatePixelData( |
| int width, |
| int height, |
| ui.Color Function(double, double) generator |
| ) { |
| final Uint8List data = Uint8List(width * height * 4); |
| int outputIndex = 0; |
| for (int y = 0; y < height; y++) { |
| for (int x = 0; x < width; x++) { |
| final ui.Color pixelColor = generator( |
| (2.0 * x / width) - 1.0, |
| (2.0 * y / height) - 1.0, |
| ); |
| data[outputIndex++] = pixelColor.red; |
| data[outputIndex++] = pixelColor.green; |
| data[outputIndex++] = pixelColor.blue; |
| data[outputIndex++] = pixelColor.alpha; |
| } |
| } |
| return data; |
| } |
| |
| emitImageTests('decodeImageFromPixels_unscaled', () { |
| final Uint8List pixels = generatePixelData(150, 150, (double x, double y) { |
| final double r = sqrt(x * x + y * y); |
| final double theta = atan2(x, y); |
| return ui.Color.fromRGBO( |
| (255 * (sin(r * 10.0) + 1.0) / 2.0).round(), |
| (255 * (sin(theta * 10.0) + 1.0) / 2.0).round(), |
| 0, |
| 1, |
| ); |
| }); |
| final Completer<ui.Image> completer = Completer<ui.Image>(); |
| ui.decodeImageFromPixels(pixels, 150, 150, ui.PixelFormat.rgba8888, completer.complete); |
| return completer.future; |
| }); |
| |
| // https://github.com/flutter/flutter/issues/126603 |
| if (!isHtml) { |
| emitImageTests('decodeImageFromPixels_scaled', () { |
| final Uint8List pixels = generatePixelData(50, 50, (double x, double y) { |
| final double r = sqrt(x * x + y * y); |
| final double theta = atan2(x, y); |
| return ui.Color.fromRGBO( |
| (255 * (sin(r * 10.0) + 1.0) / 2.0).round(), |
| (255 * (sin(theta * 10.0) + 1.0) / 2.0).round(), |
| 0, |
| 1, |
| ); |
| }); |
| final Completer<ui.Image> completer = Completer<ui.Image>(); |
| ui.decodeImageFromPixels( |
| pixels, |
| 50, |
| 50, |
| ui.PixelFormat.rgba8888, |
| completer.complete, |
| targetWidth: 150, |
| targetHeight: 150, |
| ); |
| return completer.future; |
| }); |
| } |
| |
| emitImageTests('codec_uri', () async { |
| final ui.Codec codec = await renderer.instantiateImageCodecFromUrl( |
| Uri(path: '/test_images/mandrill_128.png') |
| ); |
| expect(codec.frameCount, 1); |
| |
| final ui.FrameInfo info = await codec.getNextFrame(); |
| return info.image; |
| }); |
| |
| // This API doesn't work in headless Firefox due to requiring WebGL |
| // See https://github.com/flutter/flutter/issues/109265 |
| if (!isFirefox) { |
| emitImageTests('svg_image_bitmap', () async { |
| final DomBlob svgBlob = createDomBlob(<String>[ |
| ''' |
| <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150"> |
| <path d="M25,75 A50,50 0 1,0 125 75 L75,25 Z" stroke="blue" stroke-width="10" fill="red"></path> |
| </svg> |
| ''' |
| ], <String, String>{'type': 'image/svg+xml'}); |
| final String url = domWindow.URL.createObjectURL(svgBlob); |
| final DomHTMLImageElement image = createDomHTMLImageElement(); |
| final Completer<void> completer = Completer<void>(); |
| late final DomEventListener loadListener; |
| loadListener = createDomEventListener((DomEvent event) { |
| completer.complete(); |
| image.removeEventListener('load', loadListener); |
| }); |
| image.addEventListener('load', loadListener); |
| image.src = url; |
| await completer.future; |
| |
| final DomImageBitmap bitmap = (await createImageBitmap(image).toDart)! as DomImageBitmap; |
| |
| expect(bitmap.width.toDartInt, 150); |
| expect(bitmap.height.toDartInt, 150); |
| final ui.Image uiImage = await renderer.createImageFromImageBitmap(bitmap); |
| |
| if (isSkwasm) { |
| // Skwasm transfers the bitmap to the web worker, so it should be disposed/consumed. |
| expect(bitmap.width.toDartInt, 0); |
| expect(bitmap.height.toDartInt, 0); |
| } |
| return uiImage; |
| }); |
| } |
| |
| emitImageTests('codec_list_resized', () async { |
| final ByteBuffer data = await httpFetchByteBuffer('/test_images/mandrill_128.png'); |
| final ui.Codec codec = await renderer.instantiateImageCodec( |
| data.asUint8List(), |
| targetWidth: 150, |
| targetHeight: 150, |
| ); |
| expect(codec.frameCount, 1); |
| |
| final ui.FrameInfo info = await codec.getNextFrame(); |
| return info.image; |
| }); |
| } |