<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Corazón de Partículas Interactivo con Nombre</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="menu-toggle" id="menuToggle">
☰
</div>
<div class="controls" id="controlsMenu">
<label for="name-input">Nombre:</label>
<input type="text" id="name-input" value="Amor">
<label for="text-color-select">Color Texto:</label>
<select id="text-color-select">
<option value="#dcdcdc">Blanco</option>
<option value="#ff0000">Rojo</option>
<option value="#00ff00">Verde</option>
<option value="#0000ff">Azul</option>
<option value="#ffff00">Amarillo</option>
<option value="#ff00ff">Magenta</option>
<option value="#00ffff">Cyan</option>
</select>
<label for="font-size">Tamaño Texto Base: <span id="font-size-value">40</span></label>
<input type="range" id="font-size" min="20" max="80" value="40">
<label for="pulse-amplitude">Amplitud Pulso Texto: <span id="pulse-amplitude-value">10</span></label>
<input type="range" id="pulse-amplitude" min="0" max="30" value="10">
<label for="pulse-speed">Velocidad Pulso Texto: <span id="pulse-speed-value">0.005</span></label>
<input type="range" id="pulse-speed" min="0.001" max="0.02" value="0.005" step="0.001">
<hr style="margin: 10px 0;">
<label for="color-select">Color Partículas:</label>
<select id="color-select">
<option value="#ff007f">Rosa Fuerte</option>
<option value="#00ff00">Verde</option>
<option value="#ff0000">Rojo</option>
<option value="#0000ff">Azul</option>
<option value="#ffff00">Amarillo</option>
<option value="#ff00ff">Magenta</option>
<option value="#00ffff">Cyan</option>
<option value="#ffffff">Blanco</option>
</select>
<label for="particle-size">Tamaño Partículas: <span id="particle-size-value">3</span></label>
<input type="range" id="particle-size" min="1" max="10" value="3">
<label for="num-particles">Nº Partículas: <span id="num-particles-value">200</span></label>
<input type="range" id="num-particles" min="50" max="500" value="200" step="10">
<label for="speed">Velocidad Partículas: <span id="speed-value">0.05</span></label>
<input type="range" id="speed" min="0.01" max="0.2" value="0.05" step="0.01">
<label for="mouse-follow">Influencia Mouse: <span id="mouse-follow-value">80</span></label>
<input type="range" id="mouse-follow" min="10" max="200" value="80">
<label for="trail-alpha">Estela Partículas: <span id="trail-alpha-value">0.1</span></label>
<input type="range" id="trail-alpha" min="0.01" max="0.5" value="0.1" step="0.01">
</div>
<canvas id="particleCanvas"></canvas>
<script src="script.js"></script>
</body>
</html>
body {
margin: 0;
overflow: hidden;
background-color: #000;
color: #fff;
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#particleCanvas {
display: block;
}
.controls {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(50, 50, 50, 0.8);
padding: 15px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 10;
max-height: 95vh;
overflow-y: auto;
/* Inicio de cambios para menú ocultable */
transform: translateX(-100%);
transition: transform 0.3s ease-out;
}
.controls.show {
transform: translateX(0);
}
.menu-toggle {
position: absolute;
top: 10px;
left: 10px;
z-index: 11;
background-color: rgba(50, 50, 50, 0.8);
color: #fff;
padding: 10px 15px;
border-radius: 8px;
cursor: pointer;
font-size: 1.5em;
line-height: 1;
transition: left 0.3s ease-out;
}
.menu-toggle.hide {
left: 220px; /* Ancho del menú + padding */
}
/* Fin de cambios para menú ocultable */
.controls label {
display: block;
margin-bottom: 5px;
font-size: 0.9em;
}
.controls input[type="range"],
.controls input[type="text"],
.controls select {
width: 150px;
margin-bottom: 10px;
box-sizing: border-box;
}
/* Inicio de cambios para diseño responsivo */
@media (max-width: 768px) {
.controls {
width: 180px;
font-size: 0.8em;
}
.controls input[type="range"],
.controls input[type="text"],
.controls select {
width: 100%;
}
.menu-toggle.hide {
left: 190px; /* Ancho del menú + padding */
}
}
@media (max-width: 480px) {
.controls {
width: 150px;
padding: 10px;
}
.controls label {
font-size: 0.75em;
}
.menu-toggle {
padding: 8px 12px;
font-size: 1.2em;
}
.menu-toggle.hide {
left: 160px; /* Ancho del menú + padding */
}
}
const canvas = document.getElementById('particleCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let particlesArray = [];
let textToDisplay = "Amor";
const mouse = {
x: null,
y: null,
radius: 100
};
const colorSelect = document.getElementById('color-select');
const particleSizeSlider = document.getElementById('particle-size');
const numParticlesSlider = document.getElementById('num-particles');
const speedSlider = document.getElementById('speed');
const mouseFollowSlider = document.getElementById('mouse-follow');
const trailAlphaSlider = document.getElementById('trail-alpha');
const particleSizeValueSpan = document.getElementById('particle-size-value');
const numParticlesValueSpan = document.getElementById('num-particles-value');
const speedValueSpan = document.getElementById('speed-value');
const mouseFollowValueSpan = document.getElementById('mouse-follow-value');
const trailAlphaValueSpan = document.getElementById('trail-alpha-value');
const nameInput = document.getElementById('name-input');
const textColorSelect = document.getElementById('text-color-select');
const fontSizeSlider = document.getElementById('font-size');
const pulseAmplitudeSlider = document.getElementById('pulse-amplitude');
const pulseSpeedSlider = document.getElementById('pulse-speed');
const fontSizeValueSpan = document.getElementById('font-size-value');
const pulseAmplitudeValueSpan = document.getElementById('pulse-amplitude-value');
const pulseSpeedValueSpan = document.getElementById('pulse-speed-value');
/* Inicio de cambios para el menú ocultable */
const menuToggle = document.getElementById('menuToggle');
const controlsMenu = document.getElementById('controlsMenu');
menuToggle.addEventListener('click', () => {
controlsMenu.classList.toggle('show');
menuToggle.classList.toggle('hide');
});
/* Fin de cambios para el menú ocultable */
let config = {
particleColor: colorSelect.value,
particleSize: parseInt(particleSizeSlider.value),
numParticles: parseInt(numParticlesSlider.value),
returnSpeed: parseFloat(speedSlider.value),
mouseInfluence: parseInt(mouseFollowSlider.value),
trailAlpha: parseFloat(trailAlphaSlider.value),
textContent: nameInput.value,
textColor: textColorSelect.value,
baseFontSize: parseInt(fontSizeSlider.value),
pulseAmplitude: parseInt(pulseAmplitudeSlider.value),
pulseSpeed: parseFloat(pulseSpeedSlider.value),
textFont: 'Arial, sans-serif',
textNeonBlur: 15
};
function updateSpans() {
particleSizeValueSpan.textContent = config.particleSize;
numParticlesValueSpan.textContent = config.numParticles;
speedValueSpan.textContent = config.returnSpeed.toFixed(2);
mouseFollowValueSpan.textContent = config.mouseInfluence;
trailAlphaValueSpan.textContent = config.trailAlpha.toFixed(2);
fontSizeValueSpan.textContent = config.baseFontSize;
pulseAmplitudeValueSpan.textContent = config.pulseAmplitude;
pulseSpeedValueSpan.textContent = config.pulseSpeed.toFixed(3);
}
updateSpans();
colorSelect.addEventListener('change', (e) => {
config.particleColor = e.target.value;
});
particleSizeSlider.addEventListener('input', (e) => {
config.particleSize = parseInt(e.target.value);
particleSizeValueSpan.textContent = config.particleSize;
});
numParticlesSlider.addEventListener('input', (e) => {
config.numParticles = parseInt(e.target.value);
numParticlesValueSpan.textContent = config.numParticles;
init();
});
speedSlider.addEventListener('input', (e) => {
config.returnSpeed = parseFloat(e.target.value);
speedValueSpan.textContent = config.returnSpeed.toFixed(2);
});
mouseFollowSlider.addEventListener('input', (e) => {
config.mouseInfluence = parseInt(e.target.value);
mouseFollowValueSpan.textContent = config.mouseInfluence;
});
trailAlphaSlider.addEventListener('input', (e) => {
config.trailAlpha = parseFloat(e.target.value);
trailAlphaValueSpan.textContent = config.trailAlpha.toFixed(2);
});
nameInput.addEventListener('input', (e) => {
config.textContent = e.target.value;
});
textColorSelect.addEventListener('change', (e) => {
config.textColor = e.target.value;
});
fontSizeSlider.addEventListener('input', (e) => {
config.baseFontSize = parseInt(e.target.value);
fontSizeValueSpan.textContent = config.baseFontSize;
});
pulseAmplitudeSlider.addEventListener('input', (e) => {
config.pulseAmplitude = parseInt(e.target.value);
pulseAmplitudeValueSpan.textContent = config.pulseAmplitude;
});
pulseSpeedSlider.addEventListener('input', (e) => {
config.pulseSpeed = parseFloat(e.target.value);
pulseSpeedValueSpan.textContent = config.pulseSpeed.toFixed(3);
});
window.addEventListener('mousemove', (event) => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
window.addEventListener('mouseout', () => {
mouse.x = null;
mouse.y = null;
});
/* Inicio de cambios para touch en la pantalla */
canvas.addEventListener('touchstart', (event) => {
event.preventDefault();
mouse.x = event.touches[0].clientX;
mouse.y = event.touches[0].clientY;
});
canvas.addEventListener('touchmove', (event) => {
event.preventDefault();
mouse.x = event.touches[0].clientX;
mouse.y = event.touches[0].clientY;
});
canvas.addEventListener('touchend', () => {
mouse.x = null;
mouse.y = null;
});
/* Fin de cambios para touch en la pantalla */
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
init();
});
function getHeartPoint(t, scale) {
const x = scale * 16 * Math.pow(Math.sin(t), 3);
const y = -scale * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
return { x, y };
}
class Particle {
constructor(targetX, targetY) {
this.targetX = targetX + canvas.width / 2;
this.targetY = targetY + canvas.height / 2;
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = config.particleSize;
this.color = config.particleColor;
}
draw() {
ctx.shadowBlur = 5;
ctx.shadowColor = this.color;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
update() {
this.size = config.particleSize;
this.color = config.particleColor;
let dxMouse = 0;
let dyMouse = 0;
let distanceMouse = Infinity;
if (mouse.x !== null && mouse.y !== null) {
dxMouse = this.x - mouse.x;
dyMouse = this.y - mouse.y;
distanceMouse = Math.sqrt(dxMouse * dxMouse + dyMouse * dyMouse);
}
if (distanceMouse < mouse.radius + this.size) {
const forceDirectionX = dxMouse / distanceMouse;
const forceDirectionY = dyMouse / distanceMouse;
const force = (mouse.radius - distanceMouse) / mouse.radius * (config.mouseInfluence / 10);
this.x += forceDirectionX * force * 1.5;
this.y += forceDirectionY * force * 1.5;
} else {
const dxTarget = this.targetX - this.x;
const dyTarget = this.targetY - this.y;
this.x += dxTarget * config.returnSpeed;
this.y += dyTarget * config.returnSpeed;
}
}
}
function init() {
particlesArray = [];
const heartScale = Math.min(canvas.width, canvas.height) / 40;
for (let i = 0; i < config.numParticles; i++) {
const t = (Math.PI * 2 / config.numParticles) * i;
const point = getHeartPoint(t, heartScale);
particlesArray.push(new Particle(point.x, point.y));
}
}
function drawText() {
if (config.textContent.trim() === '') return;
const time = Date.now();
const pulseFactor = Math.sin(time * config.pulseSpeed) * config.pulseAmplitude;
const currentFontSize = config.baseFontSize + pulseFactor;
ctx.font = `bold ${currentFontSize}px ${config.textFont}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
/* Inicio de cambios para mejorar el contorno de las letras */
ctx.shadowColor = config.textColor;
ctx.shadowBlur = config.textNeonBlur;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.strokeStyle = config.textColor; /* Nuevo: Color del contorno */
ctx.lineWidth = 2; /* Nuevo: Ancho del contorno */
/* Fin de cambios para mejorar el contorno de las letras */
ctx.fillStyle = (config.textColor === "#ffffff" || config.textColor.toLowerCase() === "white") ? "#dddddd" : "#ffffff";
ctx.fillText(config.textContent, canvas.width / 2, canvas.height / 2);
ctx.strokeText(config.textContent, canvas.width / 2, canvas.height / 2); /* Nuevo: Dibuja el contorno */
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
}
function animate() {
ctx.fillStyle = `rgba(0, 0, 0, ${config.trailAlpha})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < particlesArray.length; i++) {
particlesArray[i].update();
particlesArray[i].draw();
}
drawText();
requestAnimationFrame(animate);
}
updateSpans();
init();
animate();