├── .github └── workflows │ ├── example.yml │ └── lib.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── README.md ├── lib │ ├── main.dart │ └── sampler_example.dart ├── pubspec.yaml ├── shaders │ ├── blank.frag │ ├── inkwell.frag │ ├── sampler.frag │ └── solid_color.frag └── test │ ├── animated_sampler_test.dart │ ├── goldens │ └── shaders.inkwell.png │ ├── inkwell_test.dart │ └── shader_builder_test.dart ├── lib ├── flutter_shaders.dart ├── shaders │ └── pixelation.frag └── src │ ├── animated_sampler.dart │ ├── inkwell_shader.dart │ ├── set_uniforms.dart │ └── shader_builder.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── animated_sampler_test.dart └── set_uniforms_test.dart /.github/workflows/example.yml: -------------------------------------------------------------------------------- 1 | name: analyze and test example 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**/*.md' 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: example/ 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: subosito/flutter-action@v2 20 | with: 21 | channel: 'master' 22 | - run: flutter --version 23 | - run: flutter pub get 24 | - run: dart format --set-exit-if-changed . 25 | - run: flutter analyze . 26 | - run: flutter test --coverage -------------------------------------------------------------------------------- /.github/workflows/lib.yml: -------------------------------------------------------------------------------- 1 | name: analyze and test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**/*.md' 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: subosito/flutter-action@v2 17 | with: 18 | channel: 'master' 19 | - run: flutter --version 20 | - run: flutter pub get 21 | - run: dart format --set-exit-if-changed . 22 | - run: flutter analyze . 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # Flutter/Dart/Pub related 13 | **/doc/api/ 14 | .dart_tool/ 15 | .flutter-plugins 16 | .packages 17 | .pubspec.lock 18 | .pub-cache/ 19 | .pub/ 20 | /build/ 21 | .idea/ 22 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Flutter project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | The Chromium Authors 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.3 2 | 3 | * Remove unecessary location decorations for pixelation shader. 4 | 5 | ## 0.1.2 6 | 7 | * Ensure constructed ui.Picture objects are disposed after usage. 8 | * Add [SetUniforms] to help constructing FragmentShader uniform data. 9 | 10 | ## 0.1.1 11 | 12 | * Fix `AnimatedSampler` offset assertion to no longer trigger incorrectly when shader is disabled. 13 | * Fix runtime exception when the `AnimatedSampler` was asked to construct an image with no size. 14 | 15 | ## 0.1.0 16 | 17 | * Update URLs in pubspec.yaml. 18 | 19 | ## 0.0.6 20 | 21 | * Fix bug in `AnimatedSampler` that caused children with non-zero offsets to 22 | be rendered offscreen. 23 | * Removed `offset` parameter from `AnimatedSamplerBuilder`. The offset will 24 | always be `Offset.zero` now and this is no longer necessary. 25 | 26 | 27 | ## 0.0.5 28 | 29 | * Remove restriction on `AnimatedSampler` child repainting. 30 | 31 | ## 0.0.4 32 | 33 | * Added `ShaderInkFeatureFactory` and `ShaderInkFeature` to allow configuration of a 34 | material inkwell splash with a developer authored fragment shader and configuration 35 | callback 36 | 37 | ## 0.0.3 38 | 39 | * Updated documentation on `AnimatedSampler` to account for `FragmentShader` breaking 40 | API changes. 41 | 42 | ## 0.0.2 43 | 44 | * Add pixelation shader. 45 | 46 | ## 0.0.1 47 | 48 | * First published version 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 The Flutter Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_shaders 2 | 3 | A collection of utilities to make working with the FragmentProgram API easier. 4 | 5 | 6 | ## Available Shaders 7 | 8 | This package includes a number of shaders that can be optionally included into your 9 | application by declaring them in the pubspec.yaml: 10 | 11 | ### Pixelation 12 | 13 | The pixelation shader reduces the provided sampler to MxN samples. This can be used with the [AnimatedSampler] widget. The required uniforms are: 14 | 15 | Floats: 16 | * The number of pixels in the X coordinate space. 17 | * The number of pixels in the Y coordinate space. 18 | * The width of the sampled area. 19 | * The height of the sampled area. 20 | Samplers: 21 | * The child widget, captured as a texture. 22 | 23 | To include this shader in your application, add the following line to 24 | your pubspec.yaml 25 | 26 | ```yaml 27 | flutter: 28 | shaders: 29 | - packages/flutter_shaders/shaders/pixelation.frag 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | pubspec.lock 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | # Platform directories 48 | android/ 49 | ios/ 50 | windows/ 51 | web/ 52 | macos/ 53 | linux/ 54 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A project for testing shaders with flutter_shaders 4 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:ui' as ui; 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_shaders/flutter_shaders.dart'; 9 | 10 | void main() async { 11 | final ui.FragmentProgram program = 12 | await ui.FragmentProgram.fromAsset('shaders/inkwell.frag'); 13 | runApp(MyApp(program: program)); 14 | } 15 | 16 | class MyApp extends StatelessWidget { 17 | const MyApp({super.key, required this.program}); 18 | 19 | final ui.FragmentProgram program; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return MaterialApp( 24 | title: 'Flutter Demo', 25 | theme: ThemeData( 26 | primarySwatch: Colors.blue, 27 | splashFactory: ShaderInkFeatureFactory(program, ( 28 | shader, { 29 | required double animation, 30 | required Color color, 31 | required Offset position, 32 | required Size referenceBoxSize, 33 | required double targetRadius, 34 | required TextDirection textDirection, 35 | }) { 36 | shader.setFloatUniforms((uniforms) => uniforms 37 | ..setFloat(animation) 38 | ..setColor(color, premultiply: true) 39 | ..setFloat(targetRadius) 40 | ..setOffset(position)); 41 | })), 42 | home: const MyHomePage(title: 'Flutter Demo Home Page'), 43 | ); 44 | } 45 | } 46 | 47 | class MyHomePage extends StatefulWidget { 48 | const MyHomePage({super.key, required this.title}); 49 | 50 | final String title; 51 | 52 | @override 53 | State createState() => _MyHomePageState(); 54 | } 55 | 56 | class _MyHomePageState extends State { 57 | int _counter = 0; 58 | 59 | void _incrementCounter() { 60 | setState(() { 61 | _counter++; 62 | }); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return Scaffold( 68 | appBar: AppBar( 69 | title: Text(widget.title), 70 | ), 71 | body: Center( 72 | child: Column( 73 | mainAxisAlignment: MainAxisAlignment.center, 74 | children: [ 75 | const Text( 76 | 'You have pushed the button this many times:', 77 | ), 78 | Text( 79 | '$_counter', 80 | style: Theme.of(context).textTheme.headlineMedium, 81 | ), 82 | ], 83 | ), 84 | ), 85 | floatingActionButton: FloatingActionButton( 86 | onPressed: _incrementCounter, 87 | tooltip: 'Increment', 88 | child: const Icon(Icons.add), 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example/lib/sampler_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_shaders/flutter_shaders.dart'; 3 | 4 | void main() { 5 | runApp(const ExampleApp()); 6 | } 7 | 8 | class ExampleApp extends StatefulWidget { 9 | const ExampleApp({super.key}); 10 | 11 | @override 12 | State createState() => _ExampleAppState(); 13 | } 14 | 15 | class _ExampleAppState extends State { 16 | double _value = 2.0; 17 | 18 | void _onChanged(double newValue) { 19 | setState(() { 20 | _value = newValue; 21 | }); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return MaterialApp( 27 | home: Scaffold( 28 | appBar: AppBar(title: Text('Shaders!')), 29 | body: Center( 30 | child: Column( 31 | mainAxisSize: MainAxisSize.min, 32 | children: [ 33 | SampledText(text: 'This is some sampled text', value: _value), 34 | Slider(value: _value, onChanged: _onChanged, min: 2, max: 50), 35 | ], 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | 43 | class SampledText extends StatelessWidget { 44 | const SampledText({super.key, required this.text, required this.value}); 45 | 46 | final String text; 47 | final double value; 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return ShaderBuilder((context, shader, child) { 52 | return AnimatedSampler((image, size, canvas) { 53 | shader.setFloatUniforms((uniforms) { 54 | uniforms 55 | ..setFloat(value) 56 | ..setFloat(value) 57 | ..setSize(size); 58 | }); 59 | 60 | shader.setImageSampler(0, image); 61 | 62 | canvas.drawRect( 63 | Rect.fromLTWH(0, 0, size.width, size.height), 64 | Paint()..shader = shader, 65 | ); 66 | }, child: Text(text, style: TextStyle(fontSize: 20))); 67 | }, assetKey: 'packages/flutter_shaders/shaders/pixelation.frag'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=2.19.0-0.dev <3.0.0' 6 | flutter: ">=3.7.0-0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | dev_dependencies: 13 | flutter_shaders: 14 | path: ../ 15 | flutter_test: 16 | sdk: flutter 17 | 18 | flutter: 19 | uses-material-design: true 20 | shaders: 21 | - shaders/inkwell.frag 22 | - shaders/sampler.frag 23 | - shaders/solid_color.frag 24 | - shaders/blank.frag 25 | - packages/flutter_shaders/shaders/pixelation.frag 26 | -------------------------------------------------------------------------------- /example/shaders/blank.frag: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | #version 460 core 5 | 6 | precision mediump float; 7 | 8 | out vec4 fragColor; 9 | 10 | void main() { 11 | return; 12 | } 13 | -------------------------------------------------------------------------------- /example/shaders/inkwell.frag: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | #version 460 core 5 | 6 | precision mediump float; 7 | 8 | #include 9 | 10 | uniform float uAnimation; 11 | uniform vec4 uColor; 12 | uniform float uRadius; 13 | uniform vec2 uCenter; 14 | 15 | out vec4 fragColor; 16 | 17 | void main() { 18 | float scale = distance(FlutterFragCoord(), uCenter) / uRadius; 19 | fragColor = mix(vec4(1.0), uColor, scale) * (1.0 - uAnimation); 20 | } 21 | -------------------------------------------------------------------------------- /example/shaders/sampler.frag: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | #version 460 core 5 | 6 | precision mediump float; 7 | 8 | #include 9 | 10 | uniform vec2 uSize; 11 | uniform sampler2D uTexture; 12 | 13 | out vec4 fragColor; 14 | 15 | void main() { 16 | fragColor = texture(uTexture, FlutterFragCoord().xy / uSize); 17 | } 18 | -------------------------------------------------------------------------------- /example/shaders/solid_color.frag: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | #version 460 core 5 | 6 | precision mediump float; 7 | 8 | uniform vec4 uColor; 9 | 10 | out vec4 fragColor; 11 | 12 | void main() { 13 | fragColor = uColor; 14 | } 15 | -------------------------------------------------------------------------------- /example/test/animated_sampler_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:typed_data'; 6 | import 'dart:ui' as ui; 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/rendering.dart'; 10 | import 'package:flutter_shaders/flutter_shaders.dart'; 11 | import 'package:flutter_test/flutter_test.dart'; 12 | 13 | void main() { 14 | setUpAll(() async { 15 | await ShaderBuilder.precacheShader('shaders/sampler.frag'); 16 | }); 17 | 18 | testWidgets('AnimatedSampler captures child widgets in texture', 19 | (WidgetTester tester) async { 20 | final GlobalKey globalKey = GlobalKey(); 21 | bool usedShader = false; 22 | await tester.pumpWidget(MaterialApp( 23 | home: RepaintBoundary( 24 | key: globalKey, 25 | child: ShaderBuilder(assetKey: 'shaders/sampler.frag', 26 | (BuildContext context, FragmentShader shader, Widget? child) { 27 | return AnimatedSampler((ui.Image image, Size size, Canvas canvas) { 28 | usedShader = true; 29 | shader.setFloat(0, size.width); 30 | shader.setFloat(1, size.height); 31 | shader.setImageSampler(0, image); 32 | 33 | canvas.drawRect(Offset.zero & size, Paint()..shader = shader); 34 | }, child: Container(color: Colors.red)); 35 | }), 36 | ), 37 | )); 38 | 39 | expect(usedShader, true); 40 | 41 | ByteData? snapshot; 42 | await tester.runAsync(() async { 43 | snapshot = await (await (globalKey.currentContext?.findRenderObject() 44 | as RenderRepaintBoundary?)! 45 | .toImage()) 46 | .toByteData(format: ui.ImageByteFormat.rawStraightRgba); 47 | }); 48 | 49 | // Validate that color is Colors.red from child widget. 50 | expect(_readColorFromBuffer(snapshot!, 0), Colors.red.shade500); 51 | }); 52 | } 53 | 54 | Color _readColorFromBuffer(ByteData data, int offset) { 55 | final int r = data.getUint8(offset); 56 | final int g = data.getUint8(offset + 1); 57 | final int b = data.getUint8(offset + 2); 58 | final int a = data.getUint8(offset + 3); 59 | return Color.fromARGB(a, r, g, b); 60 | } 61 | -------------------------------------------------------------------------------- /example/test/goldens/shaders.inkwell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonahwilliams/flutter_shaders/9d99a2ff207a3ce3552a6bffd724402bd3a3e05f/example/test/goldens/shaders.inkwell.png -------------------------------------------------------------------------------- /example/test/inkwell_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:io' as io; 6 | import 'dart:ui' as ui; 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:example/main.dart' as example; 11 | 12 | void main() { 13 | testWidgets('Can apply inkwell shader effects', (WidgetTester tester) async { 14 | final ui.FragmentProgram program = 15 | await ui.FragmentProgram.fromAsset('shaders/inkwell.frag'); 16 | await tester.pumpWidget(example.MyApp(program: program)); 17 | 18 | await tester.tap(find.byIcon(Icons.add)); 19 | await tester.pump(); 20 | await tester.pump(Duration(milliseconds: 100)); 21 | 22 | // Validate that color is Colors.red from child widget. 23 | await expectLater(find.byIcon(Icons.add), 24 | matchesGoldenFile('goldens/shaders.inkwell.png')); 25 | }, skip: !io.Platform.isWindows); 26 | } 27 | -------------------------------------------------------------------------------- /example/test/shader_builder_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:flutter_shaders/flutter_shaders.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | void main() { 10 | testWidgets('Can cache fragment shaders', (WidgetTester tester) async { 11 | bool shaderLoaded = false; 12 | final Widget child = ShaderBuilder( 13 | (BuildContext context, FragmentShader shader, Widget? child) { 14 | shaderLoaded = true; 15 | return child ?? const SizedBox(); 16 | }, assetKey: 'shaders/solid_color.frag'); 17 | 18 | await tester.pumpWidget(child); 19 | 20 | // Shader isn't cached yet. 21 | expect(shaderLoaded, isFalse); 22 | 23 | await tester.pumpWidget(child); 24 | await tester.pumpWidget(child); 25 | 26 | expect(shaderLoaded, isTrue); 27 | 28 | // Shader is still cached with a new widget. 29 | bool sameShaderLoaded = false; 30 | await tester.pumpWidget(ShaderBuilder( 31 | (BuildContext context, FragmentShader shader, Widget? child) { 32 | sameShaderLoaded = true; 33 | return child ?? const SizedBox(); 34 | }, assetKey: 'shaders/solid_color.frag')); 35 | 36 | expect(sameShaderLoaded, true); 37 | }); 38 | 39 | testWidgets( 40 | 'ShaderBuilder.precacheShader reports flutter error if invalid asset is provided', 41 | (WidgetTester tester) async { 42 | await ShaderBuilder.precacheShader('shaders/bogus.frag'); 43 | 44 | expect(tester.takeException(), isNotNull); 45 | }); 46 | 47 | testWidgets( 48 | 'ShaderBuilder.precacheShader makes shader available ' 49 | 'synchronously when future completes', (WidgetTester tester) async { 50 | await ShaderBuilder.precacheShader('shaders/sampler.frag'); 51 | 52 | bool shaderLoaded = false; 53 | await tester.pumpWidget(ShaderBuilder( 54 | (BuildContext context, FragmentShader shader, Widget? child) { 55 | shaderLoaded = true; 56 | return child ?? const SizedBox(); 57 | }, assetKey: 'shaders/sampler.frag')); 58 | 59 | expect(shaderLoaded, true); 60 | }); 61 | 62 | testWidgets( 63 | 'ShaderBuilder.precacheShader reports flutter error if invalid asset is provided', 64 | (WidgetTester tester) async { 65 | await ShaderBuilder.precacheShader('shaders/bogus.frag'); 66 | 67 | expect(tester.takeException(), isNotNull); 68 | }); 69 | 70 | testWidgets( 71 | 'ShaderBuilder reports flutter error if invalid asset is provided', 72 | (WidgetTester tester) async { 73 | await tester.pumpWidget(ShaderBuilder( 74 | (BuildContext context, FragmentShader shader, Widget? child) { 75 | return child ?? const SizedBox(); 76 | }, assetKey: 'shaders/bogus.frag')); 77 | 78 | expect(tester.takeException(), isNotNull); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /lib/flutter_shaders.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | export 'dart:ui' show FragmentShader; 6 | 7 | export 'src/animated_sampler.dart'; 8 | export 'src/shader_builder.dart'; 9 | export 'src/inkwell_shader.dart'; 10 | export 'src/set_uniforms.dart'; 11 | -------------------------------------------------------------------------------- /lib/shaders/pixelation.frag: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | #version 460 core 5 | 6 | precision highp float; 7 | 8 | #include 9 | 10 | uniform vec2 uPixels; 11 | uniform vec2 uSize; 12 | uniform sampler2D uTexture; 13 | 14 | out vec4 fragColor; 15 | 16 | void main() { 17 | vec2 uv = FlutterFragCoord().xy / uSize; 18 | vec2 puv = round(uv * uPixels) / uPixels; 19 | fragColor = texture(uTexture, puv); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/animated_sampler.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:ui' as ui; 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/rendering.dart'; 9 | 10 | /// A callback for the [AnimatedSamplerBuilder] widget. 11 | typedef AnimatedSamplerBuilder = void Function( 12 | ui.Image image, 13 | Size size, 14 | ui.Canvas canvas, 15 | ); 16 | 17 | /// A widget that allows access to a snapshot of the child widgets for painting 18 | /// with a sampler applied to a [FragmentProgram]. 19 | /// 20 | /// When [enabled] is true, the child widgets will be painted into a texture 21 | /// exposed as a [ui.Image]. This can then be passed to a [FragmentShader] 22 | /// instance via [FragmentShader.setSampler]. 23 | /// 24 | /// If [enabled] is false, then the child widgets are painted as normal. 25 | /// 26 | /// Caveats: 27 | /// * Platform views cannot be captured in a texture. If any are present they 28 | /// will be excluded from the texture. Texture-based platform views are OK. 29 | /// 30 | /// Example: 31 | /// 32 | /// Providing an image to a fragment shader using 33 | /// [FragmentShader.setImageSampler]. 34 | /// 35 | /// ```dart 36 | /// Widget build(BuildContext context) { 37 | /// return AnimatedSampler( 38 | /// (ui.Image image, Size size, Canvas canvas) { 39 | /// shader 40 | /// ..setFloat(0, size.width) 41 | /// ..setFloat(1, size.height) 42 | /// ..setImageSampler(0, image); 43 | /// canvas.drawRect(Offset.zero & size, Paint()..shader = shader); 44 | /// }, 45 | /// child: widget.child, 46 | /// ); 47 | /// } 48 | /// ``` 49 | /// 50 | /// See also: 51 | /// * [SnapshotWidget], which provides a similar API for the purpose of 52 | /// caching during expensive animations. 53 | class AnimatedSampler extends StatelessWidget { 54 | /// Create a new [AnimatedSampler]. 55 | const AnimatedSampler( 56 | this.builder, { 57 | required this.child, 58 | super.key, 59 | this.enabled = true, 60 | }); 61 | 62 | /// A callback used by this widget to provide the children captured in 63 | /// a texture. 64 | final AnimatedSamplerBuilder builder; 65 | 66 | /// Whether the children should be captured in a texture or displayed as 67 | /// normal. 68 | final bool enabled; 69 | 70 | /// The child widget. 71 | final Widget child; 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | return _ShaderSamplerBuilder( 76 | builder, 77 | enabled: enabled, 78 | child: child, 79 | ); 80 | } 81 | } 82 | 83 | class _ShaderSamplerBuilder extends SingleChildRenderObjectWidget { 84 | const _ShaderSamplerBuilder( 85 | this.builder, { 86 | super.child, 87 | required this.enabled, 88 | }); 89 | 90 | final AnimatedSamplerBuilder builder; 91 | final bool enabled; 92 | 93 | @override 94 | RenderObject createRenderObject(BuildContext context) { 95 | return _RenderShaderSamplerBuilderWidget( 96 | devicePixelRatio: MediaQuery.of(context).devicePixelRatio, 97 | builder: builder, 98 | enabled: enabled, 99 | ); 100 | } 101 | 102 | @override 103 | void updateRenderObject( 104 | BuildContext context, covariant RenderObject renderObject) { 105 | (renderObject as _RenderShaderSamplerBuilderWidget) 106 | ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio 107 | ..builder = builder 108 | ..enabled = enabled; 109 | } 110 | } 111 | 112 | // A render object that conditionally converts its child into a [ui.Image] 113 | // and then paints it in place of the child. 114 | class _RenderShaderSamplerBuilderWidget extends RenderProxyBox { 115 | // Create a new [_RenderSnapshotWidget]. 116 | _RenderShaderSamplerBuilderWidget({ 117 | required double devicePixelRatio, 118 | required AnimatedSamplerBuilder builder, 119 | required bool enabled, 120 | }) : _devicePixelRatio = devicePixelRatio, 121 | _builder = builder, 122 | _enabled = enabled; 123 | 124 | @override 125 | OffsetLayer updateCompositedLayer( 126 | {required covariant _ShaderSamplerBuilderLayer? oldLayer}) { 127 | final _ShaderSamplerBuilderLayer layer = 128 | oldLayer ?? _ShaderSamplerBuilderLayer(builder); 129 | layer 130 | ..callback = builder 131 | ..size = size 132 | ..devicePixelRatio = devicePixelRatio; 133 | return layer; 134 | } 135 | 136 | /// The device pixel ratio used to create the child image. 137 | double get devicePixelRatio => _devicePixelRatio; 138 | double _devicePixelRatio; 139 | set devicePixelRatio(double value) { 140 | if (value == devicePixelRatio) { 141 | return; 142 | } 143 | _devicePixelRatio = value; 144 | markNeedsCompositedLayerUpdate(); 145 | } 146 | 147 | /// The painter used to paint the child snapshot or child widgets. 148 | AnimatedSamplerBuilder get builder => _builder; 149 | AnimatedSamplerBuilder _builder; 150 | set builder(AnimatedSamplerBuilder value) { 151 | if (value == builder) { 152 | return; 153 | } 154 | _builder = value; 155 | markNeedsCompositedLayerUpdate(); 156 | } 157 | 158 | bool get enabled => _enabled; 159 | bool _enabled; 160 | set enabled(bool value) { 161 | if (value == enabled) { 162 | return; 163 | } 164 | _enabled = value; 165 | markNeedsPaint(); 166 | markNeedsCompositingBitsUpdate(); 167 | } 168 | 169 | @override 170 | bool get isRepaintBoundary => alwaysNeedsCompositing; 171 | 172 | @override 173 | bool get alwaysNeedsCompositing => enabled; 174 | 175 | @override 176 | void paint(PaintingContext context, Offset offset) { 177 | if (size.isEmpty) { 178 | return; 179 | } 180 | assert(!_enabled || offset == Offset.zero); 181 | return super.paint(context, offset); 182 | } 183 | } 184 | 185 | /// A [Layer] that uses an [AnimatedSamplerBuilder] to create a [ui.Picture] 186 | /// every time it is added to a scene. 187 | class _ShaderSamplerBuilderLayer extends OffsetLayer { 188 | _ShaderSamplerBuilderLayer(this._callback); 189 | 190 | ui.Picture? _lastPicture; 191 | 192 | Size get size => _size; 193 | Size _size = Size.zero; 194 | set size(Size value) { 195 | if (value == size) { 196 | return; 197 | } 198 | _size = value; 199 | markNeedsAddToScene(); 200 | } 201 | 202 | double get devicePixelRatio => _devicePixelRatio; 203 | double _devicePixelRatio = 1.0; 204 | set devicePixelRatio(double value) { 205 | if (value == devicePixelRatio) { 206 | return; 207 | } 208 | _devicePixelRatio = value; 209 | markNeedsAddToScene(); 210 | } 211 | 212 | AnimatedSamplerBuilder get callback => _callback; 213 | AnimatedSamplerBuilder _callback; 214 | set callback(AnimatedSamplerBuilder value) { 215 | if (value == callback) { 216 | return; 217 | } 218 | _callback = value; 219 | markNeedsAddToScene(); 220 | } 221 | 222 | ui.Image _buildChildScene(Rect bounds, double pixelRatio) { 223 | final ui.SceneBuilder builder = ui.SceneBuilder(); 224 | final Matrix4 transform = 225 | Matrix4.diagonal3Values(pixelRatio, pixelRatio, 1); 226 | builder.pushTransform(transform.storage); 227 | addChildrenToScene(builder); 228 | builder.pop(); 229 | return builder.build().toImageSync( 230 | (pixelRatio * bounds.width).ceil(), 231 | (pixelRatio * bounds.height).ceil(), 232 | ); 233 | } 234 | 235 | @override 236 | void dispose() { 237 | _lastPicture?.dispose(); 238 | super.dispose(); 239 | } 240 | 241 | @override 242 | void addToScene(ui.SceneBuilder builder) { 243 | if (size.isEmpty) return; 244 | final ui.Image image = _buildChildScene( 245 | offset & size, 246 | devicePixelRatio, 247 | ); 248 | final ui.PictureRecorder pictureRecorder = ui.PictureRecorder(); 249 | final Canvas canvas = Canvas(pictureRecorder); 250 | try { 251 | callback(image, size, canvas); 252 | } finally { 253 | image.dispose(); 254 | } 255 | final ui.Picture picture = pictureRecorder.endRecording(); 256 | _lastPicture?.dispose(); 257 | _lastPicture = picture; 258 | builder.addPicture(offset, picture); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /lib/src/inkwell_shader.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'dart:ui' as ui; 7 | import 'dart:math' as math; 8 | 9 | /// A callback used by the [ShaderInkFeature] to configure the fragment shader 10 | /// on each frame of the inkwell animation. 11 | /// 12 | /// [animation] represents the animation progress, starting at `0` and ending 13 | /// at `1` when the animation is complete. 14 | /// 15 | /// See also: 16 | /// 17 | /// * [ShaderInkFeature] for more information on the available configuration. 18 | typedef ShaderConfigCallback = void Function( 19 | ui.FragmentShader shader, { 20 | required double animation, 21 | required Size referenceBoxSize, 22 | required Color color, 23 | required Offset position, 24 | required TextDirection textDirection, 25 | required double targetRadius, 26 | }); 27 | 28 | /// Allows customization of the material inkwell effect with a user authored 29 | /// fragment shader. 30 | /// 31 | /// On each frame of the inkwell animation, the provided [callback] will be 32 | /// invoked with a fragment shader instance, as well as the configuration for 33 | /// the particular inkwell splash that is occuring. It is the responsibility 34 | /// of the developer to supply both a fragment program and a callback that 35 | /// delivers the ink well effect. 36 | /// 37 | /// Example: 38 | /// 39 | /// Configuring all inkwells in a material application with a fragment program. 40 | /// 41 | /// ```glsl 42 | /// #include 43 | /// 44 | /// uniform float uAnimation; 45 | /// uniform vec4 uColor; 46 | /// uniform float uRadius; 47 | /// uniform vec2 uCenter; 48 | /// 49 | /// out vec4 fragColor; 50 | /// 51 | /// void main() { 52 | /// float scale = distance(FlutterFragCoord(), uCenter) / uRadius; 53 | /// fragColor = mix(vec4(1.0), uColor, scale) * (1.0 - uAnimation); 54 | /// } 55 | /// ``` 56 | /// 57 | /// ```dart 58 | /// Widget build(BuildContext context) { 59 | /// return MaterialApp( 60 | /// title: 'Flutter Demo', 61 | /// theme: ThemeData( 62 | /// primarySwatch: Colors.blue, 63 | /// splashFactory: ShaderInkFeatureFactory(program, ( 64 | /// shader, { 65 | /// required double animation, 66 | /// required Color color, 67 | /// required Offset position, 68 | /// required Size referenceBoxSize, 69 | /// required double targetRadius, 70 | /// required TextDirection textDirection, 71 | /// }) { 72 | /// shader 73 | /// ..setFloat(0, animation) 74 | /// ..setFloat(1, color.red / 255.0 * color.opacity) 75 | /// ..setFloat(2, color.green / 255.0 * color.opacity) 76 | /// ..setFloat(3, color.blue / 255.0 * color.opacity) 77 | /// ..setFloat(4, color.opacity) 78 | /// ..setFloat(5, targetRadius) 79 | /// ..setFloat(6, position.dx) 80 | /// ..setFloat(7, position.dy); 81 | /// })), 82 | /// home: const MyHomePage(title: 'Flutter Demo Home Page'), 83 | /// ); 84 | /// } 85 | /// ``` 86 | /// 87 | /// See also: 88 | /// 89 | /// * [ShaderInkFeature] for more information on the available configuration. 90 | class ShaderInkFeatureFactory extends InteractiveInkFeatureFactory { 91 | const ShaderInkFeatureFactory( 92 | this.program, 93 | this.callback, { 94 | this.animationDuration = const Duration(milliseconds: 617), 95 | }); 96 | 97 | final ui.FragmentProgram program; 98 | final ShaderConfigCallback callback; 99 | final Duration animationDuration; 100 | 101 | @override 102 | InteractiveInkFeature create({ 103 | required MaterialInkController controller, 104 | required RenderBox referenceBox, 105 | required Offset position, 106 | required Color color, 107 | required TextDirection textDirection, 108 | bool containedInkWell = false, 109 | RectCallback? rectCallback, 110 | BorderRadius? borderRadius, 111 | ShapeBorder? customBorder, 112 | double? radius, 113 | VoidCallback? onRemoved, 114 | }) { 115 | return ShaderInkFeature( 116 | controller: controller, 117 | referenceBox: referenceBox, 118 | position: position, 119 | color: color, 120 | textDirection: textDirection, 121 | containedInkWell: containedInkWell, 122 | rectCallback: rectCallback, 123 | borderRadius: borderRadius, 124 | customBorder: customBorder, 125 | radius: radius, 126 | onRemoved: onRemoved, 127 | animationDuration: animationDuration, 128 | callback: callback, 129 | fragmentShader: program.fragmentShader(), 130 | ); 131 | } 132 | } 133 | 134 | /// An ink feature that is driven by a developer authored fragment shader and 135 | /// a configuration callback. 136 | class ShaderInkFeature extends InteractiveInkFeature { 137 | /// Begin a sparkly ripple effect, centered at [position] relative to 138 | /// [referenceBox]. 139 | /// 140 | /// The [color] defines the color of the splash itself. The sparkles are 141 | /// always white. 142 | /// 143 | /// The [controller] argument is typically obtained via 144 | /// `Material.of(context)`. 145 | /// 146 | /// [textDirection] is used by [customBorder] if it is non-null. This allows 147 | /// the [customBorder]'s path to be properly defined if it was the path was 148 | /// expressed in terms of "start" and "end" instead of 149 | /// "left" and "right". 150 | /// 151 | /// If [containedInkWell] is true, then the ripple will be sized to fit 152 | /// the well rectangle, then clipped to it when drawn. The well 153 | /// rectangle is the box returned by [rectCallback], if provided, or 154 | /// otherwise is the bounds of the [referenceBox]. 155 | /// 156 | /// If [containedInkWell] is false, then [rectCallback] should be null. 157 | /// The ink ripple is clipped only to the edges of the [Material]. 158 | /// This is the default. 159 | /// 160 | /// Clipping can happen in 3 different ways: 161 | /// 1. If [customBorder] is provided, it is used to determine the path for 162 | /// clipping. 163 | /// 2. If [customBorder] is null, and [borderRadius] is provided, then the 164 | /// canvas is clipped by an [RRect] created from [borderRadius]. 165 | /// 3. If [borderRadius] is the default [BorderRadius.zero], then the canvas 166 | /// is clipped with [rectCallback]. 167 | /// When the ripple is removed, [onRemoved] will be called. 168 | ShaderInkFeature({ 169 | required super.controller, 170 | required super.referenceBox, 171 | required super.color, 172 | required Offset position, 173 | required TextDirection textDirection, 174 | required Duration animationDuration, 175 | required ui.FragmentShader fragmentShader, 176 | required ShaderConfigCallback callback, 177 | bool containedInkWell = true, 178 | RectCallback? rectCallback, 179 | BorderRadius? borderRadius, 180 | ShapeBorder? customBorder, 181 | double? radius, 182 | super.onRemoved, 183 | double? turbulenceSeed, 184 | }) : _fragmentShader = fragmentShader, 185 | _callback = callback, 186 | _animationDuration = animationDuration, 187 | _position = position, 188 | _borderRadius = borderRadius ?? BorderRadius.zero, 189 | _customBorder = customBorder, 190 | _textDirection = textDirection, 191 | _targetRadius = (radius ?? 192 | _getTargetRadius( 193 | referenceBox, 194 | containedInkWell, 195 | rectCallback, 196 | position, 197 | )), 198 | _clipCallback = 199 | _getClipCallback(referenceBox, containedInkWell, rectCallback) { 200 | controller.addInkFeature(this); 201 | 202 | // Immediately begin animating the ink. 203 | _animationController = AnimationController( 204 | duration: _animationDuration, 205 | vsync: controller.vsync, 206 | ) 207 | ..addListener(controller.markNeedsPaint) 208 | ..addStatusListener(_handleStatusChanged) 209 | ..forward(); 210 | } 211 | 212 | late AnimationController _animationController; 213 | final Offset _position; 214 | final BorderRadius _borderRadius; 215 | final ShapeBorder? _customBorder; 216 | final double _targetRadius; 217 | final RectCallback? _clipCallback; 218 | final TextDirection _textDirection; 219 | final Duration _animationDuration; 220 | final ui.FragmentShader _fragmentShader; 221 | final ShaderConfigCallback _callback; 222 | 223 | void _handleStatusChanged(AnimationStatus status) { 224 | if (status == AnimationStatus.completed) { 225 | dispose(); 226 | } 227 | } 228 | 229 | @override 230 | void dispose() { 231 | _animationController.stop(); 232 | _animationController.dispose(); 233 | _fragmentShader.dispose(); 234 | super.dispose(); 235 | } 236 | 237 | @override 238 | void paintFeature(Canvas canvas, Matrix4 transform) { 239 | assert(_animationController.isAnimating); 240 | 241 | canvas.save(); 242 | _transformCanvas(canvas: canvas, transform: transform); 243 | if (_clipCallback != null) { 244 | _clipCanvas( 245 | canvas: canvas, 246 | clipCallback: _clipCallback!, 247 | textDirection: _textDirection, 248 | customBorder: _customBorder, 249 | borderRadius: _borderRadius, 250 | ); 251 | } 252 | 253 | _updateFragmentShader(); 254 | 255 | final Paint paint = Paint()..shader = _fragmentShader; 256 | if (_clipCallback != null) { 257 | canvas.drawRect(_clipCallback!(), paint); 258 | } else { 259 | canvas.drawPaint(paint); 260 | } 261 | canvas.restore(); 262 | } 263 | 264 | void _updateFragmentShader() { 265 | _callback( 266 | _fragmentShader, 267 | animation: _animationController.value, 268 | color: color, 269 | position: _position, 270 | referenceBoxSize: referenceBox.size, 271 | targetRadius: _targetRadius, 272 | textDirection: _textDirection, 273 | ); 274 | } 275 | 276 | /// Transforms the canvas for an ink feature to be painted on the [canvas]. 277 | /// 278 | /// This should be called before painting ink features that do not use 279 | /// [paintInkCircle]. 280 | /// 281 | /// The [transform] argument is the [Matrix4] transform that typically 282 | /// shifts the coordinate space of the canvas to the space in which 283 | /// the ink feature is to be painted. 284 | /// 285 | /// For examples on how the function is used, see [InkSparkle] and [paintInkCircle]. 286 | void _transformCanvas({ 287 | required Canvas canvas, 288 | required Matrix4 transform, 289 | }) { 290 | final Offset? originOffset = MatrixUtils.getAsTranslation(transform); 291 | if (originOffset == null) { 292 | canvas.transform(transform.storage); 293 | } else { 294 | canvas.translate(originOffset.dx, originOffset.dy); 295 | } 296 | } 297 | 298 | /// Clips the canvas for an ink feature to be painted on the [canvas]. 299 | /// 300 | /// This should be called before painting ink features with [paintFeature] 301 | /// that do not use [paintInkCircle]. 302 | /// 303 | /// The [clipCallback] is the callback used to obtain the [Rect] used for clipping 304 | /// the ink effect. 305 | /// 306 | /// If [clipCallback] is null, no clipping is performed on the ink circle. 307 | /// 308 | /// The [textDirection] is used by [customBorder] if it is non-null. This 309 | /// allows the [customBorder]'s path to be properly defined if the path was 310 | /// expressed in terms of "start" and "end" instead of "left" and "right". 311 | /// 312 | /// For examples on how the function is used, see [InkSparkle]. 313 | void _clipCanvas({ 314 | required Canvas canvas, 315 | required RectCallback clipCallback, 316 | TextDirection? textDirection, 317 | ShapeBorder? customBorder, 318 | BorderRadius borderRadius = BorderRadius.zero, 319 | }) { 320 | final Rect rect = clipCallback(); 321 | if (customBorder != null) { 322 | canvas.clipPath( 323 | customBorder.getOuterPath(rect, textDirection: textDirection)); 324 | } else if (borderRadius != BorderRadius.zero) { 325 | canvas.clipRRect(RRect.fromRectAndCorners( 326 | rect, 327 | topLeft: borderRadius.topLeft, 328 | topRight: borderRadius.topRight, 329 | bottomLeft: borderRadius.bottomLeft, 330 | bottomRight: borderRadius.bottomRight, 331 | )); 332 | } else { 333 | canvas.clipRect(rect); 334 | } 335 | } 336 | } 337 | 338 | double _getTargetRadius( 339 | RenderBox referenceBox, 340 | bool containedInkWell, 341 | RectCallback? rectCallback, 342 | Offset position, 343 | ) { 344 | final Size size = 345 | rectCallback != null ? rectCallback().size : referenceBox.size; 346 | final double d1 = size.bottomRight(Offset.zero).distance; 347 | final double d2 = 348 | (size.topRight(Offset.zero) - size.bottomLeft(Offset.zero)).distance; 349 | return math.max(d1, d2) / 2.0; 350 | } 351 | 352 | RectCallback? _getClipCallback( 353 | RenderBox referenceBox, 354 | bool containedInkWell, 355 | RectCallback? rectCallback, 356 | ) { 357 | if (rectCallback != null) { 358 | assert(containedInkWell); 359 | return rectCallback; 360 | } 361 | if (containedInkWell) { 362 | return () => Offset.zero & referenceBox.size; 363 | } 364 | return null; 365 | } 366 | -------------------------------------------------------------------------------- /lib/src/set_uniforms.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter/animation.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:vector_math/vector_math.dart'; 6 | 7 | /// A helper extension on [ui.FragmentShader] that allows you to set uniforms 8 | /// in a more convenient way. Withotu having to manage indices. 9 | /// 10 | /// Example: 11 | /// ```dart 12 | /// shader.setFloatUniforms((setter) { 13 | /// setter.setFloat(1.0); 14 | /// setter.setFloats([1.0, 2.0, 3.0]); 15 | /// setter.setSize(const Size(1.0, 2.0)); 16 | /// setter.setSizes([const Size(1.0, 2.0), const Size(3.0, 4.0)]); 17 | /// setter.setOffset(const Offset(1.0, 2.0)); 18 | /// setter.setOffsets([const Offset(1.0, 2.0), const Offset(3.0, 4.0)]); 19 | /// setter.setMatrix(Matrix4.identity()); 20 | /// setter.setMatrices([Matrix4.identity(), Matrix4.identity()]); 21 | /// setter.setColor(Colors.red); 22 | /// setter.setColors([Colors.red, Colors.green]); 23 | /// }); 24 | /// ``` 25 | /// 26 | /// The receiving end of this script should be: 27 | /// ```glsl 28 | /// uniform float u0; // 1.0 29 | /// uniform float[3] uFloats; // float[3](1.0, 2.0, 3.0) 30 | /// uniform vec2 size; // vec2(1.0, 2.0) 31 | /// uniform vec2[2] sizes; // vec2[2](vec2(1.0, 2.0), vec2(3.0, 4.0)) 32 | /// uniform vec2 offset; // vec2(1.0, 2.0) 33 | /// uniform vec2[2] offsets; // vec2[2](vec2(1.0, 2.0), vec2(3.0, 4.0)) 34 | /// uniform mat4 matrix; // mat4(1.0) 35 | /// uniform mat4[2] matrices; // mat4[2](mat4(1.0), mat4(1.0)) 36 | /// uniform vec4 color; // vec4(1.0, 0.0, 0.0, 1.0) 37 | /// uniform vec4[2] colors; // vec4[2](vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0)) 38 | /// ``` 39 | /// 40 | /// The [initialIndex] parameter allows you to set the index of the first 41 | /// uniform. Defaults to 0. 42 | /// 43 | /// Returns the index of the last uniform that was set. 44 | extension SetUniforms on ui.FragmentShader { 45 | int setFloatUniforms( 46 | ValueSetter callback, { 47 | int initialIndex = 0, 48 | }) { 49 | final setter = UniformsSetter(this, initialIndex); 50 | callback(setter); 51 | return setter._index; 52 | } 53 | } 54 | 55 | class UniformsSetter { 56 | UniformsSetter(this.shader, this._index); 57 | 58 | int _index; 59 | final ui.FragmentShader shader; 60 | 61 | void setFloat(double value) { 62 | shader.setFloat(_index++, value); 63 | } 64 | 65 | void setFloats(List values) { 66 | for (final value in values) { 67 | setFloat(value); 68 | } 69 | } 70 | 71 | void setSize(Size size) { 72 | shader 73 | ..setFloat(_index++, size.width) 74 | ..setFloat(_index++, size.height); 75 | } 76 | 77 | void setSizes(List sizes) { 78 | for (final size in sizes) { 79 | setSize(size); 80 | } 81 | } 82 | 83 | void setColor(Color color, {bool premultiply = false}) { 84 | final double multiplier; 85 | if (premultiply) { 86 | multiplier = color.opacity; 87 | } else { 88 | multiplier = 1.0; 89 | } 90 | 91 | setFloat(color.red / 255 * multiplier); 92 | setFloat(color.green / 255 * multiplier); 93 | setFloat(color.blue / 255 * multiplier); 94 | setFloat(color.opacity); 95 | } 96 | 97 | void setColors(List colors, {bool premultiply = false}) { 98 | for (final color in colors) { 99 | setColor(color, premultiply: premultiply); 100 | } 101 | } 102 | 103 | void setOffset(Offset offset) { 104 | shader 105 | ..setFloat(_index++, offset.dx) 106 | ..setFloat(_index++, offset.dy); 107 | } 108 | 109 | void setOffsets(List offsets) { 110 | for (final offset in offsets) { 111 | setOffset(offset); 112 | } 113 | } 114 | 115 | void setVector(Vector vector) { 116 | setFloats(vector.storage); 117 | } 118 | 119 | void setVectors(List vectors) { 120 | for (final vector in vectors) { 121 | setVector(vector); 122 | } 123 | } 124 | 125 | void setMatrix2(Matrix2 matrix2) { 126 | setFloats(matrix2.storage); 127 | } 128 | 129 | void setMatrix2s(List matrix2s) { 130 | for (final matrix2 in matrix2s) { 131 | setMatrix2(matrix2); 132 | } 133 | } 134 | 135 | void setMatrix3(Matrix3 matrix3) { 136 | setFloats(matrix3.storage); 137 | } 138 | 139 | void setMatrix3s(List matrix3s) { 140 | for (final matrix3 in matrix3s) { 141 | setMatrix3(matrix3); 142 | } 143 | } 144 | 145 | void setMatrix4(Matrix4 matrix4) { 146 | setFloats(matrix4.storage); 147 | } 148 | 149 | void setMatrix4s(List matrix4s) { 150 | for (final matrix4 in matrix4s) { 151 | setMatrix4(matrix4); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/src/shader_builder.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:ui' as ui; 6 | 7 | import 'package:flutter/widgets.dart'; 8 | 9 | /// A callback used by [ShaderBuilder]. 10 | typedef ShaderBuilderCallback = Widget Function( 11 | BuildContext, ui.FragmentShader, Widget?); 12 | 13 | /// A widget that loads and caches [FragmentProgram]s based on the asset key. 14 | /// 15 | /// Usage of this widget avoids the need for a user authored stateful widget 16 | /// for managing the lifecycle of loading a shader. Once a shader is cached, 17 | /// subsequent usages of it via a [ShaderBuilder] will always be available 18 | /// synchronously. These shaders can also be precached imperatively with 19 | /// [ShaderBuilder.precacheShader]. 20 | /// 21 | /// If the shader is not yet loaded, the provided child widget or a [SizedBox] 22 | /// is returned instead of invoking the builder callback. 23 | /// 24 | /// Example: providing access to a [FragmentShader] instance. 25 | /// 26 | /// ```dart 27 | /// Widget build(BuildContext context) { 28 | /// return ShaderBuilder( 29 | /// builder: (BuildContext context, ui.FragmentShader shader, Widget? child) { 30 | /// return WidgetThatUsesFragmentShader( 31 | /// shader: shader, 32 | /// child: child, 33 | /// ); 34 | /// }, 35 | /// child: Text('Hello, Shader'), 36 | /// ); 37 | /// } 38 | /// ``` 39 | class ShaderBuilder extends StatefulWidget { 40 | /// Create a new [ShaderBuilder]. 41 | const ShaderBuilder( 42 | this.builder, { 43 | super.key, 44 | required this.assetKey, 45 | this.child, 46 | }); 47 | 48 | /// The asset key used to a lookup a shader. 49 | final String assetKey; 50 | 51 | /// The child widget to pass through to the [builder], optional. 52 | final Widget? child; 53 | 54 | /// The builder that provides access to a [FragmentShader]. 55 | final ShaderBuilderCallback builder; 56 | 57 | @override 58 | State createState() { 59 | return _ShaderBuilderState(); 60 | } 61 | 62 | /// Precache a [FragmentProgram] based on its [assetKey]. 63 | /// 64 | /// When this future has completed, any newly created [ShaderBuilder]s that 65 | /// reference this asset will be guaranteed to immediately have access to the 66 | /// shader. 67 | static Future precacheShader(String assetKey) { 68 | if (_ShaderBuilderState._shaderCache.containsKey(assetKey)) { 69 | return Future.value(); 70 | } 71 | return ui.FragmentProgram.fromAsset(assetKey).then( 72 | (ui.FragmentProgram program) { 73 | _ShaderBuilderState._shaderCache[assetKey] = program; 74 | }, onError: (Object error, StackTrace stackTrace) { 75 | FlutterError.reportError( 76 | FlutterErrorDetails(exception: error, stack: stackTrace)); 77 | }); 78 | } 79 | } 80 | 81 | class _ShaderBuilderState extends State { 82 | ui.FragmentProgram? program; 83 | ui.FragmentShader? shader; 84 | 85 | static final Map _shaderCache = 86 | {}; 87 | 88 | @override 89 | void initState() { 90 | super.initState(); 91 | _loadShader(widget.assetKey); 92 | } 93 | 94 | @override 95 | void didUpdateWidget(covariant ShaderBuilder oldWidget) { 96 | super.didUpdateWidget(oldWidget); 97 | if (oldWidget.assetKey != widget.assetKey) { 98 | _loadShader(widget.assetKey); 99 | } 100 | } 101 | 102 | void _loadShader(String assetKey) { 103 | if (_shaderCache.containsKey(assetKey)) { 104 | program = _shaderCache[assetKey]; 105 | shader = program!.fragmentShader(); 106 | return; 107 | } 108 | 109 | ui.FragmentProgram.fromAsset(assetKey).then((ui.FragmentProgram program) { 110 | if (!mounted) { 111 | return; 112 | } 113 | setState(() { 114 | this.program = program; 115 | shader = program.fragmentShader(); 116 | _shaderCache[assetKey] = program; 117 | }); 118 | }, onError: (Object error, StackTrace stackTrace) { 119 | FlutterError.reportError( 120 | FlutterErrorDetails(exception: error, stack: stackTrace)); 121 | }); 122 | } 123 | 124 | @override 125 | Widget build(BuildContext context) { 126 | if (shader == null) { 127 | return widget.child ?? const SizedBox.shrink(); 128 | } 129 | return widget.builder(context, shader!, widget.child); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.17.2" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.1" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_test: 58 | dependency: "direct dev" 59 | description: flutter 60 | source: sdk 61 | version: "0.0.0" 62 | matcher: 63 | dependency: transitive 64 | description: 65 | name: matcher 66 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 67 | url: "https://pub.dev" 68 | source: hosted 69 | version: "0.12.16" 70 | material_color_utilities: 71 | dependency: transitive 72 | description: 73 | name: material_color_utilities 74 | sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "0.5.0" 78 | meta: 79 | dependency: transitive 80 | description: 81 | name: meta 82 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "1.9.1" 86 | path: 87 | dependency: transitive 88 | description: 89 | name: path 90 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "1.8.3" 94 | sky_engine: 95 | dependency: transitive 96 | description: flutter 97 | source: sdk 98 | version: "0.0.99" 99 | source_span: 100 | dependency: transitive 101 | description: 102 | name: source_span 103 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 104 | url: "https://pub.dev" 105 | source: hosted 106 | version: "1.10.0" 107 | stack_trace: 108 | dependency: transitive 109 | description: 110 | name: stack_trace 111 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 112 | url: "https://pub.dev" 113 | source: hosted 114 | version: "1.11.0" 115 | stream_channel: 116 | dependency: transitive 117 | description: 118 | name: stream_channel 119 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 120 | url: "https://pub.dev" 121 | source: hosted 122 | version: "2.1.1" 123 | string_scanner: 124 | dependency: transitive 125 | description: 126 | name: string_scanner 127 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "1.2.0" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.2.1" 139 | test_api: 140 | dependency: transitive 141 | description: 142 | name: test_api 143 | sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "0.6.0" 147 | vector_math: 148 | dependency: "direct main" 149 | description: 150 | name: vector_math 151 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "2.1.4" 155 | web: 156 | dependency: transitive 157 | description: 158 | name: web 159 | sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "0.1.4-beta" 163 | sdks: 164 | dart: ">=3.1.0-185.0.dev <4.0.0" 165 | flutter: ">=3.7.0-0.0" 166 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_shaders 2 | description: A collection of utilities for working with the FragmentProgram API in Flutter. 3 | repository: https://github.com/jonahwilliams/flutter_shaders 4 | issue_tracker: https://github.com/jonahwilliams/flutter_shaders/issues 5 | version: 0.1.3 6 | 7 | environment: 8 | sdk: '>=2.19.0 <4.0.0' 9 | flutter: ">=3.7.0-0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | vector_math: ^2.1.4 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | -------------------------------------------------------------------------------- /test/animated_sampler_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_shaders/flutter_shaders.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | void main() { 9 | testWidgets('does not call builder when not enabled', (tester) async { 10 | final AnimatedSamplerBuilder builder = 11 | expectAsync4((image, size, offset, canvas) {}, count: 0); 12 | 13 | await tester.pumpWidget( 14 | MaterialApp( 15 | home: AnimatedSampler( 16 | builder, 17 | enabled: false, 18 | child: SizedBox(), 19 | ), 20 | ), 21 | ); 22 | }); 23 | 24 | testWidgets('starts calling builder once enabled', (tester) async { 25 | final AnimatedSamplerBuilder builder = 26 | expectAsync4((image, size, offset, canvas) {}, count: 1); 27 | 28 | await tester.pumpWidget( 29 | MaterialApp( 30 | home: AnimatedSampler( 31 | builder, 32 | enabled: false, 33 | child: SizedBox(), 34 | ), 35 | ), 36 | ); 37 | 38 | await tester.pumpWidget( 39 | MaterialApp( 40 | home: AnimatedSampler( 41 | builder, 42 | enabled: true, 43 | child: SizedBox(), 44 | ), 45 | ), 46 | ); 47 | }); 48 | 49 | testWidgets('rebuilds when child layer is updated', (tester) async { 50 | final AnimatedSamplerBuilder builder = 51 | expectAsync4((image, size, offset, canvas) {}, count: 2); 52 | 53 | await tester.pumpWidget( 54 | MaterialApp( 55 | home: AnimatedSampler( 56 | builder, 57 | child: RepaintBoundary( 58 | child: CircularProgressIndicator(), 59 | ), 60 | ), 61 | ), 62 | ); 63 | 64 | // Pump the next frame to animate `CircularProgressIndicator`. 65 | await tester.pump(Duration(seconds: 1)); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/set_uniforms_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_shaders/flutter_shaders.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:vector_math/vector_math.dart'; 6 | 7 | class _MockFragmentShader implements FragmentShader { 8 | final floats = {}; 9 | final images = {}; 10 | 11 | @override 12 | noSuchMethod(Invocation invocation) { 13 | throw UnimplementedError(); 14 | } 15 | 16 | @override 17 | void setFloat(int index, double value) { 18 | floats[index] = value; 19 | } 20 | } 21 | 22 | void main() { 23 | group('SetUniforms', () { 24 | test('setFloat', () { 25 | final shader = _MockFragmentShader(); 26 | 27 | shader.setFloatUniforms((setter) { 28 | setter.setFloat(1.0); 29 | }); 30 | 31 | expect(shader.floats, { 32 | 0: 1.0, 33 | }); 34 | }); 35 | 36 | test('setFloats', () { 37 | final shader = _MockFragmentShader(); 38 | 39 | shader.setFloatUniforms((setter) { 40 | setter.setFloats([1.0, 2.0, 3.0]); 41 | }); 42 | 43 | expect(shader.floats, { 44 | 0: 1.0, 45 | 1: 2.0, 46 | 2: 3.0, 47 | }); 48 | }); 49 | 50 | test('setSize', () { 51 | final shader = _MockFragmentShader(); 52 | 53 | shader.setFloatUniforms((setter) { 54 | setter.setSize(const Size(1.0, 2.0)); 55 | }); 56 | 57 | expect(shader.floats, { 58 | 0: 1.0, 59 | 1: 2.0, 60 | }); 61 | }); 62 | 63 | test('setSizes', () { 64 | final shader = _MockFragmentShader(); 65 | 66 | shader.setFloatUniforms((setter) { 67 | setter.setSizes(const [ 68 | Size(1.0, 2.0), 69 | Size(3.0, 4.0), 70 | Size(5.0, 6.0), 71 | ]); 72 | }); 73 | 74 | expect(shader.floats, { 75 | 0: 1.0, 76 | 1: 2.0, 77 | 2: 3.0, 78 | 3: 4.0, 79 | 4: 5.0, 80 | 5: 6.0, 81 | }); 82 | }); 83 | 84 | test('setColor', () { 85 | final shader = _MockFragmentShader(); 86 | 87 | shader.setFloatUniforms((setter) { 88 | setter.setColor(const Color(0xFF006600)); 89 | }); 90 | 91 | expect(shader.floats, { 92 | 0: 0.0, 93 | 1: 0.4, 94 | 2: 0.0, 95 | 3: 1.0, 96 | }); 97 | }); 98 | 99 | test('setColor w/ premultiply', () { 100 | final shader = _MockFragmentShader(); 101 | 102 | shader.setFloatUniforms((setter) { 103 | setter.setColor(const Color(0x00006600), premultiply: true); 104 | }); 105 | 106 | expect(shader.floats, { 107 | 0: 0.0, 108 | 1: 0.0, 109 | 2: 0.0, 110 | 3: 0.0, 111 | }); 112 | }); 113 | 114 | test('setColors', () { 115 | final shader = _MockFragmentShader(); 116 | 117 | shader.setFloatUniforms((setter) { 118 | setter.setColors(const [ 119 | Color(0xFF006600), 120 | Color(0xFF660000), 121 | Color(0x66000066), 122 | ]); 123 | }); 124 | 125 | expect(shader.floats, { 126 | 0: 0.0, 127 | 1: 0.4, 128 | 2: 0.0, 129 | 3: 1.0, 130 | 4: 0.4, 131 | 5: 0.0, 132 | 6: 0.0, 133 | 7: 1.0, 134 | 8: 0.0, 135 | 9: 0.0, 136 | 10: 0.4, 137 | 11: 0.4 138 | }); 139 | }); 140 | 141 | test('setColors w/ premultiply', () { 142 | final shader = _MockFragmentShader(); 143 | 144 | shader.setFloatUniforms((setter) { 145 | setter.setColors( 146 | premultiply: true, 147 | const [ 148 | Color(0xFF006600), 149 | Color(0xFF660000), 150 | Color(0x00000066), 151 | ], 152 | ); 153 | }); 154 | 155 | expect(shader.floats, { 156 | 0: 0.0, 157 | 1: 0.4, 158 | 2: 0.0, 159 | 3: 1.0, 160 | 4: 0.4, 161 | 5: 0.0, 162 | 6: 0.0, 163 | 7: 1.0, 164 | 8: 0.0, 165 | 9: 0.0, 166 | 10: 0.0, 167 | 11: 0.4 168 | }); 169 | }); 170 | 171 | test('setOffset', () { 172 | final shader = _MockFragmentShader(); 173 | 174 | shader.setFloatUniforms((setter) { 175 | setter.setOffset(const Offset(1.0, 2.0)); 176 | }); 177 | 178 | expect(shader.floats, { 179 | 0: 1.0, 180 | 1: 2.0, 181 | }); 182 | }); 183 | 184 | test('setOffsets', () { 185 | final shader = _MockFragmentShader(); 186 | 187 | shader.setFloatUniforms((setter) { 188 | setter.setOffsets(const [ 189 | Offset(1.0, 2.0), 190 | Offset(3.0, 4.0), 191 | Offset(5.0, 6.0), 192 | ]); 193 | }); 194 | 195 | expect(shader.floats, { 196 | 0: 1.0, 197 | 1: 2.0, 198 | 2: 3.0, 199 | 3: 4.0, 200 | 4: 5.0, 201 | 5: 6.0, 202 | }); 203 | }); 204 | 205 | test('setVector', () { 206 | final shader = _MockFragmentShader(); 207 | 208 | shader.setFloatUniforms((setter) { 209 | setter.setVector(Vector2(1.0, 2.0)); 210 | setter.setVector(Vector3(3.0, 4.0, 5.0)); 211 | setter.setVector(Vector4(6.0, 7.0, 8.0, 9.0)); 212 | }); 213 | 214 | expect(shader.floats, { 215 | 0: 1.0, 216 | 1: 2.0, 217 | 2: 3.0, 218 | 3: 4.0, 219 | 4: 5.0, 220 | 5: 6.0, 221 | 6: 7.0, 222 | 7: 8.0, 223 | 8: 9.0, 224 | }); 225 | }); 226 | 227 | test('setMatrix2', () { 228 | final shader = _MockFragmentShader(); 229 | 230 | shader.setFloatUniforms((setter) { 231 | setter.setMatrix2(Matrix2(1.0, 2.0, 3.0, 4.0)); 232 | }); 233 | 234 | expect(shader.floats, { 235 | 0: 1.0, 236 | 1: 2.0, 237 | 2: 3.0, 238 | 3: 4.0, 239 | }); 240 | }); 241 | 242 | test('setMatrix2s', () { 243 | final shader = _MockFragmentShader(); 244 | 245 | shader.setFloatUniforms((setter) { 246 | setter.setMatrix2s([ 247 | Matrix2(1.0, 2.0, 3.0, 4.0), 248 | Matrix2(5.0, 6.0, 7.0, 8.0), 249 | Matrix2(9.0, 10.0, 11.0, 12.0), 250 | ]); 251 | }); 252 | 253 | expect(shader.floats, { 254 | 0: 1.0, 255 | 1: 2.0, 256 | 2: 3.0, 257 | 3: 4.0, 258 | 4: 5.0, 259 | 5: 6.0, 260 | 6: 7.0, 261 | 7: 8.0, 262 | 8: 9.0, 263 | 9: 10.0, 264 | 10: 11.0, 265 | 11: 12.0, 266 | }); 267 | }); 268 | 269 | test('setMatrix3', () { 270 | final shader = _MockFragmentShader(); 271 | 272 | shader.setFloatUniforms((setter) { 273 | setter.setMatrix3(Matrix3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0)); 274 | }); 275 | 276 | expect(shader.floats, { 277 | 0: 1.0, 278 | 1: 2.0, 279 | 2: 3.0, 280 | 3: 4.0, 281 | 4: 5.0, 282 | 5: 6.0, 283 | 6: 7.0, 284 | 7: 8.0, 285 | 8: 9.0, 286 | }); 287 | }); 288 | 289 | test('setMatrix3s', () { 290 | final shader = _MockFragmentShader(); 291 | 292 | shader.setFloatUniforms((setter) { 293 | setter.setMatrix3s([ 294 | Matrix3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0), 295 | Matrix3(10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0), 296 | Matrix3(19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0), 297 | ]); 298 | }); 299 | 300 | expect(shader.floats, { 301 | 0: 1.0, 302 | 1: 2.0, 303 | 2: 3.0, 304 | 3: 4.0, 305 | 4: 5.0, 306 | 5: 6.0, 307 | 6: 7.0, 308 | 7: 8.0, 309 | 8: 9.0, 310 | 9: 10.0, 311 | 10: 11.0, 312 | 11: 12.0, 313 | 12: 13.0, 314 | 13: 14.0, 315 | 14: 15.0, 316 | 15: 16.0, 317 | 16: 17.0, 318 | 17: 18.0, 319 | 18: 19.0, 320 | 19: 20.0, 321 | 20: 21.0, 322 | 21: 22.0, 323 | 22: 23.0, 324 | 23: 24.0, 325 | 24: 25.0, 326 | 25: 26.0, 327 | 26: 27.0, 328 | }); 329 | }); 330 | 331 | test('setMatrix4', () { 332 | final shader = _MockFragmentShader(); 333 | 334 | shader.setFloatUniforms((setter) { 335 | setter.setMatrix4(Matrix4( 336 | 1.0, 337 | 2.0, 338 | 3.0, 339 | 4.0, 340 | 5.0, 341 | 6.0, 342 | 7.0, 343 | 8.0, 344 | 9.0, 345 | 10.0, 346 | 11.0, 347 | 12.0, 348 | 13.0, 349 | 14.0, 350 | 15.0, 351 | 16.0, 352 | )); 353 | }); 354 | 355 | expect(shader.floats, { 356 | 0: 1.0, 357 | 1: 2.0, 358 | 2: 3.0, 359 | 3: 4.0, 360 | 4: 5.0, 361 | 5: 6.0, 362 | 6: 7.0, 363 | 7: 8.0, 364 | 8: 9.0, 365 | 9: 10.0, 366 | 10: 11.0, 367 | 11: 12.0, 368 | 12: 13.0, 369 | 13: 14.0, 370 | 14: 15.0, 371 | 15: 16.0, 372 | }); 373 | }); 374 | 375 | test('setMatrix4s', () { 376 | final shader = _MockFragmentShader(); 377 | 378 | shader.setFloatUniforms((setter) { 379 | setter.setMatrix4s([ 380 | Matrix4( 381 | 1.0, 382 | 2.0, 383 | 3.0, 384 | 4.0, 385 | 5.0, 386 | 6.0, 387 | 7.0, 388 | 8.0, 389 | 9.0, 390 | 10.0, 391 | 11.0, 392 | 12.0, 393 | 13.0, 394 | 14.0, 395 | 15.0, 396 | 16.0, 397 | ), 398 | Matrix4( 399 | 17.0, 400 | 18.0, 401 | 19.0, 402 | 20.0, 403 | 21.0, 404 | 22.0, 405 | 23.0, 406 | 24.0, 407 | 25.0, 408 | 26.0, 409 | 27.0, 410 | 28.0, 411 | 29.0, 412 | 30.0, 413 | 31.0, 414 | 32.0, 415 | ), 416 | ]); 417 | }); 418 | 419 | expect(shader.floats, { 420 | 0: 1.0, 421 | 1: 2.0, 422 | 2: 3.0, 423 | 3: 4.0, 424 | 4: 5.0, 425 | 5: 6.0, 426 | 6: 7.0, 427 | 7: 8.0, 428 | 8: 9.0, 429 | 9: 10.0, 430 | 10: 11.0, 431 | 11: 12.0, 432 | 12: 13.0, 433 | 13: 14.0, 434 | 14: 15.0, 435 | 15: 16.0, 436 | 16: 17.0, 437 | 17: 18.0, 438 | 18: 19.0, 439 | 19: 20.0, 440 | 20: 21.0, 441 | 21: 22.0, 442 | 22: 23.0, 443 | 23: 24.0, 444 | 24: 25.0, 445 | 25: 26.0, 446 | 26: 27.0, 447 | 27: 28.0, 448 | 28: 29.0, 449 | 29: 30.0, 450 | 30: 31.0, 451 | 31: 32.0, 452 | }); 453 | }); 454 | }); 455 | } 456 | --------------------------------------------------------------------------------