Files
sevenhill f74057ce9b
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Rebrand application as Qlyra
2026-08-30 13:16:17 +03:00

138 lines
3.4 KiB
Dart

import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class TiledSvgPattern extends StatefulWidget {
final String asset;
final Color color;
final double opacity;
final double tileSize;
const TiledSvgPattern({
super.key,
required this.asset,
required this.color,
this.opacity = 0.12,
this.tileSize = 120,
});
@override
State<TiledSvgPattern> createState() => _TiledSvgPatternState();
}
class _TiledSvgPatternState extends State<TiledSvgPattern> {
static final Map<String, ui.Image> _cache = {};
static final Map<String, Future<ui.Image>> _pending = {};
ui.Image? _image;
double _dpr = 1;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final dpr = MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1;
if (dpr != _dpr || _image == null) {
_dpr = dpr;
_resolve();
}
}
@override
void didUpdateWidget(TiledSvgPattern old) {
super.didUpdateWidget(old);
if (old.asset != widget.asset || old.tileSize != widget.tileSize) {
_resolve();
}
}
Future<void> _resolve() async {
final px = (widget.tileSize * _dpr).clamp(1, 4096).round();
final key = '${widget.asset}@$px';
final cached = _cache[key];
if (cached != null) {
if (_image != cached) setState(() => _image = cached);
return;
}
final future = _pending.putIfAbsent(
key,
() => _rasterize(widget.asset, px),
);
try {
final image = await future;
_cache[key] = image;
_pending.remove(key);
if (mounted) setState(() => _image = image);
} catch (_) {
_pending.remove(key);
}
}
static Future<ui.Image> _rasterize(String asset, int px) async {
final info = await vg.loadPicture(SvgAssetLoader(asset), null);
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final size = info.size;
if (size.width > 0 && size.height > 0) {
canvas.scale(px / size.width, px / size.height);
}
canvas.drawPicture(info.picture);
final picture = recorder.endRecording();
final image = await picture.toImage(px, px);
info.picture.dispose();
picture.dispose();
return image;
}
@override
Widget build(BuildContext context) {
final image = _image;
if (image == null) return const SizedBox.expand();
return CustomPaint(
size: Size.infinite,
painter: _PatternPainter(
image: image,
color: widget.color.withValues(alpha: widget.opacity),
tileSize: widget.tileSize,
dpr: _dpr,
),
);
}
}
class _PatternPainter extends CustomPainter {
final ui.Image image;
final Color color;
final double tileSize;
final double dpr;
const _PatternPainter({
required this.image,
required this.color,
required this.tileSize,
required this.dpr,
});
@override
void paint(Canvas canvas, Size size) {
final s = 1 / dpr;
final matrix = Matrix4.identity()..scaleByDouble(s, s, 1, 1);
final paint = Paint()
..shader = ImageShader(
image,
TileMode.repeated,
TileMode.repeated,
matrix.storage,
)
..colorFilter = ColorFilter.mode(color, BlendMode.srcIn);
canvas.drawRect(Offset.zero & size, paint);
}
@override
bool shouldRepaint(_PatternPainter old) =>
old.image != image ||
old.color != color ||
old.tileSize != tileSize ||
old.dpr != dpr;
}