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>
146 lines
5.9 KiB
Ruby
146 lines
5.9 KiB
Ruby
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("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
||
end
|
||
end
|
||
end
|
||
end
|