- Inicio
- Corazón Pintado con Nombres y Sakura

Corazón Pintado con Nombres y Sakura
La plantilla 'Corazón Pintado con Nombres y Sakura' crea una experiencia visualmente poética y romántica. La escena comienza con un fondo sereno de papel de arroz y pétalos de flor de cerezo (sakura) que caen suavemente. Una rama de sakura interactiva en la esquina superior derecha puede ser 'sacudida' con un clic, liberando una lluvia de pétalos. La animación principal muestra un pincel virtual que aparece y dibuja delicadamente un corazón con trazos de tinta roja, dejando pequeñas gotas de pintura. Una vez que el corazón está completo, el pincel escribe dos nombres en su interior, los cuales son fácilmente personalizables en el código JavaScript. El fondo de pétalos no es solo decorativo; también reacciona sutilmente al movimiento del cursor, creando un efecto de 'viento' que añade un toque de interactividad y vida a la escena.
Características de la plantilla
- Animación de dibujo de corazón con un pincel virtual.
- Aparición de dos nombres personalizables con efecto de tinta.
- Fondo animado con pétalos de sakura (flor de cerezo) cayendo.
- Los pétalos reaccionan al movimiento del cursor (efecto de viento).
- Rama de sakura interactiva: haz clic para liberar más pétalos.
- Efecto de salpicaduras y gotas de pintura para mayor realismo.
- Estilo artístico y orgánico con textura de papel de arroz.
- Variables de JavaScript fáciles de modificar para los nombres (name1, name2) y la cantidad de pétalos.
- Diseño totalmente responsivo que se adapta a cualquier pantalla.
- Renderizado completo en HTML5 Canvas para una animación fluida.
Tecnologías usadas
Código de la Plantilla
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Corazón y Nombres Pintados a Pincel</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas"></canvas>
<script src="script.js"></script>
</body>
</html>
body {
margin: 0;
/* MODIFICAR: El color de fondo de la página */
background-color: #fefaf5;
background-image: url('https://www.transparenttextures.com/patterns/rice-paper-3.png');
overflow: hidden;
cursor: default;
}
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
/* MODIFICAR: El primer nombre que aparecerá en el corazón. */
const name1 = "Tú";
/* MODIFICAR: El segundo nombre que aparecerá en el corazón. */
const name2 = "Yo";
/* MODIFICAR: la velocidad de dibujo del nombre. */
const TEXT_DRAW_SPEED = 5;
/*MODIFICAR: Cantidad de pétalos en pantalla */
const SAKURA_COUNT = 100;
const WIND_RADIUS = 100;
const WIND_STRENGTH = 2.5;
const sakuraPetals = [];
let mouse = { x: null, y: null, strength: 0 };
let branchIsIntact;
let branchStartX;
let branchStartY;
let flowerPositionsOnBranch;
let t;
let heartStrokes;
let droplets;
let namePoints;
let nameStrokes;
let textPointsDrawn;
let pointsCalculated;
function init() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const logicalWidth = rect.width;
const logicalHeight = rect.height;
branchIsIntact = true;
branchStartX = logicalWidth - 320;
branchStartY = 20;
flowerPositionsOnBranch = [
{ x: branchStartX, y: branchStartY + 200 },
{ x: branchStartX + 40, y: branchStartY + 160 },
{ x: branchStartX + 250, y: branchStartY + 180 },
{ x: branchStartX + 190, y: branchStartY + 90 },
{ x: branchStartX + 150, y: branchStartY + 130 },
{ x: branchStartX + 280, y: branchStartY + 40 }
];
t = 0;
heartStrokes = [];
droplets = [];
namePoints = [];
nameStrokes = [];
textPointsDrawn = 0;
pointsCalculated = false;
sakuraPetals.length = 0;
for (let i = 0; i < SAKURA_COUNT; i++) {
sakuraPetals.push(createPetal());
}
}
window.addEventListener('mousemove', (event) => {
mouse.x = event.clientX;
mouse.y = event.clientY;
mouse.strength = 1.0;
});
window.addEventListener('resize', init);
function drawSakuraPetal(ctx, petal) {
ctx.save();
ctx.translate(petal.x, petal.y);
ctx.rotate(petal.rotation);
ctx.beginPath();
ctx.fillStyle = `rgba(255, 192, 203, ${petal.alpha})`;
ctx.strokeStyle = `rgba(255, 105, 180, ${petal.alpha})`;
ctx.lineWidth = 0.5;
ctx.moveTo(0, 0);
ctx.bezierCurveTo(petal.size / 2, petal.size / 4, petal.size, petal.size / 2, 0, petal.size);
ctx.bezierCurveTo(-petal.size, petal.size / 2, -petal.size / 2, petal.size / 4, 0, 0);
ctx.fill();
ctx.stroke();
ctx.restore();
}
function createPetal() {
const logicalWidth = canvas.width / (window.devicePixelRatio || 1);
const logicalHeight = canvas.height / (window.devicePixelRatio || 1);
const size = Math.random() * 8 + 7;
return {
x: Math.random() * logicalWidth,
y: Math.random() * -logicalHeight,
size: size,
speedY: Math.random() * 1 + 0.5,
sway: Math.random() * 0.5 - 0.25,
swaySpeed: Math.random() * 0.02 + 0.01,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: Math.random() * 0.02 - 0.01,
alpha: Math.random() * 0.5 + 0.5,
landed: false,
groundLevel: logicalHeight - (Math.random() * 20 + 5)
};
}
function updateAndDrawSakura() {
const logicalWidth = canvas.width / (window.devicePixelRatio || 1);
const logicalHeight = canvas.height / (window.devicePixelRatio || 1);
mouse.strength *= 0.96;
sakuraPetals.forEach((petal, index) => {
if (mouse.x && mouse.strength > 0.01) {
const dx = petal.x - mouse.x;
const dy = petal.y - mouse.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < WIND_RADIUS) {
const forceDirectionX = dx / distance;
const forceDirectionY = dy / distance;
const force = (1 - distance / WIND_RADIUS) * mouse.strength * WIND_STRENGTH;
petal.x += forceDirectionX * force;
petal.y += forceDirectionY * force;
if (petal.landed) {
petal.landed = false;
petal.y -= Math.random() * 2;
}
}
}
if (!petal.landed) {
petal.y += petal.speedY;
petal.x += Math.sin(petal.y * petal.swaySpeed) * petal.sway;
petal.rotation += petal.rotationSpeed;
if (petal.y > petal.groundLevel && !petal.landed) {
petal.y = petal.groundLevel;
petal.landed = true;
}
}
if (petal.y > logicalHeight + petal.size || petal.x > logicalWidth + petal.size || petal.x < -petal.size) {
sakuraPetals.splice(index, 1);
sakuraPetals.push(createPetal());
}
drawSakuraPetal(ctx, petal);
});
}
function drawFlower(ctx, x, y, size) {
const petalCount = 5;
const petalRadius = size;
const centerRadius = size / 3;
ctx.fillStyle = 'rgba(255, 182, 193, 0.8)';
ctx.strokeStyle = 'rgba(255, 105, 180, 0.9)';
ctx.lineWidth = 1;
for (let i = 0; i < petalCount; i++) {
const angle = (i / petalCount) * 2 * Math.PI;
const petalX = x + Math.cos(angle) * petalRadius;
const petalY = y + Math.sin(angle) * petalRadius;
ctx.beginPath();
ctx.arc(petalX, petalY, petalRadius, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
}
ctx.beginPath();
ctx.fillStyle = '#FFD700';
ctx.arc(x, y, centerRadius, 0, 2 * Math.PI);
ctx.fill();
}
function drawSakuraBranch(ctx) {
if (!branchIsIntact) return;
ctx.save();
ctx.translate(branchStartX, branchStartY);
ctx.strokeStyle = '#6F4E37';
ctx.lineCap = 'round';
ctx.lineWidth = 12;
ctx.beginPath();
ctx.moveTo(300, 0);
ctx.bezierCurveTo(200, 100, 100, 120, 0, 200);
ctx.stroke();
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(180, 80);
ctx.quadraticCurveTo(220, 140, 250, 180);
ctx.stroke();
drawFlower(ctx, 0, 200, 12);
drawFlower(ctx, 40, 160, 10);
drawFlower(ctx, 250, 180, 11);
drawFlower(ctx, 190, 90, 9);
drawFlower(ctx, 150, 130, 12);
drawFlower(ctx, 280, 40, 10);
ctx.restore();
}
const heartPath = (t) => {
const logicalWidth = canvas.width / (window.devicePixelRatio || 1);
const logicalHeight = canvas.height / (window.devicePixelRatio || 1);
const x = 16 * Math.pow(Math.sin(t), 3);
const y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
return { x: x * 15 + logicalWidth / 2, y: y * 15 + logicalHeight / 2 + 20 };
};
function calculateNamePoints() {
const logicalWidth = canvas.width / (window.devicePixelRatio || 1);
const logicalHeight = canvas.height / (window.devicePixelRatio || 1);
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const dpr = window.devicePixelRatio || 1;
tempCtx.scale(dpr, dpr);
const centerX = logicalWidth / 2;
const centerY = logicalHeight / 2 + 20;
const calculatedSize = logicalWidth / 20;
const minSize = 24;
const maxSize = 40;
const fontSize = Math.max(minSize, Math.min(calculatedSize, maxSize));
tempCtx.font = `italic ${fontSize}px cursive`;
tempCtx.textAlign = 'center';
tempCtx.textBaseline = 'middle';
tempCtx.fillStyle = '#000';
tempCtx.fillText(name1, centerX, centerY - fontSize * 0.9);
tempCtx.fillText('&', centerX, centerY);
tempCtx.fillText(name2, centerX, centerY + fontSize * 0.9);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
const points = [];
const density = 2;
for (let y = 0; y < tempCanvas.height; y += density) {
for (let x = 0; x < tempCanvas.width; x += density) {
const index = (y * tempCanvas.width + x) * 4;
if (data[index + 3] > 128) {
points.push({ x: x / dpr, y: y / dpr });
}
}
}
for (let i = points.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[points[i], points[j]] = [points[j], points[i]];
}
return points;
}
function drawPaintBrush(x, y, angle) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillStyle = '#8B4513';
ctx.beginPath();
ctx.roundRect(-120, -6, 100, 12, 5);
ctx.fill();
ctx.fillStyle = '#C0C0C0';
ctx.fillRect(-20, -8, 20, 16);
ctx.fillStyle = '#D2B48C';
ctx.beginPath();
ctx.moveTo(-20, -7);
ctx.quadraticCurveTo(-5, 0, -20, 7);
ctx.lineTo(0, 0);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(255, 0, 85, 0.9)';
ctx.beginPath();
ctx.moveTo(-5, -3);
ctx.quadraticCurveTo(0, 0, -5, 3);
ctx.lineTo(0, 0);
ctx.closePath();
ctx.fill();
ctx.restore();
}
const maxT = Math.PI * 2;
function drawBrushStroke(stroke) {
const px = stroke.x + stroke.dx;
const py = stroke.y + stroke.dy;
ctx.beginPath();
ctx.fillStyle = `rgba(220, 20, 60, ${stroke.alpha})`;
ctx.arc(px, py, stroke.radius, 0, Math.PI * 2);
ctx.fill();
}
function updateDroplets() {
for (let i = droplets.length - 1; i >= 0; i--) {
const drop = droplets[i];
drop.y += 0.7 + Math.random();
drop.alpha *= 0.985;
ctx.beginPath();
ctx.fillStyle = `rgba(220, 20, 60, ${drop.alpha})`;
ctx.arc(drop.x, drop.y, drop.r, 0, Math.PI * 2);
ctx.fill();
if (drop.alpha < 0.01) {
droplets.splice(i, 1);
}
}
}
canvas.addEventListener('click', (event) => {
if (!branchIsIntact) return;
const clickX = event.offsetX;
const clickY = event.offsetY;
const branchClickArea = {
x: branchStartX - 20,
y: 0,
width: 340,
height: 250
};
if (
clickX >= branchClickArea.x && clickX <= branchClickArea.x + branchClickArea.width &&
clickY >= branchClickArea.y && clickY <= branchClickArea.y + branchClickArea.height
) {
branchIsIntact = false;
flowerPositionsOnBranch.forEach(pos => {
const petalsToCreate = Math.floor(Math.random() * 4) + 5;
for (let i = 0; i < petalsToCreate; i++) {
const newPetal = createPetal();
newPetal.x = pos.x + (Math.random() - 0.5) * 20;
newPetal.y = pos.y + (Math.random() - 0.5) * 20;
newPetal.landed = false;
newPetal.speedY = Math.random() * 1.5 + 1;
newPetal.sway = Math.random() * 1.0 - 0.5;
sakuraPetals.push(newPetal);
}
});
}
});
function animate() {
const logicalWidth = canvas.width / (window.devicePixelRatio || 1);
const logicalHeight = canvas.height / (window.devicePixelRatio || 1);
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
drawSakuraBranch(ctx);
updateAndDrawSakura();
if (t <= maxT) {
const point = heartPath(t);
const newStroke = {
x: point.x, y: point.y,
radius: Math.random() * 4 + 2,
alpha: Math.random() * 0.2 + 0.3,
dx: Math.random() * 10 - 5,
dy: Math.random() * 10 - 5
};
heartStrokes.push(newStroke);
droplets.push({
x: newStroke.x + newStroke.dx,
y: newStroke.y + newStroke.dy,
r: newStroke.radius * 0.6,
alpha: newStroke.alpha
});
t += 0.008;
}
if (t > maxT && !pointsCalculated) {
namePoints = calculateNamePoints();
pointsCalculated = true;
}
if (pointsCalculated && textPointsDrawn < namePoints.length) {
const batchSize = Math.min(TEXT_DRAW_SPEED, namePoints.length - textPointsDrawn);
for (let i = 0; i < batchSize; i++) {
const point = namePoints[textPointsDrawn + i];
if (point) {
nameStrokes.push({
x: point.x, y: point.y,
radius: Math.random() * 1.2 + 0.5,
alpha: Math.random() * 0.3 + 0.7,
dx: Math.random() * 2 - 1,
dy: Math.random() * 2 - 1
});
}
}
textPointsDrawn += batchSize;
}
heartStrokes.forEach(drawBrushStroke);
nameStrokes.forEach(drawBrushStroke);
updateDroplets();
if (t <= maxT && heartStrokes.length > 0) {
const lastPoint = heartStrokes[heartStrokes.length - 1];
const nextPoint = heartPath(t);
const angle = Math.atan2(nextPoint.y - lastPoint.y, nextPoint.x - lastPoint.x);
drawPaintBrush(lastPoint.x, lastPoint.y, angle);
}
requestAnimationFrame(animate);
}
init();
animate();