Files
MatchLiveTv/backend/app/services/streams/overlay/svg_builder.rb
Emiliano Frascaro ab9cb02083 Aggiunge overlay server-side burn-in e stabilizza diretta live.
Tabellone, badge e brand sono ricodificati nel flusso _air via OverlayRelay;
sync punteggio HTTP, volume condiviso rails/sidekiq, qualità 720p migliorata
e badge CONNECTING in ripresa da pausa.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 23:55:17 +02:00

146 lines
5.9 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
module Streams
module Overlay
# PNG 1280×720 trasparente: tabellone alto-sinistra, badge alto-destra, brand sotto tabellone.
class SvgBuilder
CANVAS_W = 1280
CANVAS_H = 720
def initialize(session, badge:)
@session = session
@match = session.match
@team = @match.team
@score = session.score_state
@badge = badge
end
def to_svg
cols = score_columns
<<~SVG
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="#{CANVAS_W}" height="#{CANVAS_H}" viewBox="0 0 #{CANVAS_W} #{CANVAS_H}">
#{scorebug_svg(cols)}
#{badge_svg}
</svg>
SVG
end
private
def score_columns
score = @score
unless score
return {
labels: ["1"], home: ["0"], away: ["0"], home_name: abbrev(@match.team.name),
away_name: abbrev(@match.opponent_name), sets_won: "0-0", current_set: 1
}
end
partials = Array(score.set_partials)
labels = partials.map { |p| p["set"].to_s }
home = partials.map { |p| p["home"].to_s }
away = partials.map { |p| p["away"].to_s }
labels << score.current_set.to_s
home << score.home_points.to_s
away << score.away_points.to_s
{
labels: labels,
home: home,
away: away,
home_name: abbrev(@match.team.name),
away_name: abbrev(@match.opponent_name),
sets_won: "#{score.home_sets}-#{score.away_sets}",
current_set: score.current_set
}
end
def scorebug_svg(cols)
x0 = 12
y0 = 12
col_w = 26
team_w = 118
row_h = 18
head_h = 16
pad = 8
n = cols[:labels].length
box_w = team_w + n * col_w + pad * 2
box_h = head_h + row_h * 2 + 22 + pad * 2
live_i = n - 1
home_primary = @team.effective_primary_color
away_primary = Overlay.badge_away_color(@team)
header_cells = cols[:labels].map.with_index do |label, i|
cx = x0 + pad + team_w + i * col_w + col_w / 2
"<text x=\"#{cx}\" y=\"#{y0 + pad + 12}\" text-anchor=\"middle\" font-family=\"DejaVu Sans, sans-serif\" font-size=\"10\" font-weight=\"700\" fill=\"#666666\">#{escape(label)}</text>"
end.join
home_pts = cols[:home].map.with_index do |val, i|
cx = x0 + pad + team_w + i * col_w + col_w / 2
y = y0 + pad + head_h + 14
fill = i == live_i ? home_primary : "#111111"
weight = i == live_i ? "900" : "800"
bg = i == live_i ? "<rect x=\"#{cx - 12}\" y=\"#{y - 13}\" width=\"24\" height=\"16\" rx=\"3\" fill=\"#{home_primary}\" fill-opacity=\"0.14\"/>" : ""
"#{bg}<text x=\"#{cx}\" y=\"#{y}\" text-anchor=\"middle\" font-family=\"DejaVu Sans, sans-serif\" font-size=\"13\" font-weight=\"#{weight}\" fill=\"#{fill}\">#{escape(val)}</text>"
end.join
away_pts = cols[:away].map.with_index do |val, i|
cx = x0 + pad + team_w + i * col_w + col_w / 2
y = y0 + pad + head_h + row_h + 14
fill = i == live_i ? away_primary : "#111111"
weight = i == live_i ? "900" : "800"
bg = i == live_i ? "<rect x=\"#{cx - 12}\" y=\"#{y - 13}\" width=\"24\" height=\"16\" rx=\"3\" fill=\"#{away_primary}\" fill-opacity=\"0.14\"/>" : ""
"#{bg}<text x=\"#{cx}\" y=\"#{y}\" text-anchor=\"middle\" font-family=\"DejaVu Sans, sans-serif\" font-size=\"13\" font-weight=\"#{weight}\" fill=\"#{fill}\">#{escape(val)}</text>"
end.join
<<~SVG
<g id="scorebug">
<rect x="#{x0}" y="#{y0}" width="#{box_w}" height="#{box_h}" rx="6" fill="#ffffff" fill-opacity="0.94"/>
<line x1="#{x0 + pad}" y1="#{y0 + pad + head_h}" x2="#{x0 + box_w - pad}" y2="#{y0 + pad + head_h}" stroke="#000000" stroke-opacity="0.12"/>
#{header_cells}
<text x="#{x0 + pad}" y="#{y0 + pad + head_h + 14}" font-family="DejaVu Sans, sans-serif" font-size="12" font-weight="700" fill="#{home_primary}">#{escape(cols[:home_name])}</text>
#{home_pts}
<text x="#{x0 + pad}" y="#{y0 + pad + head_h + row_h + 14}" font-family="DejaVu Sans, sans-serif" font-size="12" font-weight="700" fill="#{away_primary}">#{escape(cols[:away_name])}</text>
#{away_pts}
<rect x="#{x0 + pad}" y="#{y0 + box_h - pad - 18}" width="#{box_w - pad * 2}" height="16" rx="4" fill="url(#brandGrad)"/>
<text x="#{x0 + box_w / 2}" y="#{y0 + box_h - pad - 6}" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="9" font-weight="800" fill="#ffffff" letter-spacing="1.2">MATCH <tspan fill="#e53935">LIVE TV</tspan></text>
</g>
<defs>
<linearGradient id="brandGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#14141c"/>
<stop offset="55%" stop-color="#2a1214"/>
<stop offset="100%" stop-color="#1a1a24"/>
</linearGradient>
</defs>
SVG
end
def badge_svg
text = @badge.text
bg = @badge.background
fg = @badge.foreground
pad_x = 12
pad_y = 6
fs = 11
tw = text.length * 6.5 + pad_x * 2
th = fs + pad_y * 2
x = CANVAS_W - 12 - tw
y = 12
<<~SVG
<g id="badge">
<rect x="#{x}" y="#{y}" width="#{tw}" height="#{th}" rx="#{th / 2}" fill="#{bg}"/>
<text x="#{x + tw / 2}" y="#{y + th - pad_y - 1}" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="#{fs}" font-weight="700" fill="#{fg}" letter-spacing="0.5">#{escape(text)}</text>
</g>
SVG
end
def abbrev(name, max = 16)
n = name.to_s
n.length <= max ? n : "#{n[0, max - 1]}…"
end
def escape(str)
str.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
end
end
end
end