Rehacer avatar MSN con CSS puro en lugar de SVG
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 55s

- Eliminar SVG complejo y usar CSS radial-gradient
- Implementar degradado radial que simula iluminación real
- Agregar efectos glossy con pseudo-elementos ::before y ::after
- Usar box-shadow para profundidad 3D
- Simplificar implementación para mejor rendimiento
This commit is contained in:
2025-10-17 16:21:13 -06:00
parent 6b87902119
commit 66be233e3a

View File

@@ -1,136 +1,17 @@
<template> <template>
<div class="msn-avatar-wrapper" :class="`status-${presenceStatus}`"> <div class="msn-avatar-wrapper" :style="cssVars">
<svg <div class="avatar-frame">
class="msn-frame" <div class="avatar-container">
viewBox="0 0 140 140" <img
xmlns="http://www.w3.org/2000/svg" v-if="src && !imageError"
> :src="src"
<!-- Definiciones --> :alt="alt"
<defs> class="avatar-image"
<!-- Degradado radial para el marco (simula iluminación) --> @error="handleImageError"
<radialGradient
id="frameGradient"
cx="25%"
cy="25%"
r="100%"
fx="15%"
fy="15%"
>
<stop offset="0%" :stop-color="colors.light" />
<stop offset="50%" :stop-color="colors.medium" />
<stop offset="100%" :stop-color="colors.dark" />
</radialGradient>
<!-- Degradado para el highlight glossy -->
<linearGradient
id="glossHighlight"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stop-color="rgba(255,255,255,0.6)" />
<stop offset="40%" stop-color="rgba(255,255,255,0.2)" />
<stop offset="60%" stop-color="rgba(255,255,255,0)" />
<stop offset="100%" stop-color="rgba(0,0,0,0.1)" />
</linearGradient>
<!-- Filtro para sombra exterior -->
<filter id="dropShadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3" />
<feOffset dx="0" dy="2" result="offsetblur" />
<feComponentTransfer>
<feFuncA type="linear" slope="0.3" />
</feComponentTransfer>
<feMerge>
<feMergeNode />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<!-- Filtro para sombra interior -->
<filter id="innerShadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur" />
<feOffset in="blur" dx="1" dy="1" result="offsetBlur" />
<feFlood flood-color="rgba(0,0,0,0.4)" result="color" />
<feComposite in="color" in2="offsetBlur" operator="in" result="shadow" />
<feComposite in="shadow" in2="SourceAlpha" operator="in" result="innerShadow" />
</filter>
<!-- Clip path para el avatar (cuadrado con esquinas muy poco redondeadas) -->
<clipPath id="avatarClip">
<rect x="20" y="20" width="100" height="100" rx="3" ry="3" />
</clipPath>
<!-- Máscara para crear el marco con grosor variable -->
<mask id="frameMask">
<!-- Rectángulo exterior blanco -->
<rect x="0" y="0" width="140" height="140" fill="white" rx="10" ry="10" />
<!-- Rectángulo interior negro (el hueco del marco) -->
<!-- Usamos un path personalizado para crear el grosor variable -->
<path
d="M 20,15
Q 70,18 120,15
Q 122,70 125,120
Q 70,122 20,125
Q 18,70 15,20
Q 70,18 20,15 Z"
fill="black"
/>
</mask>
</defs>
<!-- Marco exterior con grosor variable -->
<g filter="url(#dropShadow)">
<!-- Base del marco con degradado radial -->
<rect
x="0"
y="0"
width="140"
height="140"
rx="10"
ry="10"
fill="url(#frameGradient)"
mask="url(#frameMask)"
/> />
<div v-else class="avatar-placeholder">
<!-- Capa glossy superior --> {{ initials }}
<rect </div>
x="0"
y="0"
width="140"
height="140"
rx="10"
ry="10"
fill="url(#glossHighlight)"
mask="url(#frameMask)"
opacity="0.8"
/>
<!-- Highlight especular en la esquina superior izquierda -->
<ellipse
cx="25"
cy="25"
rx="15"
ry="12"
fill="rgba(255,255,255,0.7)"
mask="url(#frameMask)"
opacity="0.9"
/>
</g>
</svg>
<!-- Avatar del usuario -->
<div class="avatar-container">
<img
v-if="src"
:src="src"
:alt="alt"
class="avatar-image"
@error="handleImageError"
/>
<div v-else class="avatar-placeholder">
{{ initials }}
</div> </div>
</div> </div>
</div> </div>
@@ -173,6 +54,14 @@ const colors = computed(() => {
const { PRESENCE_COLORS } = usePresence() const { PRESENCE_COLORS } = usePresence()
return PRESENCE_COLORS[props.presenceStatus] return PRESENCE_COLORS[props.presenceStatus]
}) })
// CSS Variables para pasar los colores dinámicos
const cssVars = computed(() => ({
'--color-light': colors.value.light,
'--color-medium': colors.value.medium,
'--color-dark': colors.value.dark,
'--avatar-size': `${props.size}px`
}))
</script> </script>
<style scoped> <style scoped>
@@ -181,27 +70,83 @@ const colors = computed(() => {
display: inline-block; display: inline-block;
width: var(--avatar-size, 128px); width: var(--avatar-size, 128px);
height: var(--avatar-size, 128px); height: var(--avatar-size, 128px);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.msn-frame { .msn-avatar-wrapper:hover {
transform: scale(1.05);
}
.avatar-frame {
position: relative;
width: 100%;
height: 100%;
border-radius: 12px;
padding: 8px;
/* Degradado radial para el borde - simula iluminación */
background: radial-gradient(
circle at 20% 20%,
var(--color-light) 0%,
var(--color-medium) 50%,
var(--color-dark) 100%
);
/* Sombra exterior para profundidad */
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.15),
0 2px 6px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
/* Efecto glossy superior con pseudo-elemento */
.avatar-frame::before {
content: '';
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; right: 0;
height: 100%; height: 50%;
z-index: 2; border-radius: 12px 12px 0 0;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.4) 0%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0) 100%
);
pointer-events: none; pointer-events: none;
z-index: 2;
}
/* Highlight especular en esquina superior izquierda */
.avatar-frame::after {
content: '';
position: absolute;
top: 6px;
left: 6px;
width: 30%;
height: 30%;
border-radius: 50%;
background: radial-gradient(
circle at 30% 30%,
rgba(255, 255, 255, 0.6) 0%,
rgba(255, 255, 255, 0.2) 50%,
transparent 100%
);
pointer-events: none;
z-index: 2;
} }
.avatar-container { .avatar-container {
position: absolute; position: relative;
top: 14.3%; /* Proporción del marco */ width: 100%;
left: 14.3%; height: 100%;
width: 71.4%; /* 100% - 2*14.3% */ border-radius: 6px;
height: 71.4%;
border-radius: 2px;
overflow: hidden; overflow: hidden;
background: var(--color-gray-200); background: var(--color-gray-200);
/* Sombra interior sutil */
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
z-index: 1; z-index: 1;
} }
@@ -218,36 +163,11 @@ const colors = computed(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 2rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
color: var(--color-gray-600); color: var(--color-gray-600);
background: linear-gradient(135deg, var(--color-gray-100), var(--color-gray-300)); background: linear-gradient(135deg, var(--color-gray-100), var(--color-gray-300));
} text-transform: uppercase;
/* Variaciones de tamaño */
.msn-avatar-wrapper.size-sm {
--avatar-size: 64px;
}
.msn-avatar-wrapper.size-md {
--avatar-size: 96px;
}
.msn-avatar-wrapper.size-lg {
--avatar-size: 128px;
}
.msn-avatar-wrapper.size-xl {
--avatar-size: 160px;
}
/* Animación de hover */
.msn-avatar-wrapper {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.msn-avatar-wrapper:hover {
transform: scale(1.05);
} }
/* Modo oscuro */ /* Modo oscuro */