Untitled
unknown
swift
8 months ago
6.5 kB
8
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