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 = [ sourceRect.left, centerSlice.left, centerSlice.right, sourceRect.right, ]; final sourceYs = [ sourceRect.top, centerSlice.top, centerSlice.bottom, sourceRect.bottom, ]; final destXs = [ canvasRect.left, canvasRect.left + dstLeft, canvasRect.right - dstRight, canvasRect.right, ]; final destYs = [ 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([ 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 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([ 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 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); } }