2026-05-08 21:30:06 +08:00

605 lines
21 KiB
Dart

part of 'player.dart';
class _SVGAPainter extends CustomPainter {
final BoxFit fit;
final SVGAAnimationController controller;
int get currentFrame => controller.currentFrame;
MovieEntity get videoItem => controller.videoItem!;
final FilterQuality filterQuality;
final Rect? centerSlice;
final Rect? ninePatchSourceRect;
/// Guaranteed to draw within the canvas bounds
final bool clipRect;
_SVGAPainter(
this.controller, {
this.fit = BoxFit.contain,
this.filterQuality = FilterQuality.low,
this.centerSlice,
this.ninePatchSourceRect,
this.clipRect = true,
}) : assert(
controller.videoItem != null, 'Invalid SVGAAnimationController!'),
super(repaint: controller);
@override
void paint(Canvas canvas, Size size) {
if (controller._canvasNeedsClear) {
// mark cleared
controller._canvasNeedsClear = false;
return;
}
if (size.isEmpty || controller.videoItem == null) return;
final params = videoItem.params;
final Size viewBoxSize = Size(params.viewBoxWidth, params.viewBoxHeight);
if (viewBoxSize.isEmpty) return;
canvas.save();
try {
final canvasRect = Offset.zero & size;
if (clipRect) canvas.clipRect(canvasRect);
final slice = centerSlice;
if (slice != null && fit == BoxFit.fill) {
drawNinePatch(canvas, canvasRect, Offset.zero & viewBoxSize, slice);
} else {
scaleCanvasToViewBox(canvas, canvasRect, Offset.zero & viewBoxSize);
drawSprites(canvas, size);
}
} finally {
canvas.restore();
}
}
void drawNinePatch(
Canvas canvas,
Rect canvasRect,
Rect viewBoxRect,
Rect rawCenterSlice,
) {
final sourceRect = _validSourceRect(viewBoxRect);
if (sourceRect == null) {
scaleCanvasToViewBox(canvas, canvasRect, viewBoxRect);
drawSprites(canvas, canvasRect.size);
return;
}
final centerSlice = _validCenterSlice(sourceRect, rawCenterSlice);
if (centerSlice == null) {
scaleCanvasToViewBox(canvas, canvasRect, viewBoxRect);
drawSprites(canvas, canvasRect.size);
return;
}
final left = centerSlice.left - sourceRect.left;
final right = sourceRect.right - centerSlice.right;
final top = centerSlice.top - sourceRect.top;
final bottom = sourceRect.bottom - centerSlice.bottom;
final horizontalBorder = left + right;
final verticalBorder = top + bottom;
final horizontalScale = horizontalBorder <= 0
? 1.0
: min(1.0, canvasRect.width / horizontalBorder);
final verticalScale = verticalBorder <= 0
? 1.0
: min(1.0, canvasRect.height / verticalBorder);
final dstLeft = left * horizontalScale;
final dstRight = right * horizontalScale;
final dstTop = top * verticalScale;
final dstBottom = bottom * verticalScale;
final sourceXs = <double>[
sourceRect.left,
centerSlice.left,
centerSlice.right,
sourceRect.right,
];
final sourceYs = <double>[
sourceRect.top,
centerSlice.top,
centerSlice.bottom,
sourceRect.bottom,
];
final destXs = <double>[
canvasRect.left,
canvasRect.left + dstLeft,
canvasRect.right - dstRight,
canvasRect.right,
];
final destYs = <double>[
canvasRect.top,
canvasRect.top + dstTop,
canvasRect.bottom - dstBottom,
canvasRect.bottom,
];
for (var row = 0; row < 3; row++) {
for (var column = 0; column < 3; column++) {
final src = Rect.fromLTRB(
sourceXs[column],
sourceYs[row],
sourceXs[column + 1],
sourceYs[row + 1],
);
final dst = Rect.fromLTRB(
destXs[column],
destYs[row],
destXs[column + 1],
destYs[row + 1],
);
if (src.width <= 0 ||
src.height <= 0 ||
dst.width <= 0 ||
dst.height <= 0) {
continue;
}
_drawSourceSegment(canvas, src, dst);
}
}
}
Rect? _validSourceRect(Rect viewBoxRect) {
final rect = ninePatchSourceRect ?? viewBoxRect;
final left = rect.left
.clamp(viewBoxRect.left, viewBoxRect.right)
.toDouble();
final top = rect.top.clamp(viewBoxRect.top, viewBoxRect.bottom).toDouble();
final right = rect.right.clamp(left, viewBoxRect.right).toDouble();
final bottom = rect.bottom.clamp(top, viewBoxRect.bottom).toDouble();
final result = Rect.fromLTRB(left, top, right, bottom);
return result.isEmpty ? null : result;
}
Rect? _validCenterSlice(Rect sourceRect, Rect rawCenterSlice) {
final left = rawCenterSlice.left
.clamp(sourceRect.left, sourceRect.right)
.toDouble();
final top = rawCenterSlice.top
.clamp(sourceRect.top, sourceRect.bottom)
.toDouble();
final right = rawCenterSlice.right.clamp(left, sourceRect.right).toDouble();
final bottom = rawCenterSlice.bottom
.clamp(top, sourceRect.bottom)
.toDouble();
final result = Rect.fromLTRB(left, top, right, bottom);
return result.isEmpty ? null : result;
}
void _drawSourceSegment(Canvas canvas, Rect sourceRect, Rect destRect) {
canvas.save();
try {
canvas.clipRect(destRect);
canvas.translate(destRect.left, destRect.top);
canvas.scale(
destRect.width / sourceRect.width,
destRect.height / sourceRect.height,
);
canvas.translate(-sourceRect.left, -sourceRect.top);
drawSprites(canvas, destRect.size);
} finally {
canvas.restore();
}
}
void scaleCanvasToViewBox(Canvas canvas, Rect canvasRect, Rect viewBoxRect) {
final fittedSizes = applyBoxFit(fit, viewBoxRect.size, canvasRect.size);
// scale viewbox size (source) to canvas size (destination)
var sx = fittedSizes.destination.width / fittedSizes.source.width;
var sy = fittedSizes.destination.height / fittedSizes.source.height;
final Size scaledHalfViewBoxSize =
Size(viewBoxRect.size.width * sx, viewBoxRect.size.height * sy) / 2.0;
final Size halfCanvasSize = canvasRect.size / 2.0;
// center align
final Offset shift = Offset(
halfCanvasSize.width - scaledHalfViewBoxSize.width,
halfCanvasSize.height - scaledHalfViewBoxSize.height,
);
if (shift != Offset.zero) canvas.translate(shift.dx, shift.dy);
if (sx != 1.0 && sy != 1.0) canvas.scale(sx, sy);
}
void drawSprites(Canvas canvas, Size size) {
for (final sprite in videoItem.sprites) {
final imageKey = sprite.imageKey;
// var matteKey = sprite.matteKey;
if (imageKey.isEmpty ||
videoItem.dynamicItem.dynamicHidden[imageKey] == true) {
continue;
}
final frameItem = sprite.frames[currentFrame];
final needTransform = frameItem.hasTransform();
final needClip = frameItem.hasClipPath();
if (needTransform) {
canvas.save();
canvas.transform(Float64List.fromList(<double>[
frameItem.transform.a,
frameItem.transform.b,
0.0,
0.0,
frameItem.transform.c,
frameItem.transform.d,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
frameItem.transform.tx,
frameItem.transform.ty,
0.0,
1.0
]));
}
if (needClip) {
canvas.save();
canvas.clipPath(buildDPath(frameItem.clipPath));
}
final frameRect =
Rect.fromLTRB(0, 0, frameItem.layout.width, frameItem.layout.height);
final frameAlpha =
frameItem.hasAlpha() ? (frameItem.alpha * 255).toInt() : 255;
drawBitmap(canvas, imageKey, frameRect, frameAlpha);
drawShape(canvas, frameItem.shapes, frameAlpha);
// draw dynamic
final dynamicDrawer = videoItem.dynamicItem.dynamicDrawer[imageKey];
if (dynamicDrawer != null) {
dynamicDrawer(canvas, currentFrame);
}
if (needClip) {
canvas.restore();
}
if (needTransform) {
canvas.restore();
}
}
}
void drawBitmap(Canvas canvas, String imageKey, Rect frameRect, int alpha) {
final bitmap = videoItem.dynamicItem.dynamicImages[imageKey] ??
videoItem.bitmapCache[imageKey];
if (bitmap == null) return;
final bitmapPaint = Paint();
bitmapPaint.filterQuality = filterQuality;
//解决bitmap锯齿问题
bitmapPaint.isAntiAlias = true;
bitmapPaint.color = Color.fromARGB(alpha, 0, 0, 0);
Rect srcRect =
Rect.fromLTRB(0, 0, bitmap.width.toDouble(), bitmap.height.toDouble());
Rect dstRect = frameRect;
canvas.drawImageRect(bitmap, srcRect, dstRect, bitmapPaint);
drawTextOnBitmap(canvas, imageKey, frameRect, alpha);
}
void drawShape(Canvas canvas, List<ShapeEntity> shapes, int frameAlpha) {
if (shapes.isEmpty) return;
for (var shape in shapes) {
final path = buildPath(shape);
if (shape.hasTransform()) {
canvas.save();
canvas.transform(Float64List.fromList(<double>[
shape.transform.a,
shape.transform.b,
0.0,
0.0,
shape.transform.c,
shape.transform.d,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
shape.transform.tx,
shape.transform.ty,
0.0,
1.0
]));
}
final fill = shape.styles.fill;
if (fill.isInitialized()) {
final paint = Paint();
paint.isAntiAlias = true;
paint.style = PaintingStyle.fill;
paint.color = Color.fromARGB(
(fill.a * frameAlpha).toInt(),
(fill.r * 255).toInt(),
(fill.g * 255).toInt(),
(fill.b * 255).toInt(),
);
canvas.drawPath(path, paint);
}
final strokeWidth = shape.styles.strokeWidth;
if (strokeWidth > 0) {
final paint = Paint();
paint.style = PaintingStyle.stroke;
if (shape.styles.stroke.isInitialized()) {
paint.color = Color.fromARGB(
(shape.styles.stroke.a * frameAlpha).toInt(),
(shape.styles.stroke.r * 255).toInt(),
(shape.styles.stroke.g * 255).toInt(),
(shape.styles.stroke.b * 255).toInt(),
);
}
paint.strokeWidth = strokeWidth;
final lineCap = shape.styles.lineCap;
switch (lineCap) {
case ShapeEntity_ShapeStyle_LineCap.LineCap_BUTT:
paint.strokeCap = StrokeCap.butt;
break;
case ShapeEntity_ShapeStyle_LineCap.LineCap_ROUND:
paint.strokeCap = StrokeCap.round;
break;
case ShapeEntity_ShapeStyle_LineCap.LineCap_SQUARE:
paint.strokeCap = StrokeCap.square;
break;
default:
}
final lineJoin = shape.styles.lineJoin;
switch (lineJoin) {
case ShapeEntity_ShapeStyle_LineJoin.LineJoin_MITER:
paint.strokeJoin = StrokeJoin.miter;
break;
case ShapeEntity_ShapeStyle_LineJoin.LineJoin_ROUND:
paint.strokeJoin = StrokeJoin.round;
break;
case ShapeEntity_ShapeStyle_LineJoin.LineJoin_BEVEL:
paint.strokeJoin = StrokeJoin.bevel;
break;
default:
}
paint.strokeMiterLimit = shape.styles.miterLimit;
List<double> lineDash = [
shape.styles.lineDashI,
shape.styles.lineDashII,
shape.styles.lineDashIII
];
if (lineDash[0] > 0 || lineDash[1] > 0) {
canvas.drawPath(
dashPath(
path,
dashArray: CircularIntervalList([
lineDash[0] < 1.0 ? 1.0 : lineDash[0],
lineDash[1] < 0.1 ? 0.1 : lineDash[1],
]),
dashOffset: DashOffset.absolute(lineDash[2]),
),
paint);
} else {
canvas.drawPath(path, paint);
}
}
if (shape.hasTransform()) {
canvas.restore();
}
}
}
static const _validMethods = 'MLHVCSQRZmlhvcsqrz';
Path buildPath(ShapeEntity shape) {
final path = Path();
if (shape.type == ShapeEntity_ShapeType.SHAPE) {
final args = shape.shape;
final argD = args.d;
return buildDPath(argD, path: path);
} else if (shape.type == ShapeEntity_ShapeType.ELLIPSE) {
final args = shape.ellipse;
final xv = args.x;
final yv = args.y;
final rxv = args.radiusX;
final ryv = args.radiusY;
final rect = Rect.fromLTWH(xv - rxv, yv - ryv, rxv * 2, ryv * 2);
if (!rect.isEmpty) path.addOval(rect);
} else if (shape.type == ShapeEntity_ShapeType.RECT) {
final args = shape.rect;
final xv = args.x;
final yv = args.y;
final wv = args.width;
final hv = args.height;
final crv = args.cornerRadius;
final rrect = RRect.fromRectAndRadius(
Rect.fromLTWH(xv, yv, wv, hv), Radius.circular(crv));
if (!rrect.isEmpty) path.addRRect(rrect);
}
return path;
}
Path buildDPath(String argD, {Path? path}) {
if (videoItem.pathCache[argD] != null) {
return videoItem.pathCache[argD]!;
}
path ??= Path();
final d = argD.replaceAllMapped(RegExp('([a-df-zA-Z])'), (match) {
return "|||${match.group(1)} ";
}).replaceAll(RegExp(","), " ");
var currentPointX = 0.0;
var currentPointY = 0.0;
double? currentPointX1;
double? currentPointY1;
double? currentPointX2;
double? currentPointY2;
d.split("|||").forEach((segment) {
if (segment.isEmpty) {
return;
}
final firstLetter = segment.substring(0, 1);
if (_validMethods.contains(firstLetter)) {
final args = segment.substring(1).trim().split(" ");
if (firstLetter == "M") {
currentPointX = double.parse(args[0]);
currentPointY = double.parse(args[1]);
path!.moveTo(currentPointX, currentPointY);
} else if (firstLetter == "m") {
currentPointX += double.parse(args[0]);
currentPointY += double.parse(args[1]);
path!.moveTo(currentPointX, currentPointY);
} else if (firstLetter == "L") {
currentPointX = double.parse(args[0]);
currentPointY = double.parse(args[1]);
path!.lineTo(currentPointX, currentPointY);
} else if (firstLetter == "l") {
currentPointX += double.parse(args[0]);
currentPointY += double.parse(args[1]);
path!.lineTo(currentPointX, currentPointY);
} else if (firstLetter == "H") {
currentPointX = double.parse(args[0]);
path!.lineTo(currentPointX, currentPointY);
} else if (firstLetter == "h") {
currentPointX += double.parse(args[0]);
path!.lineTo(currentPointX, currentPointY);
} else if (firstLetter == "V") {
currentPointY = double.parse(args[0]);
path!.lineTo(currentPointX, currentPointY);
} else if (firstLetter == "v") {
currentPointY += double.parse(args[0]);
path!.lineTo(currentPointX, currentPointY);
} else if (firstLetter == "C") {
currentPointX1 = double.parse(args[0]);
currentPointY1 = double.parse(args[1]);
currentPointX2 = double.parse(args[2]);
currentPointY2 = double.parse(args[3]);
currentPointX = double.parse(args[4]);
currentPointY = double.parse(args[5]);
path!.cubicTo(
currentPointX1!,
currentPointY1!,
currentPointX2!,
currentPointY2!,
currentPointX,
currentPointY,
);
} else if (firstLetter == "c") {
currentPointX1 = currentPointX + double.parse(args[0]);
currentPointY1 = currentPointY + double.parse(args[1]);
currentPointX2 = currentPointX + double.parse(args[2]);
currentPointY2 = currentPointY + double.parse(args[3]);
currentPointX += double.parse(args[4]);
currentPointY += double.parse(args[5]);
path!.cubicTo(
currentPointX1!,
currentPointY1!,
currentPointX2!,
currentPointY2!,
currentPointX,
currentPointY,
);
} else if (firstLetter == "S") {
if (currentPointX1 != null &&
currentPointY1 != null &&
currentPointX2 != null &&
currentPointY2 != null) {
currentPointX1 = currentPointX - currentPointX2! + currentPointX;
currentPointY1 = currentPointY - currentPointY2! + currentPointY;
currentPointX2 = double.parse(args[0]);
currentPointY2 = double.parse(args[1]);
currentPointX = double.parse(args[2]);
currentPointY = double.parse(args[3]);
path!.cubicTo(
currentPointX1!,
currentPointY1!,
currentPointX2!,
currentPointY2!,
currentPointX,
currentPointY,
);
} else {
currentPointX1 = double.parse(args[0]);
currentPointY1 = double.parse(args[1]);
currentPointX = double.parse(args[2]);
currentPointY = double.parse(args[3]);
path!.quadraticBezierTo(
currentPointX1!, currentPointY1!, currentPointX, currentPointY);
}
} else if (firstLetter == "s") {
if (currentPointX1 != null &&
currentPointY1 != null &&
currentPointX2 != null &&
currentPointY2 != null) {
currentPointX1 = currentPointX - currentPointX2! + currentPointX;
currentPointY1 = currentPointY - currentPointY2! + currentPointY;
currentPointX2 = currentPointX + double.parse(args[0]);
currentPointY2 = currentPointY + double.parse(args[1]);
currentPointX += double.parse(args[2]);
currentPointY += double.parse(args[3]);
path!.cubicTo(
currentPointX1!,
currentPointY1!,
currentPointX2!,
currentPointY2!,
currentPointX,
currentPointY,
);
} else {
currentPointX1 = currentPointX + double.parse(args[0]);
currentPointY1 = currentPointY + double.parse(args[1]);
currentPointX += double.parse(args[2]);
currentPointY += double.parse(args[3]);
path!.quadraticBezierTo(
currentPointX1!,
currentPointY1!,
currentPointX,
currentPointY,
);
}
} else if (firstLetter == "Q") {
currentPointX1 = double.parse(args[0]);
currentPointY1 = double.parse(args[1]);
currentPointX = double.parse(args[2]);
currentPointY = double.parse(args[3]);
path!.quadraticBezierTo(
currentPointX1!, currentPointY1!, currentPointX, currentPointY);
} else if (firstLetter == "q") {
currentPointX1 = currentPointX + double.parse(args[0]);
currentPointY1 = currentPointY + double.parse(args[1]);
currentPointX += double.parse(args[2]);
currentPointY += double.parse(args[3]);
path!.quadraticBezierTo(
currentPointX1!,
currentPointY1!,
currentPointX,
currentPointY,
);
} else if (firstLetter == "Z" || firstLetter == "z") {
path!.close();
}
}
videoItem.pathCache[argD] = path!;
});
return path;
}
void drawTextOnBitmap(
Canvas canvas, String imageKey, Rect frameRect, int frameAlpha) {
var dynamicText = videoItem.dynamicItem.dynamicText;
if (dynamicText.isEmpty) return;
if (dynamicText[imageKey] == null) return;
TextPainter? textPainter = dynamicText[imageKey];
textPainter?.paint(
canvas,
Offset(
(frameRect.width - textPainter.width) / 2.0,
(frameRect.height - textPainter.height) / 2.0,
),
);
}
@override
bool shouldRepaint(_SVGAPainter oldDelegate) {
if (controller._canvasNeedsClear == true) {
return true;
}
return !(oldDelegate.controller == controller &&
oldDelegate.controller.videoItem == controller.videoItem &&
oldDelegate.fit == fit &&
oldDelegate.filterQuality == filterQuality &&
oldDelegate.clipRect == clipRect);
}
}