Mudanças entre as edições de "Widget:C.Skins"
Ir para navegação
Ir para pesquisar
m |
m |
||
| Linha 1: | Linha 1: | ||
<!-- SKINS SYSTEM - Layout Isométrico --> | <!-- SKINS SYSTEM - Layout Isométrico --> | ||
<script> | <script> | ||
(function initSkinsPodiumUI() { | |||
const podium = document.querySelector(".skins-podium"); | |||
if (!podium || podium.dataset.wired === "1") return; | |||
podium.dataset.wired = "1"; | |||
// ---------- Função para aplicar offset_x individualmente em cada skin ---------- | |||
// IMPORTANTE: Usa margin-left em vez de transform para afetar o layout do flexbox | |||
function applyOffsetX() { | |||
const allSlots = podium.querySelectorAll(".podium-slot"); | |||
allSlots.forEach((slot, index) => { | |||
// Lê o atributo data-offset-x diretamente do elemento | |||
const offsetXAttr = slot.getAttribute("data-offset-x"); | |||
if ( | |||
offsetXAttr !== null && | |||
offsetXAttr !== "" && | |||
offsetXAttr !== "undefined" | |||
) { | |||
const numOffset = parseFloat(offsetXAttr); | |||
if (!isNaN(numOffset) && numOffset !== 0) { | |||
// Usa margin-left negativo para afetar o layout do flexbox | |||
// Isso faz com que o elemento realmente se aproxime dos outros | |||
const currentStyle = slot.getAttribute("style") || ""; | |||
// Remove qualquer margin-left anterior e transform (legado) | |||
const cleanedStyle = currentStyle | |||
.replace(/margin-left\s*:[^;]+;?/gi, "") | |||
.replace(/transform\s*:[^;]+;?/gi, "") | |||
.trim(); | |||
// Aplica o margin-left preservando outros estilos (como z-index) | |||
const newMargin = `margin-left: ${numOffset}px`; | |||
if (cleanedStyle && cleanedStyle.length > 0) { | |||
slot.setAttribute( | |||
"style", | |||
cleanedStyle + | |||
(cleanedStyle.endsWith(";") ? " " : "; ") + | |||
newMargin + | |||
";" | |||
); | |||
} else { | |||
slot.setAttribute("style", newMargin + ";"); | |||
} | |||
// Força a atualização do layout | |||
slot.offsetHeight; | |||
} else { | |||
// Se o offset for 0 ou inválido, remove margin-left e transform | |||
const currentStyle = slot.getAttribute("style") || ""; | |||
const cleanedStyle = currentStyle | |||
.replace(/margin-left\s*:[^;]+;?/gi, "") | |||
.replace(/transform\s*:[^;]+;?/gi, "") | |||
.trim(); | |||
if (cleanedStyle) { | |||
slot.setAttribute( | |||
"style", | |||
cleanedStyle + (cleanedStyle.endsWith(";") ? "" : ";") | |||
); | |||
} else { | |||
slot.removeAttribute("style"); | |||
} | |||
} | |||
} else { | |||
// Se não tem offset_x, remove margin-left e transform (preserva outros estilos) | |||
const currentStyle = slot.getAttribute("style") || ""; | |||
const cleanedStyle = currentStyle | |||
.replace(/margin-left\s*:[^;]+;?/gi, "") | |||
.replace(/transform\s*:[^;]+;?/gi, "") | |||
.trim(); | |||
if (cleanedStyle) { | |||
slot.setAttribute( | |||
"style", | |||
cleanedStyle + (cleanedStyle.endsWith(";") ? "" : ";") | |||
); | |||
} else { | |||
slot.removeAttribute("style"); | |||
} | |||
} | } | ||
}); | |||
} | |||
// Flag para evitar múltiplas execuções simultâneas | |||
let isApplying = false; | |||
function applyOffsetXOnce() { | |||
if (isApplying) return; | |||
isApplying = true; | |||
applyOffsetX(); | |||
requestAnimationFrame(() => { | |||
isApplying = false; | |||
}); | |||
} | |||
// Aplica usando requestAnimationFrame para garantir que o DOM esteja renderizado | |||
requestAnimationFrame(() => { | |||
applyOffsetXOnce(); | |||
// Reaplica após pequenos delays para garantir | |||
setTimeout(applyOffsetXOnce, 50); | |||
setTimeout(applyOffsetXOnce, 200); | |||
}); | |||
// Também aplica quando o DOM estiver pronto | |||
if (document.readyState === "loading") { | |||
document.addEventListener("DOMContentLoaded", () => { | |||
requestAnimationFrame(applyOffsetXOnce); | |||
}); | |||
} | |||
// Observa mudanças no DOM para reaplicar se necessário (apenas para novos elementos) | |||
if (window.MutationObserver) { | |||
const observer = new MutationObserver((mutations) => { | |||
// Só reaplica se houver mudanças relevantes | |||
const hasRelevantChanges = mutations.some( | |||
(m) => | |||
m.type === "childList" || | |||
(m.type === "attributes" && | |||
(m.attributeName === "data-offset-x" || | |||
m.attributeName === "style")) | |||
); | |||
if (hasRelevantChanges) { | |||
clearTimeout(observer.timeout); | |||
observer.timeout = setTimeout(applyOffsetXOnce, 50); | |||
} | } | ||
}); | |||
observer.observe(podium, { | |||
childList: true, | |||
subtree: false, // Apenas observa filhos diretos | |||
attributes: true, | |||
attributeFilter: ["data-offset-x"], | |||
}); | |||
} | |||
// ---------- Tooltip único ---------- | |||
function ensureTip() { | |||
let tip = document.querySelector(".skin-tooltip"); | |||
if (!tip) { | |||
tip = document.createElement("div"); | |||
tip.className = "skin-tooltip"; | |||
tip.setAttribute("role", "tooltip"); | |||
tip.setAttribute("aria-hidden", "true"); | |||
tip.style.position = "fixed"; | |||
tip.style.transform = "translate(-9999px,-9999px)"; | |||
tip.style.opacity = "0"; | |||
tip.style.transition = "opacity .15s ease"; | |||
document.body.appendChild(tip); | |||
} | |||
return tip; | |||
} | |||
const tip = ensureTip(); | |||
let hoveredSlot = null; | |||
function place(card) { | |||
if (!card || tip.getAttribute("aria-hidden") === "true") return; | |||
// Calcula nova posição - tooltip centralizada em relação à imagem do piso | |||
tip.style.transform = "translate(-9999px,-9999px)"; | |||
const tr = tip.getBoundingClientRect(); | |||
// Pega a posição da imagem do tile (piso) para centralizar | |||
const platformImg = card.querySelector(".podium-platform-top img"); | |||
let platformRect = card.getBoundingClientRect(); // Fallback para o card se não encontrar | |||
if (platformImg) { | |||
platformRect = platformImg.getBoundingClientRect(); | |||
} else { | |||
// Se não encontrar a img, tenta o container do tile | |||
const platform = card.querySelector(".podium-platform"); | |||
if (platform) { | |||
platformRect = platform.getBoundingClientRect(); | |||
} | |||
} | |||
// Centraliza horizontalmente baseado na imagem do tile | |||
let leftPos = Math.round( | |||
platformRect.left + (platformRect.width - tr.width) / 2 | |||
); | |||
leftPos = Math.max( | |||
8, | |||
Math.min(leftPos, window.innerWidth - tr.width - 8) | |||
); | |||
// Posiciona logo abaixo da imagem do tile, com pequeno espaçamento | |||
let top = Math.round(platformRect.bottom + 15); | |||
// Se não couber embaixo, coloca em cima | |||
if (top + tr.height > window.innerHeight - 8) { | |||
top = Math.round(platformRect.top - tr.height - 15); | |||
if (top < 8) top = 8; // Fallback se não couber em cima também | |||
} | |||
tip.style.transform = `translate(${leftPos}px, ${top}px)`; | |||
} | |||
function show(card) { | |||
const tooltipText = card.getAttribute("data-skin-tooltip") || ""; | |||
tip.innerHTML = tooltipText; | |||
tip.setAttribute("aria-hidden", "false"); | |||
place(card); | |||
tip.style.opacity = "1"; | |||
} | |||
function hide() { | |||
tip.setAttribute("aria-hidden", "true"); | |||
tip.style.opacity = "0"; | |||
tip.style.transform = "translate(-9999px,-9999px)"; | |||
} | |||
// Função para restaurar z-index original baseado na posição do slot | |||
function restoreOriginalZIndex(slot) { | |||
if (!slot) return; | |||
// Determina a posição do slot (1, 2, 3, 4, etc) | |||
const allSlots = Array.from(podium.querySelectorAll(".podium-slot")); | |||
const slotIndex = allSlots.indexOf(slot); | |||
// Z-index: esquerda (0) = menor, direita (último) = maior (direita passa na frente) | |||
const originalZIndex = slotIndex + 1; | |||
const originalSpriteZIndex = (slotIndex + 1) * 10; | |||
slot.style.zIndex = originalZIndex.toString(); | |||
const spriteContainer = slot.querySelector(".podium-sprite-container"); | |||
if (spriteContainer) | |||
spriteContainer.style.zIndex = originalSpriteZIndex.toString(); | |||
// CRÍTICO: Garante que TODOS os tiles sempre fiquem atrás de TODAS as sprites | |||
// Força z-index negativo nos tiles de todos os slots | |||
const platform = slot.querySelector(".podium-platform"); | |||
const platformTop = slot.querySelector(".podium-platform-top"); | |||
const platformImg = slot.querySelector(".podium-platform-top img"); | |||
if (platform) platform.style.zIndex = "-1"; | |||
if (platformTop) platformTop.style.zIndex = "-2"; | |||
if (platformImg) platformImg.style.zIndex = "-2"; | |||
} | |||
function setHovered(card) { | |||
if (hoveredSlot === card) { | |||
// Se já está hovered, não reposiciona - mantém posição fixa | |||
return; | |||
} | |||
// Remove hover anterior e restaura z-index original | |||
if (hoveredSlot) { | |||
hoveredSlot.classList.remove("hovered"); | |||
restoreOriginalZIndex(hoveredSlot); | |||
// Garante que tiles continuem atrás após restaurar z-index | |||
forceTilesBehind(); | |||
} | |||
podium.classList.remove("hovering"); | |||
podium | |||
.querySelectorAll(".podium-slot.dim") | |||
.forEach((n) => n.classList.remove("dim")); | |||
if (!card) { | |||
hoveredSlot = null; | |||
hide(); | |||
return; | |||
} | |||
hoveredSlot = card; | |||
hoveredSlot.classList.add("hovered"); | |||
podium.classList.add("hovering"); | |||
podium.querySelectorAll(".podium-slot").forEach((n) => { | |||
if (n !== hoveredSlot) n.classList.add("dim"); | |||
}); | |||
// Qualquer skin com hover fica acima de tudo | |||
card.style.zIndex = "9999"; | |||
const spriteContainer = card.querySelector(".podium-sprite-container"); | |||
if (spriteContainer) spriteContainer.style.zIndex = "9999"; | |||
// CRÍTICO: Garante que tiles continuem atrás mesmo durante hover | |||
forceTilesBehind(); | |||
show(card); | |||
} | |||
// Função para verificar se o pixel na posição do mouse é transparente | |||
function isPixelTransparent(img, x, y) { | |||
if (!img.complete || img.naturalWidth === 0) return true; | |||
try { | |||
const canvas = document.createElement("canvas"); | |||
canvas.width = img.naturalWidth; | |||
canvas.height = img.naturalHeight; | |||
const ctx = canvas.getContext("2d"); | |||
ctx.drawImage(img, 0, 0); | |||
// Calcula a posição relativa na imagem | |||
const rect = img.getBoundingClientRect(); | |||
const scaleX = img.naturalWidth / rect.width; | |||
const scaleY = img.naturalHeight / rect.height; | |||
const imgX = Math.floor((x - rect.left) * scaleX); | |||
const imgY = Math.floor((y - rect.top) * scaleY); | |||
// Verifica se está dentro dos limites | |||
if ( | |||
imgX < 0 || | |||
imgX >= img.naturalWidth || | |||
imgY < 0 || | |||
imgY >= img.naturalHeight | |||
) { | |||
return true; | |||
} | |||
// Obtém o pixel | |||
const pixelData = ctx.getImageData(imgX, imgY, 1, 1).data; | |||
const alpha = pixelData[3]; // Canal alpha | |||
return alpha < 10; // Considera transparente se alpha < 10 | |||
} catch (e) { | |||
return false; // Em caso de erro, permite o hover | |||
} | |||
} | |||
// ---------- Clique YouTube (sem duplicar) ---------- | |||
podium.addEventListener( | |||
"click", | |||
(ev) => { | |||
const slot = ev.target?.closest(".podium-slot[data-youtube]"); | |||
if (!slot) return; | |||
const url = (slot.dataset.youtube || "").trim(); | |||
if (!url) return; | |||
if (slot.dataset._opening === "1") return; | |||
slot.dataset._opening = "1"; | |||
ev.preventDefault(); | |||
ev.stopPropagation(); | |||
ev.stopImmediatePropagation(); | |||
try { | |||
window.open(url, "_blank", "noopener,noreferrer"); | |||
} catch (e) { | |||
location.href = url; | |||
} | } | ||
setTimeout(() => { | |||
delete slot.dataset._opening; | |||
}, 500); | |||
}, | |||
{ capture: true } | |||
); | |||
podium.addEventListener( | |||
"keydown", | |||
(ev) => { | |||
if (ev.key !== "Enter" && ev.key !== " ") return; | |||
const slot = ev.target?.closest(".podium-slot[data-youtube]"); | |||
if (!slot) return; | |||
const url = (slot.dataset.youtube || "").trim(); | |||
if (!url) return; | |||
if (slot.dataset._opening === "1") return; | |||
slot.dataset._opening = "1"; | |||
ev.preventDefault(); | |||
ev.stopPropagation(); | |||
ev.stopImmediatePropagation(); | |||
try { | |||
window.open(url, "_blank", "noopener,noreferrer"); | |||
} catch (e) { | |||
location.href = url; | |||
} | |||
setTimeout(() => { | |||
delete slot.dataset._opening; | |||
}, 500); | |||
}, | |||
{ capture: true } | |||
); | |||
// ---------- Hitbox pixel-perfect por sprite ---------- | |||
// CRÍTICO: Move tiles para fora do sprite-container para separar contexto de empilhamento | |||
function moveTilesOutOfSpriteContainer() { | |||
const allSlots = Array.from(podium.querySelectorAll(".podium-slot")); | |||
allSlots.forEach((slot) => { | |||
const spriteContainer = slot.querySelector(".podium-sprite-container"); | |||
const platform = slot.querySelector(".podium-platform"); | |||
// Se o tile ainda está dentro do sprite-container, move para o slot | |||
if (platform && spriteContainer && spriteContainer.contains(platform)) { | |||
// Salva os estilos inline atuais antes de mover | |||
const currentRight = platform.style.right || ""; | |||
const currentBottom = platform.style.bottom || ""; | |||
const computedRight = getComputedStyle(platform).right; | |||
const computedBottom = getComputedStyle(platform).bottom; | |||
// Move o tile para fora do sprite-container, mas mantém dentro do slot | |||
slot.appendChild(platform); | |||
// Restaura a posição (os valores já estão corretos, apenas precisa manter) | |||
platform.style.position = "absolute"; | |||
if (currentRight) { | |||
platform.style.right = currentRight; | |||
} else if (computedRight && computedRight !== "auto") { | |||
platform.style.right = computedRight; | |||
} else { | |||
// Usa o valor padrão do CSS | |||
platform.style.right = "-25px"; | |||
} | |||
if (currentBottom) { | |||
platform.style.bottom = currentBottom; | |||
} else if (computedBottom && computedBottom !== "auto") { | |||
platform.style.bottom = computedBottom; | |||
} else { | |||
// Usa o valor padrão do CSS | |||
platform.style.bottom = "-15px"; | |||
} | |||
} | } | ||
}); | |||
} | |||
// CRÍTICO: Força todos os tiles a terem z-index negativo na inicialização | |||
function forceTilesBehind() { | |||
const allSlots = Array.from(podium.querySelectorAll(".podium-slot")); | |||
allSlots.forEach((slot) => { | |||
const platform = slot.querySelector(".podium-platform"); | |||
const platformTop = slot.querySelector(".podium-platform-top"); | |||
const platformImg = slot.querySelector(".podium-platform-top img"); | |||
if (platform) { | |||
platform.style.zIndex = "0"; | |||
platform.style.setProperty("z-index", "0", "important"); | |||
} | |||
if (platformTop) { | |||
platformTop.style.zIndex = "0"; | |||
platformTop.style.setProperty("z-index", "0", "important"); | |||
} | |||
if (platformImg) { | |||
platformImg.style.zIndex = "0"; | |||
platformImg.style.setProperty("z-index", "0", "important"); | |||
} | } | ||
}); | |||
} | |||
// Move tiles para fora do sprite-container primeiro | |||
moveTilesOutOfSpriteContainer(); | |||
setTimeout(moveTilesOutOfSpriteContainer, 10); | |||
setTimeout(moveTilesOutOfSpriteContainer, 100); | |||
// Depois força z-index negativo | |||
forceTilesBehind(); | |||
setTimeout(forceTilesBehind, 10); | |||
setTimeout(forceTilesBehind, 100); | |||
const slots = Array.from(podium.querySelectorAll(".podium-slot")); | |||
slots.forEach((slot) => { | |||
const spriteImg = slot.querySelector(".podium-sprite img"); | |||
// Hitbox apenas na imagem do sprite, verificando transparência | |||
if (spriteImg) { | |||
spriteImg.addEventListener( | |||
"pointermove", | |||
(ev) => { | |||
if (!slot.hasAttribute("data-skin-tooltip")) return; | |||
// Verifica se o pixel é transparente | |||
if (isPixelTransparent(spriteImg, ev.clientX, ev.clientY)) { | |||
// Se for transparente e estiver hovered, remove o hover | |||
if (hoveredSlot === slot) { | |||
setHovered(null); | |||
} | |||
return; | |||
} | |||
// Se não for transparente, ativa o hover | |||
if (hoveredSlot !== slot) { | |||
setHovered(slot); | |||
} | |||
}, | |||
{ passive: true } | |||
); | |||
spriteImg.addEventListener( | |||
"pointerenter", | |||
(ev) => { | |||
if (!slot.hasAttribute("data-skin-tooltip")) return; | |||
// Verifica transparência no enter também | |||
if (!isPixelTransparent(spriteImg, ev.clientX, ev.clientY)) { | |||
setHovered(slot); | |||
} | |||
}, | |||
{ passive: true } | |||
); | |||
spriteImg.addEventListener( | |||
"pointerleave", | |||
(ev) => { | |||
const toCard = | |||
ev.relatedTarget && | |||
ev.relatedTarget.closest && | |||
ev.relatedTarget.closest(".podium-slot"); | |||
if (toCard && podium.contains(toCard)) { | |||
// Se está indo para outro slot, verifica transparência | |||
const otherImg = toCard.querySelector(".podium-sprite img"); | |||
if ( | |||
otherImg && | |||
ev.relatedTarget && | |||
!isPixelTransparent(otherImg, ev.clientX, ev.clientY) | |||
) { | |||
return; // Não remove hover se está indo para pixel não-transparente | |||
} | |||
} | } | ||
}); | setHovered(null); | ||
}, | |||
{ passive: true } | |||
); | |||
} | |||
}); | |||
podium.addEventListener( | |||
"pointerleave", | |||
() => { | |||
setHovered(null); | |||
}, | |||
{ passive: true } | |||
); | |||
// Só atualiza em scroll/resize, não em mousemove | |||
window.addEventListener( | |||
"scroll", | |||
() => { | |||
if (hoveredSlot) place(hoveredSlot); | |||
}, | |||
true | |||
); | |||
window.addEventListener("resize", () => { | |||
if (hoveredSlot) place(hoveredSlot); | |||
}); | |||
// Função para ajustar sombra ao tamanho exato da imagem do tile | |||
function updateShadows() { | |||
const platformTops = document.querySelectorAll(".podium-platform-top"); | |||
platformTops.forEach((top, index) => { | |||
const img = top.querySelector("img"); | |||
if (img) { | |||
const updateShadow = () => { | |||
// Usa naturalWidth/naturalHeight primeiro (mais rápido) | |||
let imgWidth = img.naturalWidth || img.offsetWidth || 0; | |||
let imgHeight = img.naturalHeight || img.offsetHeight || 0; | |||
// Se ainda não tem dimensões, tenta forçar o carregamento | |||
if (imgWidth === 0 || imgHeight === 0) { | |||
// Força o recálculo das dimensões | |||
imgWidth = img.naturalWidth || img.width || img.offsetWidth || 0; | |||
imgHeight = | |||
img.naturalHeight || img.height || img.offsetHeight || 0; | |||
} | |||
if (imgWidth > 0 && imgHeight > 0) { | |||
top.style.setProperty("--img-width", imgWidth + "px"); | |||
top.style.setProperty("--img-height", imgHeight + "px"); | |||
} else { | |||
// Se ainda não tem dimensões, agenda nova tentativa | |||
// Especialmente importante para a última skin que pode carregar por último | |||
setTimeout(() => { | |||
const retryWidth = | |||
img.naturalWidth || img.offsetWidth || img.width || 0; | |||
const retryHeight = | |||
img.naturalHeight || img.offsetHeight || img.height || 0; | |||
if (retryWidth > 0 && retryHeight > 0) { | |||
top.style.setProperty("--img-width", retryWidth + "px"); | |||
top.style.setProperty("--img-height", retryHeight + "px"); | |||
} | |||
}, 100 + index * 20); // Delay progressivo para cada tile | |||
} | |||
}; | |||
// Tenta atualizar imediatamente | |||
updateShadow(); | |||
// Se a imagem já carregou, atualiza novamente para garantir | |||
if (img.complete && img.naturalWidth > 0) { | |||
requestAnimationFrame(updateShadow); | |||
} else { | |||
// Usa 'load' e também verifica periodicamente | |||
img.addEventListener( | |||
"load", | |||
() => { | |||
requestAnimationFrame(updateShadow); | |||
}, | |||
{ once: true } | |||
); | |||
// Fallback: verifica após um tempo curto (com delay progressivo para última skin) | |||
setTimeout(() => { | |||
if (img.complete || img.naturalWidth > 0) { | |||
requestAnimationFrame(updateShadow); | |||
} | |||
}, 50 + index * 10); | |||
// Fallback adicional para garantir que a última skin seja atualizada | |||
if (index === platformTops.length - 1) { | |||
setTimeout(() => { | |||
updateShadow(); | |||
}, 200); | |||
setTimeout(() => { | |||
updateShadow(); | |||
}, 500); | |||
} | |||
} | |||
} | } | ||
}); | |||
} | |||
// Executa imediatamente e também após DOMContentLoaded | |||
updateShadows(); | |||
if (document.readyState === "loading") { | |||
document.addEventListener("DOMContentLoaded", updateShadows); | |||
window.addEventListener("load", updateShadows); | |||
} else { | |||
// Se já carregou, executa imediatamente e depois de um frame | |||
requestAnimationFrame(updateShadows); | |||
} | |||
// Garante que todas as imagens dos tiles sejam atualizadas após o carregamento completo | |||
window.addEventListener("load", () => { | |||
setTimeout(updateShadows, 100); | |||
setTimeout(updateShadows, 300); | |||
setTimeout(updateShadows, 500); | |||
}); | |||
// Observa mudanças nas imagens | |||
if ("MutationObserver" in window) { | |||
const observer = new MutationObserver(() => { | |||
updateShadows(); | |||
}); | |||
observer.observe(podium, { childList: true, subtree: true }); | |||
} | |||
// Observa quando imagens dos tiles são carregadas | |||
const tileImages = document.querySelectorAll(".podium-platform-top img"); | |||
tileImages.forEach((img, index) => { | |||
if (img.complete) { | |||
// Se já carregou, força atualização | |||
setTimeout(() => updateShadows(), 50 + index * 20); | |||
} else { | |||
img.addEventListener( | |||
"load", | |||
() => { | |||
updateShadows(); | |||
}, | |||
{ once: true } | |||
); | |||
} | |||
}); | |||
})(); | |||
</script> | </script> | ||
<style> | <style> | ||
/* Container escuro para área de skins - igual ao content-card */ | |||
.card-skins { | |||
width: min(1600px, 96vw); | |||
max-width: 96vw; | |||
margin: 10px auto; | |||
background: rgba(28, 28, 34, 0.95); | |||
border-radius: 12px; | |||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); | |||
padding: 18px; | |||
z-index: 2; | |||
box-sizing: border-box; | |||
overflow-x: hidden; | |||
overflow-y: visible; | |||
} | } | ||
/* Regras globais para filhos do card-skins - igual ao content-card */ | |||
.card-skins, | |||
.skins-podium { | |||
box-sizing: border-box; | |||
max-width: 100%; | |||
} | |||
.card-skins * { | |||
max-width: 100%; | |||
box-sizing: border-box; | |||
} | |||
/* Exceção: imagens do tile e sprite mantêm tamanho natural (exceto no mobile) */ | |||
.podium-platform-top img, | |||
.podium-sprite img { | |||
max-width: none; | |||
} | |||
/* Podium de skins - layout isométrico */ | |||
.skins-podium { | |||
display: flex; | |||
align-items: flex-end; | |||
/* Alinha todos os slots pelo bottom */ | |||
justify-content: center; | |||
gap: 30px; | |||
padding: 20px 0 50px 0; | |||
/* Padding extra na parte inferior para os tiles */ | |||
position: relative; | |||
width: 100%; | |||
flex-wrap: wrap; | |||
box-sizing: border-box; | |||
overflow-x: hidden; | |||
overflow-y: visible; | |||
} | |||
.podium-slot { | |||
position: relative; | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
justify-content: flex-end; | |||
/* Alinha pelo bottom */ | |||
cursor: pointer; | |||
transition: filter 0.14s ease, box-shadow 0.14s ease; | |||
flex-shrink: 0; | |||
max-width: 100%; | |||
box-sizing: border-box; | |||
overflow: visible; | |||
/* Permite que os tiles saiam do slot se necessário */ | |||
/* Permite margin-left negativo para offset_x */ | |||
} | |||
/* Z-index: sprites da direita passam na frente das da esquerda */ | |||
/* Aplicado dinamicamente via JS, mas CSS base para primeiros slots */ | |||
.podium-slot:nth-child(1) { | |||
z-index: 1; | |||
/* Esquerda: mais atrás */ | |||
} | |||
.podium-slot:nth-child(2) { | |||
z-index: 2; | |||
} | |||
.podium-slot:nth-child(3) { | |||
z-index: 3; | |||
} | |||
.podium-slot:nth-child(4) { | |||
z-index: 4; | |||
/* Direita: mais na frente */ | |||
} | |||
/* Z-index para sprite-containers: direita passa na frente da esquerda */ | |||
/* IMPORTANTE: z-index alto para garantir que sprites sempre fiquem acima dos tiles (z-index negativo) */ | |||
.podium-slot:nth-child(1) .podium-sprite-container { | |||
z-index: 1000 !important; | |||
} | /* Esquerda: mais atrás */ | ||
} | |||
.podium-slot:nth-child(2) .podium-sprite-container { | |||
z-index: 2000 !important; | |||
} | |||
.podium-slot:nth-child(3) .podium-sprite-container { | |||
z-index: 3000 !important; | |||
} | |||
.podium-slot:nth-child(4) .podium-sprite-container { | |||
z-index: 4000 !important; | |||
/* Direita: mais na frente, acima de todos os tiles */ | |||
} | |||
/* Hitbox baseada na imagem, não no container */ | |||
.podium-slot > * { | |||
pointer-events: none; | |||
} | |||
/* Apenas a imagem do sprite tem hitbox - container não interfere */ | |||
.podium-sprite img { | |||
pointer-events: auto; | |||
} | |||
/* Container e sprite não têm hitbox para não interferir */ | |||
.podium-sprite-container, | |||
.podium-sprite { | |||
pointer-events: none; | |||
} | |||
/* Piso nunca tem hitbox */ | |||
.podium-platform, | |||
.podium-platform-top, | |||
.podium-platform-top img { | |||
pointer-events: none !important; | |||
} | |||
/* Container do sprite - tamanho natural, não afetado pelo tile */ | |||
.podium-sprite-container { | |||
} | position: relative; | ||
z-index: 10; | |||
display: flex; | |||
flex-direction: column; | |||
align-items: flex-end; | |||
/* Alinha pelo bottom (pés) - horizontalmente */ | |||
justify-content: center; | |||
/* Centraliza horizontalmente */ | |||
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.5)); | |||
transform: none !important; | |||
flex-shrink: 0; | |||
max-width: 100%; | |||
box-sizing: border-box; | |||
/* REMOVIDO isolation: isolate - estava criando contexto que impedia tiles de ficarem atrás */ | |||
} | |||
/* Garante que TODAS as sprites fiquem acima de TODOS os tiles */ | |||
/* IMPORTANTE: z-index mínimo de 1 para garantir que sprites sempre fiquem acima dos tiles (z-index negativo) */ | |||
.podium-slot .podium-sprite-container { | |||
position: relative; | |||
z-index: 1 !important; | |||
} | |||
.podium-sprite { | |||
display: block; | |||
position: relative; | |||
transform: none !important; | |||
flex-shrink: 0; | |||
} | |||
/* | .podium-sprite img { | ||
width: auto; | |||
height: auto; | |||
max-width: none; | |||
max-height: none; | |||
image-rendering: pixelated; | |||
image-rendering: -moz-crisp-edges; | |||
image-rendering: -webkit-optimize-contrast; | |||
image-rendering: crisp-edges; | |||
display: block; | |||
position: relative; | |||
z-index: 10 !important; | |||
/* Imagem sempre acima do piso (z-index maior que tiles) */ | |||
transform: none !important; | |||
will-change: auto; | |||
flex-shrink: 0; | |||
} | |||
/* Tile isométrico nos pés da sprite - atrás da sprite */ | |||
/* IMPORTANTE: z-index 0 para ficar acima do background do card-skins, mas abaixo das sprites (z-index 10-40) */ | |||
.podium-platform { | |||
} | position: absolute; | ||
width: auto; | |||
height: auto; | |||
bottom: -15px; | |||
right: -25px; | |||
z-index: 0 !important; | |||
/* z-index 0 fica acima do background do card-skins, mas abaixo das sprites */ | |||
} | |||
/* Todos os pisos com z-index 0 - acima do background, mas abaixo das sprites */ | |||
.podium-slot .podium-platform { | |||
z-index: 0 !important; | |||
} | |||
/* CRÍTICO: Garante que tiles fiquem acima do background do card, mas abaixo de TODAS as sprites */ | |||
.podium-slot .podium-platform, | |||
.podium-slot .podium-platform-top, | |||
.podium-slot .podium-platform-top img { | |||
/* z-index 0 garante que fiquem acima do background do card-skins */ | |||
/* mas as sprites têm z-index 10-40, então ficam na frente */ | |||
z-index: 0 !important; | |||
} | |||
/* Tile - usando imagem no tamanho natural */ | |||
.podium-platform-top { | |||
position: absolute; | |||
width: auto; | |||
height: auto; | |||
transform-origin: center bottom; | |||
transform: rotateX(15deg); | |||
/* Inclinação fixa em 15 graus */ | |||
z-index: 0 !important; | |||
/* z-index 0 fica acima do background do card, mas abaixo das sprites */ | |||
} | |||
.podium-platform-top img { | |||
/* | display: block; | ||
width: auto; | |||
height: auto; | |||
max-width: none; | |||
} | max-height: none; | ||
image-rendering: pixelated; | |||
image-rendering: -moz-crisp-edges; | |||
image-rendering: -webkit-optimize-contrast; | |||
image-rendering: crisp-edges; | |||
position: relative; | |||
z-index: 0 !important; | |||
/* z-index 0 fica acima do background do card, mas abaixo das sprites (z-index 10-40) */ | |||
border: none !important; | |||
outline: none; | |||
box-shadow: none; | |||
opacity: 1 !important; | |||
/* Garante que a imagem do tile sempre esteja visível */ | |||
} | |||
/* Sombra quadrada sólida - perfeitamente alinhada com a imagem */ | |||
.podium-platform-top::before { | |||
content: ""; | |||
position: absolute; | |||
top: 0; | |||
} | left: 0; | ||
width: var(--img-width, 100%); | |||
height: var(--img-height, 100%); | |||
background: rgba(0, 0, 0, 0.5); | |||
z-index: -1 !important; | |||
/* z-index -1 para ficar atrás da imagem do tile, mas ainda acima do background do card */ | |||
transform: translate(3px, 3px); | |||
pointer-events: none; | |||
opacity: 0; | |||
transition: opacity 0.1s ease; | |||
} | |||
/* Mostra a sombra quando as dimensões estão definidas */ | |||
.podium-platform-top[style*="--img-width"]::before { | |||
opacity: 1; | |||
} | |||
/* Sistema de hover - dim outras skins, destaque na hovered */ | |||
.skins-podium.hovering .podium-slot.dim { | |||
filter: brightness(0.55) saturate(0.85); | |||
transition: filter 0.14s ease; | |||
} | |||
.skins-podium.hovering .podium-slot.hovered { | |||
filter: none; | |||
} | |||
/* CRÍTICO: Remove efeito de hover da imagem do piso (sem "borda fantasma") */ | |||
.skins-podium.hovering .podium-slot.dim .podium-platform-top, | |||
.skins-podium.hovering .podium-slot.hovered .podium-platform-top, | |||
.skins-podium.hovering .podium-slot.dim .podium-platform-top img, | |||
.skins-podium.hovering .podium-slot.hovered .podium-platform-top img { | |||
filter: none !important; | |||
box-shadow: none !important; | |||
} | |||
/* Borda no tile quando hovered - apenas no container, não na imagem */ | |||
.skins-podium.hovering .podium-slot.hovered .podium-platform-top { | |||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12), | |||
0 10px 28px rgba(0, 0, 0, 0.45); | |||
} | |||
/* Remove sombra da imagem do tile no hover */ | |||
.skins-podium.hovering .podium-slot.hovered .podium-platform-top img { | |||
box-shadow: none !important; | |||
filter: none !important; | |||
} | |||
/* Tooltip */ | |||
.skin-tooltip { | |||
position: fixed; | |||
z-index: 9999; | |||
left: 0; | |||
top: 0; | |||
pointer-events: none; | |||
padding: 10px 12px; | |||
border-radius: 8px; | |||
background: rgba(40, 45, 60, 0.98); /* Azul escuro para melhor contraste */ | |||
color: #f0f0f0; | |||
font-size: 13px; | |||
line-height: 1.4; | |||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), | |||
0 0 0 1px rgba(80, 90, 120, 0.4), /* Borda azulada para definição */ | |||
} | inset 0 0 0 1px rgba(255, 255, 255, 0.08); | ||
transform: translate(-9999px, -9999px); | |||
opacity: 0; | |||
transition: opacity 0.15s ease; | |||
text-align: center; | |||
white-space: normal; | |||
max-width: 280px; | |||
min-width: 200px; | |||
} | |||
.skin-tooltip b { | |||
color: #ffffff; | |||
font-weight: 600; | |||
} | } | ||
.podium-slot.is-clickable { | |||
cursor: pointer; | |||
} | |||
.podium-slot.is-clickable:focus { | |||
outline: 2px solid #156bc7; | |||
} | outline-offset: 2px; | ||
} | |||
/* Responsivo */ | |||
@media (max-width: 1100px) { | |||
.skins-podium | .skins-podium { | ||
gap: 24px; | |||
} | } | ||
} | |||
@media (max-width: 768px) { | |||
.skins | .card-skins { | ||
padding: 14px; | |||
} | } | ||
.skins-podium { | |||
.skins-podium | gap: 20px; | ||
padding: 15px 0; | |||
} | } | ||
} | |||
@media (max-width: 600px) { | |||
. | .card-skins { | ||
box-sizing: border-box; | |||
max-width: calc( | |||
100vw - env(safe-area-inset-left) - env(safe-area-inset-right) - 16px | |||
); | |||
width: 100%; | |||
margin: 10px auto; | |||
padding: 12px; | |||
border-radius: 10px; | |||
overflow-x: hidden; | |||
overflow-y: visible; | |||
} | } | ||
. | .skins-podium { | ||
gap: 16px; | |||
padding: 10px 8px 50px 8px; | |||
/* Padding extra na parte inferior para os tiles */ | |||
justify-content: center; | |||
overflow-y: visible; | |||
} | } | ||
.podium-slot | .podium-slot { | ||
max-width: calc(50% - 8px); | |||
flex: 0 0 auto; | |||
min-width: 0; | |||
} | } | ||
.podium- | .podium-sprite-container { | ||
max-width: 100%; | |||
min-width: 0; | |||
} | } | ||
.podium-sprite img { | |||
max-width: 100%; | |||
height: auto; | |||
} | } | ||
.podium-platform-top img { | |||
max-width: 100%; | |||
height: auto; | |||
} | } | ||
} | |||
@media (max-width: 400px) { | |||
.skins-podium { | |||
gap: 12px; | |||
padding-left: 4px; | |||
padding-right: 4px; | |||
} | } | ||
.podium-slot { | |||
max-width: calc(50% - 6px); | |||
} | } | ||
} | |||
</style> | </style> | ||
Edição das 14h54min de 13 de janeiro de 2026
<script>
(function initSkinsPodiumUI() {
const podium = document.querySelector(".skins-podium");
if (!podium || podium.dataset.wired === "1") return;
podium.dataset.wired = "1";
// ---------- Função para aplicar offset_x individualmente em cada skin ----------
// IMPORTANTE: Usa margin-left em vez de transform para afetar o layout do flexbox
function applyOffsetX() {
const allSlots = podium.querySelectorAll(".podium-slot");
allSlots.forEach((slot, index) => {
// Lê o atributo data-offset-x diretamente do elemento
const offsetXAttr = slot.getAttribute("data-offset-x");
if (
offsetXAttr !== null &&
offsetXAttr !== "" &&
offsetXAttr !== "undefined"
) {
const numOffset = parseFloat(offsetXAttr);
if (!isNaN(numOffset) && numOffset !== 0) {
// Usa margin-left negativo para afetar o layout do flexbox
// Isso faz com que o elemento realmente se aproxime dos outros
const currentStyle = slot.getAttribute("style") || "";
// Remove qualquer margin-left anterior e transform (legado)
const cleanedStyle = currentStyle
.replace(/margin-left\s*:[^;]+;?/gi, "")
.replace(/transform\s*:[^;]+;?/gi, "")
.trim();
// Aplica o margin-left preservando outros estilos (como z-index)
const newMargin = `margin-left: ${numOffset}px`;
if (cleanedStyle && cleanedStyle.length > 0) {
slot.setAttribute(
"style",
cleanedStyle +
(cleanedStyle.endsWith(";") ? " " : "; ") +
newMargin +
";"
);
} else {
slot.setAttribute("style", newMargin + ";");
}
// Força a atualização do layout
slot.offsetHeight;
} else {
// Se o offset for 0 ou inválido, remove margin-left e transform
const currentStyle = slot.getAttribute("style") || "";
const cleanedStyle = currentStyle
.replace(/margin-left\s*:[^;]+;?/gi, "")
.replace(/transform\s*:[^;]+;?/gi, "")
.trim();
if (cleanedStyle) {
slot.setAttribute(
"style",
cleanedStyle + (cleanedStyle.endsWith(";") ? "" : ";")
);
} else {
slot.removeAttribute("style");
}
}
} else {
// Se não tem offset_x, remove margin-left e transform (preserva outros estilos)
const currentStyle = slot.getAttribute("style") || "";
const cleanedStyle = currentStyle
.replace(/margin-left\s*:[^;]+;?/gi, "")
.replace(/transform\s*:[^;]+;?/gi, "")
.trim();
if (cleanedStyle) {
slot.setAttribute(
"style",
cleanedStyle + (cleanedStyle.endsWith(";") ? "" : ";")
);
} else {
slot.removeAttribute("style");
}
}
});
}
// Flag para evitar múltiplas execuções simultâneas let isApplying = false;
function applyOffsetXOnce() {
if (isApplying) return;
isApplying = true;
applyOffsetX();
requestAnimationFrame(() => {
isApplying = false;
});
}
// Aplica usando requestAnimationFrame para garantir que o DOM esteja renderizado
requestAnimationFrame(() => {
applyOffsetXOnce();
// Reaplica após pequenos delays para garantir
setTimeout(applyOffsetXOnce, 50);
setTimeout(applyOffsetXOnce, 200);
});
// Também aplica quando o DOM estiver pronto
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
requestAnimationFrame(applyOffsetXOnce);
});
}
// Observa mudanças no DOM para reaplicar se necessário (apenas para novos elementos)
if (window.MutationObserver) {
const observer = new MutationObserver((mutations) => {
// Só reaplica se houver mudanças relevantes
const hasRelevantChanges = mutations.some(
(m) =>
m.type === "childList" ||
(m.type === "attributes" &&
(m.attributeName === "data-offset-x" ||
m.attributeName === "style"))
);
if (hasRelevantChanges) {
clearTimeout(observer.timeout);
observer.timeout = setTimeout(applyOffsetXOnce, 50);
}
});
observer.observe(podium, {
childList: true,
subtree: false, // Apenas observa filhos diretos
attributes: true,
attributeFilter: ["data-offset-x"],
});
}
// ---------- Tooltip único ----------
function ensureTip() {
let tip = document.querySelector(".skin-tooltip");
if (!tip) {
tip = document.createElement("div");
tip.className = "skin-tooltip";
tip.setAttribute("role", "tooltip");
tip.setAttribute("aria-hidden", "true");
tip.style.position = "fixed";
tip.style.transform = "translate(-9999px,-9999px)";
tip.style.opacity = "0";
tip.style.transition = "opacity .15s ease";
document.body.appendChild(tip);
}
return tip;
}
const tip = ensureTip(); let hoveredSlot = null;
function place(card) {
if (!card || tip.getAttribute("aria-hidden") === "true") return;
// Calcula nova posição - tooltip centralizada em relação à imagem do piso
tip.style.transform = "translate(-9999px,-9999px)";
const tr = tip.getBoundingClientRect();
// Pega a posição da imagem do tile (piso) para centralizar
const platformImg = card.querySelector(".podium-platform-top img");
let platformRect = card.getBoundingClientRect(); // Fallback para o card se não encontrar
if (platformImg) {
platformRect = platformImg.getBoundingClientRect();
} else {
// Se não encontrar a img, tenta o container do tile
const platform = card.querySelector(".podium-platform");
if (platform) {
platformRect = platform.getBoundingClientRect();
}
}
// Centraliza horizontalmente baseado na imagem do tile
let leftPos = Math.round(
platformRect.left + (platformRect.width - tr.width) / 2
);
leftPos = Math.max(
8,
Math.min(leftPos, window.innerWidth - tr.width - 8)
);
// Posiciona logo abaixo da imagem do tile, com pequeno espaçamento
let top = Math.round(platformRect.bottom + 15);
// Se não couber embaixo, coloca em cima
if (top + tr.height > window.innerHeight - 8) {
top = Math.round(platformRect.top - tr.height - 15);
if (top < 8) top = 8; // Fallback se não couber em cima também
}
tip.style.transform = `translate(${leftPos}px, ${top}px)`;
}
function show(card) {
const tooltipText = card.getAttribute("data-skin-tooltip") || "";
tip.innerHTML = tooltipText;
tip.setAttribute("aria-hidden", "false");
place(card);
tip.style.opacity = "1";
}
function hide() {
tip.setAttribute("aria-hidden", "true");
tip.style.opacity = "0";
tip.style.transform = "translate(-9999px,-9999px)";
}
// Função para restaurar z-index original baseado na posição do slot
function restoreOriginalZIndex(slot) {
if (!slot) return;
// Determina a posição do slot (1, 2, 3, 4, etc)
const allSlots = Array.from(podium.querySelectorAll(".podium-slot"));
const slotIndex = allSlots.indexOf(slot);
// Z-index: esquerda (0) = menor, direita (último) = maior (direita passa na frente)
const originalZIndex = slotIndex + 1;
const originalSpriteZIndex = (slotIndex + 1) * 10;
slot.style.zIndex = originalZIndex.toString();
const spriteContainer = slot.querySelector(".podium-sprite-container");
if (spriteContainer)
spriteContainer.style.zIndex = originalSpriteZIndex.toString();
// CRÍTICO: Garante que TODOS os tiles sempre fiquem atrás de TODAS as sprites
// Força z-index negativo nos tiles de todos os slots
const platform = slot.querySelector(".podium-platform");
const platformTop = slot.querySelector(".podium-platform-top");
const platformImg = slot.querySelector(".podium-platform-top img");
if (platform) platform.style.zIndex = "-1";
if (platformTop) platformTop.style.zIndex = "-2";
if (platformImg) platformImg.style.zIndex = "-2";
}
function setHovered(card) {
if (hoveredSlot === card) {
// Se já está hovered, não reposiciona - mantém posição fixa
return;
}
// Remove hover anterior e restaura z-index original
if (hoveredSlot) {
hoveredSlot.classList.remove("hovered");
restoreOriginalZIndex(hoveredSlot);
// Garante que tiles continuem atrás após restaurar z-index
forceTilesBehind();
}
podium.classList.remove("hovering");
podium
.querySelectorAll(".podium-slot.dim")
.forEach((n) => n.classList.remove("dim"));
if (!card) {
hoveredSlot = null;
hide();
return;
}
hoveredSlot = card;
hoveredSlot.classList.add("hovered");
podium.classList.add("hovering");
podium.querySelectorAll(".podium-slot").forEach((n) => {
if (n !== hoveredSlot) n.classList.add("dim");
});
// Qualquer skin com hover fica acima de tudo
card.style.zIndex = "9999";
const spriteContainer = card.querySelector(".podium-sprite-container");
if (spriteContainer) spriteContainer.style.zIndex = "9999";
// CRÍTICO: Garante que tiles continuem atrás mesmo durante hover
forceTilesBehind();
show(card); }
// Função para verificar se o pixel na posição do mouse é transparente
function isPixelTransparent(img, x, y) {
if (!img.complete || img.naturalWidth === 0) return true;
try {
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// Calcula a posição relativa na imagem
const rect = img.getBoundingClientRect();
const scaleX = img.naturalWidth / rect.width;
const scaleY = img.naturalHeight / rect.height;
const imgX = Math.floor((x - rect.left) * scaleX);
const imgY = Math.floor((y - rect.top) * scaleY);
// Verifica se está dentro dos limites
if (
imgX < 0 ||
imgX >= img.naturalWidth ||
imgY < 0 ||
imgY >= img.naturalHeight
) {
return true;
}
// Obtém o pixel
const pixelData = ctx.getImageData(imgX, imgY, 1, 1).data;
const alpha = pixelData[3]; // Canal alpha
return alpha < 10; // Considera transparente se alpha < 10
} catch (e) {
return false; // Em caso de erro, permite o hover
}
}
// ---------- Clique YouTube (sem duplicar) ----------
podium.addEventListener(
"click",
(ev) => {
const slot = ev.target?.closest(".podium-slot[data-youtube]");
if (!slot) return;
const url = (slot.dataset.youtube || "").trim();
if (!url) return;
if (slot.dataset._opening === "1") return;
slot.dataset._opening = "1";
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation();
try {
window.open(url, "_blank", "noopener,noreferrer");
} catch (e) {
location.href = url;
}
setTimeout(() => {
delete slot.dataset._opening;
}, 500);
},
{ capture: true }
);
podium.addEventListener(
"keydown",
(ev) => {
if (ev.key !== "Enter" && ev.key !== " ") return;
const slot = ev.target?.closest(".podium-slot[data-youtube]");
if (!slot) return;
const url = (slot.dataset.youtube || "").trim();
if (!url) return;
if (slot.dataset._opening === "1") return;
slot.dataset._opening = "1";
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation();
try {
window.open(url, "_blank", "noopener,noreferrer");
} catch (e) {
location.href = url;
}
setTimeout(() => {
delete slot.dataset._opening;
}, 500);
},
{ capture: true }
);
// ---------- Hitbox pixel-perfect por sprite ----------
// CRÍTICO: Move tiles para fora do sprite-container para separar contexto de empilhamento
function moveTilesOutOfSpriteContainer() {
const allSlots = Array.from(podium.querySelectorAll(".podium-slot"));
allSlots.forEach((slot) => {
const spriteContainer = slot.querySelector(".podium-sprite-container");
const platform = slot.querySelector(".podium-platform");
// Se o tile ainda está dentro do sprite-container, move para o slot
if (platform && spriteContainer && spriteContainer.contains(platform)) {
// Salva os estilos inline atuais antes de mover
const currentRight = platform.style.right || "";
const currentBottom = platform.style.bottom || "";
const computedRight = getComputedStyle(platform).right;
const computedBottom = getComputedStyle(platform).bottom;
// Move o tile para fora do sprite-container, mas mantém dentro do slot
slot.appendChild(platform);
// Restaura a posição (os valores já estão corretos, apenas precisa manter)
platform.style.position = "absolute";
if (currentRight) {
platform.style.right = currentRight;
} else if (computedRight && computedRight !== "auto") {
platform.style.right = computedRight;
} else {
// Usa o valor padrão do CSS
platform.style.right = "-25px";
}
if (currentBottom) {
platform.style.bottom = currentBottom;
} else if (computedBottom && computedBottom !== "auto") {
platform.style.bottom = computedBottom;
} else {
// Usa o valor padrão do CSS
platform.style.bottom = "-15px";
}
}
});
}
// CRÍTICO: Força todos os tiles a terem z-index negativo na inicialização
function forceTilesBehind() {
const allSlots = Array.from(podium.querySelectorAll(".podium-slot"));
allSlots.forEach((slot) => {
const platform = slot.querySelector(".podium-platform");
const platformTop = slot.querySelector(".podium-platform-top");
const platformImg = slot.querySelector(".podium-platform-top img");
if (platform) {
platform.style.zIndex = "0";
platform.style.setProperty("z-index", "0", "important");
}
if (platformTop) {
platformTop.style.zIndex = "0";
platformTop.style.setProperty("z-index", "0", "important");
}
if (platformImg) {
platformImg.style.zIndex = "0";
platformImg.style.setProperty("z-index", "0", "important");
}
});
}
// Move tiles para fora do sprite-container primeiro moveTilesOutOfSpriteContainer(); setTimeout(moveTilesOutOfSpriteContainer, 10); setTimeout(moveTilesOutOfSpriteContainer, 100);
// Depois força z-index negativo forceTilesBehind(); setTimeout(forceTilesBehind, 10); setTimeout(forceTilesBehind, 100);
const slots = Array.from(podium.querySelectorAll(".podium-slot"));
slots.forEach((slot) => {
const spriteImg = slot.querySelector(".podium-sprite img");
// Hitbox apenas na imagem do sprite, verificando transparência
if (spriteImg) {
spriteImg.addEventListener(
"pointermove",
(ev) => {
if (!slot.hasAttribute("data-skin-tooltip")) return;
// Verifica se o pixel é transparente
if (isPixelTransparent(spriteImg, ev.clientX, ev.clientY)) {
// Se for transparente e estiver hovered, remove o hover
if (hoveredSlot === slot) {
setHovered(null);
}
return;
}
// Se não for transparente, ativa o hover
if (hoveredSlot !== slot) {
setHovered(slot);
}
},
{ passive: true }
);
spriteImg.addEventListener(
"pointerenter",
(ev) => {
if (!slot.hasAttribute("data-skin-tooltip")) return;
// Verifica transparência no enter também
if (!isPixelTransparent(spriteImg, ev.clientX, ev.clientY)) {
setHovered(slot);
}
},
{ passive: true }
);
spriteImg.addEventListener(
"pointerleave",
(ev) => {
const toCard =
ev.relatedTarget &&
ev.relatedTarget.closest &&
ev.relatedTarget.closest(".podium-slot");
if (toCard && podium.contains(toCard)) {
// Se está indo para outro slot, verifica transparência
const otherImg = toCard.querySelector(".podium-sprite img");
if (
otherImg &&
ev.relatedTarget &&
!isPixelTransparent(otherImg, ev.clientX, ev.clientY)
) {
return; // Não remove hover se está indo para pixel não-transparente
}
}
setHovered(null);
},
{ passive: true }
);
}
});
podium.addEventListener(
"pointerleave",
() => {
setHovered(null);
},
{ passive: true }
);
// Só atualiza em scroll/resize, não em mousemove
window.addEventListener(
"scroll",
() => {
if (hoveredSlot) place(hoveredSlot);
},
true
);
window.addEventListener("resize", () => {
if (hoveredSlot) place(hoveredSlot);
});
// Função para ajustar sombra ao tamanho exato da imagem do tile
function updateShadows() {
const platformTops = document.querySelectorAll(".podium-platform-top");
platformTops.forEach((top, index) => {
const img = top.querySelector("img");
if (img) {
const updateShadow = () => {
// Usa naturalWidth/naturalHeight primeiro (mais rápido)
let imgWidth = img.naturalWidth || img.offsetWidth || 0;
let imgHeight = img.naturalHeight || img.offsetHeight || 0;
// Se ainda não tem dimensões, tenta forçar o carregamento
if (imgWidth === 0 || imgHeight === 0) {
// Força o recálculo das dimensões
imgWidth = img.naturalWidth || img.width || img.offsetWidth || 0;
imgHeight =
img.naturalHeight || img.height || img.offsetHeight || 0;
}
if (imgWidth > 0 && imgHeight > 0) {
top.style.setProperty("--img-width", imgWidth + "px");
top.style.setProperty("--img-height", imgHeight + "px");
} else {
// Se ainda não tem dimensões, agenda nova tentativa
// Especialmente importante para a última skin que pode carregar por último
setTimeout(() => {
const retryWidth =
img.naturalWidth || img.offsetWidth || img.width || 0;
const retryHeight =
img.naturalHeight || img.offsetHeight || img.height || 0;
if (retryWidth > 0 && retryHeight > 0) {
top.style.setProperty("--img-width", retryWidth + "px");
top.style.setProperty("--img-height", retryHeight + "px");
}
}, 100 + index * 20); // Delay progressivo para cada tile
}
};
// Tenta atualizar imediatamente
updateShadow();
// Se a imagem já carregou, atualiza novamente para garantir
if (img.complete && img.naturalWidth > 0) {
requestAnimationFrame(updateShadow);
} else {
// Usa 'load' e também verifica periodicamente
img.addEventListener(
"load",
() => {
requestAnimationFrame(updateShadow);
},
{ once: true }
);
// Fallback: verifica após um tempo curto (com delay progressivo para última skin)
setTimeout(() => {
if (img.complete || img.naturalWidth > 0) {
requestAnimationFrame(updateShadow);
}
}, 50 + index * 10);
// Fallback adicional para garantir que a última skin seja atualizada
if (index === platformTops.length - 1) {
setTimeout(() => {
updateShadow();
}, 200);
setTimeout(() => {
updateShadow();
}, 500);
}
}
}
});
}
// Executa imediatamente e também após DOMContentLoaded updateShadows();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", updateShadows);
window.addEventListener("load", updateShadows);
} else {
// Se já carregou, executa imediatamente e depois de um frame
requestAnimationFrame(updateShadows);
}
// Garante que todas as imagens dos tiles sejam atualizadas após o carregamento completo
window.addEventListener("load", () => {
setTimeout(updateShadows, 100);
setTimeout(updateShadows, 300);
setTimeout(updateShadows, 500);
});
// Observa mudanças nas imagens
if ("MutationObserver" in window) {
const observer = new MutationObserver(() => {
updateShadows();
});
observer.observe(podium, { childList: true, subtree: true });
}
// Observa quando imagens dos tiles são carregadas
const tileImages = document.querySelectorAll(".podium-platform-top img");
tileImages.forEach((img, index) => {
if (img.complete) {
// Se já carregou, força atualização
setTimeout(() => updateShadows(), 50 + index * 20);
} else {
img.addEventListener(
"load",
() => {
updateShadows();
},
{ once: true }
);
}
});
})();
</script> <style>
/* Container escuro para área de skins - igual ao content-card */
.card-skins {
width: min(1600px, 96vw);
max-width: 96vw;
margin: 10px auto;
background: rgba(28, 28, 34, 0.95);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
padding: 18px;
z-index: 2;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: visible;
}
/* Regras globais para filhos do card-skins - igual ao content-card */
.card-skins,
.skins-podium {
box-sizing: border-box;
max-width: 100%;
}
.card-skins * {
max-width: 100%;
box-sizing: border-box;
}
/* Exceção: imagens do tile e sprite mantêm tamanho natural (exceto no mobile) */
.podium-platform-top img,
.podium-sprite img {
max-width: none;
}
/* Podium de skins - layout isométrico */
.skins-podium {
display: flex;
align-items: flex-end;
/* Alinha todos os slots pelo bottom */
justify-content: center;
gap: 30px;
padding: 20px 0 50px 0;
/* Padding extra na parte inferior para os tiles */
position: relative;
width: 100%;
flex-wrap: wrap;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: visible;
}
.podium-slot {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
/* Alinha pelo bottom */
cursor: pointer;
transition: filter 0.14s ease, box-shadow 0.14s ease;
flex-shrink: 0;
max-width: 100%;
box-sizing: border-box;
overflow: visible;
/* Permite que os tiles saiam do slot se necessário */
/* Permite margin-left negativo para offset_x */
}
/* Z-index: sprites da direita passam na frente das da esquerda */
/* Aplicado dinamicamente via JS, mas CSS base para primeiros slots */
.podium-slot:nth-child(1) {
z-index: 1;
/* Esquerda: mais atrás */
}
.podium-slot:nth-child(2) {
z-index: 2;
}
.podium-slot:nth-child(3) {
z-index: 3;
}
.podium-slot:nth-child(4) {
z-index: 4;
/* Direita: mais na frente */
}
/* Z-index para sprite-containers: direita passa na frente da esquerda */
/* IMPORTANTE: z-index alto para garantir que sprites sempre fiquem acima dos tiles (z-index negativo) */
.podium-slot:nth-child(1) .podium-sprite-container {
z-index: 1000 !important;
/* Esquerda: mais atrás */
}
.podium-slot:nth-child(2) .podium-sprite-container {
z-index: 2000 !important;
}
.podium-slot:nth-child(3) .podium-sprite-container {
z-index: 3000 !important;
}
.podium-slot:nth-child(4) .podium-sprite-container {
z-index: 4000 !important;
/* Direita: mais na frente, acima de todos os tiles */
}
/* Hitbox baseada na imagem, não no container */
.podium-slot > * {
pointer-events: none;
}
/* Apenas a imagem do sprite tem hitbox - container não interfere */
.podium-sprite img {
pointer-events: auto;
}
/* Container e sprite não têm hitbox para não interferir */
.podium-sprite-container,
.podium-sprite {
pointer-events: none;
}
/* Piso nunca tem hitbox */
.podium-platform,
.podium-platform-top,
.podium-platform-top img {
pointer-events: none !important;
}
/* Container do sprite - tamanho natural, não afetado pelo tile */
.podium-sprite-container {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: flex-end;
/* Alinha pelo bottom (pés) - horizontalmente */
justify-content: center;
/* Centraliza horizontalmente */
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.5));
transform: none !important;
flex-shrink: 0;
max-width: 100%;
box-sizing: border-box;
/* REMOVIDO isolation: isolate - estava criando contexto que impedia tiles de ficarem atrás */
}
/* Garante que TODAS as sprites fiquem acima de TODOS os tiles */
/* IMPORTANTE: z-index mínimo de 1 para garantir que sprites sempre fiquem acima dos tiles (z-index negativo) */
.podium-slot .podium-sprite-container {
position: relative;
z-index: 1 !important;
}
.podium-sprite {
display: block;
position: relative;
transform: none !important;
flex-shrink: 0;
}
.podium-sprite img {
width: auto;
height: auto;
max-width: none;
max-height: none;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
display: block;
position: relative;
z-index: 10 !important;
/* Imagem sempre acima do piso (z-index maior que tiles) */
transform: none !important;
will-change: auto;
flex-shrink: 0;
}
/* Tile isométrico nos pés da sprite - atrás da sprite */
/* IMPORTANTE: z-index 0 para ficar acima do background do card-skins, mas abaixo das sprites (z-index 10-40) */
.podium-platform {
position: absolute;
width: auto;
height: auto;
bottom: -15px;
right: -25px;
z-index: 0 !important;
/* z-index 0 fica acima do background do card-skins, mas abaixo das sprites */
}
/* Todos os pisos com z-index 0 - acima do background, mas abaixo das sprites */
.podium-slot .podium-platform {
z-index: 0 !important;
}
/* CRÍTICO: Garante que tiles fiquem acima do background do card, mas abaixo de TODAS as sprites */
.podium-slot .podium-platform,
.podium-slot .podium-platform-top,
.podium-slot .podium-platform-top img {
/* z-index 0 garante que fiquem acima do background do card-skins */
/* mas as sprites têm z-index 10-40, então ficam na frente */
z-index: 0 !important;
}
/* Tile - usando imagem no tamanho natural */
.podium-platform-top {
position: absolute;
width: auto;
height: auto;
transform-origin: center bottom;
transform: rotateX(15deg);
/* Inclinação fixa em 15 graus */
z-index: 0 !important;
/* z-index 0 fica acima do background do card, mas abaixo das sprites */
}
.podium-platform-top img {
display: block;
width: auto;
height: auto;
max-width: none;
max-height: none;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
position: relative;
z-index: 0 !important;
/* z-index 0 fica acima do background do card, mas abaixo das sprites (z-index 10-40) */
border: none !important;
outline: none;
box-shadow: none;
opacity: 1 !important;
/* Garante que a imagem do tile sempre esteja visível */
}
/* Sombra quadrada sólida - perfeitamente alinhada com a imagem */
.podium-platform-top::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: var(--img-width, 100%);
height: var(--img-height, 100%);
background: rgba(0, 0, 0, 0.5);
z-index: -1 !important;
/* z-index -1 para ficar atrás da imagem do tile, mas ainda acima do background do card */
transform: translate(3px, 3px);
pointer-events: none;
opacity: 0;
transition: opacity 0.1s ease;
}
/* Mostra a sombra quando as dimensões estão definidas */
.podium-platform-top[style*="--img-width"]::before {
opacity: 1;
}
/* Sistema de hover - dim outras skins, destaque na hovered */
.skins-podium.hovering .podium-slot.dim {
filter: brightness(0.55) saturate(0.85);
transition: filter 0.14s ease;
}
.skins-podium.hovering .podium-slot.hovered {
filter: none;
}
/* CRÍTICO: Remove efeito de hover da imagem do piso (sem "borda fantasma") */
.skins-podium.hovering .podium-slot.dim .podium-platform-top,
.skins-podium.hovering .podium-slot.hovered .podium-platform-top,
.skins-podium.hovering .podium-slot.dim .podium-platform-top img,
.skins-podium.hovering .podium-slot.hovered .podium-platform-top img {
filter: none !important;
box-shadow: none !important;
}
/* Borda no tile quando hovered - apenas no container, não na imagem */
.skins-podium.hovering .podium-slot.hovered .podium-platform-top {
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12),
0 10px 28px rgba(0, 0, 0, 0.45);
}
/* Remove sombra da imagem do tile no hover */
.skins-podium.hovering .podium-slot.hovered .podium-platform-top img {
box-shadow: none !important;
filter: none !important;
}
/* Tooltip */
.skin-tooltip {
position: fixed;
z-index: 9999;
left: 0;
top: 0;
pointer-events: none;
padding: 10px 12px;
border-radius: 8px;
background: rgba(40, 45, 60, 0.98); /* Azul escuro para melhor contraste */
color: #f0f0f0;
font-size: 13px;
line-height: 1.4;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(80, 90, 120, 0.4), /* Borda azulada para definição */
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
transform: translate(-9999px, -9999px);
opacity: 0;
transition: opacity 0.15s ease;
text-align: center;
white-space: normal;
max-width: 280px;
min-width: 200px;
}
.skin-tooltip b {
color: #ffffff;
font-weight: 600;
}
.podium-slot.is-clickable {
cursor: pointer;
}
.podium-slot.is-clickable:focus {
outline: 2px solid #156bc7;
outline-offset: 2px;
}
/* Responsivo */
@media (max-width: 1100px) {
.skins-podium {
gap: 24px;
}
}
@media (max-width: 768px) {
.card-skins {
padding: 14px;
}
.skins-podium {
gap: 20px;
padding: 15px 0;
}
}
@media (max-width: 600px) {
.card-skins {
box-sizing: border-box;
max-width: calc(
100vw - env(safe-area-inset-left) - env(safe-area-inset-right) - 16px
);
width: 100%;
margin: 10px auto;
padding: 12px;
border-radius: 10px;
overflow-x: hidden;
overflow-y: visible;
}
.skins-podium {
gap: 16px;
padding: 10px 8px 50px 8px;
/* Padding extra na parte inferior para os tiles */
justify-content: center;
overflow-y: visible;
}
.podium-slot {
max-width: calc(50% - 8px);
flex: 0 0 auto;
min-width: 0;
}
.podium-sprite-container {
max-width: 100%;
min-width: 0;
}
.podium-sprite img {
max-width: 100%;
height: auto;
}
.podium-platform-top img {
max-width: 100%;
height: auto;
}
}
@media (max-width: 400px) {
.skins-podium {
gap: 12px;
padding-left: 4px;
padding-right: 4px;
}
.podium-slot {
max-width: calc(50% - 6px);
}
}
</style>