feat: add refractor tier-specific card art rendering
Implement tier-aware visual styling for card PNG rendering (T0-T4). Each refractor tier gets distinct borders, header backgrounds, column header gradients, diamond tier indicators, and decorative effects. - New tier_style.html template: per-tier CSS overrides (borders, headers, gradients, inset glow, diamond positioning, corner accents) - Diamond indicator: 2x2 CSS grid rotated 45deg at header/result boundary, progressive fill (1B→2B→3B→Home) with tier-specific colors - T4 Superfractor: bold gold borders, dual gold-teal glow, corner accents, purple diamond with glow pulse animation - resolve_refractor_tier() helper: pure-math tier lookup from variant hash - T3/T4 animations defined but paused for static PNG capture (APNG follow-up) Relates-to: initiative #19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ffe07ec54c
commit
b32e19a4ac
@ -32,6 +32,7 @@ from ..db_engine import (
|
|||||||
)
|
)
|
||||||
from ..db_helpers import upsert_players
|
from ..db_helpers import upsert_players
|
||||||
from ..dependencies import oauth2_scheme, valid_token
|
from ..dependencies import oauth2_scheme, valid_token
|
||||||
|
from ..services.refractor_boost import compute_variant_hash
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Persistent browser instance (WP-02)
|
# Persistent browser instance (WP-02)
|
||||||
@ -132,6 +133,19 @@ def normalize_franchise(franchise: str) -> str:
|
|||||||
return FRANCHISE_NORMALIZE.get(titled, titled)
|
return FRANCHISE_NORMALIZE.get(titled, titled)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_refractor_tier(player_id: int, variant: int) -> int:
|
||||||
|
"""Determine the refractor tier (0-4) from a player's variant hash.
|
||||||
|
|
||||||
|
Pure math — no DB query needed. Returns 0 for base cards or unknown variants.
|
||||||
|
"""
|
||||||
|
if variant == 0:
|
||||||
|
return 0
|
||||||
|
for tier in range(1, 5):
|
||||||
|
if compute_variant_hash(player_id, tier) == variant:
|
||||||
|
return tier
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v2/players", tags=["players"])
|
router = APIRouter(prefix="/api/v2/players", tags=["players"])
|
||||||
|
|
||||||
|
|
||||||
@ -786,6 +800,7 @@ async def get_batter_card(
|
|||||||
card_data["cardset_name"] = this_player.cardset.name
|
card_data["cardset_name"] = this_player.cardset.name
|
||||||
else:
|
else:
|
||||||
card_data["cardset_name"] = this_player.description
|
card_data["cardset_name"] = this_player.description
|
||||||
|
card_data["refractor_tier"] = resolve_refractor_tier(player_id, variant)
|
||||||
card_data["request"] = request
|
card_data["request"] = request
|
||||||
html_response = templates.TemplateResponse("player_card.html", card_data)
|
html_response = templates.TemplateResponse("player_card.html", card_data)
|
||||||
|
|
||||||
@ -823,6 +838,7 @@ async def get_batter_card(
|
|||||||
card_data["cardset_name"] = this_player.cardset.name
|
card_data["cardset_name"] = this_player.cardset.name
|
||||||
else:
|
else:
|
||||||
card_data["cardset_name"] = this_player.description
|
card_data["cardset_name"] = this_player.description
|
||||||
|
card_data["refractor_tier"] = resolve_refractor_tier(player_id, variant)
|
||||||
card_data["request"] = request
|
card_data["request"] = request
|
||||||
html_response = templates.TemplateResponse("player_card.html", card_data)
|
html_response = templates.TemplateResponse("player_card.html", card_data)
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,32 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% include 'style.html' %}
|
{% include 'style.html' %}
|
||||||
|
{% include 'tier_style.html' %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="fullCard" style="width: 1200px; height: 600px;">
|
<div id="fullCard" style="width: 1200px; height: 600px;">
|
||||||
|
{%- set diamond_colors = {
|
||||||
|
1: {'color': '#1a6b1a', 'highlight': '#40b040'},
|
||||||
|
2: {'color': '#2070b0', 'highlight': '#50a0e8'},
|
||||||
|
3: {'color': '#a82020', 'highlight': '#e85050'},
|
||||||
|
4: {'color': '#6b2d8e', 'highlight': '#a060d0'},
|
||||||
|
} -%}
|
||||||
|
{% if refractor_tier is defined and refractor_tier > 0 %}
|
||||||
|
{%- set dc = diamond_colors[refractor_tier] -%}
|
||||||
|
{%- set filled_bg = 'linear-gradient(135deg, ' ~ dc.highlight ~ ' 0%, ' ~ dc.color ~ ' 50%, ' ~ dc.color ~ ' 100%)' -%}
|
||||||
|
<div class="tier-diamond{% if refractor_tier == 4 %} diamond-glow{% endif %}">
|
||||||
|
<div class="diamond-quad{% if refractor_tier >= 2 %} filled{% endif %}" {% if refractor_tier >= 2 %}style="background: {{ filled_bg }}; box-shadow: inset 0 1px 2px rgba(255,255,255,0.45), inset 0 -1px 2px rgba(0,0,0,0.35), inset 1px 0 2px rgba(255,255,255,0.15);"{% endif %}></div>
|
||||||
|
<div class="diamond-quad{% if refractor_tier >= 1 %} filled{% endif %}" {% if refractor_tier >= 1 %}style="background: {{ filled_bg }}; box-shadow: inset 0 1px 2px rgba(255,255,255,0.45), inset 0 -1px 2px rgba(0,0,0,0.35), inset 1px 0 2px rgba(255,255,255,0.15);"{% endif %}></div>
|
||||||
|
<div class="diamond-quad{% if refractor_tier >= 3 %} filled{% endif %}" {% if refractor_tier >= 3 %}style="background: {{ filled_bg }}; box-shadow: inset 0 1px 2px rgba(255,255,255,0.45), inset 0 -1px 2px rgba(0,0,0,0.35), inset 1px 0 2px rgba(255,255,255,0.15);"{% endif %}></div>
|
||||||
|
<div class="diamond-quad{% if refractor_tier >= 4 %} filled{% endif %}" {% if refractor_tier >= 4 %}style="background: {{ filled_bg }}; box-shadow: inset 0 1px 2px rgba(255,255,255,0.45), inset 0 -1px 2px rgba(0,0,0,0.35), inset 1px 0 2px rgba(255,255,255,0.15);"{% endif %}></div>
|
||||||
|
</div>
|
||||||
|
{% if refractor_tier == 4 %}
|
||||||
|
<div class="corner-accent tl" style="width: 35px; height: 35px; border-top: 3px solid #c9a94e; border-left: 3px solid #c9a94e;"></div>
|
||||||
|
<div class="corner-accent tr" style="width: 35px; height: 35px; border-top: 3px solid #c9a94e; border-right: 3px solid #c9a94e;"></div>
|
||||||
|
<div class="corner-accent bl" style="width: 35px; height: 35px; border-bottom: 3px solid #c9a94e; border-left: 3px solid #c9a94e;"></div>
|
||||||
|
<div class="corner-accent br" style="width: 35px; height: 35px; border-bottom: 3px solid #c9a94e; border-right: 3px solid #c9a94e;"></div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<div id="header" class="row-wrapper header-text border-bot" style="height: 65px">
|
<div id="header" class="row-wrapper header-text border-bot" style="height: 65px">
|
||||||
<!-- <div id="headerLeft" style="flex-grow: 3; height: auto">-->
|
<!-- <div id="headerLeft" style="flex-grow: 3; height: auto">-->
|
||||||
<div id="headerLeft" style="width: 477px; height: auto">
|
<div id="headerLeft" style="width: 477px; height: auto">
|
||||||
|
|||||||
233
storage/templates/tier_style.html
Normal file
233
storage/templates/tier_style.html
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
{% if refractor_tier is defined and refractor_tier > 0 %}
|
||||||
|
<style>
|
||||||
|
/* Shared rules for all refractor tiers */
|
||||||
|
#fullCard {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-diamond {
|
||||||
|
position: absolute;
|
||||||
|
left: 600px;
|
||||||
|
top: 78.5px;
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
display: grid;
|
||||||
|
grid-template: 1fr 1fr / 1fr 1fr;
|
||||||
|
gap: 2px;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgba(0,0,0,0.75);
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0 0 1.5px rgba(0,0,0,0.7), 0 2px 5px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diamond-quad {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-accent {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
pointer-events: none;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.corner-accent.tl { top: 0; left: 0; }
|
||||||
|
.corner-accent.tr { top: 0; right: 0; }
|
||||||
|
.corner-accent.bl { bottom: 0; left: 0; }
|
||||||
|
.corner-accent.br { bottom: 0; right: 0; }
|
||||||
|
|
||||||
|
{% if refractor_tier == 1 %}
|
||||||
|
/* T1 — Base Chrome */
|
||||||
|
#header {
|
||||||
|
background: linear-gradient(135deg, rgba(185,195,210,0.25) 0%, rgba(210,218,228,0.35) 50%, rgba(185,195,210,0.25) 100%), #ffffff;
|
||||||
|
}
|
||||||
|
.border-bot {
|
||||||
|
border-bottom-color: #8e9baf;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
}
|
||||||
|
#resultHeader.border-bot {
|
||||||
|
border-bottom-width: 3px;
|
||||||
|
}
|
||||||
|
.border-right-thick {
|
||||||
|
border-right-color: #8e9baf;
|
||||||
|
}
|
||||||
|
.border-right-thin {
|
||||||
|
border-right-color: #8e9baf;
|
||||||
|
}
|
||||||
|
.vline {
|
||||||
|
border-left-color: #8e9baf;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% elif refractor_tier == 2 %}
|
||||||
|
/* T2 — Refractor */
|
||||||
|
#header {
|
||||||
|
background: linear-gradient(135deg, rgba(100,155,230,0.28) 0%, rgba(155,90,220,0.18) 25%, rgba(90,200,210,0.24) 50%, rgba(185,80,170,0.16) 75%, rgba(100,155,230,0.28) 100%), #ffffff;
|
||||||
|
}
|
||||||
|
#fullCard {
|
||||||
|
box-shadow: inset 0 0 14px 3px rgba(90,143,207,0.22);
|
||||||
|
}
|
||||||
|
.border-bot {
|
||||||
|
border-bottom-color: #7a9cc4;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
}
|
||||||
|
#resultHeader .border-right-thick {
|
||||||
|
border-right-width: 6px;
|
||||||
|
}
|
||||||
|
.border-right-thick {
|
||||||
|
border-right-color: #7a9cc4;
|
||||||
|
}
|
||||||
|
.border-right-thin {
|
||||||
|
border-right-color: #7a9cc4;
|
||||||
|
border-right-width: 3px;
|
||||||
|
}
|
||||||
|
.vline {
|
||||||
|
border-left-color: #7a9cc4;
|
||||||
|
}
|
||||||
|
.blue-gradient {
|
||||||
|
background-image: linear-gradient(to right, rgba(60,110,200,1), rgba(100,55,185,0.55), rgba(60,110,200,1));
|
||||||
|
}
|
||||||
|
.red-gradient {
|
||||||
|
background-image: linear-gradient(to right, rgba(190,35,80,1), rgba(165,25,100,0.55), rgba(190,35,80,1));
|
||||||
|
}
|
||||||
|
|
||||||
|
{% elif refractor_tier == 3 %}
|
||||||
|
/* T3 — Gold Refractor */
|
||||||
|
#header {
|
||||||
|
background: linear-gradient(135deg, rgba(195,155,35,0.26) 0%, rgba(235,200,70,0.2) 50%, rgba(195,155,35,0.26) 100%), #ffffff;
|
||||||
|
}
|
||||||
|
#fullCard {
|
||||||
|
box-shadow: inset 0 0 16px 4px rgba(200,165,48,0.22);
|
||||||
|
}
|
||||||
|
.border-bot {
|
||||||
|
border-bottom-color: #c9a94e;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
}
|
||||||
|
.border-right-thick {
|
||||||
|
border-right-color: #c9a94e;
|
||||||
|
}
|
||||||
|
.border-right-thin {
|
||||||
|
border-right-color: #c9a94e;
|
||||||
|
border-right-width: 3px;
|
||||||
|
}
|
||||||
|
.vline {
|
||||||
|
border-left-color: #c9a94e;
|
||||||
|
}
|
||||||
|
.blue-gradient {
|
||||||
|
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
||||||
|
}
|
||||||
|
.red-gradient {
|
||||||
|
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* T3 shimmer animation — paused for static PNG capture */
|
||||||
|
#header { overflow: hidden; position: relative; }
|
||||||
|
@keyframes t3-shimmer {
|
||||||
|
0% { transform: translateX(-130%); }
|
||||||
|
100% { transform: translateX(230%); }
|
||||||
|
}
|
||||||
|
#header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
105deg,
|
||||||
|
transparent 38%,
|
||||||
|
rgba(255,240,140,0.18) 44%,
|
||||||
|
rgba(255,220,80,0.38) 50%,
|
||||||
|
rgba(255,200,60,0.30) 53%,
|
||||||
|
rgba(255,240,140,0.14) 58%,
|
||||||
|
transparent 64%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
animation: t3-shimmer 2.5s ease-in-out infinite;
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% elif refractor_tier == 4 %}
|
||||||
|
/* T4 — Superfractor */
|
||||||
|
#header {
|
||||||
|
background: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#fullCard {
|
||||||
|
box-shadow: inset 0 0 22px 6px rgba(45,212,191,0.28), inset 0 0 39px 9px rgba(200,165,48,0.15);
|
||||||
|
}
|
||||||
|
.border-bot {
|
||||||
|
border-bottom-color: #c9a94e;
|
||||||
|
border-bottom-width: 6px;
|
||||||
|
}
|
||||||
|
#resultHeader.border-bot {
|
||||||
|
border-bottom-width: 5px;
|
||||||
|
}
|
||||||
|
.border-right-thick {
|
||||||
|
border-right-color: #c9a94e;
|
||||||
|
border-right-width: 7px;
|
||||||
|
}
|
||||||
|
.border-right-thin {
|
||||||
|
border-right-color: #c9a94e;
|
||||||
|
border-right-width: 4px;
|
||||||
|
}
|
||||||
|
.vline {
|
||||||
|
border-left-color: #c9a94e;
|
||||||
|
}
|
||||||
|
.blue-gradient {
|
||||||
|
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
||||||
|
}
|
||||||
|
.red-gradient {
|
||||||
|
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* T4 prismatic header sweep — paused for static PNG capture */
|
||||||
|
@keyframes t4-prismatic-sweep {
|
||||||
|
0% { transform: translateX(0%); }
|
||||||
|
100% { transform: translateX(-50%); }
|
||||||
|
}
|
||||||
|
#header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 200%; height: 100%;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
transparent 2%, rgba(255,100,100,0.28) 8%, rgba(255,200,50,0.32) 14%,
|
||||||
|
rgba(100,255,150,0.30) 20%, rgba(50,190,255,0.32) 26%, rgba(140,80,255,0.28) 32%,
|
||||||
|
rgba(255,100,180,0.24) 38%, transparent 44%,
|
||||||
|
transparent 52%, rgba(255,100,100,0.28) 58%, rgba(255,200,50,0.32) 64%,
|
||||||
|
rgba(100,255,150,0.30) 70%, rgba(50,190,255,0.32) 76%, rgba(140,80,255,0.28) 82%,
|
||||||
|
rgba(255,100,180,0.24) 88%, transparent 94%
|
||||||
|
);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: t4-prismatic-sweep 6s linear infinite;
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
#header > * { position: relative; z-index: 2; }
|
||||||
|
|
||||||
|
/* T4 diamond glow pulse — paused for static PNG */
|
||||||
|
@keyframes diamond-glow-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 1.5px rgba(0,0,0,0.7), 0 2px 5px rgba(0,0,0,0.5),
|
||||||
|
0 0 8px 2px rgba(107,45,142,0.6); }
|
||||||
|
50% { box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5), 0 2px 4px rgba(0,0,0,0.3),
|
||||||
|
0 0 14px 5px rgba(107,45,142,0.8),
|
||||||
|
0 0 24px 8px rgba(107,45,142,0.3); }
|
||||||
|
}
|
||||||
|
.tier-diamond.diamond-glow {
|
||||||
|
animation: diamond-glow-pulse 2s ease-in-out infinite;
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</style>
|
||||||
|
{% else %}
|
||||||
|
<style>
|
||||||
|
/* T0 Standard — no tier overrides */
|
||||||
|
#fullCard {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endif %}
|
||||||
Loading…
Reference in New Issue
Block a user