Files
like_pq/love.js
2025-08-29 21:54:28 +08:00

626 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 定义一个表示二维坐标点的类
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类