[{"data":1,"prerenderedAt":87},["ShallowReactive",2],{"tutorial-shader-glsl-personalizzati-in-flutter-con-fragmentprogram":3,"comments-tutorial-shader-glsl-personalizzati-in-flutter-con-fragmentprogram":86},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},110,"Shader GLSL personalizzati in Flutter con FragmentProgram","shader-glsl-personalizzati-in-flutter-con-fragmentprogram","Impara a scrivere, caricare e animare fragment shader GLSL in Flutter con ui.FragmentProgram, CustomPainter e flutter_shaders, applicandoli anche come effetto su widget esistenti.","Dalla versione 3.7 Flutter permette di compilare ed eseguire **fragment shader GLSL** direttamente nella pipeline di rendering, sia con Skia sia con Impeller. È lo strumento definitivo quando gradienti, `BackdropFilter` e `CustomPainter` non bastano più: effetti di dissolvenza pixel-per-pixel, distorsioni liquide, noise procedurale, transizioni cinematiche — tutto calcolato sulla GPU, senza costi sul thread UI.\n\nIn questo tutorial avanzato vedremo:\n\n- come dichiarare gli shader nel `pubspec.yaml` e quali vincoli impone Impeller;\n- la struttura di un fragment shader compatibile con Flutter (`FlutterFragCoord`, uniform, `out vec4 fragColor`);\n- come caricare un `ui.FragmentProgram` e disegnarlo con un `CustomPainter`;\n- come animarlo con un `Ticker` senza ricostruire l'albero dei widget;\n- come usare un widget reale come **texture di input** grazie a `AnimatedSampler` del pacchetto `flutter_shaders`;\n- le regole di performance, il ciclo di vita degli shader e il debug degli errori di compilazione.\n\n**Prerequisiti**: buona conoscenza di `CustomPainter`, del ciclo di vita dei widget e almeno una infarinatura di algebra vettoriale. Non serve essere esperti di GLSL: partiamo da zero sul linguaggio, ma a ritmo sostenuto.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Ff2eafed2-0c48-473a-9cc4-6dff5d76d816.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1649385652265-0b9d8197a4b0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA3NDI3MjR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Chris Appano","https:\u002F\u002Funsplash.com\u002F@chrisappano","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-blurry-image-of-a-red-and-blue-background-OBR5uAXF1mQ","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=eAfrOrbylJg",{"youtube_id":17,"duration_seconds":18,"published_at":19},"eAfrOrbylJg",170,"2026-09-30T10:52:13+00:00","advanced",45,"3.x (consigliata 3.19 o superiore)","published","2026-09-30T04:32:04+00:00","Shader GLSL in Flutter con FragmentProgram","Guida avanzata ai fragment shader GLSL in Flutter: FragmentProgram, uniform, animazione con Ticker, AnimatedSampler e ottimizzazione su Impeller.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,58,65,72,79],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},735,"Configurare il progetto e dichiarare gli shader nel pubspec.yaml","Gli shader in Flutter sono asset speciali: non vanno sotto `assets:` ma sotto la chiave dedicata `shaders:`. Al momento della build, il tool li compila in **Impeller Scene \u002F SkSL bytecode** (`.frag` → formato intermedio) e li rende caricabili a runtime.\n\nCrea la cartella `shaders\u002F` nella root del progetto e aggiungi la sezione al `pubspec.yaml`. Aggiungiamo anche `flutter_shaders`, che ci servirà più avanti per usare un widget come texture.\n\n**Attenzione ai vincoli**:\n\n- l'estensione consigliata è `.frag`;\n- ogni file deve contenere **un solo** fragment shader;\n- non sono supportati i *vertex shader* personalizzati;\n- con Impeller non sono ammessi cicli con numero di iterazioni non determinabile a compile-time, né array di uniform di dimensione dinamica;\n- i nomi dei file sono case-sensitive su Android\u002FiOS.\n\nDopo la modifica esegui sempre `flutter pub get` e un **full restart** (non un hot reload): la compilazione degli shader avviene in fase di build.","name: shader_lab\ndescription: Laboratorio di fragment shader in Flutter\n\nenvironment:\n  sdk: ^3.4.0\n\ndependencies:\n  flutter:\n    sdk: flutter\n  flutter_shaders: ^0.1.3\n\nflutter:\n  uses-material-design: true\n\n  # Gli shader NON vanno sotto assets: hanno una sezione dedicata\n  shaders:\n    - shaders\u002Fwave.frag\n    - shaders\u002Fdissolve.frag","yaml","Il progetto compila senza errori e la cartella `shaders\u002F` viene inclusa nel bundle. Se il file non esiste, `flutter run` fallisce con un errore esplicito sull'asset mancante.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},736,2,"Scrivere il primo fragment shader compatibile con Flutter","Un fragment shader è una funzione eseguita **per ogni pixel** dell'area disegnata. Flutter impone una struttura precisa:\n\n1. `#version 460 core` come prima riga (il tool lo traduce per la piattaforma target);\n2. `#include \u003Cflutter\u002Fruntime_effect.glsl>`, che espone la funzione `FlutterFragCoord()` — usala **sempre** al posto di `gl_FragCoord`, perché tiene conto della trasformazione del canvas;\n3. le `uniform` dichiarate **nell'ordine in cui le imposterai da Dart**;\n4. una sola variabile di output: `out vec4 fragColor`.\n\nPunto cruciale: gli uniform scalari e vettoriali condividono un unico array di float. Un `vec2` occupa gli indici 0 e 1, il `float` successivo l'indice 2, e così via. I `sampler2D` hanno invece una numerazione separata, gestita da `setImageSampler`.\n\nCreiamo `shaders\u002Fwave.frag`: un gradiente animato con onde sinusoidali e un alone radiale.","#version 460 core\n#include \u003Cflutter\u002Fruntime_effect.glsl>\n\nprecision highp float;\n\n\u002F\u002F Ordine = indici dei float lato Dart\nuniform vec2 uSize;    \u002F\u002F indici 0 e 1\nuniform float uTime;   \u002F\u002F indice 2\nuniform vec3 uColorA;  \u002F\u002F indici 3, 4, 5\nuniform vec3 uColorB;  \u002F\u002F indici 6, 7, 8\n\nout vec4 fragColor;\n\nvoid main() {\n  \u002F\u002F Coordinate normalizzate 0..1 rispetto all'area disegnata\n  vec2 uv = FlutterFragCoord().xy \u002F uSize;\n\n  \u002F\u002F Onde sovrapposte su due frequenze\n  float wave = sin(uv.x * 8.0 + uTime * 1.5) * 0.5\n             + sin(uv.y * 12.0 - uTime * 0.9) * 0.5;\n  float t = clamp(uv.y + wave * 0.12, 0.0, 1.0);\n\n  vec3 color = mix(uColorA, uColorB, t);\n\n  \u002F\u002F Alone radiale al centro\n  float d = distance(uv, vec2(0.5));\n  color += 0.18 * smoothstep(0.6, 0.0, d);\n\n  \u002F\u002F Flutter si aspetta colori PREMOLTIPLICATI per l'alpha\n  fragColor = vec4(color, 1.0);\n}","glsl","Il file viene compilato al prossimo `flutter run`. Un errore di sintassi GLSL blocca la build con il messaggio del compilatore e il numero di riga.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":56,"expected_result":57,"demo_url":41,"video_url":41},737,3,"Caricare il FragmentProgram e disegnarlo con un CustomPainter","Il caricamento è asincrono: `ui.FragmentProgram.fromAsset()` restituisce un `Future`. Il risultato è **cachato internamente da Flutter per path**, quindi chiamarlo più volte non ricompila lo shader, ma conviene comunque caricarlo una sola volta a livello di app o di feature.\n\nDa un `FragmentProgram` si ottiene un `ui.FragmentShader` con `program.fragmentShader()`. Questo oggetto è **mutabile e stateful**: contiene i valori degli uniform. Creane uno per ogni superficie animata e riutilizzalo, invece di ricrearlo a ogni frame.\n\nRegola fondamentale: **tutti** gli uniform dichiarati devono essere impostati prima del `drawRect`, altrimenti il comportamento è indefinito (spesso pixel neri o artefatti).\n\nNel painter usiamo `Paint()..shader = shader` e copriamo l'intera area con `drawRect`. Il `shouldRepaint` confronta il tempo, così ridisegniamo solo quando serve.","import 'dart:ui' as ui;\nimport 'package:flutter\u002Fmaterial.dart';\n\nclass WavePainter extends CustomPainter {\n  WavePainter({\n    required this.shader,\n    required this.time,\n    required this.colorA,\n    required this.colorB,\n  });\n\n  final ui.FragmentShader shader;\n  final double time;\n  final Color colorA;\n  final Color colorB;\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    shader\n      ..setFloat(0, size.width)\n      ..setFloat(1, size.height)\n      ..setFloat(2, time)\n      ..setFloat(3, colorA.red \u002F 255.0)\n      ..setFloat(4, colorA.green \u002F 255.0)\n      ..setFloat(5, colorA.blue \u002F 255.0)\n      ..setFloat(6, colorB.red \u002F 255.0)\n      ..setFloat(7, colorB.green \u002F 255.0)\n      ..setFloat(8, colorB.blue \u002F 255.0);\n\n    canvas.drawRect(Offset.zero & size, Paint()..shader = shader);\n  }\n\n  @override\n  bool shouldRepaint(covariant WavePainter oldDelegate) =>\n      oldDelegate.time != time ||\n      oldDelegate.colorA != colorA ||\n      oldDelegate.colorB != colorB;\n}\n\n\u002F\u002F\u002F Caricamento una tantum, riutilizzabile in tutta l'app.\nclass ShaderCache {\n  ShaderCache._();\n  static final Map\u003CString, Future\u003Cui.FragmentProgram>> _programs = {};\n\n  static Future\u003Cui.FragmentProgram> load(String asset) =>\n      _programs.putIfAbsent(asset, () => ui.FragmentProgram.fromAsset(asset));\n}","dart","Il painter è pronto: dato uno `ui.FragmentShader` e un valore di tempo, dipinge il gradiente ondulato sull'intera area del `CustomPaint`.",{"id":59,"position":60,"title":61,"body":62,"code_snippet":63,"code_language":56,"expected_result":64,"demo_url":41,"video_url":41},738,4,"Animare lo shader con un Ticker (senza rebuild dell'albero)","Per animare uno shader non serve un `AnimationController` con `setState`: basta un `Ticker` che aggiorna un `ValueNotifier\u003Cdouble>` con i secondi trascorsi. Passando il notifier a `CustomPaint` tramite `repaint:` (secondo il pattern `CustomPainter(repaint: listenable)`), Flutter ripassa dalla fase di **paint** senza rieseguire `build` né il layout: è la strada più efficiente.\n\nQui il painter estende `CustomPainter` con `super(repaint: time)` e legge il valore direttamente dal notifier. Il risultato: 60\u002F120 fps con zero rebuild.\n\nRicorda inoltre di chiamare `shader.dispose()` e `ticker.dispose()` in `dispose()`: `FragmentShader` alloca risorse native che il GC di Dart non libera immediatamente.","import 'dart:ui' as ui;\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter\u002Fscheduler.dart';\n\nclass AnimatedWave extends StatefulWidget {\n  const AnimatedWave({super.key});\n  @override\n  State\u003CAnimatedWave> createState() => _AnimatedWaveState();\n}\n\nclass _AnimatedWaveState extends State\u003CAnimatedWave>\n    with SingleTickerProviderStateMixin {\n  final ValueNotifier\u003Cdouble> _time = ValueNotifier(0);\n  late final Ticker _ticker;\n  ui.FragmentShader? _shader;\n\n  @override\n  void initState() {\n    super.initState();\n    _ticker = createTicker((elapsed) {\n      _time.value = elapsed.inMicroseconds \u002F 1e6;\n    });\n    _init();\n  }\n\n  Future\u003Cvoid> _init() async {\n    final program = await ShaderCache.load('shaders\u002Fwave.frag');\n    if (!mounted) return;\n    setState(() => _shader = program.fragmentShader());\n    _ticker.start();\n  }\n\n  @override\n  void dispose() {\n    _ticker.dispose();\n    _shader?.dispose();\n    _time.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final shader = _shader;\n    if (shader == null) {\n      return const ColoredBox(color: Colors.black12);\n    }\n    return RepaintBoundary(\n      child: CustomPaint(\n        painter: _WaveTickerPainter(\n          shader: shader,\n          time: _time,\n          colorA: const Color(0xFF1A237E),\n          colorB: const Color(0xFF00BCD4),\n        ),\n        size: Size.infinite,\n      ),\n    );\n  }\n}\n\nclass _WaveTickerPainter extends CustomPainter {\n  _WaveTickerPainter({\n    required this.shader,\n    required this.time,\n    required this.colorA,\n    required this.colorB,\n  }) : super(repaint: time);\n\n  final ui.FragmentShader shader;\n  final ValueNotifier\u003Cdouble> time;\n  final Color colorA;\n  final Color colorB;\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    shader\n      ..setFloat(0, size.width)\n      ..setFloat(1, size.height)\n      ..setFloat(2, time.value)\n      ..setFloat(3, colorA.red \u002F 255.0)\n      ..setFloat(4, colorA.green \u002F 255.0)\n      ..setFloat(5, colorA.blue \u002F 255.0)\n      ..setFloat(6, colorB.red \u002F 255.0)\n      ..setFloat(7, colorB.green \u002F 255.0)\n      ..setFloat(8, colorB.blue \u002F 255.0);\n    canvas.drawRect(Offset.zero & size, Paint()..shader = shader);\n  }\n\n  @override\n  bool shouldRepaint(covariant _WaveTickerPainter old) =>\n      old.shader != shader || old.colorA != colorA || old.colorB != colorB;\n}","Uno sfondo animato fluido: le onde scorrono a 60 fps e nel DevTools Performance non compaiono rebuild del widget, solo operazioni di paint.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":48,"expected_result":71,"demo_url":41,"video_url":41},739,5,"Usare un widget reale come texture con sampler2D e AnimatedSampler","Il salto di qualità arriva quando lo shader riceve in input **il rendering di un widget**. Il pacchetto `flutter_shaders` mette a disposizione `AnimatedSampler`: cattura il sottoalbero in una `ui.Image` (via layer offscreen) e ti passa `image`, `size` e `canvas` per disegnarlo come preferisci.\n\nCreiamo un effetto **dissolve** con noise: i pixel scompaiono in ordine pseudo-casuale a blocchi, come una disintegrazione.\n\nNota su alpha: Flutter lavora con colori **premoltiplicati**, quindi per far svanire un pixel devi moltiplicare l'intero `vec4` (RGB inclusi) per il fattore di trasparenza, non solo la componente `a`.\n\nDue accortezze pratiche:\n\n- `setImageSampler` usa una numerazione indipendente dagli indici float (il primo `sampler2D` è l'indice 0);\n- `AnimatedSampler` forza un layer offscreen: usalo su porzioni contenute della UI, non sull'intera schermata se puoi evitarlo.","\u002F\u002F shaders\u002Fdissolve.frag\n#version 460 core\n#include \u003Cflutter\u002Fruntime_effect.glsl>\n\nprecision mediump float;\n\nuniform vec2 uSize;       \u002F\u002F 0, 1\nuniform float uProgress;  \u002F\u002F 2  (0 = intatto, 1 = dissolto)\nuniform sampler2D uTexture; \u002F\u002F sampler index 0\n\nout vec4 fragColor;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nvoid main() {\n  vec2 fragCoord = FlutterFragCoord().xy;\n  vec2 uv = fragCoord \u002F uSize;\n\n  vec4 color = texture(uTexture, uv);\n\n  \u002F\u002F Noise a blocchi di 5px per un effetto \"granulare\"\n  float noise = hash(floor(fragCoord \u002F 5.0));\n\n  \u002F\u002F Bordo incandescente sulla soglia di dissolvenza\n  float edge = smoothstep(uProgress, uProgress + 0.08, noise);\n  float glow = (1.0 - edge) * step(uProgress - 0.08, noise);\n\n  vec4 burn = vec4(1.0, 0.45, 0.1, 1.0) * glow * color.a;\n\n  \u002F\u002F Colore premoltiplicato: moltiplico tutto il vec4\n  fragColor = color * edge + burn;\n}","Il file `dissolve.frag` compila e dichiara un sampler pronto a ricevere la texture del widget.",{"id":73,"position":74,"title":75,"body":76,"code_snippet":77,"code_language":56,"expected_result":78,"demo_url":41,"video_url":41},740,6,"Collegare lo shader dissolve a un widget con AnimatedSampler","Ora avvolgiamo un widget qualsiasi (una `Card`, una foto, una lista) nel nostro effetto. `ShaderBuilder` di `flutter_shaders` si occupa del caricamento asincrono del programma e della `dispose` dello shader; `AnimatedSampler` fornisce la texture.\n\nIl `progress` arriva da un `AnimationController`: poiché il callback di `AnimatedSampler` viene invocato in fase di paint, un `AnimatedBuilder` che ricostruisce solo il wrapper è più che sufficiente.\n\nUn dettaglio spesso trascurato: quando `progress == 0` conviene **bypassare lo shader** e restituire direttamente il child. Eviti il layer offscreen nello stato di riposo, che è quello in cui il widget passa il 99% del tempo.","import 'dart:ui' as ui;\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_shaders\u002Fflutter_shaders.dart';\n\nclass DissolveEffect extends StatelessWidget {\n  const DissolveEffect({\n    super.key,\n    required this.progress,\n    required this.child,\n  });\n\n  final double progress; \u002F\u002F 0.0 .. 1.0\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    if (progress \u003C= 0.001) return child; \u002F\u002F nessun costo a riposo\n\n    return ShaderBuilder(\n      assetKey: 'shaders\u002Fdissolve.frag',\n      (context, shader, _) {\n        return AnimatedSampler(\n          (ui.Image image, Size size, Canvas canvas) {\n            shader\n              ..setFloat(0, size.width)\n              ..setFloat(1, size.height)\n              ..setFloat(2, progress)\n              ..setImageSampler(0, image);\n            canvas.drawRect(Offset.zero & size, Paint()..shader = shader);\n          },\n          child: child,\n        );\n      },\n      child: child,\n    );\n  }\n}\n\n\u002F\u002F Utilizzo tipico\nclass DissolveDemo extends StatefulWidget {\n  const DissolveDemo({super.key});\n  @override\n  State\u003CDissolveDemo> createState() => _DissolveDemoState();\n}\n\nclass _DissolveDemoState extends State\u003CDissolveDemo>\n    with SingleTickerProviderStateMixin {\n  late final AnimationController _c = AnimationController(\n    vsync: this,\n    duration: const Duration(milliseconds: 900),\n  );\n\n  @override\n  void dispose() {\n    _c.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: GestureDetector(\n        onTap: () => _c.isCompleted ? _c.reverse() : _c.forward(),\n        child: AnimatedBuilder(\n          animation: _c,\n          builder: (context, child) =>\n              DissolveEffect(progress: _c.value, child: child!),\n          child: const Card(\n            elevation: 4,\n            child: Padding(\n              padding: EdgeInsets.all(24),\n              child: Text('Toccami per dissolvermi',\n                  style: TextStyle(fontSize: 20)),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}","Toccando la card, questa si disintegra in granuli con un bordo arancione incandescente e ricompare al tocco successivo, mantenendo i 60 fps.",{"id":80,"position":81,"title":82,"body":83,"code_snippet":84,"code_language":56,"expected_result":85,"demo_url":41,"video_url":41},741,7,"Performance, Impeller e debug: le regole da rispettare","Gli shader girano sulla GPU, ma non sono gratis. Ecco la checklist che uso in produzione.\n\n**1. Compilazione e jank iniziale.** Con Skia il primo frame che usa uno shader può causare un micro-jank per la compilazione runtime. Con **Impeller** (default su iOS e Android moderni) il problema è in gran parte risolto perché la traduzione avviene in fase di build. Se sei ancora su Skia, \"scalda\" lo shader disegnandolo in un `Offstage` di 1×1 pixel all'avvio.\n\n**2. Riusa lo `ui.FragmentShader`.** Creane uno per superficie e aggiorna solo gli uniform. Crearne uno per frame genera allocazioni native e pressione sul GC.\n\n**3. Libera le risorse.** `shader.dispose()` in `dispose()`. `ShaderBuilder` lo fa per te, il codice manuale no.\n\n**4. Isola i repaint.** Avvolgi sempre l'area shaderizzata in un `RepaintBoundary`: eviti che il repaint continuo dell'animazione invalidi il layer del resto della schermata.\n\n**5. Occhio alla complessità per-pixel.** Il costo è `larghezza × altezza × costo_del_main()`. Un `for` con 64 iterazioni su un'area full-screen a 3x di device pixel ratio è una ricetta per il thermal throttling. Preferisci funzioni analitiche a loop, e `mediump` quando la precisione alta non serve.\n\n**6. Limiti di Impeller.** Niente loop con bound dinamici, niente ricorsione, niente `while` non risolvibili a compile-time. Su **Flutter Web** il supporto dei fragment shader esiste ma va verificato sul renderer in uso (CanvasKit\u002FSkwasm): prevedi sempre un fallback grafico.\n\n**7. Debug.** Un errore di sintassi emerge in build. Se invece vedi pixel neri o trasparenti: hai quasi certamente dimenticato un `setFloat` o sbagliato l'indice. Tecnica rapida: sostituisci il corpo di `main()` con `fragColor = vec4(uv, 0.0, 1.0);` per verificare che le coordinate arrivino corrette.\n\n**8. Golden test.** Gli shader possono rendere non deterministici i golden test: isolali dietro un flag e disattivali nei test di regressione visiva.\n\nIl codice qui sotto mostra il warm-up e un widget di fallback difensivo.","import 'dart:ui' as ui;\nimport 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Precarica e \"scalda\" lo shader disegnandolo su una superficie minima.\nFuture\u003Cvoid> warmUpShader(String asset) async {\n  final program = await ui.FragmentProgram.fromAsset(asset);\n  final shader = program.fragmentShader()\n    ..setFloat(0, 1)\n    ..setFloat(1, 1)\n    ..setFloat(2, 0);\n\n  final recorder = ui.PictureRecorder();\n  final canvas = Canvas(recorder);\n  canvas.drawRect(\n    const Rect.fromLTWH(0, 0, 1, 1),\n    Paint()..shader = shader,\n  );\n  final picture = recorder.endRecording();\n  \u002F\u002F Forza il rasterizzatore a processare davvero il comando\n  await picture.toImage(1, 1);\n  picture.dispose();\n  shader.dispose();\n}\n\n\u002F\u002F\u002F Wrapper difensivo: se lo shader non è disponibile mostra il child intatto.\nclass SafeShaderEffect extends StatelessWidget {\n  const SafeShaderEffect({\n    super.key,\n    required this.future,\n    required this.builder,\n    required this.child,\n  });\n\n  final Future\u003Cui.FragmentProgram> future;\n  final Widget Function(ui.FragmentShader shader, Widget child) builder;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder\u003Cui.FragmentProgram>(\n      future: future,\n      builder: (context, snapshot) {\n        if (snapshot.hasError || !snapshot.hasData) return child;\n        return builder(snapshot.data!.fragmentShader(), child);\n      },\n    );\n  }\n}\n\n\u002F\u002F In main(): await warmUpShader('shaders\u002Fwave.frag');","Nessun jank al primo utilizzo dello shader, memoria nativa stabile nel tempo e degradazione elegante della UI sulle piattaforme in cui lo shader non è supportato.",[],1792316928553]