Untitled
unknown
swift
16 days ago
6.5 kB
5
Indexable
struct LyricsRenderer: TextRenderer { var animatableData: Double { get { currentTick } set { currentTick = newValue } } var currentTick: Double var line: MuseLandLyricsLine var inactiveOpacity: Double = 0.3 var blendRadius: Double = 20 var brightness: Double = 0.5 // 计算每个slice对应的时间戳 func groupByWord(layout: Text.Layout) -> [Text.Layout.RunSlice: (Int, Int)] { let slices = Array(layout.flattenedRunSlices) var result: [Text.Layout.RunSlice: (Int, Int)] = [:] var index = 0 for string in line.blocks { for time in string.timestampByWord { result[slices[index]] = (time.0, time.1) index += 1 } } return result } func groupRunsByLineAndColor(layout: Text.Layout) -> [CGFloat: [ColorAttribute?: [Text.Layout.Run]]] { var lineGroups: [CGFloat: [ColorAttribute?: [Text.Layout.Run]]] = [:] for run in layout.flattenedRuns { let lineY = run.typographicBounds.origin.y let colorAttr = run[ColorAttribute.self] if lineGroups[lineY] == nil { lineGroups[lineY] = [:] } if lineGroups[lineY]![colorAttr] == nil { lineGroups[lineY]![colorAttr] = [] } lineGroups[lineY]![colorAttr]!.append(run) } return lineGroups } func draw(layout: Text.Layout, in ctx: inout GraphicsContext) { /// TODO: 添加缓存避免每一帧都计算 let group = self.groupByWord(layout: layout) let lineGroups = self.groupRunsByLineAndColor(layout: layout) var usedRuby : [String] = [] // 渲染底层文字 for run in layout.flattenedRuns { var copy = ctx copy.opacity = inactiveOpacity copy.draw(run) } // 渲染注音文字 for run in layout.flattenedRuns { if let ruby = run[RubyAttribute.self] { if !usedRuby.contains(ruby.value) { let rubyText = Text(ruby.value) .font(.system(size: 10)) usedRuby.append(ruby.value) ctx.draw( rubyText, at: CGPoint( x: run.typographicBounds.origin.x + run.typographicBounds.width / 2, y: run.typographicBounds.origin.y - run.typographicBounds.rect.height - 1 ) ) } } } // 渲染上层行 for (_, colorGroups) in lineGroups { for (colorAttr, runs) in colorGroups { // 渲染着色文字 if let colorAttr = colorAttr { // 计算这组runs的总边界 var combinedRect = runs[0].typographicBounds.rect for run in runs.dropFirst() { combinedRect = combinedRect.union(run.typographicBounds.rect) } let path = Path(combinedRect) // 为整组runs应用单一渐变 ctx.drawLayer { context in context.clipToLayer { c in for run in runs { drawMask(run, group: group, in: &c) } } // 只为有多个颜色的文字应用渐变 if colorAttr.colors.count > 1 { context.fill( path, with: .linearGradient( .init(colors: colorAttr.colors), startPoint: .init(x: combinedRect.minX + combinedRect.width / 3, y: 0), endPoint: .init(x: combinedRect.minX + combinedRect.width / 1.5, y: 0) ) ) } else { context.fill(path, with: .color(colorAttr.colors[0])) } } } else { // 没有颜色属性 for run in runs { drawMask(run, group: group, in: &ctx) } } } } } func drawMask(_ run: Text.Layout.Run, group: [Text.Layout.RunSlice: (Int, Int)], in context: inout GraphicsContext) { for rs in run { if let lyric = group[rs] { let beginTime = Double(lyric.0) let endTime = Double(lyric.1) draw( slice: rs, begin: beginTime, end: endTime, in: &context ) } } } func draw(slice: Text.Layout.RunSlice, begin: Double, end: Double, in context: inout GraphicsContext) { let elapsed = currentTick - begin let duration = end - begin let unclampedProgress = elapsed / duration let progress = max(0, min(1, unclampedProgress)) if progress == 0 || (begin == 0 && end == 0) { return } if progress < 1 { var context = context let rect = slice.typographicBounds.rect let unclampedFilledWidth = rect.width * progress let filledWidth = rect.width * progress let mask = Path( .init( x: rect.minX, y: rect.minY, width: filledWidth + blendRadius / 2, height: rect.height ) ) context.clipToLayer { context in context.fill( mask, with: .linearGradient( .init(colors: [.white, .clear]), startPoint: .init(x: rect.minX + unclampedFilledWidth - blendRadius / 2, y: 0), endPoint: .init(x: rect.minX + unclampedFilledWidth + blendRadius / 2, y: 0) ) ) } context.draw(slice) } else { context.draw(slice) } } }
Editor is loading...
Leave a Comment