MoodCanvas/lib/screens/editor/wallpaper_painter.dart
fengshengxiong 91b7eebbf2 接入TopON
2026-01-22 16:34:55 +08:00

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;
}
}