218 lines
5.9 KiB
Dart
218 lines
5.9 KiB
Dart
import 'dart:ui' as ui;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:aesthetica_wallpaper/models/recipe.dart';
|
|
|
|
// -------------------------------------
|
|
// --- 3. 画布绘制器 (WallpaperPainter) ---
|
|
// -------------------------------------
|
|
class WallpaperPainter extends CustomPainter {
|
|
final ui.Image image;
|
|
final Recipe recipe;
|
|
final Color timeOverlay;
|
|
final double batterySaturation; // 1.0 = 正常, 0.0 = 黑白
|
|
|
|
WallpaperPainter({
|
|
required this.image,
|
|
required this.recipe,
|
|
required this.timeOverlay,
|
|
required this.batterySaturation,
|
|
});
|
|
|
|
// 辅助函数,用于根据 亮度、对比度、饱和度 生成 5x5 颜色矩阵
|
|
ColorFilter _buildColorMatrix() {
|
|
// 亮度 (-1 to 1, default 0)
|
|
final double brightness = recipe.brightness;
|
|
// 对比度 (0 to 4, default 1)
|
|
final double contrast = recipe.contrast;
|
|
// 饱和度 (0 to 4, default 1)
|
|
final double saturation = recipe.saturation * batterySaturation; // 应用电量效果
|
|
|
|
// 矩阵从单位矩阵开始
|
|
List<double> matrix = [
|
|
1,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
0,
|
|
];
|
|
|
|
// 1. 应用饱和度
|
|
if (saturation != 1.0) {
|
|
final sat = saturation;
|
|
const lumR = 0.3086;
|
|
const lumG = 0.6094;
|
|
const lumB = 0.0820;
|
|
matrix = [
|
|
lumR * (1 - sat) + sat,
|
|
lumG * (1 - sat),
|
|
lumB * (1 - sat),
|
|
0,
|
|
0,
|
|
lumR * (1 - sat),
|
|
lumG * (1 - sat) + sat,
|
|
lumB * (1 - sat),
|
|
0,
|
|
0,
|
|
lumR * (1 - sat),
|
|
lumG * (1 - sat),
|
|
lumB * (1 - sat) + sat,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
0,
|
|
];
|
|
}
|
|
|
|
// 2. 应用对比度
|
|
if (contrast != 1.0) {
|
|
final translate = (1.0 - contrast) * 128;
|
|
// 注意:这里需要矩阵乘法,为了简单起见,我们假设在饱和度*之后*应用
|
|
matrix[0] *= contrast;
|
|
matrix[5] *= contrast;
|
|
matrix[10] *= contrast;
|
|
matrix[4] += translate;
|
|
matrix[9] += translate;
|
|
matrix[14] += translate;
|
|
}
|
|
|
|
// 3. 应用亮度
|
|
if (brightness != 0.0) {
|
|
final b = brightness * 255;
|
|
matrix[4] += b;
|
|
matrix[9] += b;
|
|
matrix[14] += b;
|
|
}
|
|
|
|
return ColorFilter.matrix(matrix);
|
|
}
|
|
|
|
// 获取字体样式
|
|
TextStyle _getFont() {
|
|
final style = TextStyle(fontSize: 32, color: Color(recipe.textColor));
|
|
|
|
try {
|
|
if (recipe.fontFamily == 'Roboto') {
|
|
return GoogleFonts.roboto(
|
|
fontSize: 32,
|
|
color: Color(recipe.textColor),
|
|
fontWeight: FontWeight.bold,
|
|
);
|
|
}
|
|
// 默认
|
|
return GoogleFonts.lato(fontSize: 32, color: Color(recipe.textColor));
|
|
} catch (e) {
|
|
// 字体加载失败时的回退
|
|
return style;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
// 裁剪画布,防止绘制到边界之外
|
|
canvas.clipRect(Offset.zero & size);
|
|
|
|
// --- 1. 准备画笔 ---
|
|
final Paint paint = Paint()
|
|
..filterQuality = FilterQuality.low; // 默认使用低质量(更快)
|
|
|
|
// 定义源矩形 (整张图片)
|
|
final Rect srcRect = Rect.fromLTWH(
|
|
0,
|
|
0,
|
|
image.width.toDouble(),
|
|
image.height.toDouble(),
|
|
);
|
|
// 定义目标矩形 (填满画布)
|
|
final Rect dstRect = Rect.fromLTWH(0, 0, size.width, size.height);
|
|
|
|
// --- 2. 应用滤镜 ---
|
|
// 应用颜色矩阵 (B/C/S + Battery)
|
|
paint.colorFilter = _buildColorMatrix();
|
|
|
|
// 应用模糊
|
|
if (recipe.blur > 0.0) {
|
|
paint.imageFilter = ui.ImageFilter.blur(
|
|
sigmaX: recipe.blur,
|
|
sigmaY: recipe.blur,
|
|
);
|
|
}
|
|
|
|
// --- 3. 绘制图片 ---
|
|
// `drawImageRect` 会使用 `paint` 中定义的滤镜来绘制
|
|
canvas.drawImageRect(image, srcRect, dstRect, paint);
|
|
|
|
// --- 4. 绘制动态蒙版 ---
|
|
if (timeOverlay.a > 0) {
|
|
canvas.drawRect(dstRect, Paint()..color = timeOverlay);
|
|
}
|
|
|
|
// --- 5. 绘制文字 (使用 TextPainter) ---
|
|
if (recipe.overlayText.isNotEmpty) {
|
|
final textPainter = TextPainter(
|
|
text: TextSpan(text: recipe.overlayText, style: _getFont()),
|
|
textDirection: ui.TextDirection.ltr,
|
|
textAlign: TextAlign.center,
|
|
);
|
|
|
|
// 布局文字,限制最大宽度
|
|
textPainter.layout(maxWidth: size.width - 40); // 左右各留 20 padding
|
|
|
|
// 计算居中位置
|
|
final offset = Offset(
|
|
(size.width - textPainter.width) / 2,
|
|
(size.height - textPainter.height) / 2,
|
|
);
|
|
|
|
// 绘制文字
|
|
textPainter.paint(canvas, offset);
|
|
}
|
|
}
|
|
|
|
// 优化 shouldRepaint
|
|
// 仅当绘制所需的数据发生变化时才重绘
|
|
@override
|
|
bool shouldRepaint(covariant WallpaperPainter oldDelegate) {
|
|
// 简单的比较 (因为 Recipe 是可变的,这可能不总是触发)
|
|
// return oldDelegate.image != image ||
|
|
// oldDelegate.recipe != recipe ||
|
|
// oldDelegate.timeOverlay != timeOverlay ||
|
|
// oldDelegate.batterySaturation != batterySaturation;
|
|
|
|
// 优化:比较所有字段
|
|
final oldRecipe = oldDelegate.recipe;
|
|
final newRecipe = recipe;
|
|
|
|
return oldDelegate.image != image ||
|
|
oldRecipe.baseImagePath != newRecipe.baseImagePath ||
|
|
oldRecipe.brightness != newRecipe.brightness ||
|
|
oldRecipe.contrast != newRecipe.contrast ||
|
|
oldRecipe.saturation != newRecipe.saturation ||
|
|
oldRecipe.blur != newRecipe.blur ||
|
|
oldRecipe.pixelate != newRecipe.pixelate ||
|
|
oldRecipe.overlayText != newRecipe.overlayText ||
|
|
oldRecipe.fontFamily != newRecipe.fontFamily ||
|
|
oldRecipe.textColor != newRecipe.textColor ||
|
|
oldDelegate.timeOverlay != timeOverlay ||
|
|
oldDelegate.batterySaturation != batterySaturation;
|
|
}
|
|
}
|