first commit

This commit is contained in:
编码猿
2025-08-29 21:54:28 +08:00
commit 825098a26c
15 changed files with 1416 additions and 0 deletions

626
love.js Normal file
View File

@@ -0,0 +1,626 @@
// 定义一个表示二维坐标点的类
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类