<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- MODIFICAR: Título de la pestaña del navegador -->
<title>Corazón de Corazones</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js"></script>
</head>
<body>
<!-- NOTA: Este título se modifica desde el archivo script.js -->
<h1>10 cosas que me encantan de ti</h1>
<!-- NOTA: Estas instrucciones se modifican desde el archivo script.js -->
<p class="instructions">Haz clic en los corazones para descubrir los mensajes.</p>
<div class="controls-container">
<div id="counter-container">
<span id="counter">0 / 10</span> ❤️
</div>
<!-- MODIFICAR: Texto del botón de pista -->
<button id="hint-button" class="hint-button">
Pista ✨
</button>
<!-- MODIFICAR: Texto del botón de reinicio -->
<button id="reset-button" class="hint-button" style=" display: none; background-color: #c0392b;">
Reiniciar
</button>
</div>
<div id="heart-container">
<svg id="heart-svg" viewBox="0 0 500 450">
<path id="heart-path" d="M433.8,63.2c-26.6-26.6-62.5-41.3-100.5-41.3s-73.9,14.7-100.5,41.3L225,71.1L217.2,63.2
c-26.6-26.6-62.5-41.3-100.5-41.3S42.8,36.6,16.2,63.2C-10.4,89.8-10.4,133.4,16.2,160l186,186c2.6,2.6,6,4,9.5,4.4
c0.3,0,0.5,0,0.8,0s0.5,0,0.8,0c3.5-0.3,6.9-1.8,9.5-4.4l186-186C460.4,133.4,460.4,89.8,433.8,63.2z"/>
</svg>
</div>
<div id="final-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<span class="close-button">×</span>
<!-- NOTA: El contenido de este modal se modifica desde script.js -->
<h2></h2>
<p></p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
/* MODIFICAR: Estilos globales como la fuente y el color de fondo */
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f0f2f5;
margin: 0;
padding: 20px;
color: #333;
text-align: center;
overflow-x: hidden;
}
#counter-container.updated {
transform: scale(1.15);
transition: transform 0.2s ease-out;
}
/* MODIFICAR: Color del título principal */
h1 {
color: #c0392b;
margin-bottom: 8px;
font-size: 2.2rem;
}
.instructions {
margin-top: 0;
margin-bottom: 15px;
font-size: 1.1rem;
color: #555;
}
.controls-container {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
justify-content: center;
}
/* MODIFICAR: Estilos del contador (color de texto, fondo) */
#counter-container {
font-size: 1.5rem;
font-weight: bold;
color: #e74c3c;
background-color: white;
padding: 8px 16px;
border-radius: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
/* MODIFICAR: Estilos del botón de pista (color de fondo, color de texto) */
.hint-button {
background-color: #ffc107;
color: #333;
border: none;
border-radius: 20px;
padding: 10px 20px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.15);
transition: background-color 0.2s, transform 0.2s;
}
.hint-button:hover {
background-color: #ffb300;
transform: translateY(-2px);
}
.hint-button:active {
transform: translateY(0);
}
#heart-container {
position: relative;
width: 90vw;
max-width: 500px;
aspect-ratio: 500 / 450;
margin: 0 auto;
}
#heart-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
visibility: hidden;
pointer-events: none;
}
.small-heart {
position: absolute;
background-color: var(--heart-color);
transform-origin: center center;
animation: pop-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.2));
transition: transform 0.2s ease-out, filter 0.2s ease-out;
}
.small-heart::before,
.small-heart::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
background-color: var(--heart-color);
border-radius: 50%;
}
.small-heart::before {
top: -50%;
left: 0;
}
.small-heart::after {
top: 0;
left: 50%;
}
@keyframes pop-in {
from {
opacity: 0;
transform: scale(0.2) rotate(var(--initial-rotation, 0deg));
}
to {
opacity: 1;
transform: scale(1) rotate(var(--final-rotation, 0deg));
}
}
.small-heart:hover {
transform: scale(1.3) rotate(-30deg) !important;
z-index: 10;
}
.special-heart {
cursor: pointer;
}
.heart-beat {
animation: heart-beat-animation 1.5s ease-in-out infinite;
}
@keyframes heart-beat-animation {
0%, 100% { transform: scale(1.05); }
50% { transform: scale(1.3); }
}
.special-heart.discovered {
filter: grayscale(40%) drop-shadow(1px 1px 1px rgba(0,0,0,0.1));
}
.special-heart.is-active {
animation: shine-effect 1.5s ease-in-out;
z-index: 20;
filter: none !important;
}
@keyframes shine-effect {
0%, 100% { filter: drop-shadow(0 0 4px white) drop-shadow(0 0 8px var(--heart-color)); transform: scale(1.1) rotate(-45deg); }
50% { filter: drop-shadow(0 0 8px white) drop-shadow(0 0 16px var(--heart-color)); transform: scale(1.3) rotate(-45deg); }
}
.phrase-popup {
position: absolute;
background-color: rgba(0, 0, 0, 0.75);
color: #fff;
padding: 8px 15px;
border-radius: 20px;
font-size: 16px;
font-weight: bold;
white-space: nowrap;
pointer-events: none;
z-index: 30;
transform: translate(-50%, -100%);
text-shadow: 0 0 5px #fff, 0 0 10px #ffdd59;
animation: fade-in-out 3s ease-in-out forwards;
}
@keyframes fade-in-out {
0% { opacity: 0; transform: translate(-50%, -80%) scale(0.5); }
20% { opacity: 1; transform: translate(-50%, -150%) scale(1); }
80% { opacity: 1; transform: translate(-50%, -160%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -200%) scale(0.8); }
}
/* MODIFICAR: Estilos del modal final (fondo, etc.) */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
animation: fadeIn 0.3s ease-in-out;
}
.modal-content {
background-color: white;
padding: 30px 40px;
border-radius: 15px;
text-align: center;
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
position: relative;
transform: scale(0.8);
animation: zoomIn 0.3s 0.1s ease-in-out forwards;
width: 90%;
max-width: 450px;
}
.modal-content h2 {
color: #c0392b;
margin-top: 0;
}
.close-button {
position: absolute;
top: 10px;
right: 15px;
font-size: 28px;
font-weight: bold;
color: #aaa;
cursor: pointer;
transition: color 0.2s;
}
.close-button:hover {
color: #333;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes zoomIn { from { transform: scale(0.8); } to { transform: scale(1); } }
@media (max-width: 600px) {
body { padding: 15px; }
h1 { font-size: 1.8rem; }
.instructions { font-size: 1rem; }
.controls-container { flex-direction: column; gap: 12px; }
#counter-container { font-size: 1.3rem; }
#heart-container { width: 95vw; }
}
window.addEventListener('load', () => {
const container = document.getElementById('heart-container');
const svgPath = document.getElementById('heart-path');
const counterElement = document.getElementById('counter');
const resetButton = document.getElementById('reset-button');
const hintButton = document.getElementById('hint-button');
const finalModal = document.getElementById('final-modal');
const closeModalButton = document.querySelector('.close-button');
if (!container || !svgPath || !counterElement || !hintButton || !finalModal || !closeModalButton) {
console.error("No se encontraron uno o más de los elementos necesarios.");
return;
}
// MODIFICAR: Todo el contenido de texto se personaliza aquí.
const config = {
titulo: "10 cosas que me encantan de ti",
instrucciones: "Haz clic en los corazones para descubrir los mensajes.",
// MODIFICAR: Escribe aquí las 10 frases que quieres que aparezcan.
frases: [
"Tu increíble sentido del humor", "La forma en que iluminas un lugar",
"Tu corazón generoso y amable", "Tu energía y alegría contagiosas",
"La pasión con la que persigues tus sueños", "Lo inteligente y brillante que eres",
"Tu sonrisa, que es mi favorita", "Tu fortaleza y valentía ante todo",
"Lo mucho que te preocupas por los demás", "Simplemente, que seas tú"
],
// MODIFICAR: Título y párrafo del mensaje final.
mensajeFinal: {
titulo: "¡Me encantas!",
parrafo: "Y lo más importante: ¡Te quiero muchísimo! Gracias por ser como eres."
}
};
// MODIFICAR: Número total de corazones que se generan.
const TOTAL_HEARTS = 80;
// MODIFICAR: Rango de tamaño de los corazones (mínimo y máximo).
const MIN_SIZE = 10;
const MAX_SIZE = 35;
// MODIFICAR: Paleta de colores para los corazones.
const COLORS = ['#e74c3c', '#c0392b', '#ff7979', '#ff4d4d', '#f19066', '#d63031', '#e84393'];
// NOTA: Este número debe coincidir con la cantidad de frases en el array 'config.frases'.
const SPECIAL_HEART_COUNT = 10;
let placedHearts = [];
let shuffledPhrases = [];
let specialHeartsCreated = 0;
let discoveredCount = 0;
let scale = 1;
function initGame() {
const viewBoxWidth = 500;
const containerWidth = container.clientWidth;
scale = containerWidth / viewBoxWidth;
discoveredCount = 0;
specialHeartsCreated = 0;
counterElement.textContent = `0 / ${SPECIAL_HEART_COUNT}`;
finalModal.style.display = 'none';
resetButton.style.display = 'none';
shuffledPhrases = [...config.frases].sort(() => 0.5 - Math.random());
generateHearts();
}
function createHeart(x, y, size) {
const heart = document.createElement('div');
heart.classList.add('small-heart');
const rotation = Math.random() * 90 - 45;
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
const scaledSize = size * scale;
const scaledX = x * scale;
const scaledY = y * scale;
heart.style.setProperty('--heart-color', color);
heart.style.width = `${scaledSize}px`;
heart.style.height = `${scaledSize}px`;
heart.style.left = `${scaledX - scaledSize / 2}px`;
heart.style.top = `${scaledY - scaledSize / 2}px`;
heart.style.transform = `rotate(${-45 + rotation}deg)`;
if (specialHeartsCreated < SPECIAL_HEART_COUNT && Math.random() > 0.5) {
heart.classList.add('special-heart');
heart.dataset.phrase = shuffledPhrases[specialHeartsCreated];
specialHeartsCreated++;
}
container.appendChild(heart);
}
function isColliding(newHeart) {
for (const placed of placedHearts) {
const dx = newHeart.x - placed.x;
const dy = newHeart.y - placed.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = ((newHeart.size / 1.2) + (placed.size / 1.2));
if (distance < minDistance) return true;
}
return false;
}
function generateHearts() {
const existingHearts = container.querySelectorAll('.small-heart');
existingHearts.forEach(heart => heart.remove());
placedHearts = [];
const svg = svgPath.ownerSVGElement;
const pathBox = svgPath.getBBox();
if (pathBox.width === 0 || pathBox.height === 0) {
console.error("Error: Las dimensiones del SVG son 0.");
return;
}
let heartsPlaced = 0;
let attempts = 0;
const MAX_ATTEMPTS = TOTAL_HEARTS * 150;
while (heartsPlaced < TOTAL_HEARTS && attempts < MAX_ATTEMPTS) {
attempts++;
const randomSize = Math.random() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE;
const randomX = pathBox.x + Math.random() * pathBox.width;
const randomY = pathBox.y + Math.random() * pathBox.height;
const point = svg.createSVGPoint();
point.x = randomX;
point.y = randomY;
if (svgPath.isPointInFill(point)) {
const newHeart = { x: randomX, y: randomY, size: randomSize };
if (!isColliding(newHeart)) {
createHeart(newHeart.x, newHeart.y, newHeart.size);
placedHearts.push(newHeart);
heartsPlaced++;
}
}
}
if (attempts >= MAX_ATTEMPTS) {
console.warn(`Máximo de intentos. Corazones: ${heartsPlaced}/${TOTAL_HEARTS}`);
}
}
function showPhrase(event) {
const clickedHeart = event.target.closest('.special-heart');
if (!clickedHeart || clickedHeart.classList.contains('is-active')) return;
if (!clickedHeart.classList.contains('discovered')) {
clickedHeart.classList.add('discovered', 'is-active');
clickedHeart.classList.remove('heart-beat');
discoveredCount++;
counterElement.textContent = `${discoveredCount} / ${SPECIAL_HEART_COUNT}`;
counterElement.parentElement.classList.add('updated');
setTimeout(() => counterElement.parentElement.classList.remove('updated'), 400);
if (discoveredCount === SPECIAL_HEART_COUNT) {
setTimeout(() => {
finalModal.style.display = 'flex';
resetButton.style.display = 'inline-block';
// MODIFICAR: Propiedades del confeti (cantidad, dispersión, etc.).
confetti({ particleCount: 150, spread: 90, origin: { y: 0.6 } });
}, 3100);
}
} else {
clickedHeart.classList.add('is-active');
}
const phrase = clickedHeart.dataset.phrase;
const phrasePopup = document.createElement('div');
phrasePopup.classList.add('phrase-popup');
phrasePopup.textContent = phrase;
const heartRect = clickedHeart.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
phrasePopup.style.left = `${heartRect.left - containerRect.left + heartRect.width / 2}px`;
phrasePopup.style.top = `${heartRect.top - containerRect.top}px`;
container.appendChild(phrasePopup);
setTimeout(() => {
phrasePopup.remove();
clickedHeart.classList.remove('is-active');
}, 3000);
}
function showHint() {
document.querySelectorAll('.special-heart:not(.discovered)').forEach(heart => {
heart.classList.add('heart-beat');
setTimeout(() => heart.classList.remove('heart-beat'), 4000);
});
}
function closeModal() {
finalModal.style.display = 'none';
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
document.querySelector('h1').textContent = config.titulo;
document.querySelector('.instructions').textContent = config.instrucciones;
document.querySelector('#final-modal h2').textContent = config.mensajeFinal.titulo;
document.querySelector('#final-modal p').textContent = config.mensajeFinal.parrafo;
initGame();
container.addEventListener('click', showPhrase);
hintButton.addEventListener('click', showHint);
resetButton.addEventListener('click', initGame);
closeModalButton.addEventListener('click', closeModal);
finalModal.addEventListener('click', (event) => {
if (event.target === finalModal) closeModal();
});
window.addEventListener('resize', debounce(initGame, 250));
});