import dash
from dash import html, dcc, Input, Output, clientside_callback
import dash_mantine_components as dmc
# Inicializar la app
app = dash.Dash(__name__)
# ============================================
# ESTILOS ORGANIZADOS EN DICCIONARIOS
# ============================================
# Estilo del contenedor principal (body)
BODY_STYLE = {
"minHeight": "100vh",
"background": "linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%)",
"display": "flex",
"justifyContent": "center",
"alignItems": "center"
}
# Estilo del contenedor completo del personaje
CHARACTER_CONTAINER_STYLE = {
"position": "relative",
"background":"white",
"width": "450px",
"height": "450px",
"display": "flex",
"justifyContent": "center",
"alignItems": "center"
}
# # Estilo del borde negro interno
# HEAD_INNER_BORDER_STYLE = {
# "width": "320px",
# "height": "270px",
# "background": "#000",
# "borderRadius": "170px 170px 150px 150px",
# "clipPath": "polygon(11% 0%, 89% 0%, 100% 100%, 0% 100%)",
# "display": "flex",
# "justifyContent": "center",
# "alignItems": "center",
# "paddingTop": "5px"
# }
# Estilo del rostro interior (gris claro, trapecio)
FACE_STYLE = {
"position": "relative",
"width": "305px",
"height": "255px",
"backgroundColor": "#efe1e1",
"borderRadius": "165px 165px 145px 145px",
"border": "5px solid red",
"clipPath": "polygon(11.5% 0%, 88.5% 0%, 100% 100%, 0% 100%)",
"display": "flex",
"flexDirection": "column",
"alignItems": "center",
"justifyContent": "center",
"zIndex": "3"
}
# Estilo de la diadema negra
HEADBAND_OUTER_STYLE = {
"position": "absolute",
"width": "380px",
"height": "50px",
"background": "#000",
"top": "calc(50% - 110px)",
"left": "50%",
"transform": "translateX(-50%)",
"zIndex": "10",
"borderRadius": "25px",
"display": "flex",
"justifyContent": "center",
"alignItems": "center"
}
# Rectángulos decorativos en la diadema
HEADBAND_RECT_STYLE = {
"width": "25px",
"height": "15px",
"background": "#555",
"margin": "0 8px"
}
# Estilo del auricular izquierdo
HEADPHONE_LEFT_STYLE = {
"position": "absolute",
"width": "50px",
"height": "60px",
"background": "#1a1a1a",
"borderRadius": "50%",
"border": "8px solid #000",
"left": "-25px",
"top": "50%",
"transform": "translateY(-50%)",
"zIndex": "9",
"boxShadow": "inset 5px 5px 10px rgba(255,255,255,0.1)"
}
# Estilo del auricular derecho
HEADPHONE_RIGHT_STYLE = {
"position": "absolute",
"width": "50px",
"height": "60px",
"background": "#1a1a1a",
"borderRadius": "50%",
"border": "8px solid #000",
"right": "-25px",
"top": "50%",
"transform": "translateY(-50%)",
"zIndex": "9",
"boxShadow": "inset -5px 5px 10px rgba(255,255,255,0.1)"
}
# Estilo del soporte del micrófono
MIC_ARM_STYLE = {
"position": "absolute",
"width": "4px",
"height": "70px",
"background": "#333",
"right": "15px",
"top": "50%",
"transform": "translateY(-50%) rotate(-5deg)",
"transformOrigin": "top",
"zIndex": "11"
}
# Estilo del micrófono
MIC_HEAD_STYLE = {
"position": "absolute",
"width": "22px",
"height": "28px",
"background": "#1a1a1a",
"borderRadius": "50%",
"right": "7px",
"bottom": "-5px",
"border": "3px solid #000"
}
# Estilo del contenedor de ojos
EYES_CONTAINER_STYLE = {
"position": "relative",
"display": "flex",
"gap": "70px",
"marginTop": "-20px",
"marginBottom": "25px"
}
# Estilo del ojo (BLANCO, no gris)
EYE_WHITE_STYLE = {
"background": "#000000",
"width": "40px",
"height": "56px",
"position": "relative",
"borderRadius": "50%",
"display": "flex",
"justifyContent": "center",
"alignItems": "center"
}
# Estilo de la pupila (parte negra del ojo)
PUPIL_STYLE = {
"background": "#efe1e1",
"width": "10px",
"height": "20px",
"position": "absolute",
"borderRadius": "50%",
"top": "50%",
"left": "50%",
"transform": "translate(-50%, -50%)"
}
# Estilo del brillo en el ojo
EYE_SHINE_STYLE = {
"position": "absolute",
"width": "6px",
"height": "9px",
"background": "#fff",
"borderRadius": "50%",
"top": "32%",
"left": "38%",
"zIndex": "1"
}
# Estilo del contenedor de la boca
MOUTH_CONTAINER_STYLE = {
"position": "relative",
"display": "flex",
"alignItems": "flex-end",
"marginTop": "5px"
}
# Estilo de la parte izquierda de la boca (rectángulo negro)
MOUTH_LEFT_STYLE = {
"padding":"0px",
"margin":"0px",
"width": "45px",
"height": "32px",
"background": "#000",
"borderRadius": "0 0 0 8px"
}
# Estilo de la parte derecha de la boca (rectángulo negro)
MOUTH_RIGHT_STYLE = {
"padding":"0px",
"margin":"0px",
"width": "45px",
"height": "32px",
"background": "#000",
"borderRadius": "0 0 8px 0"
}
# Estilo de la lengua/parte rosada
TONGUE_STYLE = {
"position": "absolute",
"width": "28px",
"height": "18px",
"background": "#ffb3ba",
"borderRadius": "50%",
"bottom": "3px",
"left": "50%",
"transform": "translateX(-50%)"
}
# ============================================
# LAYOUT DE LA APLICACIÓN
# ============================================
app.layout = html.Div(
style=BODY_STYLE,
children=[
html.Div(
style=CHARACTER_CONTAINER_STYLE,
children=[
# Diadema con auriculares
# html.Div(
# style=HEADBAND_OUTER_STYLE,
# children=[
# # Auricular izquierdo
# html.Div(style=HEADPHONE_LEFT_STYLE),
# # Rectángulos decorativos
# html.Div(style=HEADBAND_RECT_STYLE),
# html.Div(style=HEADBAND_RECT_STYLE),
# html.Div(style=HEADBAND_RECT_STYLE),
# # Auricular derecho con micrófono
# html.Div(
# style=HEADPHONE_RIGHT_STYLE,
# children=[
# html.Div(
# style=MIC_ARM_STYLE,
# children=[
# html.Div(style=MIC_HEAD_STYLE)
# ]
# )
# ]
# )
# ]
# ),
# Cabeza (trapecio con bordes)
html.Div(
id='face',
style=FACE_STYLE,
children=[
# Contenedor de ojos
html.Div(
style=EYES_CONTAINER_STYLE,
children=[
# Ojo izquierdo
html.Div(
className='eye',
style=EYE_WHITE_STYLE,
children=[
html.Div(
className='pupil',
style=PUPIL_STYLE,
),
]
),
# Ojo derecho
html.Div(
className='eye',
style=EYE_WHITE_STYLE,
children=[
html.Div(
className='pupil',
style=PUPIL_STYLE,
),
]
),
]
),
# Contenedor de boca
html.Div(
id='mouth-container',
style=MOUTH_CONTAINER_STYLE,
children=[
html.Div(id='mouth-left', style=MOUTH_LEFT_STYLE),
html.Div(id='mouth-right', style=MOUTH_RIGHT_STYLE),
html.Div(style=TONGUE_STYLE),
]
)
]
)
]
),
# Store para inicializar
dcc.Store(id='init-store')
]
)
# ============================================
# CALLBACKS
# ============================================
# Callback para el movimiento de pupilas
clientside_callback(
"""
function(n) {
function eyeball(event) {
let pupils = document.querySelectorAll(".pupil");
pupils.forEach(function(pupil) {
let eye = pupil.closest('.eye');
// Obtener las coordenadas del centro del ojo
let x = eye.getBoundingClientRect().left + eye.clientWidth/2;
let y = eye.getBoundingClientRect().top + eye.clientHeight/2;
// Calcular la distancia y dirección
let deltaX = -event.pageX + x;
let deltaY = -event.pageY + y;
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Limitar el movimiento de la pupila
let maxDistance = 10;
let moveX = (deltaX / distance) * Math.min(distance / 15, maxDistance);
let moveY = (deltaY / distance) * Math.min(distance / 15, maxDistance);
pupil.style.transform = `translate(calc(-50% + ${moveX}px), calc(-50% + ${moveY}px))`;
});
}
// Agregar el event listener al body
document.body.addEventListener('mousemove', eyeball);
return window.dash_clientside.no_update;
}
""",
Output('init-store', 'data'),
Input('init-store', 'id')
)
# Callback para el efecto hover en la boca
clientside_callback(
"""
function(n) {
const face = document.getElementById('face');
const mouthLeft = document.getElementById('mouth-left');
const mouthRight = document.getElementById('mouth-right');
if (face && mouthLeft && mouthRight) {
face.addEventListener('mouseenter', function() {
mouthLeft.style.width = '55px';
mouthLeft.style.height = '40px';
mouthRight.style.width = '55px';
mouthRight.style.height = '40px';
mouthLeft.style.transition = 'all 0.3s ease';
mouthRight.style.transition = 'all 0.3s ease';
});
face.addEventListener('mouseleave', function() {
mouthLeft.style.width = '45px';
mouthLeft.style.height = '32px';
mouthRight.style.width = '45px';
mouthRight.style.height = '32px';
});
}
return window.dash_clientside.no_update;
}
""",
Output('face', 'data-hover'),
Input('face', 'id')
)
if __name__ == '__main__':
app.run_server(debug=True, port=8050)