当前位置: 首页 > news >正文

React 实现爱心花园动画

主页:

import React, { useEffect, useRef, useState } from 'react';
import '@/assets/css/Love.less';
import { Garden } from '@/utils/GardenClasses';// 组件属性接口
interface LoveAnimationProps {startDate?: Date; // 可选的开始日期messages?: {      // 可自定义的文本消息initial?: string;   // 初始文字love?: string;      // 告白文字signature?: string; // 落款};
}// 默认开始日期:2010年11月2日20点
const DEFAULT_START_DATE = new Date(2010, 10, 2, 20, 0, 0);// 默认文本配置
const DEFAULT_MESSAGES = {initial: "亲爱的,这是我们相爱在一起的时光。",love: "爱你直到永永远远。",signature: "-爱你的人"
};const LoveAnimation: React.FC<LoveAnimationProps> = ({startDate = DEFAULT_START_DATE,messages = DEFAULT_MESSAGES
}) => {// ========== Refs定义 ==========const canvasRef = useRef<HTMLCanvasElement>(null);      // 画布引用const gardenRef = useRef<Garden | null>(null);          // 花园实例引用const loveHeartRef = useRef<HTMLDivElement>(null);      // 心形容器const contentRef = useRef<HTMLDivElement>(null);        // 内容容器const codeRef = useRef<HTMLDivElement>(null);           // 代码区域const wordsRef = useRef<HTMLDivElement>(null);          // 文字区域const messagesRef = useRef<HTMLDivElement>(null);       // 消息区域const loveURef = useRef<HTMLDivElement>(null);          // 告白区域const elapseClockRef = useRef<HTMLDivElement>(null);    // 计时器const errorMsgRef = useRef<HTMLDivElement>(null);       // 错误信息// ========== 状态定义 ==========const [showMessages, setShowMessages] = useState(false); // 是否显示消息const [showLoveU, setShowLoveU] = useState(false);      // 是否显示告白const [codeContent, setCodeContent] = useState('');     // 代码内容const [showCursor, setShowCursor] = useState(false);    // 是否显示光标const [clearVal, setClearVal] = useState(true);         // 清除标志const clearValRef = useRef(clearVal);                   // 清除标志的ref// 动画定时器存储const animationRefs = useRef<{ intervals: NodeJS.Timeout[]; // 间隔定时器timeouts: NodeJS.Timeout[];  // 延时定时器}>({ intervals: [], timeouts: [] });// 完整的代码内容(带HTML格式)const fullCodeContent = `<br />/**<br />*2013—02-14,<br />*2013-02-28.<br />*/<br />Boy name = <span class="keyword">Mr</span> ***<br />Girl name = <span class="keyword">Mrs</span> ***<br /><span class="comments">// Fall in love river.</span><br />The boy love the girl;<br /><span class="comments">// They love each other.</span><br />The girl loved the boy;<br /><span class="comments">// AS time goes on.</span><br />The boy can not be separated the girl;<br /><span class="comments">// At the same time.</span><br />The girl can not be separated the boy;<br /><span class="comments">// Both wind and snow all over the sky.</span><br /><span class="comments">// Whether on foot or 5 kilometers.</span><br /><span class="keyword">The boy</span> very <span class="keyword">happy</span>;<br /><span class="keyword">The girl</span> is also very <span class="keyword">happy</span>;<br /><span class="comments">// Whether it is right now</span><br /><span class="comments">// Still in the distant future.</span><br />The boy has but one dream;<br /><span class="comments">// The boy wants the girl could well have been happy.</span><br />I want to say:<br />Baby, I love you forever;`;// ========== 主要副作用 ==========useEffect(() => {if (!canvasRef.current || !loveHeartRef.current || !contentRef.current) return;// 检查浏览器是否支持canvasif (!document.createElement('canvas').getContext) {if (errorMsgRef.current) {errorMsgRef.current.innerHTML ="您的浏览器不支持HTML5!<br/>推荐使用 Chrome 14+/IE 9+/Firefox 7+/Safari 4+";}if (codeRef.current) {codeRef.current.style.display = "none";}return;}// 初始化画布const gardenCanvas = canvasRef.current;gardenCanvas.width = loveHeartRef.current.offsetWidth;gardenCanvas.height = loveHeartRef.current.offsetHeight;// 获取2D上下文const ctx = gardenCanvas.getContext('2d');if (!ctx) return;// 设置混合模式ctx.globalCompositeOperation = "lighter";// 创建花园实例gardenRef.current = new Garden(ctx, gardenCanvas);// 调整布局adjustLayout();// 花园渲染循环const renderInterval = setInterval(() => {gardenRef.current?.render();}, Garden.options.growSpeed);animationRefs.current.intervals.push(renderInterval);// 启动代码打字效果typeWriterCodeContent();// 光标闪烁效果const cursorInterval = setInterval(() => {if (clearValRef.current) {setShowCursor(prev => !prev);} else {clearInterval(cursorInterval);setShowCursor(false);}}, 600);animationRefs.current.intervals.push(cursorInterval);// 5秒后开始心形动画const heartTimeout = setTimeout(() => {startHeartAnimation();}, 5000);animationRefs.current.timeouts.push(heartTimeout);// 初始化计时器timeElapse(startDate);const timeInterval = setInterval(() => timeElapse(startDate), 500);animationRefs.current.intervals.push(timeInterval);// 窗口大小变化监听const handleResize = () => adjustLayout();window.addEventListener('resize', handleResize);// 清理函数return () => {animationRefs.current.intervals.forEach(interval => clearInterval(interval));animationRefs.current.timeouts.forEach(timeout => clearTimeout(timeout));window.removeEventListener('resize', handleResize);};}, [startDate]);// 显示消息后的副作用useEffect(() => {if (showMessages) {adjustWordsPosition();const timer = setTimeout(() => setShowLoveU(true), 5000);animationRefs.current.timeouts.push(timer);return () => clearTimeout(timer);}}, [showMessages]);// 显示告白后的副作用useEffect(() => {if (showLoveU && loveURef.current) {const loveUContent = `${messages.love}<br/><div class='signature'>${messages.signature}</div>`;loveURef.current.innerHTML = '';typeWriter(loveURef.current, loveUContent, 75);}}, [showLoveU, messages]);// 同步clearVal状态到refuseEffect(() => {clearValRef.current = clearVal;}, [clearVal]);// ========== 工具函数 ==========/*** 代码打字效果*/const typeWriterCodeContent = () => {setShowCursor(true);let i = 0;const speed = 10; // 打字速度(毫秒/字符)const typing = setInterval(() => {if (i < fullCodeContent.length) {setCodeContent(fullCodeContent.substring(0, i + 1));i++;} else {clearInterval(typing);setClearVal(false); // 打字完成,停止光标闪烁}}, speed);animationRefs.current.intervals.push(typing);};/*** 计算心形曲线上的点* @param angle 角度(弧度)* @returns [x, y]坐标*/const getHeartPoint = (angle: number): [number, number] => {// 心形曲线参数方程const x = 19.5 * (16 * Math.pow(Math.sin(angle), 3));const y = -20 * (13 * Math.cos(angle) - 5 * Math.cos(2 * angle) - 2 * Math.cos(3 * angle) - Math.cos(4 * angle));// 计算相对于心形容器中心的坐标const offsetX = loveHeartRef.current?.offsetWidth ? loveHeartRef.current.offsetWidth / 2 : 0;const offsetY = loveHeartRef.current?.offsetHeight ? loveHeartRef.current.offsetHeight / 2 - 55 : 0;return [offsetX + x, offsetY + y];};/*** 开始心形动画*/const startHeartAnimation = () => {const interval = 50; // 花朵生成间隔(毫秒)const speed = 0.2;   // 角度变化速度let angle = 10;      // 起始角度const points: [number, number][] = []; // 已生成的点const animation = setInterval(() => {const point = getHeartPoint(angle);let valid = true;// 检查新点与已有点的距离for (const p of points) {const distance = Math.sqrt(Math.pow(p[0] - point[0], 2) + Math.pow(p[1] - point[1], 2));if (distance < Garden.options.bloomRadius.max * 1.3) {valid = false;break;}}// 如果点有效,创建花朵if (valid && gardenRef.current) {points.push(point);gardenRef.current.createRandomBloom(point[0], point[1]);}// 动画结束条件if (angle >= 30) {clearInterval(animation);setShowMessages(true); // 显示消息} else {angle += speed; // 继续动画}}, interval);animationRefs.current.intervals.push(animation);};/*** 通用打字机效果* @param element 目标DOM元素* @param text 要显示的文本* @param speed 打字速度(毫秒/字符)*/const typeWriter = (element: HTMLElement, text: string, speed: number) => {let i = 0;element.innerHTML = '';const typing = setInterval(() => {if (i < text.length) {const char = text.substr(i, 1);// 跳过HTML标签if (char === '<') {const closingIndex = text.indexOf('>', i);i = closingIndex === -1 ? text.length : closingIndex + 1;} else {i++;}// 更新内容并添加光标element.innerHTML = text.substring(0, i) + (i % 2 ? '_' : '');} else {clearInterval(typing);}}, speed);animationRefs.current.intervals.push(typing);};/*** 计算并显示恋爱时长* @param date 开始日期*/const timeElapse = (date: Date) => {if (!elapseClockRef.current) return;const now = new Date();const seconds = (now.getTime() - date.getTime()) / 1000;// 计算天数const days = Math.floor(seconds / (3600 * 24));let remaining = seconds % (3600 * 24);// 计算小时const hours = Math.floor(remaining / 3600);remaining %= 3600;// 计算分钟const minutes = Math.floor(remaining / 60);remaining %= 60;// 格式化显示(补零)const formattedHours = hours < 10 ? `0${hours}` : hours.toString();const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes.toString();const formattedSeconds = remaining < 10 ? `0${Math.floor(remaining)}` : Math.floor(remaining).toString();// 更新DOMelapseClockRef.current.innerHTML = `<span class="digit">${days}</span> 天 <span class="digit">${formattedHours}</span> 小时 <span class="digit">${formattedMinutes}</span> 分钟 <span class="digit">${formattedSeconds}</span> 秒`;};/*** 调整文字位置*/const adjustWordsPosition = () => {if (!wordsRef.current || !canvasRef.current) return;const garden = canvasRef.current;const words = wordsRef.current;words.style.position = 'absolute';words.style.top = `${garden.offsetTop + 195}px`;words.style.left = `${garden.offsetLeft + 70}px`;};/*** 调整代码区域位置*/const adjustCodePosition = () => {if (!codeRef.current || !canvasRef.current) return;const garden = canvasRef.current;const code = codeRef.current;code.style.marginTop = `${(garden.offsetHeight - code.offsetHeight) / 2}px`;};/*** 响应式布局调整*/const adjustLayout = () => {if (!contentRef.current || !loveHeartRef.current || !codeRef.current) return;const content = contentRef.current;const loveHeart = loveHeartRef.current;const code = codeRef.current;// 计算合适尺寸const width = loveHeart.offsetWidth + code.offsetWidth;const height = Math.max(loveHeart.offsetHeight, code.offsetHeight);// 设置容器尺寸(考虑窗口边界)content.style.width = `${Math.min(width, window.innerWidth - 40)}px`;content.style.height = `${Math.min(height, window.innerHeight - 40)}px`;// 居中显示content.style.marginTop = `${Math.max((window.innerHeight - content.offsetHeight) / 2, 10)}px`;content.style.marginLeft = `${Math.max((window.innerWidth - content.offsetWidth) / 2, 10)}px`;// 调整代码区域垂直居中adjustCodePosition();};/*** 渲染代码区域*/const renderCodeContent = () => {return (<div id="code" ref={codeRef}>{/* 使用dangerouslySetInnerHTML显示带HTML格式的代码 */}<div dangerouslySetInnerHTML={{ __html: codeContent }} />{/* 闪烁的光标(心形) */}{showCursor && (<span className="heart-cursor" style={{ color: 'red' }}></span>)}</div>);};// ========== 组件渲染 ==========return (<div className="btnbg lovePage">{/* 背景层 */}<div id="mainDiv">{/* 主内容容器 */}<div id="content" ref={contentRef}>{/* 左侧:代码区域 */}{renderCodeContent()}{/* 右侧:心形动画区域 */}<div id="loveHeart" ref={loveHeartRef}>{/* 花园画布 */}<canvas id="garden" ref={canvasRef}></canvas>{/* 情话文本区域(默认隐藏) */}<divid="words"ref={wordsRef}style={{display: showMessages ? 'block' : 'none',opacity: showMessages ? 1 : 0,transition: 'opacity 1s ease-in-out'}}>{/* 初始消息 */}<div id="messages" ref={messagesRef}>{messages.initial}{/* 恋爱计时器 */}<div id="elapseClock" ref={elapseClockRef}></div></div>{/* 最终告白(默认隐藏) */}<divid="loveu"ref={loveURef}style={{display: showLoveU ? 'block' : 'none',opacity: showLoveU ? 1 : 0,transition: 'opacity 1s ease-in-out'}}/></div></div></div></div>{/* 浏览器兼容性错误提示 */}<div id="errorMsg" ref={errorMsgRef}></div></div>);
};export default LoveAnimation;

GardenClasses.ts文件:

// GardenClasses.ts
export interface VectorProps {x: number;y: number;
}export interface PetalOptions {stretchA: number;stretchB: number;startAngle: number;angle: number;growFactor: number;bloom: Bloom;
}export interface BloomOptions {p: Vector;r: number;c: string;pc: number;garden: Garden;
}export interface GardenOptions {petalCount: { min: number; max: number };petalStretch: { min: number; max: number };growFactor: { min: number; max: number };bloomRadius: { min: number; max: number };density: number;growSpeed: number;color: {rmin: number;rmax: number;gmin: number;gmax: number;bmin: number;bmax: number;opacity: number;};tanAngle: number;
}export class Vector {x: number;y: number;constructor(x: number, y: number) {this.x = x;this.y = y;}rotate(angle: number): Vector {const x = this.x;const y = this.y;this.x = Math.cos(angle) * x - Math.sin(angle) * y;this.y = Math.sin(angle) * x + Math.cos(angle) * y;return this;}mult(factor: number): Vector {this.x *= factor;this.y *= factor;return this;}clone(): Vector {return new Vector(this.x, this.y);}length(): number {return Math.sqrt(this.x * this.x + this.y * this.y);}subtract(v: Vector): Vector {this.x -= v.x;this.y -= v.y;return this;}set(x: number, y: number): Vector {this.x = x;this.y = y;return this;}
}export class Petal {stretchA: number;stretchB: number;startAngle: number;angle: number;bloom: Bloom;growFactor: number;r: number;isfinished: boolean;constructor(options: PetalOptions) {this.stretchA = options.stretchA;this.stretchB = options.stretchB;this.startAngle = options.startAngle;this.angle = options.angle;this.bloom = options.bloom;this.growFactor = options.growFactor;this.r = 1;this.isfinished = false;}draw(): void {const ctx = this.bloom.garden.ctx;const e = new Vector(0, this.r).rotate(Garden.degrad(this.startAngle));const d = e.clone().rotate(Garden.degrad(this.angle));const c = e.clone().mult(this.stretchA);const b = d.clone().mult(this.stretchB);ctx.strokeStyle = this.bloom.c;ctx.beginPath();ctx.moveTo(e.x, e.y);ctx.bezierCurveTo(c.x, c.y, b.x, b.y, d.x, d.y);ctx.stroke();}render(): void {if (this.r <= this.bloom.r) {this.r += this.growFactor;this.draw();} else {this.isfinished = true;}}
}export class Bloom {p: Vector;r: number;c: string;pc: number;petals: Petal[];garden: Garden;constructor(options: BloomOptions) {this.p = options.p;this.r = options.r;this.c = options.c;this.pc = options.pc;this.petals = [];this.garden = options.garden;this.init();this.garden.addBloom(this);}draw(): void {let isFinished = true;this.garden.ctx.save();this.garden.ctx.translate(this.p.x, this.p.y);for (const petal of this.petals) {petal.render();isFinished = isFinished && petal.isfinished;}this.garden.ctx.restore();if (isFinished) {this.garden.removeBloom(this);}}init(): void {const angle = 360 / this.pc;const startAngle = Garden.randomInt(0, 90);for (let i = 0; i < this.pc; i++) {this.petals.push(new Petal({stretchA: Garden.random(Garden.options.petalStretch.min, Garden.options.petalStretch.max),stretchB: Garden.random(Garden.options.petalStretch.min, Garden.options.petalStretch.max),startAngle: startAngle + i * angle,angle: angle,growFactor: Garden.random(Garden.options.growFactor.min, Garden.options.growFactor.max),bloom: this,}));}}
}export class Garden {blooms: Bloom[];element: HTMLCanvasElement;ctx: CanvasRenderingContext2D;static options: GardenOptions = {petalCount: { min: 8, max: 15 },petalStretch: { min: 0.1, max: 3 },growFactor: { min: 0.1, max: 1 },bloomRadius: { min: 8, max: 10 },density: 10,growSpeed: 1000 / 60,color: {rmin: 128,rmax: 255,gmin: 0,gmax: 128,bmin: 0,bmax: 128,opacity: 0.1,},tanAngle: 60,};constructor(ctx: CanvasRenderingContext2D, element: HTMLCanvasElement) {this.blooms = [];this.element = element;this.ctx = ctx;}render(): void {for (const bloom of this.blooms) {bloom.draw();}}addBloom(bloom: Bloom): void {this.blooms.push(bloom);}removeBloom(bloom: Bloom): void {const index = this.blooms.indexOf(bloom);if (index !== -1) {this.blooms.splice(index, 1);}}createRandomBloom(x: number, y: number): void {this.createBloom(x,y,Garden.randomInt(Garden.options.bloomRadius.min, Garden.options.bloomRadius.max),Garden.randomrgba(Garden.options.color.rmin,Garden.options.color.rmax,Garden.options.color.gmin,Garden.options.color.gmax,Garden.options.color.bmin,Garden.options.color.bmax,Garden.options.color.opacity),Garden.randomInt(Garden.options.petalCount.min, Garden.options.petalCount.max));}createBloom(x: number, y: number, radius: number, color: string, petalCount: number): void {new Bloom({p: new Vector(x, y),r: radius,c: color,pc: petalCount,garden: this,});}clear(): void {this.blooms = [];this.ctx.clearRect(0, 0, this.element.width, this.element.height);}static random(min: number, max: number): number {return Math.random() * (max - min) + min;}static randomInt(min: number, max: number): number {return Math.floor(Math.random() * (max - min + 1)) + min;}static readonly circle = 2 * Math.PI;static degrad(angle: number): number {return (Garden.circle / 360) * angle;}static raddeg(angle: number): number {return (angle / Garden.circle) * 360;}static rgba(r: number, g: number, b: number, a: number): string {return `rgba(${r},${g},${b},${a})`;}static randomrgba(rmin: number,rmax: number,gmin: number,gmax: number,bmin: number,bmax: number,a: number): string {const r = Math.round(Garden.random(rmin, rmax));const g = Math.round(Garden.random(gmin, gmax));const b = Math.round(Garden.random(bmin, bmax));const threshold = 5;if (Math.abs(r - g) <= threshold &&Math.abs(g - b) <= threshold &&Math.abs(b - r) <= threshold) {return Garden.rgba(rmin, rmax, gmin, gmax, bmin, bmax, a);} else {return Garden.rgba(r, g, b, a);}}
}

Love.less

// 主色调(基于 #ffc0cb 扩展的渐变色系)
@color-1: #ffc0cb; // 粉红
@color-2: #ffb6c1; // 稍暗的粉
@color-3: #ffd1dc; // 浅粉
@color-4: #ffdfed; // 更浅的粉
@color-5: #ffecf2; // 接近白色
@font-face {font-family: digit;src: url('digital-7_mono.ttf') format("truetype");
}
// 动画定义
.keyframes() {@keyframes gentleFlow {0% {background-position: 0% 50%;}50% {background-position: 100% 50%;}100% {background-position: 0% 50%;}}
}// 主背景样式
.lovePage {min-height: 100vh;background: linear-gradient(45deg,@color-1,@color-2,@color-3,@color-4,@color-5,@color-1 );background-size: 300% 300%;animation: gentleFlow 12s ease infinite;position: relative;overflow: hidden;.keyframes();// 光斑效果(增强层次感)&::before {content: '';position: absolute;width: 200%;height: 200%;background:radial-gradient(circle at 70% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 30%),radial-gradient(circle at 30% 80%, rgba(255, 255, 255, 0.15) 0%, transparent 30%);animation: gentleFlow 20s linear infinite reverse;}}canvas {padding: 0;margin: 0;
}div.btnbg {width: 100%;height: 100%;}#code,#messages,#loveu{color: #333;
}
#mainDiv {width: 100%;height: 100%
}#loveHeart {width: 670px;height: 625px
}#garden {width: 100%;height: 100%
}#elapseClock {text-align: right;font-size: 18px;margin-top: 10px;margin-bottom: 10px
}#words {font-family: "sans-serif";width: 500px;font-size: 24px;color: #666
}#elapseClock .digit {font-family: "digit";font-size: 36px
}#loveu {padding: 5px;font-size: 22px;margin-top: 40px;margin-right: 120px;text-align: right;display: none
}#loveu .signature {margin-top: 10px;font-size: 20px;font-style: italic
}#clickSound {display: none
}
#content{display: flex;justify-content: center;align-items: center;
}#code {width: 440px;height: 400px;color: #333;font-family: "Consolas","Monaco","Bitstream Vera Sans Mono","Courier New","sans-serif";font-size: 12px;margin: 0 !important;
}.string {color: #2a36ff
}.keyword {color: #7f0055;font-weight: bold
}.placeholder {margin-left: 15px
}.space {margin-left: 7px
}.comments {color: #3f7f5f
}#copyright {margin-top: 10px;text-align: center;width: 100%;color: #666
}#errorMsg {width: 100%;text-align: center;font-size: 24px;position: absolute;top: 100px;left: 0
}#copyright a {color: #666
}
.heart-cursor {animation: blink 1s infinite;font-size: 1em;vertical-align: middle;
}@keyframes blink {0%, 100% { opacity: 1; }50% { opacity: 0; }
}

在这里插入图片描述

相关文章:

  • AWS Glue ETL设计与调度最佳实践
  • 2025上海车展 | 移远通信48 TOPS座舱方案落地加速,AI大模型赋能多域融合新突破
  • 【器件专题1——IGBT第1讲】IGBT:电力电子领域的 “万能开关”,如何撑起新能源时代?
  • Estimands与Intercurrent Events:临床试验与统计学核心框架
  • 高等数学第一章---函数与极限(1.8连续函数及其连续性)
  • C++初窥门径
  • 三格电子——如何解决工业场景中以太网设备布线不方便的问题
  • Linux 系统用户管理与权限掌控:从基础到精通
  • Framework模块编译脚本利器
  • 1688商品采集|下单接口实战演示(含请求示例)
  • 【C语言练习】003. 声明不同数据类型的变量并赋值
  • Python图形界面编程(二)
  • TCP协议理解
  • c#接口_抽象类_多态学习
  • Golang日志模块之xlog
  • Linux字符设备驱动开发的详细步骤
  • Vue3实现高仿word自定义颜色选择器组件(支持 v-model)
  • 矩阵运营:抢占市场与流量的利器
  • 如何避免IDEA每次打开新项目都重复配置Maven?
  • 【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门
  • 重新认识中国女性|婚姻,古代传统家庭再生产的根本之道
  • 亚振家居控制权将变更:济南域潇集团实控人成新控股股东
  • 钱学森数据服务中心在沪上线,十万个数字资源向公众开放
  • 广东省发展改革委原副主任、省能源局原局长吴道闻被开除公职
  • 还山记——走进山水、感受山水艺术的魅力
  • 中国牵头制定,在线旅游机构和展览与活动领域ISO国际标准发布