From 66a8b612fa7fa384acbc281e836cd3735186638e Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Wed, 4 Dec 2024 19:18:44 +0400 Subject: [PATCH 01/21] Add simple shader --- example/lib/src/common/widget/routes.dart | 6 + example/lib/src/feature/home/home_screen.dart | 4 + .../src/feature/shaders/shaders_screen.dart | 142 ++++++++++++++++++ .../flutter/generated_plugin_registrant.cc | 4 + example/linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + example/pubspec.yaml | 4 + example/shaders/simple.glsl | 17 +++ .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + lib/repaint.dart | 1 + lib/src/resource_manager.dart | 17 +++ 12 files changed, 202 insertions(+) create mode 100644 example/lib/src/feature/shaders/shaders_screen.dart create mode 100644 example/shaders/simple.glsl create mode 100644 lib/src/resource_manager.dart diff --git a/example/lib/src/common/widget/routes.dart b/example/lib/src/common/widget/routes.dart index d1faa46..3ec6aaa 100644 --- a/example/lib/src/common/widget/routes.dart +++ b/example/lib/src/common/widget/routes.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:repaintexample/src/feature/clock/clock_screen.dart'; import 'package:repaintexample/src/feature/home/home_screen.dart'; +import 'package:repaintexample/src/feature/shaders/shaders_screen.dart'; /// The routes to navigate to. final Map Function(Map?)> $routes = @@ -15,4 +16,9 @@ final Map Function(Map?)> $routes = child: const ClockScreen(), arguments: arguments, ), + 'shaders': (arguments) => MaterialPage( + name: 'shaders', + child: const ShadersScreen(), + arguments: arguments, + ), }; diff --git a/example/lib/src/feature/home/home_screen.dart b/example/lib/src/feature/home/home_screen.dart index 729d4b5..995032f 100644 --- a/example/lib/src/feature/home/home_screen.dart +++ b/example/lib/src/feature/home/home_screen.dart @@ -28,6 +28,10 @@ class HomeScreen extends StatelessWidget { title: 'Clock', page: 'clock', ), + HomeTile( + title: 'Shaders', + page: 'shaders', + ), ], ), ), diff --git a/example/lib/src/feature/shaders/shaders_screen.dart b/example/lib/src/feature/shaders/shaders_screen.dart new file mode 100644 index 0000000..a1e34c6 --- /dev/null +++ b/example/lib/src/feature/shaders/shaders_screen.dart @@ -0,0 +1,142 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:repaint/repaint.dart'; +import 'package:repaintexample/src/common/widget/app.dart'; +import 'package:url_launcher/url_launcher_string.dart' as url_launcher; + +/// {@template shaders_screen} +/// ShadersScreen widget. +/// {@endtemplate} +class ShadersScreen extends StatelessWidget { + /// {@macro shaders_screen} + const ShadersScreen({ + super.key, // ignore: unused_element + }); + + @override + Widget build(BuildContext context) => Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + leading: BackButton( + onPressed: () => App.pop(context), + ), + title: const Text('Fragment Shaders'), + centerTitle: true, + floating: true, + snap: true, + actions: [ + IconButton( + tooltip: 'Flutter Docs', + icon: const Icon(Icons.book), + onPressed: () => url_launcher.launchUrlString( + 'https://docs.flutter.dev/ui/design/graphics/fragment-shaders'), + ), + IconButton( + tooltip: 'The Book of Shaders', + icon: const Icon(Icons.book), + onPressed: () => url_launcher + .launchUrlString('https://thebookofshaders.com'), + ), + ], + ), + ShaderContainer( + shader: 'simple', + blendMode: BlendMode.src, + render: (canvas, size, paint) => canvas.drawRect( + Offset.zero & size, + paint, + ), + ), + ], + ), + ); +} + +class ShaderContainer extends StatelessWidget { + const ShaderContainer({ + required this.shader, + required this.render, + this.blendMode = BlendMode.src, + this.height = 256, + this.frameRate = 24, + super.key, // ignore: unused_element + }); + + /// Shader file to load. + final String shader; + + /// Height of the container. + final double height; + + /// Render the shader. + final void Function(Canvas canvas, Size size, Paint paint) render; + + /// Blend mode for the shader. + final BlendMode blendMode; + + /// Frame rate for the shader. + final int? frameRate; + + @override + Widget build(BuildContext context) => SliverToBoxAdapter( + child: SizedBox( + height: 256, + child: FutureBuilder( + initialData: null, + future: ResourceManager.loadShader('shaders/$shader.glsl'), + builder: (context, snapshot) => switch (snapshot.connectionState) { + ConnectionState.waiting || + ConnectionState.active || + ConnectionState.none => + const Center( + child: CircularProgressIndicator(), + ), + ConnectionState.done => switch (snapshot.data) { + ui.FragmentShader fragmentShader => Stack( + children: [ + const Positioned.fill( + child: ColoredBox( + color: Colors.grey, + child: SizedBox.expand(), + ), + ), + Positioned.fill( + child: RePaint.inline( + frameRate: frameRate, + setUp: (box) => Paint() + ..blendMode = blendMode + ..shader = fragmentShader, + update: (_, paint, ___) => + paint..blendMode = blendMode, + render: (box, paint, canvas) => + render(canvas, box.size, paint), + ), + ), + Positioned( + top: 8, + left: 8, + height: 24, + right: 8, + child: Align( + alignment: Alignment.topLeft, + child: Text( + shader, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelLarge, + ), + ), + ), + ], + ), + null => const Center( + child: Text('Failed to load shader.'), + ), + }, + }, + ), + ), + ); +} diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..8236f57 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c1c6daa..73a94ec 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: async: any convert: any path: any + url_launcher: ^6.3.1 # Annotations meta: any @@ -59,3 +60,6 @@ dependency_overrides: flutter: uses-material-design: true + + shaders: + - shaders/simple.glsl \ No newline at end of file diff --git a/example/shaders/simple.glsl b/example/shaders/simple.glsl new file mode 100644 index 0000000..b2314fa --- /dev/null +++ b/example/shaders/simple.glsl @@ -0,0 +1,17 @@ +// https://thebookofshaders.com/02/ +// https://docs.flutter.dev/ui/design/graphics/fragment-shaders + +#version 460 core + +#ifdef GL_ES +precision mediump float; +#endif + +//#include + +out vec4 fragColor; + +void main() { + //vec2 currentPos = FlutterFragCoord().xy; + fragColor = vec4(1.0, 0.0, 0.5, 1.0); +} \ No newline at end of file diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/lib/repaint.dart b/lib/repaint.dart index 93f688d..b3852e3 100644 --- a/lib/repaint.dart +++ b/lib/repaint.dart @@ -3,3 +3,4 @@ library; export 'src/repaint.dart'; export 'src/repainter_base.dart'; export 'src/repainter_interface.dart'; +export 'src/resource_manager.dart'; diff --git a/lib/src/resource_manager.dart b/lib/src/resource_manager.dart new file mode 100644 index 0000000..7325ca5 --- /dev/null +++ b/lib/src/resource_manager.dart @@ -0,0 +1,17 @@ +// ignore_for_file: avoid_classes_with_only_static_members + +import 'dart:ui' as ui; + +/// A resource manager for loading and caching resources. +abstract final class ResourceManager { + /// A cache for loaded fragment shaders. + static final Map> _shaderCache = + >{}; + + /// Loads a shader from assets. + static Future loadShader(String assetPath) => + _shaderCache[assetPath] ??= Future(() async { + final program = await ui.FragmentProgram.fromAsset(assetPath); + return program.fragmentShader(); + }); +} From 5182f2c17d6f895ca4873b1b1b980a742a2d2b5a Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Wed, 4 Dec 2024 19:21:18 +0400 Subject: [PATCH 02/21] Rename shaders screen --- example/lib/src/common/widget/routes.dart | 8 +- example/lib/src/feature/home/home_screen.dart | 4 +- .../shaders/fragment_shaders_screen.dart | 158 ++++++++++++++++++ .../src/feature/shaders/shaders_screen.dart | 142 ---------------- 4 files changed, 164 insertions(+), 148 deletions(-) create mode 100644 example/lib/src/feature/shaders/fragment_shaders_screen.dart delete mode 100644 example/lib/src/feature/shaders/shaders_screen.dart diff --git a/example/lib/src/common/widget/routes.dart b/example/lib/src/common/widget/routes.dart index 3ec6aaa..2f3f518 100644 --- a/example/lib/src/common/widget/routes.dart +++ b/example/lib/src/common/widget/routes.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:repaintexample/src/feature/clock/clock_screen.dart'; import 'package:repaintexample/src/feature/home/home_screen.dart'; -import 'package:repaintexample/src/feature/shaders/shaders_screen.dart'; +import 'package:repaintexample/src/feature/shaders/fragment_shaders_screen.dart'; /// The routes to navigate to. final Map Function(Map?)> $routes = @@ -16,9 +16,9 @@ final Map Function(Map?)> $routes = child: const ClockScreen(), arguments: arguments, ), - 'shaders': (arguments) => MaterialPage( - name: 'shaders', - child: const ShadersScreen(), + 'fragment-shaders': (arguments) => MaterialPage( + name: 'fragment-shaders', + child: const FragmentShadersScreen(), arguments: arguments, ), }; diff --git a/example/lib/src/feature/home/home_screen.dart b/example/lib/src/feature/home/home_screen.dart index 995032f..8acac69 100644 --- a/example/lib/src/feature/home/home_screen.dart +++ b/example/lib/src/feature/home/home_screen.dart @@ -29,8 +29,8 @@ class HomeScreen extends StatelessWidget { page: 'clock', ), HomeTile( - title: 'Shaders', - page: 'shaders', + title: 'Fragment Shaders', + page: 'fragment-shaders', ), ], ), diff --git a/example/lib/src/feature/shaders/fragment_shaders_screen.dart b/example/lib/src/feature/shaders/fragment_shaders_screen.dart new file mode 100644 index 0000000..3ecab6f --- /dev/null +++ b/example/lib/src/feature/shaders/fragment_shaders_screen.dart @@ -0,0 +1,158 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:repaint/repaint.dart'; +import 'package:repaintexample/src/common/widget/app.dart'; +import 'package:url_launcher/url_launcher_string.dart' as url_launcher; + +/// {@template shaders_screen} +/// ShadersScreen widget. +/// {@endtemplate} +class FragmentShadersScreen extends StatelessWidget { + /// {@macro shaders_screen} + const FragmentShadersScreen({ + super.key, // ignore: unused_element + }); + + @override + Widget build(BuildContext context) => Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + leading: BackButton( + onPressed: () => App.pop(context), + ), + title: const Text('Fragment Shaders'), + centerTitle: true, + floating: true, + snap: true, + actions: [ + IconButton( + tooltip: 'Flutter Docs', + icon: const Icon(Icons.book), + onPressed: () => url_launcher.launchUrlString( + 'https://docs.flutter.dev/ui/design/graphics/fragment-shaders'), + ), + IconButton( + tooltip: 'The Book of Shaders', + icon: const Icon(Icons.book), + onPressed: () => url_launcher + .launchUrlString('https://thebookofshaders.com'), + ), + ], + ), + ShaderContainer( + shader: 'simple', + blendMode: BlendMode.src, + render: (canvas, size, paint) => canvas.drawRect( + Offset.zero & size, + paint, + ), + ), + ], + ), + ); +} + +class ShaderContainer extends StatelessWidget { + const ShaderContainer({ + required this.shader, + required this.render, + this.blendMode = BlendMode.src, + this.height = 256, + this.frameRate = 24, + super.key, // ignore: unused_element + }); + + /// Shader file to load. + final String shader; + + /// Height of the container. + final double height; + + /// Render the shader. + final void Function(Canvas canvas, Size size, Paint paint) render; + + /// Blend mode for the shader. + final BlendMode blendMode; + + /// Frame rate for the shader. + final int? frameRate; + + @override + Widget build(BuildContext context) => SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: SizedBox( + height: 256, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: FutureBuilder( + initialData: null, + future: ResourceManager.loadShader('shaders/$shader.glsl'), + builder: (context, snapshot) => + switch (snapshot.connectionState) { + ConnectionState.waiting || + ConnectionState.active || + ConnectionState.none => + const Center( + child: CircularProgressIndicator(), + ), + ConnectionState.done => switch (snapshot.data) { + ui.FragmentShader fragmentShader => Stack( + children: [ + const Positioned.fill( + child: ColoredBox( + color: Colors.grey, + child: SizedBox.expand(), + ), + ), + Positioned.fill( + child: RePaint.inline( + frameRate: frameRate, + setUp: (box) => Paint() + ..blendMode = blendMode + ..shader = fragmentShader, + update: (_, paint, ___) => + paint..blendMode = blendMode, + render: (box, paint, canvas) => + render(canvas, box.size, paint), + ), + ), + Positioned( + top: 8, + left: 8, + height: 24, + right: 8, + child: Align( + alignment: Alignment.topLeft, + child: Text( + shader, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelLarge, + ), + ), + ), + ], + ), + null => const Center( + child: Text('Failed to load shader.'), + ), + }, + }, + ), + ), + const Divider(height: 1, color: Colors.black26, thickness: 1), + ], + ), + ), + ), + ); +} diff --git a/example/lib/src/feature/shaders/shaders_screen.dart b/example/lib/src/feature/shaders/shaders_screen.dart deleted file mode 100644 index a1e34c6..0000000 --- a/example/lib/src/feature/shaders/shaders_screen.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:repaint/repaint.dart'; -import 'package:repaintexample/src/common/widget/app.dart'; -import 'package:url_launcher/url_launcher_string.dart' as url_launcher; - -/// {@template shaders_screen} -/// ShadersScreen widget. -/// {@endtemplate} -class ShadersScreen extends StatelessWidget { - /// {@macro shaders_screen} - const ShadersScreen({ - super.key, // ignore: unused_element - }); - - @override - Widget build(BuildContext context) => Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - leading: BackButton( - onPressed: () => App.pop(context), - ), - title: const Text('Fragment Shaders'), - centerTitle: true, - floating: true, - snap: true, - actions: [ - IconButton( - tooltip: 'Flutter Docs', - icon: const Icon(Icons.book), - onPressed: () => url_launcher.launchUrlString( - 'https://docs.flutter.dev/ui/design/graphics/fragment-shaders'), - ), - IconButton( - tooltip: 'The Book of Shaders', - icon: const Icon(Icons.book), - onPressed: () => url_launcher - .launchUrlString('https://thebookofshaders.com'), - ), - ], - ), - ShaderContainer( - shader: 'simple', - blendMode: BlendMode.src, - render: (canvas, size, paint) => canvas.drawRect( - Offset.zero & size, - paint, - ), - ), - ], - ), - ); -} - -class ShaderContainer extends StatelessWidget { - const ShaderContainer({ - required this.shader, - required this.render, - this.blendMode = BlendMode.src, - this.height = 256, - this.frameRate = 24, - super.key, // ignore: unused_element - }); - - /// Shader file to load. - final String shader; - - /// Height of the container. - final double height; - - /// Render the shader. - final void Function(Canvas canvas, Size size, Paint paint) render; - - /// Blend mode for the shader. - final BlendMode blendMode; - - /// Frame rate for the shader. - final int? frameRate; - - @override - Widget build(BuildContext context) => SliverToBoxAdapter( - child: SizedBox( - height: 256, - child: FutureBuilder( - initialData: null, - future: ResourceManager.loadShader('shaders/$shader.glsl'), - builder: (context, snapshot) => switch (snapshot.connectionState) { - ConnectionState.waiting || - ConnectionState.active || - ConnectionState.none => - const Center( - child: CircularProgressIndicator(), - ), - ConnectionState.done => switch (snapshot.data) { - ui.FragmentShader fragmentShader => Stack( - children: [ - const Positioned.fill( - child: ColoredBox( - color: Colors.grey, - child: SizedBox.expand(), - ), - ), - Positioned.fill( - child: RePaint.inline( - frameRate: frameRate, - setUp: (box) => Paint() - ..blendMode = blendMode - ..shader = fragmentShader, - update: (_, paint, ___) => - paint..blendMode = blendMode, - render: (box, paint, canvas) => - render(canvas, box.size, paint), - ), - ), - Positioned( - top: 8, - left: 8, - height: 24, - right: 8, - child: Align( - alignment: Alignment.topLeft, - child: Text( - shader, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelLarge, - ), - ), - ), - ], - ), - null => const Center( - child: Text('Failed to load shader.'), - ), - }, - }, - ), - ), - ); -} From 04378b232ef866ba4a01603fdc5f57f4d64917d9 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Wed, 4 Dec 2024 19:43:51 +0400 Subject: [PATCH 03/21] Rename shader --- .../src/feature/shaders/fragment_shaders_screen.dart | 2 +- example/pubspec.yaml | 2 +- example/shaders/{simple.glsl => simple.frag} | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) rename example/shaders/{simple.glsl => simple.frag} (62%) diff --git a/example/lib/src/feature/shaders/fragment_shaders_screen.dart b/example/lib/src/feature/shaders/fragment_shaders_screen.dart index 3ecab6f..718672f 100644 --- a/example/lib/src/feature/shaders/fragment_shaders_screen.dart +++ b/example/lib/src/feature/shaders/fragment_shaders_screen.dart @@ -93,7 +93,7 @@ class ShaderContainer extends StatelessWidget { Expanded( child: FutureBuilder( initialData: null, - future: ResourceManager.loadShader('shaders/$shader.glsl'), + future: ResourceManager.loadShader('shaders/$shader.frag'), builder: (context, snapshot) => switch (snapshot.connectionState) { ConnectionState.waiting || diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 73a94ec..8634310 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -62,4 +62,4 @@ flutter: uses-material-design: true shaders: - - shaders/simple.glsl \ No newline at end of file + - shaders/simple.frag \ No newline at end of file diff --git a/example/shaders/simple.glsl b/example/shaders/simple.frag similarity index 62% rename from example/shaders/simple.glsl rename to example/shaders/simple.frag index b2314fa..8749b97 100644 --- a/example/shaders/simple.glsl +++ b/example/shaders/simple.frag @@ -1,17 +1,17 @@ -// https://thebookofshaders.com/02/ -// https://docs.flutter.dev/ui/design/graphics/fragment-shaders - #version 460 core #ifdef GL_ES +// For mobile and web devices, precision mediump is recommended precision mediump float; #endif //#include -out vec4 fragColor; +out vec4 pc_fragColor; +// https://thebookofshaders.com/02/ +// https://docs.flutter.dev/ui/design/graphics/fragment-shaders void main() { //vec2 currentPos = FlutterFragCoord().xy; - fragColor = vec4(1.0, 0.0, 0.5, 1.0); + pc_fragColor = vec4(1.0, 0.0, 0.5, 1.0); // red, green, blue, alpha } \ No newline at end of file From 6dacc2dab8cafce29f65ac8aff9faa3517f93c80 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Wed, 4 Dec 2024 20:24:56 +0400 Subject: [PATCH 04/21] Update shader --- example/shaders/simple.frag | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example/shaders/simple.frag b/example/shaders/simple.frag index 8749b97..2b15f2c 100644 --- a/example/shaders/simple.frag +++ b/example/shaders/simple.frag @@ -1,4 +1,5 @@ -#version 460 core +#version 300 es // OpenGL ES 3.0 +//#version 460 core // OpenGL 4.6 Core Profile #ifdef GL_ES // For mobile and web devices, precision mediump is recommended @@ -11,7 +12,8 @@ out vec4 pc_fragColor; // https://thebookofshaders.com/02/ // https://docs.flutter.dev/ui/design/graphics/fragment-shaders +// https://github.com/Hixie/sky_engine/blob/master/impeller/compiler/shader_lib/flutter/runtime_effect.glsl void main() { //vec2 currentPos = FlutterFragCoord().xy; - pc_fragColor = vec4(1.0, 0.0, 0.5, 1.0); // red, green, blue, alpha + pc_fragColor = vec4(1.0f, 0.0f, 0.5f, 1.0f); // red, green, blue, alpha } \ No newline at end of file From 713905284a8070798484b128822748af32946ce0 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Wed, 4 Dec 2024 20:29:50 +0400 Subject: [PATCH 05/21] Add shaders settings to VS Code --- .vscode/extensions.json | 2 ++ .vscode/settings.json | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2a676cd..767a67f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,5 +3,7 @@ "dart-code.dart-code", "Dart-Code.flutter", "github.vscode-github-actions", + "raczzalan.webgl-glsl-editor", + "circledev.glsl-canvas" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c9f5533..d7b3b3e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -64,7 +64,7 @@ ".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*", "readme.*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md", "*.dart": "$(capture).g.dart, $(capture).freezed.dart, $(capture).config.dart" - } + }, /* "files.associations": { "*.drift": "sql" }, */ @@ -80,5 +80,19 @@ } ] } - } */ + }, */ + // -- Shaders -- // + "glsl-canvas.refreshOnChange": false, + "glsl-canvas.refreshOnSave": true, + "[glsl]": { + "editor.defaultFormatter": "raczzalan.webgl-glsl-editor" + }, + "[frag]": { + "editor.defaultFormatter": "raczzalan.webgl-glsl-editor" + }, + "[vert]": { + "editor.defaultFormatter": "raczzalan.webgl-glsl-editor" + }, + "editor.defaultFormatter": "raczzalan.webgl-glsl-editor", + "notebook.defaultFormatter": "raczzalan.webgl-glsl-editor", } \ No newline at end of file From 07a2e3d8b2610a5b560da75acaafaf3c37fc5fa9 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Wed, 4 Dec 2024 20:41:46 +0400 Subject: [PATCH 06/21] Add comment --- example/shaders/simple.frag | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/shaders/simple.frag b/example/shaders/simple.frag index 2b15f2c..aa87df2 100644 --- a/example/shaders/simple.frag +++ b/example/shaders/simple.frag @@ -12,7 +12,8 @@ out vec4 pc_fragColor; // https://thebookofshaders.com/02/ // https://docs.flutter.dev/ui/design/graphics/fragment-shaders -// https://github.com/Hixie/sky_engine/blob/master/impeller/compiler/shader_lib/flutter/runtime_effect.glsl +// https://github.com/Hixie/sky_engine/tree/master/impeller/entity/shaders +// https://github.com/Hixie/sky_engine/tree/master/impeller/compiler/shader_lib/flutter/runtime_effect.glsl void main() { //vec2 currentPos = FlutterFragCoord().xy; pc_fragColor = vec4(1.0f, 0.0f, 0.5f, 1.0f); // red, green, blue, alpha From e3ca381006d7e0d0d0f8caa0e9f711809552639d Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 00:27:02 +0400 Subject: [PATCH 07/21] Update --- .../shaders/fragment_shaders_screen.dart | 56 +++++++++++++++---- example/pubspec.yaml | 3 +- example/shaders/simple.frag | 12 +++- example/shaders/uniforms.frag | 37 ++++++++++++ example/test/math_test.dart | 49 ++++++++++++++++ 5 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 example/shaders/uniforms.frag create mode 100644 example/test/math_test.dart diff --git a/example/lib/src/feature/shaders/fragment_shaders_screen.dart b/example/lib/src/feature/shaders/fragment_shaders_screen.dart index 718672f..2716cad 100644 --- a/example/lib/src/feature/shaders/fragment_shaders_screen.dart +++ b/example/lib/src/feature/shaders/fragment_shaders_screen.dart @@ -1,3 +1,4 @@ +import 'dart:math' as math; // ignore: unused_import import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -43,12 +44,28 @@ class FragmentShadersScreen extends StatelessWidget { ), ShaderContainer( shader: 'simple', - blendMode: BlendMode.src, - render: (canvas, size, paint) => canvas.drawRect( + render: (canvas, size, shader, paint) => canvas.drawRect( Offset.zero & size, paint, ), ), + ShaderContainer( + shader: 'uniforms', + frameRate: null, + render: (canvas, size, shader, paint) { + final now = DateTime.now().millisecond / 1000; + // clamp(abs(0.5f + 0.5f * sin(time * PI / 4.0f)), 0.0f, 1.0f) + //print((math.sin(now.clamp(0, 1) / (2 * math.pi))).abs()); + shader + ..setFloat(0, size.width) + ..setFloat(1, size.height) + ..setFloat(2, now); + canvas.drawRect( + Offset.zero & size, + paint, + ); + }, + ), ], ), ); @@ -71,7 +88,12 @@ class ShaderContainer extends StatelessWidget { final double height; /// Render the shader. - final void Function(Canvas canvas, Size size, Paint paint) render; + final void Function( + Canvas canvas, + Size size, + ui.FragmentShader shader, + Paint paint, + ) render; /// Blend mode for the shader. final BlendMode blendMode; @@ -112,15 +134,27 @@ class ShaderContainer extends StatelessWidget { ), ), Positioned.fill( - child: RePaint.inline( + child: RePaint.inline< + ({ + ui.FragmentShader shader, + Paint paint + })>( frameRate: frameRate, - setUp: (box) => Paint() - ..blendMode = blendMode - ..shader = fragmentShader, - update: (_, paint, ___) => - paint..blendMode = blendMode, - render: (box, paint, canvas) => - render(canvas, box.size, paint), + setUp: (box) => ( + shader: fragmentShader, + paint: Paint() + ..blendMode = blendMode + ..shader = fragmentShader + ..filterQuality = FilterQuality.none + ..isAntiAlias = false, + ), + update: (_, state, ___) => + state..paint.blendMode = blendMode, + render: (box, state, canvas) => render( + canvas, + box.size, + state.shader, + state.paint), ), ), Positioned( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8634310..47faec6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -62,4 +62,5 @@ flutter: uses-material-design: true shaders: - - shaders/simple.frag \ No newline at end of file + - shaders/simple.frag + - shaders/uniforms.frag \ No newline at end of file diff --git a/example/shaders/simple.frag b/example/shaders/simple.frag index aa87df2..c007219 100644 --- a/example/shaders/simple.frag +++ b/example/shaders/simple.frag @@ -8,7 +8,13 @@ precision mediump float; //#include -out vec4 pc_fragColor; +out vec4 fragColor; + +// Returns a pink color +// https://www.rapidtables.com/web/color/pink-color.html +vec4 pink() { + return vec4(vec3(1.0f, 0.0f, 0.5f), 1.0f); +} // https://thebookofshaders.com/02/ // https://docs.flutter.dev/ui/design/graphics/fragment-shaders @@ -16,5 +22,5 @@ out vec4 pc_fragColor; // https://github.com/Hixie/sky_engine/tree/master/impeller/compiler/shader_lib/flutter/runtime_effect.glsl void main() { //vec2 currentPos = FlutterFragCoord().xy; - pc_fragColor = vec4(1.0f, 0.0f, 0.5f, 1.0f); // red, green, blue, alpha -} \ No newline at end of file + fragColor = pink(); // red, green, blue, alpha +} diff --git a/example/shaders/uniforms.frag b/example/shaders/uniforms.frag new file mode 100644 index 0000000..8fca3bc --- /dev/null +++ b/example/shaders/uniforms.frag @@ -0,0 +1,37 @@ +#version 300 es // OpenGL ES 3.0 + +#ifdef GL_ES +precision mediump float; +#endif + +//#include + +#define PI 3.1415926538 + +uniform vec2 iResolution; // viewport resolution (in pixels) +uniform float iTime; // time in seconds since the shader started + +out vec4 fragColor; + +float interpolate(float a, float b, float t) { + return a + (b - a) * t; +} + +float colorSin(float time) { + return (sin(time * 2.0f * PI) + 1.0f) / 2.0f; +} + +float colorCos(float time) { + return (cos(time * 2.0f * PI) + 1.0f) / 2.0f; +} + +float colorSinSlow(float time) { + return (sin(time * 2.0f * PI / 4.0f) + 1.0f) / 2.0f; +} + +void main() { + //vec2 currentPos = FlutterFragCoord().xy; + float r = colorSin(iTime); + float g = colorCos(iTime); + fragColor = vec4(r, g, .0f, 1.0f); +} \ No newline at end of file diff --git a/example/test/math_test.dart b/example/test/math_test.dart new file mode 100644 index 0000000..ac60b08 --- /dev/null +++ b/example/test/math_test.dart @@ -0,0 +1,49 @@ +/*float evalColor(float time, float scale, float min, float max) { + return ((sin(time / scale) + 1.0f) / 2.0f) * ((max - min) + min); +}*/ + +import 'dart:math' as math; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('math', () { + test('interpolation', () { + double interpolate(double a, double b, double t) => a + (b - a) * t; + expect(interpolate(0, 1, 0), 0); + expect(interpolate(0, 1, 1), 1); + expect(interpolate(0, 1, 0.5), 0.5); + expect(interpolate(0, 1, 0.25), 0.25); + expect(interpolate(0, 1, 0.75), 0.75); + print(interpolate(0, 4, 0.5)); + }); + + test('clamp', () { + double clamp(double value, double min, double max) => + math.min(math.max(value, min), max); + expect(clamp(0, 0, 1), 0); + expect(clamp(1, 0, 1), 1); + expect(clamp(0.5, 0, 1), 0.5); + expect(clamp(0.25, 0, 1), 0.25); + expect(clamp(0.75, 0, 1), 0.75); + }); + + test('Time sinus', () { + double evalColor( + double time, { + double scale = 4, + double min = 0, + double max = 1, + }) => + ((math.sin(time * scale * math.pi) + 1) / 2) * (max - min) + min; + final values = [ + for (var i = 0; i < 30; i++) (i % 10) / 10, + ]; + /* print(values + .map((e) => e) + .map(evalColor) + .map((e) => e.toStringAsFixed(2)) + .join('\n')); */ + }); + }); +} From 8a4e8419864085eb88d4edf876eba7bc924af4fe Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 01:01:20 +0400 Subject: [PATCH 08/21] Update podfile --- example/ios/Flutter/Debug.xcconfig | 1 + example/ios/Flutter/Release.xcconfig | 1 + example/ios/Podfile | 44 +++++++++++++++++++ example/macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + example/macos/Podfile | 43 ++++++++++++++++++ 6 files changed, 91 insertions(+) create mode 100644 example/ios/Podfile create mode 100644 example/macos/Podfile diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/example/macos/Flutter/Flutter-Release.xcconfig +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end From b191d8ee4984eaba654702c281bc2a757cf8f89c Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 01:13:02 +0400 Subject: [PATCH 09/21] Update colors --- .../shaders/fragment_shaders_screen.dart | 2 +- example/macos/Podfile.lock | 22 +++++ .../macos/Runner.xcodeproj/project.pbxproj | 98 ++++++++++++++++++- .../contents.xcworkspacedata | 3 + example/shaders/uniforms.frag | 9 +- 5 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 example/macos/Podfile.lock diff --git a/example/lib/src/feature/shaders/fragment_shaders_screen.dart b/example/lib/src/feature/shaders/fragment_shaders_screen.dart index 2716cad..28e862e 100644 --- a/example/lib/src/feature/shaders/fragment_shaders_screen.dart +++ b/example/lib/src/feature/shaders/fragment_shaders_screen.dart @@ -53,7 +53,7 @@ class FragmentShadersScreen extends StatelessWidget { shader: 'uniforms', frameRate: null, render: (canvas, size, shader, paint) { - final now = DateTime.now().millisecond / 1000; + final now = DateTime.now().millisecondsSinceEpoch / 10000 % 10; // clamp(abs(0.5f + 0.5f * sin(time * PI / 4.0f)), 0.0f, 1.0f) //print((math.sin(now.clamp(0, 1) / (2 * math.pi))).abs()); shader diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 0000000..5314c84 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - FlutterMacOS (1.0.0) + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.15.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 2fd66c8..04e2fb1 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 8B44CD05B61E648EE31B0A67 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D4AF172F016E35BE4FF6F61 /* Pods_RunnerTests.framework */; }; + DE63A9DB3D171A2FECA38BA6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49921E897DDF326A06AC5D46 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0541E01E14C5B3DCDBA83596 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 0CB76903A0D9FDB2109D3E88 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1F0F03426DD4B6A72FD389DB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* repaintexample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "repaintexample.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* repaintexample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = repaintexample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +81,13 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 41F4054FD8FCDD3E3858BC12 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 49921E897DDF326A06AC5D46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6D4AF172F016E35BE4FF6F61 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8207A0EEA88DB31D7A6901B0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C0F311E356E37D9241C0B153 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8B44CD05B61E648EE31B0A67 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DE63A9DB3D171A2FECA38BA6 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 86DE12BA381506768705D336 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 86DE12BA381506768705D336 /* Pods */ = { + isa = PBXGroup; + children = ( + C0F311E356E37D9241C0B153 /* Pods-Runner.debug.xcconfig */, + 41F4054FD8FCDD3E3858BC12 /* Pods-Runner.release.xcconfig */, + 0CB76903A0D9FDB2109D3E88 /* Pods-Runner.profile.xcconfig */, + 1F0F03426DD4B6A72FD389DB /* Pods-RunnerTests.debug.xcconfig */, + 8207A0EEA88DB31D7A6901B0 /* Pods-RunnerTests.release.xcconfig */, + 0541E01E14C5B3DCDBA83596 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 49921E897DDF326A06AC5D46 /* Pods_Runner.framework */, + 6D4AF172F016E35BE4FF6F61 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 1A85B5CF9755F830D0B700F9 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 63F2E4DFFB60A160154AC858 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + D45A4D70A90267133238469C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -291,6 +323,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1A85B5CF9755F830D0B700F9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -329,6 +383,45 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 63F2E4DFFB60A160154AC858 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D45A4D70A90267133238469C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1F0F03426DD4B6A72FD389DB /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8207A0EEA88DB31D7A6901B0 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0541E01E14C5B3DCDBA83596 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/example/shaders/uniforms.frag b/example/shaders/uniforms.frag index 8fca3bc..62022a6 100644 --- a/example/shaders/uniforms.frag +++ b/example/shaders/uniforms.frag @@ -25,13 +25,10 @@ float colorCos(float time) { return (cos(time * 2.0f * PI) + 1.0f) / 2.0f; } -float colorSinSlow(float time) { - return (sin(time * 2.0f * PI / 4.0f) + 1.0f) / 2.0f; -} - void main() { //vec2 currentPos = FlutterFragCoord().xy; float r = colorSin(iTime); - float g = colorCos(iTime); - fragColor = vec4(r, g, .0f, 1.0f); + float g = colorCos(iTime / 4.0f); + float b = interpolate(0.25f, 0.75f, colorSin(iTime / 2.0f)) + 0.25f; + fragColor = vec4(r, g, b, 1.0f); } \ No newline at end of file From 0de1cee36b8ea55985a82dd1573b14f7d799af84 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 01:54:42 +0400 Subject: [PATCH 10/21] Update shaders --- .../shaders/fragment_shaders_screen.dart | 141 ++++++++++-------- example/shaders/uniforms.frag | 60 +++++++- 2 files changed, 130 insertions(+), 71 deletions(-) diff --git a/example/lib/src/feature/shaders/fragment_shaders_screen.dart b/example/lib/src/feature/shaders/fragment_shaders_screen.dart index 28e862e..d17a9fd 100644 --- a/example/lib/src/feature/shaders/fragment_shaders_screen.dart +++ b/example/lib/src/feature/shaders/fragment_shaders_screen.dart @@ -44,7 +44,7 @@ class FragmentShadersScreen extends StatelessWidget { ), ShaderContainer( shader: 'simple', - render: (canvas, size, shader, paint) => canvas.drawRect( + render: (canvas, size, _, shader, paint) => canvas.drawRect( Offset.zero & size, paint, ), @@ -52,14 +52,16 @@ class FragmentShadersScreen extends StatelessWidget { ShaderContainer( shader: 'uniforms', frameRate: null, - render: (canvas, size, shader, paint) { + render: (canvas, size, mouse, shader, paint) { final now = DateTime.now().millisecondsSinceEpoch / 10000 % 10; // clamp(abs(0.5f + 0.5f * sin(time * PI / 4.0f)), 0.0f, 1.0f) //print((math.sin(now.clamp(0, 1) / (2 * math.pi))).abs()); shader ..setFloat(0, size.width) ..setFloat(1, size.height) - ..setFloat(2, now); + ..setFloat(2, mouse.dx) + ..setFloat(3, mouse.dy) + ..setFloat(4, now); canvas.drawRect( Offset.zero & size, paint, @@ -91,6 +93,7 @@ class ShaderContainer extends StatelessWidget { final void Function( Canvas canvas, Size size, + Offset mouse, ui.FragmentShader shader, Paint paint, ) render; @@ -102,38 +105,43 @@ class ShaderContainer extends StatelessWidget { final int? frameRate; @override - Widget build(BuildContext context) => SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverToBoxAdapter( - child: SizedBox( - height: 256, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: FutureBuilder( - initialData: null, - future: ResourceManager.loadShader('shaders/$shader.frag'), - builder: (context, snapshot) => - switch (snapshot.connectionState) { - ConnectionState.waiting || - ConnectionState.active || - ConnectionState.none => - const Center( - child: CircularProgressIndicator(), - ), - ConnectionState.done => switch (snapshot.data) { - ui.FragmentShader fragmentShader => Stack( - children: [ - const Positioned.fill( - child: ColoredBox( - color: Colors.grey, - child: SizedBox.expand(), - ), + Widget build(BuildContext context) { + var mouse = Offset.zero; + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: SizedBox( + height: 256, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: FutureBuilder( + initialData: null, + future: ResourceManager.loadShader('shaders/$shader.frag'), + builder: (context, snapshot) => + switch (snapshot.connectionState) { + ConnectionState.waiting || + ConnectionState.active || + ConnectionState.none => + const Center( + child: CircularProgressIndicator(), + ), + ConnectionState.done => switch (snapshot.data) { + ui.FragmentShader fragmentShader => Stack( + children: [ + const Positioned.fill( + child: ColoredBox( + color: Colors.grey, + child: SizedBox.expand(), ), - Positioned.fill( + ), + Positioned.fill( + child: MouseRegion( + onHover: (event) => + mouse = event.localPosition, child: RePaint.inline< ({ ui.FragmentShader shader, @@ -151,42 +159,45 @@ class ShaderContainer extends StatelessWidget { update: (_, state, ___) => state..paint.blendMode = blendMode, render: (box, state, canvas) => render( - canvas, - box.size, - state.shader, - state.paint), + canvas, + box.size, + mouse, + state.shader, + state.paint, + ), ), ), - Positioned( - top: 8, - left: 8, - height: 24, - right: 8, - child: Align( - alignment: Alignment.topLeft, - child: Text( - shader, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .labelLarge, - ), + ), + Positioned( + top: 8, + left: 8, + height: 24, + right: 8, + child: Align( + alignment: Alignment.topLeft, + child: Text( + shader, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: + Theme.of(context).textTheme.labelLarge, ), ), - ], - ), - null => const Center( - child: Text('Failed to load shader.'), - ), - }, - }, - ), + ), + ], + ), + null => const Center( + child: Text('Failed to load shader.'), + ), + }, + }, ), - const Divider(height: 1, color: Colors.black26, thickness: 1), - ], - ), + ), + const Divider(height: 1, color: Colors.black26, thickness: 1), + ], ), ), - ); + ), + ); + } } diff --git a/example/shaders/uniforms.frag b/example/shaders/uniforms.frag index 62022a6..3b07829 100644 --- a/example/shaders/uniforms.frag +++ b/example/shaders/uniforms.frag @@ -4,11 +4,12 @@ precision mediump float; #endif -//#include +#include #define PI 3.1415926538 uniform vec2 iResolution; // viewport resolution (in pixels) +uniform vec2 iMouse; // mouse pixel coords uniform float iTime; // time in seconds since the shader started out vec4 fragColor; @@ -26,9 +27,56 @@ float colorCos(float time) { } void main() { - //vec2 currentPos = FlutterFragCoord().xy; - float r = colorSin(iTime); - float g = colorCos(iTime / 4.0f); - float b = interpolate(0.25f, 0.75f, colorSin(iTime / 2.0f)) + 0.25f; - fragColor = vec4(r, g, b, 1.0f); + // gl_FragCoord.xy - координаты текущего пикселя в пикселях + // Отсчет начинается в левом нижнем углу + //vec2 uv = gl_FragCoord.xy / iResolution; // normalized coordinates + + // Flutter меряет координаты в пикселях, а не в диапазоне от 0.0 до 1.0 + // Отсчет начинается в левом верхнем углу + vec2 pos = FlutterFragCoord().xy; // current pixel coordinates + + // Нормализуем координаты в диапазон от 0.0 до 1.0 + vec2 uv = pos / iResolution; // normalized coordinates + + // Цвета зависят от времени + float r = colorSin(iTime); // red (0.0 to 1.0) + float g = colorCos(iTime / 4.0f); // green (0.0 to 1.0) + float b = interpolate(0.5f, 1.0f, colorSin(iTime / 2.0f)); // blue (0.5 to 1.0) + + // Разбиваем экран на 4 части, каждая часть будет рисовать что-то свое + if(uv.x < 0.5f && uv.y < 0.5f) { + // Верхний левый угол + // Заливаем градиентом зависимым от времени + fragColor = vec4(r, g, b, 1.0f); + } else if(uv.x > 0.5f && uv.y < 0.5f) { + // Верхний правый угол + // Рисум круг вокруг мыши с радиусом 5 пикселей + vec2 mouse = iMouse / iResolution; // normalized mouse coordinates + if(mouse.x > 0.5f && mouse.y < 0.5f) { + float dst = distance(iMouse.xy, pos.xy); + if(dst < 5.0f) { + // Дистанция от мыши до пикселя меньше 5 пикселей - белый цвет + fragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f); + } else { + // Дистанция от мыши до пикселя больше 5 пикселей - черный цвет + fragColor = vec4(0, 0, 0, 1.0f); + } + } + } else if(uv.x < 0.5f && uv.y > 0.5f) { + // Нижний левый угол + // Высвечиваем расстояние от мыши до центра прямоугольника + vec2 mouse = iMouse / iResolution; // normalized mouse coordinates + if(mouse.x < 0.5f && mouse.y > 0.5f) { + float dst = distance(mouse.xy, vec2(0.25f, 0.75f)); + // Чем ближе мышь к центру, тем светлее цвет + fragColor = vec4(1 - (dst * 4), 1 - (dst * 4), 1 - (dst * 4), 1.0f); + } else { + // Если мышь не в нижнем левом углу - черный цвет + fragColor = vec4(0, 0, 0, 1.0f); + } + } else if(uv.x > 0.5f && uv.y > 0.5f) { + // Нижний правый угол + // Заливаем цветом зависимым от координаты и времени + fragColor = vec4(uv.x * 4 * r, uv.y * 4 * g, b, 1.0f); + } } \ No newline at end of file From 81b40331678b9b64475f656dd93fb270e890849f Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 02:17:21 +0400 Subject: [PATCH 11/21] Update shaders --- .../shaders/fragment_shaders_screen.dart | 33 ++++++++++++++----- example/shaders/uniforms.frag | 18 +++++----- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/example/lib/src/feature/shaders/fragment_shaders_screen.dart b/example/lib/src/feature/shaders/fragment_shaders_screen.dart index d17a9fd..e87a628 100644 --- a/example/lib/src/feature/shaders/fragment_shaders_screen.dart +++ b/example/lib/src/feature/shaders/fragment_shaders_screen.dart @@ -145,7 +145,7 @@ class ShaderContainer extends StatelessWidget { child: RePaint.inline< ({ ui.FragmentShader shader, - Paint paint + Paint paint, })>( frameRate: frameRate, setUp: (box) => ( @@ -174,13 +174,30 @@ class ShaderContainer extends StatelessWidget { height: 24, right: 8, child: Align( - alignment: Alignment.topLeft, - child: Text( - shader, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: - Theme.of(context).textTheme.labelLarge, + alignment: Alignment.centerLeft, + child: DecoratedBox( + position: DecorationPosition.background, + decoration: ShapeDecoration( + color: Colors.black54, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + shader, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + height: 1, + color: Colors.white, + ), + ), + ), ), ), ), diff --git a/example/shaders/uniforms.frag b/example/shaders/uniforms.frag index 3b07829..a7250e3 100644 --- a/example/shaders/uniforms.frag +++ b/example/shaders/uniforms.frag @@ -52,15 +52,17 @@ void main() { // Верхний правый угол // Рисум круг вокруг мыши с радиусом 5 пикселей vec2 mouse = iMouse / iResolution; // normalized mouse coordinates + float dst = 10.0f; if(mouse.x > 0.5f && mouse.y < 0.5f) { - float dst = distance(iMouse.xy, pos.xy); - if(dst < 5.0f) { - // Дистанция от мыши до пикселя меньше 5 пикселей - белый цвет - fragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f); - } else { - // Дистанция от мыши до пикселя больше 5 пикселей - черный цвет - fragColor = vec4(0, 0, 0, 1.0f); - } + // Мышь в верхнем правом углу + dst = abs(distance(iMouse.xy, pos.xy)); // distance from mouse to pixel + } + if(dst < 5.0f) { + // Дистанция от мыши до пикселя меньше 5 пикселей - рисуем круг + fragColor = vec4(uv.x, uv.y, uv.x * uv.y, 1.0f); + } else { + // Дистанция от мыши до пикселя больше 5 пикселей - инвертируем градиент + fragColor = vec4(1.0f - uv.x, 1.0f - uv.y, 1.0f - uv.x * uv.y, 1.0f); } } else if(uv.x < 0.5f && uv.y > 0.5f) { // Нижний левый угол From 5105759f1a54b1486184f3f6ee0dca605445bd18 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 15:44:35 +0400 Subject: [PATCH 12/21] Fix uniforms shaders --- example/shaders/uniforms.frag | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/example/shaders/uniforms.frag b/example/shaders/uniforms.frag index a7250e3..3a43061 100644 --- a/example/shaders/uniforms.frag +++ b/example/shaders/uniforms.frag @@ -4,7 +4,7 @@ precision mediump float; #endif -#include +//#include #define PI 3.1415926538 @@ -29,11 +29,11 @@ float colorCos(float time) { void main() { // gl_FragCoord.xy - координаты текущего пикселя в пикселях // Отсчет начинается в левом нижнем углу - //vec2 uv = gl_FragCoord.xy / iResolution; // normalized coordinates + vec2 pos = gl_FragCoord.xy; // current pixel coordinates // Flutter меряет координаты в пикселях, а не в диапазоне от 0.0 до 1.0 // Отсчет начинается в левом верхнем углу - vec2 pos = FlutterFragCoord().xy; // current pixel coordinates + //vec2 pos = FlutterFragCoord().xy; // current pixel coordinates // Нормализуем координаты в диапазон от 0.0 до 1.0 vec2 uv = pos / iResolution; // normalized coordinates @@ -71,7 +71,7 @@ void main() { if(mouse.x < 0.5f && mouse.y > 0.5f) { float dst = distance(mouse.xy, vec2(0.25f, 0.75f)); // Чем ближе мышь к центру, тем светлее цвет - fragColor = vec4(1 - (dst * 4), 1 - (dst * 4), 1 - (dst * 4), 1.0f); + fragColor = vec4(1.0f - (dst * 4.0f), 1.0f - (dst * 4.0f), 1.0f - (dst * 4.0f), 1.0f); } else { // Если мышь не в нижнем левом углу - черный цвет fragColor = vec4(0, 0, 0, 1.0f); @@ -79,6 +79,6 @@ void main() { } else if(uv.x > 0.5f && uv.y > 0.5f) { // Нижний правый угол // Заливаем цветом зависимым от координаты и времени - fragColor = vec4(uv.x * 4 * r, uv.y * 4 * g, b, 1.0f); + fragColor = vec4(uv.x * 4.0f * r, uv.y * 4.0f * g, b, 1.0f); } } \ No newline at end of file From f7c8d4da9d2eb1eeaab66bbd3cbf77c78dbb1d69 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 16:06:37 +0400 Subject: [PATCH 13/21] Fix tests --- example/lib/src/feature/shaders/fragment_shaders_screen.dart | 3 ++- example/shaders/uniforms.frag | 2 +- example/test/math_test.dart | 5 ++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/lib/src/feature/shaders/fragment_shaders_screen.dart b/example/lib/src/feature/shaders/fragment_shaders_screen.dart index e87a628..62733c4 100644 --- a/example/lib/src/feature/shaders/fragment_shaders_screen.dart +++ b/example/lib/src/feature/shaders/fragment_shaders_screen.dart @@ -44,6 +44,7 @@ class FragmentShadersScreen extends StatelessWidget { ), ShaderContainer( shader: 'simple', + frameRate: 24, render: (canvas, size, _, shader, paint) => canvas.drawRect( Offset.zero & size, paint, @@ -51,7 +52,7 @@ class FragmentShadersScreen extends StatelessWidget { ), ShaderContainer( shader: 'uniforms', - frameRate: null, + frameRate: 60, render: (canvas, size, mouse, shader, paint) { final now = DateTime.now().millisecondsSinceEpoch / 10000 % 10; // clamp(abs(0.5f + 0.5f * sin(time * PI / 4.0f)), 0.0f, 1.0f) diff --git a/example/shaders/uniforms.frag b/example/shaders/uniforms.frag index 3a43061..c281a69 100644 --- a/example/shaders/uniforms.frag +++ b/example/shaders/uniforms.frag @@ -79,6 +79,6 @@ void main() { } else if(uv.x > 0.5f && uv.y > 0.5f) { // Нижний правый угол // Заливаем цветом зависимым от координаты и времени - fragColor = vec4(uv.x * 4.0f * r, uv.y * 4.0f * g, b, 1.0f); + fragColor = vec4(uv.x * r, uv.y * g, b, 1.0f); } } \ No newline at end of file diff --git a/example/test/math_test.dart b/example/test/math_test.dart index ac60b08..d38d1e9 100644 --- a/example/test/math_test.dart +++ b/example/test/math_test.dart @@ -15,7 +15,6 @@ void main() { expect(interpolate(0, 1, 0.5), 0.5); expect(interpolate(0, 1, 0.25), 0.25); expect(interpolate(0, 1, 0.75), 0.75); - print(interpolate(0, 4, 0.5)); }); test('clamp', () { @@ -28,7 +27,7 @@ void main() { expect(clamp(0.75, 0, 1), 0.75); }); - test('Time sinus', () { + /* test('Time sinus', () { double evalColor( double time, { double scale = 4, @@ -44,6 +43,6 @@ void main() { .map(evalColor) .map((e) => e.toStringAsFixed(2)) .join('\n')); */ - }); + }); */ }); } From 58a4cb2be89f393242a71a52399e68c7fe0886b4 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 21:05:32 +0400 Subject: [PATCH 14/21] Update frame rate setup --- .vscode/launch.json | 12 ++ example/lib/src/common/widget/routes.dart | 6 + example/lib/src/feature/fps/fps_screen.dart | 158 ++++++++++++++++++ example/lib/src/feature/home/home_screen.dart | 4 + lib/src/repaint.dart | 51 +++++- test/unit_test.dart | 34 ++++ 6 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 example/lib/src/feature/fps/fps_screen.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 3bdd6b8..34a8c7e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,18 @@ "--dart-define-from-file=config/development.json", ], "env": {} + }, + { + "name": "Example (Release)", + "type": "dart", + "program": "lib/main.dart", + "request": "launch", + "flutterMode": "release", + "cwd": "${workspaceFolder}/example", + "args": [ + "--dart-define-from-file=config/development.json", + ], + "env": {} } ] } \ No newline at end of file diff --git a/example/lib/src/common/widget/routes.dart b/example/lib/src/common/widget/routes.dart index 2f3f518..6e3637f 100644 --- a/example/lib/src/common/widget/routes.dart +++ b/example/lib/src/common/widget/routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:repaintexample/src/feature/clock/clock_screen.dart'; +import 'package:repaintexample/src/feature/fps/fps_screen.dart'; import 'package:repaintexample/src/feature/home/home_screen.dart'; import 'package:repaintexample/src/feature/shaders/fragment_shaders_screen.dart'; @@ -21,4 +22,9 @@ final Map Function(Map?)> $routes = child: const FragmentShadersScreen(), arguments: arguments, ), + 'fps': (arguments) => MaterialPage( + name: 'fps', + child: const FpsScreen(), + arguments: arguments, + ), }; diff --git a/example/lib/src/feature/fps/fps_screen.dart b/example/lib/src/feature/fps/fps_screen.dart new file mode 100644 index 0000000..b58627a --- /dev/null +++ b/example/lib/src/feature/fps/fps_screen.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:repaint/repaint.dart'; +import 'package:repaintexample/src/common/widget/app.dart'; + +/// {@template fps_screen} +/// FpsScreen widget. +/// {@endtemplate} +class FpsScreen extends StatefulWidget { + /// {@macro fps_screen} + const FpsScreen({ + super.key, // ignore: unused_element + }); + + @override + State createState() => _FpsScreenState(); +} + +class _FpsScreenState extends State { + Widget fps(int? frameRate) { + int second = 0; + int counter = 0; + int result = 0; + return SizedBox.square( + dimension: 128, + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + Positioned.fill( + child: RePaint.inline( + frameRate: frameRate, + render: (box, state, canvas) { + final now = DateTime.now(); + if (now.second != second) { + second = now.second; + result = counter; + counter = 0; + } else { + counter++; + } + canvas.drawRect( + Offset.zero & box.size, + Paint() + ..color = Colors.black + ..style = PaintingStyle.fill, + ); + final text = TextPainter( + maxLines: 1, + text: TextSpan( + text: 'FPS: $result', + style: const TextStyle( + height: 1, + color: Colors.white, + fontSize: 24, + overflow: TextOverflow.ellipsis, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + text.paint( + canvas, + Offset( + box.size.width / 2 - text.width / 2, + box.size.height / 2 - text.height / 2, + ), + ); + }, + ), + ), + Positioned( + left: 8, + top: 8, + height: 12, + right: 8, + child: Text( + 'Frame rate: ${frameRate ?? 'unlimited'}', + style: const TextStyle( + height: 1, + color: Colors.white, + fontSize: 10, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('FPS'), + leading: BackButton( + onPressed: () => App.pop(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.center, + child: SizedBox( + width: 128 * 4 + 8 * 3, + height: 128 * 2 + 8, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + width: 128 * 4 + 8 * 3, + height: 128, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: fps(0)), + const SizedBox(width: 8), + Expanded(child: fps(15)), + const SizedBox(width: 8), + Expanded(child: fps(24)), + const SizedBox(width: 8), + Expanded(child: fps(30)), + ], + ), + ), + const SizedBox(height: 8), + SizedBox( + width: 128 * 4 + 8 * 3, + height: 128, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: fps(60)), + const SizedBox(width: 8), + Expanded(child: fps(90)), + const SizedBox(width: 8), + Expanded(child: fps(120)), + const SizedBox(width: 8), + Expanded(child: fps(null)), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); +} diff --git a/example/lib/src/feature/home/home_screen.dart b/example/lib/src/feature/home/home_screen.dart index 8acac69..71dbe0b 100644 --- a/example/lib/src/feature/home/home_screen.dart +++ b/example/lib/src/feature/home/home_screen.dart @@ -24,6 +24,10 @@ class HomeScreen extends StatelessWidget { itemExtent: 128, delegate: SliverChildListDelegate( const [ + HomeTile( + title: 'FPS', + page: 'fps', + ), HomeTile( title: 'Clock', page: 'clock', diff --git a/lib/src/repaint.dart b/lib/src/repaint.dart index 43dac98..5a4bf1b 100644 --- a/lib/src/repaint.dart +++ b/lib/src/repaint.dart @@ -17,7 +17,8 @@ class RePaint extends LeafRenderObjectWidget { /// Create a new [RePaint] widget with an inline controller. /// The [T] is the custom state type. - /// The [frameRate] is used to limit the frame rate. + /// The [frameRate] is used to limit the frame rate, (limitter and throttler). + /// After the [frameRate] is set, the real frame rate will be lower. /// The [setUp] is called when the controller is attached to the render box. /// The [update] is called periodically by the loop. /// The [render] is called to render the scene after the update. @@ -177,28 +178,62 @@ class RePaintBox extends RenderBox with WidgetsBindingObserver { } /// Total amount of time passed since the game loop was started. - Duration _previous = Duration.zero; + Duration _lastFrameTime = Duration.zero; + int _frameCount = 0; /// This method is periodically invoked by the [_ticker]. void _onTick(Duration elapsed) { if (!attached) return; - final delta = - (elapsed - _previous).inMicroseconds / Duration.microsecondsPerSecond; + final delta = elapsed - _lastFrameTime; + final deltaMs = delta.inMicroseconds / Duration.microsecondsPerMillisecond; switch (_painter.frameRate) { case null: // No frame rate limit. + _lastFrameTime = elapsed; break; case <= 0: // Skip updating and rendering the game scene. - _previous = elapsed; + _lastFrameTime = elapsed; return; - case int fr when fr > 0 && fr > delta * 1000: + case int fr when fr > 0: + // Show the next frame if the time has come. + // Вычисляем интервал между кадрами + final frameDuration = Duration(milliseconds: (1000 / fr).round()); + if (_lastFrameTime == Duration.zero) { + _lastFrameTime = elapsed; + _frameCount = 1; + break; // Show the first frame. + } + // Вычисляем текущую секунду + final currentSecond = elapsed.inSeconds; + final lastFrameSecond = _lastFrameTime.inSeconds; + + // Если мы перешли в новую секунду + if (currentSecond > lastFrameSecond) { + // Сбрасываем счетчик кадров + _frameCount = 0; + } + + // Проверяем, прошло ли достаточно времени с последнего кадра + final timeSinceLastFrame = elapsed - _lastFrameTime; + + // Вычисляем теоретически допустимое количество кадров в текущей секунде + final expectedFramesThisSecond = (currentSecond + 1) * fr; + + // Если текущее количество кадров меньше ожидаемого + // И прошло достаточно времени с последнего кадра + if (_frameCount < expectedFramesThisSecond / (currentSecond + 1) && + timeSinceLastFrame >= frameDuration) { + _lastFrameTime = elapsed; + _frameCount++; + break; // Show the next frame. + } + // Limit frame rate return; } - _previous = elapsed; // Update game scene and prepare for rendering. - _painter.update(this, elapsed, delta); + _painter.update(this, elapsed, deltaMs); markNeedsPaint(); // Mark this game scene as dirty and schedule a repaint. } diff --git a/test/unit_test.dart b/test/unit_test.dart index 590652a..5cd31b1 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -5,6 +5,40 @@ import 'package:test/test.dart'; import 'src/repainter_fake.dart'; void main() { + group('Time', () { + test('Seconds', () { + const duration = Duration(seconds: 5); + expect( + duration.inSeconds * Duration.millisecondsPerSecond, + equals(5 * 1000), + ); + expect( + duration.inSeconds * + Duration.millisecondsPerSecond * + Duration.microsecondsPerMillisecond, + equals(5 * 1000 * 1000), + ); + expect( + duration.inSeconds * Duration.microsecondsPerSecond, + equals(5 * 1000 * 1000), + ); + }); + + test('Delta', () { + const elapsed = Duration(seconds: 15); + const previous = Duration(seconds: 14); + final delta = elapsed - previous; + final ms = delta.inMicroseconds / Duration.microsecondsPerMillisecond; + expect(ms, equals(1000)); + }); + + test('Frame rate', () { + const frameRate = 60; + const deltaMs = 1000 / frameRate; + expect(deltaMs, equals(1000 / 60)); + }); + }); + group('Unit', () { test('Instance', () { final widget = RePaint(painter: RePainterFake()); From c7c698e8511cc783552efdde8c193c27ee160c39 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 21:16:23 +0400 Subject: [PATCH 15/21] Update fps --- example/lib/src/feature/fps/fps_screen.dart | 3 +-- lib/src/repaint.dart | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/lib/src/feature/fps/fps_screen.dart b/example/lib/src/feature/fps/fps_screen.dart index b58627a..a622fd2 100644 --- a/example/lib/src/feature/fps/fps_screen.dart +++ b/example/lib/src/feature/fps/fps_screen.dart @@ -35,9 +35,8 @@ class _FpsScreenState extends State { second = now.second; result = counter; counter = 0; - } else { - counter++; } + counter++; canvas.drawRect( Offset.zero & box.size, Paint() diff --git a/lib/src/repaint.dart b/lib/src/repaint.dart index 5a4bf1b..eabb19d 100644 --- a/lib/src/repaint.dart +++ b/lib/src/repaint.dart @@ -196,6 +196,7 @@ class RePaintBox extends RenderBox with WidgetsBindingObserver { _lastFrameTime = elapsed; return; case int fr when fr > 0: + // Show the next frame if the time has come. // Вычисляем интервал между кадрами final frameDuration = Duration(milliseconds: (1000 / fr).round()); @@ -204,6 +205,7 @@ class RePaintBox extends RenderBox with WidgetsBindingObserver { _frameCount = 1; break; // Show the first frame. } + // Вычисляем текущую секунду final currentSecond = elapsed.inSeconds; final lastFrameSecond = _lastFrameTime.inSeconds; From b13d2aa2f22b107de0f00fbebf6b7943c06389b7 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 21:24:05 +0400 Subject: [PATCH 16/21] Add information about frame Rate. --- lib/src/repaint.dart | 48 ++++++-------------------------- lib/src/repainter_interface.dart | 7 ++++- 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/lib/src/repaint.dart b/lib/src/repaint.dart index eabb19d..e72d6f7 100644 --- a/lib/src/repaint.dart +++ b/lib/src/repaint.dart @@ -18,13 +18,15 @@ class RePaint extends LeafRenderObjectWidget { /// Create a new [RePaint] widget with an inline controller. /// The [T] is the custom state type. /// The [frameRate] is used to limit the frame rate, (limitter and throttler). - /// After the [frameRate] is set, the real frame rate will be lower. /// The [setUp] is called when the controller is attached to the render box. /// The [update] is called periodically by the loop. /// The [render] is called to render the scene after the update. /// The [tearDown] is called to unmount and dispose the controller. /// The [key] is used to identify the widget. /// + /// After the [frameRate] is set, the real frame rate will be lower. + /// Before the frame rate, updates are limited by the flutter ticker, + /// so the resulting frame rate will be noticeably lower. /// {@macro repaint} static Widget inline({ required void Function(RePaintBox box, T state, Canvas canvas) render, @@ -145,7 +147,7 @@ class RePaintBox extends RenderBox with WidgetsBindingObserver { ..lifecycle( WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed); WidgetsBinding.instance.addObserver(this); - _ticker = Ticker(_onTick)..start(); + _ticker = Ticker(_onTick, debugLabel: 'RePaintBox')..start(); } @override @@ -179,7 +181,6 @@ class RePaintBox extends RenderBox with WidgetsBindingObserver { /// Total amount of time passed since the game loop was started. Duration _lastFrameTime = Duration.zero; - int _frameCount = 0; /// This method is periodically invoked by the [_ticker]. void _onTick(Duration elapsed) { @@ -196,43 +197,10 @@ class RePaintBox extends RenderBox with WidgetsBindingObserver { _lastFrameTime = elapsed; return; case int fr when fr > 0: - - // Show the next frame if the time has come. - // Вычисляем интервал между кадрами - final frameDuration = Duration(milliseconds: (1000 / fr).round()); - if (_lastFrameTime == Duration.zero) { - _lastFrameTime = elapsed; - _frameCount = 1; - break; // Show the first frame. - } - - // Вычисляем текущую секунду - final currentSecond = elapsed.inSeconds; - final lastFrameSecond = _lastFrameTime.inSeconds; - - // Если мы перешли в новую секунду - if (currentSecond > lastFrameSecond) { - // Сбрасываем счетчик кадров - _frameCount = 0; - } - - // Проверяем, прошло ли достаточно времени с последнего кадра - final timeSinceLastFrame = elapsed - _lastFrameTime; - - // Вычисляем теоретически допустимое количество кадров в текущей секунде - final expectedFramesThisSecond = (currentSecond + 1) * fr; - - // Если текущее количество кадров меньше ожидаемого - // И прошло достаточно времени с последнего кадра - if (_frameCount < expectedFramesThisSecond / (currentSecond + 1) && - timeSinceLastFrame >= frameDuration) { - _lastFrameTime = elapsed; - _frameCount++; - break; // Show the next frame. - } - - // Limit frame rate - return; + final targetFrameTime = 1000 / fr; + if (deltaMs < targetFrameTime) return; // Limit frame rate + _lastFrameTime = elapsed; + break; } // Update game scene and prepare for rendering. _painter.update(this, elapsed, deltaMs); diff --git a/lib/src/repainter_interface.dart b/lib/src/repainter_interface.dart index 9974a1d..4c4be2c 100644 --- a/lib/src/repainter_interface.dart +++ b/lib/src/repainter_interface.dart @@ -5,12 +5,17 @@ import 'repaint.dart'; /// The interface for a custom scene painter. abstract interface class IRePainter { - /// Whether the controller should limit the frame rate. + /// The [frameRate] is used to limit the frame rate, (limitter and throttler). + /// /// If `null`, the frame rate is not limited. /// 0 - Do not render the scene. /// 30 - 30 frames per second. /// 60 - 60 frames per second. /// 120 - 120 frames per second. + /// + /// After the [frameRate] is set, the real frame rate will be lower. + /// Before the frame rate, updates are limited by the flutter ticker, + /// so the resulting frame rate will be noticeably lower. int? get frameRate; /// Mount the controller. From ba6c4f2f2d47cf43a26d7cb0373b70ebf253ded8 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 22:27:53 +0400 Subject: [PATCH 17/21] Add FPS graph --- example/lib/src/feature/fps/fps_screen.dart | 193 +++++++++++++++++++- lib/src/repaint.dart | 8 +- lib/src/repainter_base.dart | 2 +- lib/src/repainter_interface.dart | 6 +- test/unit_test.dart | 4 +- test/widget_test.dart | 2 +- 6 files changed, 203 insertions(+), 12 deletions(-) diff --git a/example/lib/src/feature/fps/fps_screen.dart b/example/lib/src/feature/fps/fps_screen.dart index a622fd2..fb0f95a 100644 --- a/example/lib/src/feature/fps/fps_screen.dart +++ b/example/lib/src/feature/fps/fps_screen.dart @@ -16,6 +16,8 @@ class FpsScreen extends StatefulWidget { } class _FpsScreenState extends State { + final RePainter _painter = FrameRateGraph(); + Widget fps(int? frameRate) { int second = 0; int counter = 0; @@ -72,11 +74,11 @@ class _FpsScreenState extends State { height: 12, right: 8, child: Text( - 'Frame rate: ${frameRate ?? 'unlimited'}', + 'Frame rate limit: ${frameRate ?? 'unlimited'}', style: const TextStyle( height: 1, color: Colors.white, - fontSize: 10, + fontSize: 9, overflow: TextOverflow.ellipsis, ), ), @@ -103,7 +105,7 @@ class _FpsScreenState extends State { alignment: Alignment.center, child: SizedBox( width: 128 * 4 + 8 * 3, - height: 128 * 2 + 8, + height: 128 * 3 + 8 * 2, child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -146,6 +148,13 @@ class _FpsScreenState extends State { ], ), ), + const SizedBox(height: 8), + SizedBox( + height: 128, + child: RePaint( + painter: _painter, + ), + ), ], ), ), @@ -155,3 +164,181 @@ class _FpsScreenState extends State { ), ); } + +final class FrameRateGraph extends RePainterBase { + final List _updates = []; + final List _renders = []; + int _updatesSync = 0; + int _rendersSync = 0; + + final Paint _bgPaint = Paint() + ..color = Colors.black + ..style = PaintingStyle.fill + ..isAntiAlias = false; + + final Paint _linePaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..isAntiAlias = false; + + final Paint _fgPaint = Paint() + ..color = Colors.white38 + ..style = PaintingStyle.stroke + ..isAntiAlias = false; + + final Paint _updatesPaint = Paint() + ..color = Colors.blue + ..style = PaintingStyle.fill + ..isAntiAlias = false; + + final Paint _rendersPaint = Paint() + ..color = Colors.red + ..style = PaintingStyle.fill + ..isAntiAlias = false; + + final TextPainter _text = TextPainter( + maxLines: 1, + textDirection: TextDirection.ltr, + ); + + void _addUpdate() { + final newSync = DateTime.now().millisecondsSinceEpoch; + if (newSync - _updatesSync < Duration.millisecondsPerSecond) { + _updates[_updates.length - 1]++; + } else { + _updates.add(1); + _updatesSync = newSync; + } + } + + void _addRender() { + final newSync = DateTime.now().millisecondsSinceEpoch; + if (newSync - _rendersSync < Duration.millisecondsPerSecond) { + _renders.last++; + } else { + _renders.add(1); + _rendersSync = newSync; + } + } + + @override + void update(RePaintBox box, Duration elapsed, double delta) { + _addUpdate(); + } + + @override + void render(RePaintBox box, Canvas canvas) { + _addRender(); + + // Draw background + canvas.drawRect( + Offset.zero & box.size, + _bgPaint, + ); + + canvas.save(); + canvas.translate(8, 8); + { + // Draw chart + final size = Size(box.size.width - 16, box.size.height - 16); + + // Draw chart lines + canvas + ..drawLine( + const Offset(0, 0), + Offset(0, size.height), + _linePaint, + ) + ..drawLine( + Offset(0, size.height), + Offset(size.width, size.height), + _linePaint, + ); + + // Draw last 60 updates + final updates = _updates.reversed.take(60).toList(growable: false); + final w = size.width / 60; + final h = size.height / 120; + for (var i = 0; i < updates.length; i++) { + final x = size.width - i * w - 8; + final y = size.height - updates[i] * h; + canvas.drawRect( + Rect.fromLTWH(x, y, w, updates[i] * h), + _updatesPaint, + ); + } + + // Draw last 60 renders + final renders = _renders.reversed.take(60).toList(growable: false); + for (var i = 0; i < renders.length; i++) { + final x = size.width - i * w - 8; + final y = size.height - renders[i] * h; + canvas.drawRect( + Rect.fromLTWH(x, y, w, renders[i] * h), + _rendersPaint, + ); + } + + // Draw FPS lines + const fps = [120, 90, 60, 30]; + for (final f in fps) { + final y = size.height - size.height * f / 120; + canvas.drawLine( + Offset(0, y), + Offset(size.width, y), + _fgPaint, + ); + _text + ..text = TextSpan( + text: '$f', + style: const TextStyle( + height: 1, + color: Colors.white, + fontSize: 10, + overflow: TextOverflow.ellipsis, + ), + ) + ..layout() + ..paint( + canvas, + Offset(4, y + 1), + ); + } + + // Draw legend + _text + ..text = TextSpan( + text: 'Updates: ${updates.length > 1 ? updates[1] : '_'}', + style: const TextStyle( + height: 1, + color: Colors.blue, + fontSize: 10, + fontWeight: FontWeight.bold, + overflow: TextOverflow.clip, + ), + ) + ..layout(maxWidth: 72) + ..paint( + canvas, + const Offset(32, 4), + ) + ..text = TextSpan( + text: 'Renders: ${renders.length > 1 ? renders[1] : '_'}', + style: const TextStyle( + height: 1, + color: Colors.red, + fontSize: 10, + fontWeight: FontWeight.bold, + overflow: TextOverflow.clip, + ), + ) + ..layout(maxWidth: 72) + ..paint( + canvas, + const Offset(100, 4), + ); + } + + canvas.restore(); + } +} diff --git a/lib/src/repaint.dart b/lib/src/repaint.dart index e72d6f7..ed39257 100644 --- a/lib/src/repaint.dart +++ b/lib/src/repaint.dart @@ -46,7 +46,7 @@ class RePaint extends LeafRenderObjectWidget { ); /// The painter controller. - final IRePainter painter; + final RePainter painter; @override RePaintElement createElement() => RePaintElement(this); @@ -106,14 +106,14 @@ class RePaintElement extends LeafRenderObjectElement { class RePaintBox extends RenderBox with WidgetsBindingObserver { /// {@macro repaint_render_box} RePaintBox({ - required IRePainter painter, + required RePainter painter, required BuildContext context, }) : _painter = painter, _context = context; /// Current controller. - IRePainter get painter => _painter; - IRePainter _painter; + RePainter get painter => _painter; + RePainter _painter; /// Current build context. BuildContext get context => _context; diff --git a/lib/src/repainter_base.dart b/lib/src/repainter_base.dart index 24fe20a..7784903 100644 --- a/lib/src/repainter_base.dart +++ b/lib/src/repainter_base.dart @@ -8,7 +8,7 @@ import 'repainter_interface.dart'; /// {@template repainter_base} /// The base class for a custom scene painter. /// {@endtemplate} -abstract base class RePainterBase implements IRePainter { +abstract base class RePainterBase implements RePainter { /// {@macro repainter_base} const RePainterBase(); diff --git a/lib/src/repainter_interface.dart b/lib/src/repainter_interface.dart index 4c4be2c..62f105a 100644 --- a/lib/src/repainter_interface.dart +++ b/lib/src/repainter_interface.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'repaint.dart'; /// The interface for a custom scene painter. -abstract interface class IRePainter { +abstract interface class RePainter { /// The [frameRate] is used to limit the frame rate, (limitter and throttler). /// /// If `null`, the frame rate is not limited. @@ -16,6 +16,10 @@ abstract interface class IRePainter { /// After the [frameRate] is set, the real frame rate will be lower. /// Before the frame rate, updates are limited by the flutter ticker, /// so the resulting frame rate will be noticeably lower. + /// Because calling the [update] does not immediately cause a redraw, + /// but only marks the render object as needing a redraw with + /// [RenderObject.markNeedsPaint], + /// thats why the frame rate is lower than expected. int? get frameRate; /// Mount the controller. diff --git a/test/unit_test.dart b/test/unit_test.dart index 5cd31b1..e7900b1 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -53,7 +53,7 @@ void main() { 'painter', allOf( isNotNull, - isA(), + isA(), isA(), isA(), ), @@ -76,7 +76,7 @@ void main() { 'painter', allOf( isNotNull, - isA(), + isA(), isA(), isA(), ), diff --git a/test/widget_test.dart b/test/widget_test.dart index 23205d3..f089c04 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -67,7 +67,7 @@ void main() { 'painter', allOf( isNotNull, - isA(), + isA(), isA(), isA(), same(painter), From efccf3610de06ab6b708094077ac4d763d91e9c6 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 22:29:17 +0400 Subject: [PATCH 18/21] 0.0.3 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a83f0..4c32b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.3 + +- Updated Frame Limiter logic + ## 0.0.2 - Added example project diff --git a/pubspec.yaml b/pubspec.yaml index a4de405..10856c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ name: repaint description: > Library for creating and managing a canvas similar to CustomPaint but with more features. -version: 0.0.2 +version: 0.0.3 homepage: https://github.com/PlugFox/repaint From b57d239d0aef3d8b727de812db222cafb4a4de39 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 22:30:20 +0400 Subject: [PATCH 19/21] Change publish makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f7aa628..766340a 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ deploy-check: publish-check .PHONY: publish publish: ## Publish the package - @yes | flutter pub publish + @flutter pub publish .PHONY: deploy deploy: publish From 9ff5241bed82751fd07baa8738aa6260eb0835c8 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 22:48:00 +0400 Subject: [PATCH 20/21] Update fps graph --- example/lib/src/feature/fps/fps_screen.dart | 49 +++++++++++++-------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/example/lib/src/feature/fps/fps_screen.dart b/example/lib/src/feature/fps/fps_screen.dart index fb0f95a..6ed680b 100644 --- a/example/lib/src/feature/fps/fps_screen.dart +++ b/example/lib/src/feature/fps/fps_screen.dart @@ -178,11 +178,15 @@ final class FrameRateGraph extends RePainterBase { final Paint _linePaint = Paint() ..color = Colors.white + ..strokeWidth = 2 + ..strokeCap = StrokeCap.square ..style = PaintingStyle.stroke ..isAntiAlias = false; final Paint _fgPaint = Paint() ..color = Colors.white38 + ..strokeWidth = 1 + ..strokeCap = StrokeCap.square ..style = PaintingStyle.stroke ..isAntiAlias = false; @@ -241,19 +245,10 @@ final class FrameRateGraph extends RePainterBase { { // Draw chart final size = Size(box.size.width - 16, box.size.height - 16); - - // Draw chart lines - canvas - ..drawLine( - const Offset(0, 0), - Offset(0, size.height), - _linePaint, - ) - ..drawLine( - Offset(0, size.height), - Offset(size.width, size.height), - _linePaint, - ); + canvas.clipRect( + Rect.fromLTRB(-2, -2, size.width + 2, size.height + 2), + doAntiAlias: false, + ); // Draw last 60 updates final updates = _updates.reversed.take(60).toList(growable: false); @@ -304,11 +299,16 @@ final class FrameRateGraph extends RePainterBase { Offset(4, y + 1), ); } + canvas.drawLine( + Offset(size.width + 1, 0), + Offset(size.width + 1, size.height), + _fgPaint, + ); // Draw legend _text ..text = TextSpan( - text: 'Updates: ${updates.length > 1 ? updates[1] : '_'}', + text: 'Updates: ${updates.length > 1 ? updates[1] : '_'}/s', style: const TextStyle( height: 1, color: Colors.blue, @@ -317,13 +317,13 @@ final class FrameRateGraph extends RePainterBase { overflow: TextOverflow.clip, ), ) - ..layout(maxWidth: 72) + ..layout(maxWidth: 96) ..paint( canvas, const Offset(32, 4), ) ..text = TextSpan( - text: 'Renders: ${renders.length > 1 ? renders[1] : '_'}', + text: 'Renders: ${renders.length > 1 ? renders[1] : '_'}/s', style: const TextStyle( height: 1, color: Colors.red, @@ -332,10 +332,23 @@ final class FrameRateGraph extends RePainterBase { overflow: TextOverflow.clip, ), ) - ..layout(maxWidth: 72) + ..layout(maxWidth: 96) ..paint( canvas, - const Offset(100, 4), + const Offset(128, 4), + ); + + // Draw chart lines + canvas + ..drawLine( + const Offset(0, 0), + Offset(0, size.height), + _linePaint, + ) + ..drawLine( + Offset(0, size.height), + Offset(size.width + 1, size.height), + _linePaint, ); } From 31cf11e57bd125d27195d18e8fe2abb207c6854d Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 5 Dec 2024 22:55:32 +0400 Subject: [PATCH 21/21] Update clock example --- .../lib/src/feature/clock/clock_screen.dart | 180 +++++++++--------- 1 file changed, 95 insertions(+), 85 deletions(-) diff --git a/example/lib/src/feature/clock/clock_screen.dart b/example/lib/src/feature/clock/clock_screen.dart index 1a756f5..85e1a57 100644 --- a/example/lib/src/feature/clock/clock_screen.dart +++ b/example/lib/src/feature/clock/clock_screen.dart @@ -18,100 +18,110 @@ class ClockScreen extends StatelessWidget { ), ), body: SafeArea( - child: Center( - child: RePaint.inline( - frameRate: 1, - setUp: (box) => Paint() - ..color = Colors.black - ..style = PaintingStyle.stroke - ..strokeWidth = 2, - render: (box, paint, canvas) { - final size = box.size; // Get the size of the box - final now = DateTime.now(); // Get the current time + child: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: AspectRatio( + aspectRatio: 1, + child: RePaint.inline( + frameRate: 6, + setUp: (box) => Paint()..style = PaintingStyle.stroke, + render: (box, paint, canvas) { + final size = box.size; // Get the size of the box + final now = DateTime.now(); // Get the current time - final center = size.center(Offset.zero); - final radius = size.shortestSide / 3; + final center = size.center(Offset.zero); + final radius = size.shortestSide / 2 - 8; - // Draw the clock face - canvas.drawCircle(center, radius, paint); + // Draw the clock face + canvas.drawCircle( + center, + radius, + paint + ..color = Colors.black + ..strokeWidth = 2, + ); + + final textPainter = TextPainter( + textAlign: TextAlign.center, + textDirection: TextDirection.ltr, + ); - final textPainter = TextPainter( - textAlign: TextAlign.center, - textDirection: TextDirection.ltr, - ); + // Draw the clock numbers + for (int i = 1; i <= 12; i++) { + final angle = + math.pi / 6 * (i - 3); // Positioning the numbers + final numberPosition = Offset( + center.dx + radius * 0.8 * math.cos(angle), + center.dy + radius * 0.8 * math.sin(angle), + ); - // Draw the clock numbers - for (int i = 1; i <= 12; i++) { - final angle = - math.pi / 6 * (i - 3); // Positioning the numbers - final numberPosition = Offset( - center.dx + radius * 0.8 * math.cos(angle), - center.dy + radius * 0.8 * math.sin(angle), - ); + textPainter + ..text = TextSpan( + text: '$i', + style: TextStyle( + color: Colors.black, + fontSize: radius * 0.15, + ), + ) + ..layout() + ..paint( + canvas, + numberPosition - + Offset(textPainter.width / 2, + textPainter.height / 2), + ); + } - textPainter - ..text = TextSpan( - text: '$i', - style: TextStyle( - color: Colors.black, - fontSize: radius * 0.15, + // Draw the hour hand + final hourAngle = + math.pi / 6 * (now.hour % 12 + now.minute / 60) - + math.pi / 2; + final hourHandLength = radius * 0.5; + paint + ..color = Colors.black + ..strokeWidth = 4; + canvas.drawLine( + center, + Offset( + center.dx + hourHandLength * math.cos(hourAngle), + center.dy + hourHandLength * math.sin(hourAngle), ), - ) - ..layout() - ..paint( - canvas, - numberPosition - - Offset(textPainter.width / 2, textPainter.height / 2), + paint, ); - } - - // Draw the hour hand - final hourAngle = - math.pi / 6 * (now.hour % 12 + now.minute / 60) - - math.pi / 2; - final hourHandLength = radius * 0.5; - paint - ..color = Colors.black - ..strokeWidth = 4; - canvas.drawLine( - center, - Offset( - center.dx + hourHandLength * math.cos(hourAngle), - center.dy + hourHandLength * math.sin(hourAngle), - ), - paint, - ); - // Draw the minute hand - final minuteAngle = math.pi / 30 * now.minute - math.pi / 2; - final minuteHandLength = radius * 0.7; - paint - ..color = Colors.blue - ..strokeWidth = 3; - canvas.drawLine( - center, - Offset( - center.dx + minuteHandLength * math.cos(minuteAngle), - center.dy + minuteHandLength * math.sin(minuteAngle), - ), - paint, - ); + // Draw the minute hand + final minuteAngle = math.pi / 30 * now.minute - math.pi / 2; + final minuteHandLength = radius * 0.7; + paint + ..color = Colors.blue + ..strokeWidth = 3; + canvas.drawLine( + center, + Offset( + center.dx + minuteHandLength * math.cos(minuteAngle), + center.dy + minuteHandLength * math.sin(minuteAngle), + ), + paint, + ); - // Draw the second hand - final secondAngle = math.pi / 30 * now.second - math.pi / 2; - final secondHandLength = radius * 0.9; - paint - ..color = Colors.red - ..strokeWidth = 2; - canvas.drawLine( - center, - Offset( - center.dx + secondHandLength * math.cos(secondAngle), - center.dy + secondHandLength * math.sin(secondAngle), - ), - paint, - ); - }, + // Draw the second hand + final secondAngle = math.pi / 30 * now.second - math.pi / 2; + final secondHandLength = radius * 0.9; + paint + ..color = Colors.red + ..strokeWidth = 2; + canvas.drawLine( + center, + Offset( + center.dx + secondHandLength * math.cos(secondAngle), + center.dy + secondHandLength * math.sin(secondAngle), + ), + paint, + ); + }, + ), + ), ), ), ),