// 定义一个表示二维坐标点的类 class Point { // 构造函数,设置x和y坐标,默认值为0 constructor(x = 0, y = 0) { this.x = x; // 设置x坐标 this.y = y; // 设置y坐标 } // 克隆当前点,返回一个新的Point对象 clone() { return new Point(this.x, this.y); // 使用当前坐标创建新点 } // 点加法,将另一个点的坐标加到当前点 add(o) { const p = this.clone(); // 创建当前点的副本 p.x += o.x; // x坐标相加 p.y += o.y; // y坐标相加 return p; // 返回新点 } // 点减法,从当前点减去另一个点的坐标 sub(o) { const p = this.clone(); // 创建当前点的副本 p.x -= o.x; // x坐标相减 p.y -= o.y; // y坐标相减 return p; // 返回新点 } // 点除法,将当前点坐标除以一个数 div(n) { const p = this.clone(); // 创建当前点的副本 p.x /= n; // x坐标除以n p.y /= n; // y坐标除以n return p; // 返回新点 } // 点乘法,将当前点坐标乘以一个数 mul(n) { const p = this.clone(); // 创建当前点的副本 p.x *= n; // x坐标乘以n p.y *= n; // y坐标乘以n return p; // 返回新点 } } // 心形曲线类 class Heart { constructor() { this.points = []; // 存储心形曲线上的点 // 使用参数方程生成心形曲线 for (let i = 10; i < 30; i += 0.2) { const t = i / Math.PI; // 参数t // 心形曲线的x坐标公式 const x = 16 * Math.pow(Math.sin(t), 3); // 心形曲线的y坐标公式 const y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t); this.points.push(new Point(x, y)); // 将点添加到数组中 } this.length = this.points.length; // 存储点的数量 } // 获取指定索引的点,并可选择缩放 get(i, scale = 1) { return this.points[i].mul(scale); // 返回缩放后的点 } } // 种子类,代表树的核心元素 class Seed { constructor(tree, point, scale = 1, color = '#FF0000') { this.tree = tree; // 关联的树对象 this.heart = { // 心形配置 point: point, // 位置点 scale: scale, // 缩放比例 color: color, // 颜色 figure: new Heart(), // 心形图形 }; this.cirle = { // 圆形配置 point: point, // 位置点 scale: scale, // 缩放比例 color: color, // 颜色 radius: 5, // 半径 }; } // 绘制种子 draw() { this.drawHeart(); // 绘制心形 this.drawText(); // 绘制文字 } // 添加位置偏移 addPosition(x, y) { this.cirle.point = this.cirle.point.add(new Point(x, y)); // 更新圆形位置 } // 检查是否可以移动 canMove() { return this.cirle.point.y < (this.tree.height + 20); // 检查是否在树高度范围内 } // 移动种子 move(x, y) { this.clear(); // 清除原有绘制 this.drawCirle(); // 绘制圆形 this.addPosition(x, y); // 更新位置 } // 检查是否可以缩放 canScale() { return this.heart.scale > 0.2; // 检查缩放比例是否大于0.2 } // 设置心形缩放比例 setHeartScale(scale) { this.heart.scale *= scale; // 更新缩放比例 } // 缩放种子 scale(scale) { this.clear(); // 清除原有绘制 this.drawCirle(); // 绘制圆形 this.drawHeart(); // 绘制心形 this.setHeartScale(scale); // 更新缩放比例 } // 绘制心形 drawHeart() { const { ctx } = this.tree; // 获取画布上下文 const { point, color, scale, figure } = this.heart; // 解构心形配置 ctx.save(); // 保存当前绘图状态 ctx.fillStyle = color; // 设置填充颜色 ctx.translate(point.x, point.y); // 移动坐标系到心形中心 ctx.beginPath(); // 开始绘制路径 ctx.moveTo(0, 0); // 移动到起点 // 绘制心形曲线 for (let i = 0; i < figure.length; i++) { const p = figure.get(i, scale); // 获取缩放后的点 ctx.lineTo(p.x, -p.y); // 绘制线到该点(y坐标取反) } ctx.closePath(); // 闭合路径 ctx.fill(); // 填充心形 ctx.restore(); // 恢复绘图状态 } // 绘制圆形 drawCirle() { const { ctx } = this.tree; // 获取画布上下文 const { point, color, scale, radius } = this.cirle; // 解构圆形配置 ctx.save(); // 保存当前绘图状态 ctx.fillStyle = color; // 设置填充颜色 ctx.translate(point.x, point.y); // 移动坐标系到圆心 ctx.scale(scale, scale); // 应用缩放 ctx.beginPath(); // 开始绘制路径 ctx.moveTo(0, 0); // 移动到圆心 ctx.arc(0, 0, radius, 0, 2 * Math.PI); // 绘制圆形 ctx.closePath(); // 闭合路径 ctx.fill(); // 填充圆形 ctx.restore(); // 恢复绘图状态 } // 绘制文字 drawText() { const { ctx } = this.tree; // 获取画布上下文 const { point, color, scale } = this.heart; // 解构心形配置 ctx.save(); // 保存当前绘图状态 ctx.strokeStyle = color; // 设置描边颜色 ctx.fillStyle = color; // 设置填充颜色 ctx.translate(point.x, point.y); // 移动坐标系到文字起点 ctx.scale(scale, scale); // 应用缩放 ctx.moveTo(0, 0); // 移动到起点 ctx.lineTo(15, 15); // 绘制第一条线 ctx.lineTo(60, 15); // 绘制第二条线 ctx.stroke(); // 描边路径 ctx.moveTo(0, 0); // 移动到新起点 ctx.scale(0.75, 0.75); // 调整缩放比例 ctx.font = "14px 微软雅黑,Verdana"; // 设置字体 ctx.fillText(" [ 点爱心 ]", 23, 10); // 绘制文字 ctx.restore(); // 恢复绘图状态 } // 清除绘制 clear() { let h = 0; const { ctx } = this.tree; // 获取画布上下文 const { point, scale } = this.cirle; // 解构圆形配置 const radius = 26; // 清除区域半径 const w = h = (radius * scale); // 计算清除区域宽高 ctx.clearRect(point.x - w, point.y - h, 4 * w, 4 * h); // 清除矩形区域 } // 检查鼠标是否悬停在元素上 hover(x, y) { const { ctx } = this.tree; // 获取画布上下文 const pixel = ctx.getImageData(x, y, 1, 1); // 获取像素数据 return pixel.data[3] === 255; // 检查alpha通道是否为255(不透明) } } // 页脚类 class Footer { constructor(tree, width, height, speed = 2) { this.tree = tree; // 关联的树对象 // 初始化位置点(位于树底部中间) this.point = new Point(tree.seed.heart.point.x, tree.height - height / 2); this.width = width; // 页脚宽度 this.height = height; // 页脚高度 this.speed = speed; // 绘制速度 this.length = 0; // 当前绘制长度 } // 绘制页脚 draw() { const { ctx } = this.tree; // 获取画布上下文 const { point } = this; // 解构位置点 const len = this.length / 2; // 计算当前线长的一半 ctx.save(); // 保存当前绘图状态 ctx.strokeStyle = 'rgb(35, 31, 32)'; // 设置描边颜色 ctx.lineWidth = this.height; // 设置线宽 ctx.lineCap = 'round'; // 设置线端样式 ctx.lineJoin = 'round'; // 设置线连接样式 ctx.translate(point.x, point.y); // 移动坐标系到中心点 ctx.beginPath(); // 开始绘制路径 ctx.moveTo(0, 0); // 移动到中心 ctx.lineTo(len, 0); // 向右绘制 ctx.lineTo(-len, 0); // 向左绘制 ctx.stroke(); // 描边路径 ctx.restore(); // 恢复绘图状态 // 如果当前长度小于总宽度,继续增长 if (this.length < this.width) { this.length += this.speed; // 增加长度 } } } // 树枝类 class Branch { constructor(tree, point1, point2, point3, radius, length = 100, branchs = []) { this.tree = tree; // 关联的树对象 this.point1 = point1; // 贝塞尔曲线起点 this.point2 = point2; // 贝塞尔曲线控制点 this.point3 = point3; // 贝塞尔曲线终点 this.radius = radius; // 树枝半径 this.length = length; // 树枝长度 this.len = 0; // 当前绘制长度 this.t = 1 / (this.length - 1); // 计算步长 this.branchs = branchs; // 子树枝数组 } // 树枝生长 grow() { if (this.len <= this.length) { // 如果还没长到最大长度 // 计算贝塞尔曲线上的点 const p = bezier([this.point1, this.point2, this.point3], this.len * this.t); this.draw(p); // 绘制当前点 this.len += 1; // 增加长度 this.radius *= 0.97; // 半径逐渐减小 } else { // 如果已经长到最大长度 this.tree.removeBranch(this); // 从树中移除当前树枝 this.tree.addBranchs(this.branchs); // 添加子树枝 } } // 绘制树枝上的一个点 draw(p) { const { ctx } = this.tree; // 获取画布上下文 ctx.save(); // 保存当前绘图状态 ctx.beginPath(); // 开始绘制路径 ctx.fillStyle = 'rgb(35, 31, 32)'; // 设置填充颜色 ctx.shadowColor = 'rgb(31, 35, 31)'; // 设置阴影颜色 ctx.shadowBlur = 2; // 设置阴影模糊度 ctx.moveTo(p.x, p.y); // 移动到指定点 ctx.arc(p.x, p.y, this.radius, 0, 2 * Math.PI); // 绘制圆形 ctx.closePath(); // 闭合路径 ctx.fill(); // 填充圆形 ctx.restore(); // 恢复绘图状态 } } // 花朵类 class Bloom { constructor(tree, point, figure, color = null, alpha = null, angle = null, scale = 0.1, place = null, speed = null) { this.tree = tree; // 关联的树对象 this.point = point; // 花朵位置 // 设置颜色,如果没有则随机生成 this.color = color || `rgb(255,${random(0, 255)},${random(0, 255)})`; // 设置透明度,如果没有则随机生成(0.3-1) this.alpha = alpha || random(0.3, 1); // 设置角度,如果没有则随机生成(0-360度) this.angle = angle || random(0, 360); this.scale = scale; // 缩放比例 this.place = place; // 目标位置(用于跳跃动画) this.speed = speed; // 速度(用于跳跃动画) this.figure = figure; // 花朵形状 } // 设置花朵形状 setFigure(figure) { this.figure = figure; // 更新形状 } // 花朵绽放动画 flower() { this.draw(); // 绘制花朵 this.scale += 0.1; // 增加缩放比例 if (this.scale > 1) { // 如果完全绽放 this.tree.removeBloom(this); // 从树中移除 } } // 绘制花朵 draw() { const { ctx } = this.tree; // 获取画布上下文 ctx.save(); // 保存当前绘图状态 ctx.fillStyle = this.color; // 设置填充颜色 ctx.globalAlpha = this.alpha; // 设置全局透明度 ctx.translate(this.point.x, this.point.y); // 移动坐标系到花朵中心 ctx.scale(this.scale, this.scale); // 应用缩放 ctx.rotate(this.angle); // 应用旋转 ctx.beginPath(); // 开始绘制路径 ctx.moveTo(0, 0); // 移动到中心 // 绘制花朵形状 for (let i = 0; i < this.figure.length; i++) { const p = this.figure.get(i); // 获取形状上的点 ctx.lineTo(p.x, -p.y); // 绘制线到该点(y坐标取反) } ctx.closePath(); // 闭合路径 ctx.fill(); // 填充形状 ctx.restore(); // 恢复绘图状态 } // 花朵跳跃动画 jump() { const { height } = this.tree; // 获取树的高度 // 如果花朵超出边界,则移除 if (this.point.x < -20 || this.point.y > height + 20) { this.tree.removeBloom(this); } else { this.draw(); // 绘制花朵 // 计算新位置(向目标位置移动) this.point = this.place.sub(this.point).div(this.speed).add(this.point); this.angle += 0.05; // 增加旋转角度 this.speed -= 1; // 减小速度 } } } // 树类 class Tree { constructor(canvas, width, height, opt = {}) { this.canvas = canvas; // 画布元素 this.ctx = canvas.getContext('2d'); // 画布上下文 this.width = width; // 画布宽度 this.height = height; // 画布高度 this.opt = opt; // 配置选项 this.record = {}; // 记录快照 // 初始化各个组件 this.initSeed(); // 初始化种子 this.initFooter(); // 初始化页脚 this.initBranch(); // 初始化树枝 this.initBloom(); // 初始化花朵 } // 初始化种子 initSeed() { const { seed = {} } = this.opt; // 获取种子配置 // 设置x坐标,默认为画布中心 const x = seed.x || this.width / 2; // 设置y坐标,默认为画布中心 const y = seed.y || this.height / 2; const point = new Point(x, y); // 创建位置点 const color = seed.color || '#FF0000'; // 设置颜色,默认为红色 const scale = seed.scale || 1; // 设置缩放比例,默认为1 this.seed = new Seed(this, point, scale, color); // 创建种子对象 } // 初始化页脚 initFooter() { const { footer = {} } = this.opt; // 获取页脚配置 const width = footer.width || this.width; // 设置宽度,默认为画布宽度 const height = footer.height || 5; // 设置高度,默认为5 const speed = footer.speed || 2; // 设置速度,默认为2 this.footer = new Footer(this, width, height, speed); // 创建页脚对象 } // 初始化树枝 initBranch() { const branchs = this.opt.branch || []; // 获取树枝配置 this.branchs = []; // 初始化树枝数组 this.addBranchs(branchs); // 添加树枝 } // 初始化花朵 initBloom() { const { bloom = {} } = this.opt; // 获取花朵配置 const cache = []; // 初始化花朵缓存 const num = bloom.num || 500; // 设置花朵数量,默认为500 const width = bloom.width || this.width; // 设置区域宽度 const height = bloom.height || this.height; // 设置区域高度 const figure = this.seed.heart.figure; // 获取心形图形 const r = 240; // 心形半径 // 创建指定数量的花朵 for (let i = 0; i < num; i++) { cache.push(this.createBloom(width, height, r, figure)); } this.blooms = []; // 初始化花朵数组 this.bloomsCache = cache; // 存储花朵缓存 } // 将画布转换为数据URL toDataURL(type) { return this.canvas.toDataURL(type); // 返回数据URL } // 绘制记录的快照 draw(k) { const rec = this.record[k]; // 获取指定键的记录 if (!rec) return; // 如果没有记录则返回 const { ctx } = this; // 获取画布上下文 const { point, image } = rec; // 解构位置点和图像数据 ctx.save(); // 保存当前绘图状态 ctx.putImageData(image, point.x, point.y); // 绘制图像数据 ctx.restore(); // 恢复绘图状态 } // 添加单个树枝 addBranch(branch) { this.branchs.push(branch); // 将树枝添加到数组 } // 添加多个树枝 addBranchs(branchs) { for (const b of branchs) { // 遍历树枝配置 // 创建三个控制点 const p1 = new Point(b[0], b[1]); const p2 = new Point(b[2], b[3]); const p3 = new Point(b[4], b[5]); const r = b[6]; // 半径 const l = b[7]; // 长度 const c = b[8]; // 子树枝配置 // 创建树枝并添加到数组 this.addBranch(new Branch(this, p1, p2, p3, r, l, c)); } } // 移除树枝 removeBranch(branch) { // 过滤掉指定的树枝 this.branchs = this.branchs.filter(b => b !== branch); } // 检查是否可以生长 canGrow() { return this.branchs.length > 0; // 是否有树枝需要生长 } // 树枝生长 grow() { for (const branch of this.branchs) { // 遍历所有树枝 if (branch) { branch.grow(); // 让树枝生长 } } } // 添加花朵 addBloom(bloom) { this.blooms.push(bloom); // 将花朵添加到数组 } // 移除花朵 removeBloom(bloom) { // 过滤掉指定的花朵 this.blooms = this.blooms.filter(b => b !== bloom); } // 创建花朵 createBloom(width, height, radius, figure, color, alpha, angle, scale, place, speed) { let x, y; while (true) { // 循环直到找到合适的位置 // 随机生成x,y坐标 x = random(20, width - 20); y = random(20, height - 20); // 检查是否在心形区域内 if (inheart(x - width / 2, height - (height - 40) / 2 - y, radius)) { // 创建并返回花朵对象 return new Bloom(this, new Point(x, y), figure, color, alpha, angle, scale, place, speed); } } } // 检查是否可以开花 canFlower() { return this.blooms.length > 0; // 是否有花朵需要绽放 } // 花朵绽放 flower(num) { // 从缓存中取出指定数量的花朵 const blooms = this.bloomsCache.splice(0, num); blooms.forEach(bloom => this.addBloom(bloom)); // 添加到花朵数组 // 让所有花朵绽放 this.blooms.forEach(bloom => bloom.flower()); } // 拍摄快照 snapshot(k, x, y, width, height) { // 获取指定区域的图像数据 const image = this.ctx.getImageData(x, y, width, height); // 存储快照记录 this.record[k] = { image, // 图像数据 point: new Point(x, y), // 位置点 width, // 宽度 height // 高度 }; } // 设置移动速度 setSpeed(k, speed) { this.record[k || "move"].speed = speed; // 更新速度 } // 移动元素 move(k, x, y) { const rec = this.record[k || "move"]; // 获取记录 const { ctx } = this; // 获取画布上下文 // 解构记录属性 const { point, image, speed = 10, width, height } = rec; // 计算新位置(考虑速度) const i = point.x + speed < x ? point.x + speed : x; const j = point.y + speed < y ? point.y + speed : y; ctx.save(); // 保存当前绘图状态 ctx.clearRect(point.x, point.y, width, height); // 清除原区域 ctx.putImageData(image, i, j); // 在新位置绘制图像 ctx.restore(); // 恢复绘图状态 rec.point = new Point(i, j); // 更新位置点 rec.speed = speed * 0.95; // 降低速度 if (rec.speed < 2) { // 确保最小速度 rec.speed = 2; } return i < x || j < y; // 返回是否到达目标 } jump() { this.blooms.forEach(bloom => bloom.jump()); // 让所有花朵跳跃 // TODO 这里可以修改花朵的数量 // 如果花朵数量不足,创建新的花朵 if ((this.blooms.length && this.blooms.length < 30) || !this.blooms.length) { const { bloom = {} } = this.opt; // 获取花朵配置 const width = bloom.width || this.width; // 区域宽度 const height = bloom.height || this.height; // 区域高度 const figure = this.seed.heart.figure; // 心形图形 const r = 240; // 心形半径 // 随机创建1-2朵新花朵 for (let i = 0; i < random(1, 2); i++) { this.blooms.push(this.createBloom( width / 2 + width, // x范围扩大 height, // 高度 r, // 半径 figure, // 形状 null, 1, null, 1, // 默认颜色、透明度、角度、缩放 // 随机目标位置 new Point(random(-100, 600), 720), random(200, 300) // 随机速度 )); } } } } function random(min, max) { return min + Math.floor(Math.random() * (max - min + 1)); } // 工具函数 - 计算贝塞尔曲线上的点 function bezier(cp, t) { // 计算三次贝塞尔曲线的点 const p1 = cp[0].mul((1 - t) * (1 - t)); // 第一项 const p2 = cp[1].mul(2 * t * (1 - t)); // 第二项 const p3 = cp[2].mul(t * t); // 第三项 return p1.add(p2).add(p3); // 返回总和 } // 工具函数 - 检查点是否在心形区域内 function inheart(x, y, r) { // 心形曲线方程 const z = ((x / r) * (x / r) + (y / r) * (y / r) - 1) * ((x / r) * (x / r) + (y / r) * (y / r) - 1) * ((x / r) * (x / r) + (y / r) * (y / r) - 1) - (x / r) * (x / r) * (y / r) * (y / r) * (y / r); return z < 0; // 返回是否在心形内 } // 导出到全局对象 window.random = random; // 随机数函数 window.bezier = bezier; // 贝塞尔曲线函数 window.Point = Point; // Point类 window.Tree = Tree; // Tree类