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

BIN
1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

BIN
2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

BIN
3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

BIN
4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

BIN
5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

BIN
6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

99
default.css Normal file
View File

@@ -0,0 +1,99 @@
 body {
margin: 0;
padding: 0;
background: #fafafa;
font-size: 14px;
font-family: "微软雅黑", "宋体", sans-serif;
color: #231f20;
overflow: auto;
user-select: none;
}
a {
color: #000;
font-size: 14px;
user-select: none;
}
#main {
width: 100%;
margin-top: 100px;
user-select: none;
}
#wrap {
position: relative;
margin: 0 auto;
width: 1100px;
height: 680px;
margin-top: 10px;
user-select: none;
}
#text {
width: 460px;
left: 50px;
top: -25px;
position: absolute;
user-select: none;
}
#code {
display: none;
font-size: 16px;
user-select: none;
}
#clock-box {
position: absolute;
left: 50px;
top: 550px;
font-size: 28px;
display: none;
user-select: none;
}
#clock-box a {
font-size: 28px;
text-decoration: none;
user-select: none;
}
#clock {
margin-left: 48px;
user-select: none;
}
#clock .digit {
font-size: 64px;
user-select: none;
}
#canvas {
margin: 0 auto;
width: 1100px;
height: 680px;
user-select: none;
}
#error {
margin: 0 auto;
text-align: center;
margin-top: 60px;
display: none;
user-select: none;
}
.hand {
cursor: pointer;
}
.say {
margin-left: 0px;
user-select: none;
}
.space {
margin-right: 150px;
user-select: none;
}

56
functions.js Normal file
View File

@@ -0,0 +1,56 @@
// variables
var $win = $(window);
var clientWidth = $win.width();
var clientHeight = $win.height();
$(window).resize(function() {
var newWidth = $win.width();
var newHeight = $win.height();
if (newWidth != clientWidth && newHeight != clientHeight) {
location.replace(location);
}
});
(function($) {
$.fn.typewriter = function() {
this.each(function() {
var $ele = $(this), str = $ele.html(), progress = 0;
$ele.html('');
var timer = setInterval(function() {
var current = str.substr(progress, 1);
if (current == '<') {
progress = str.indexOf('>', progress) + 1;
} else {
progress++;
}
$ele.html(str.substring(0, progress) + (progress & 1 ? '_' : ''));
if (progress >= str.length) {
clearInterval(timer);
}
}, 120);
});
return this;
};
})(jQuery);
function timeElapse(date){
var current = Date();
var seconds = (Date.parse(current) - Date.parse(date)) / 1000;
var days = Math.floor(seconds / (3600 * 24));
seconds = seconds % (3600 * 24);
var hours = Math.floor(seconds / 3600);
if (hours < 10) {
hours = "0" + hours;
}
seconds = seconds % 3600;
var minutes = Math.floor(seconds / 60);
if (minutes < 10) {
minutes = "0" + minutes;
}
seconds = seconds % 60;
if (seconds < 10) {
seconds = "0" + seconds;
}
var result = "第 <span class=\"digit\">" + days + "</span> 天 <span class=\"digit\">" + hours + "</span> 小时 <span class=\"digit\">" + minutes + "</span> 分钟 <span class=\"digit\">" + seconds + "</span> 秒";
$("#clock").html(result);
}

228
index.html Normal file
View File

@@ -0,0 +1,228 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>520 快乐</title>
<link type="text/css" rel="stylesheet" href="./default.css">
<script type="text/javascript" src="./jquery.min.js"></script>
<script type="text/javascript" src="./functions.js" charset="utf-8"></script>
<script type="text/javascript" src="./love.js" charset="utf-8"></script>
</head>
<body>
<audio class="music" autoplay="autopaly" loop="loop" src="./like.mp3"></audio>
<div id="main">
<div id="wrap">
<div id="text">
<div id="code">
<font color="black" size="3">
<span
class="say"><span style="font-size: 20px; font-weight: bolder;">遇见你 </span>,像是一场温柔的巧合和缘分。第一次你只是订单列表中的一个名字,当第二次接你上车,却成了心跳加速的开始,之后的每次畅聊,我才知道原来缘分真的会以最奇妙的方式降临。从工作趣事到彼此的学习生活琐碎,从爱好到日常分享,点点滴滴汇聚在一起形成了如今我对你的情感</span><br>
<span class="say"></span><br>
<span
class="say">不知不觉间,你如阳光般温暖的笑容、春风般细腻的心思、孩子般纯真的可爱,就像久旱逢甘霖的第一缕晨曦,悄然融化了我的心墙。那些与你共度的时光,无论是车内的谈笑风生,或是偶然交汇的目光,都为我原本按部就班的生活注入了最动人的色彩与活力。</span><br>
<span class="say"></span><br>
<span class="say"><span style="font-size: 28px; font-weight: bolder;"> 520 </span>这个特殊的日子,我鼓起勇气想告诉你: <span style="color: #FF0000; font-weight: bolder;">"我喜欢你,比想象中还要多!"</span>,可理智却在不断提醒我,我们之间横亘着现实的鸿沟:年龄差距、人生轨迹,父辈思想差异..等,我不知道这段对我来说无法遏制喷薄而出的情感是否正确,是否合理,也不知道最终能有何种结果。</span><br>
<span class="say"></span><br>
<span
class="say">即便如此,我依然感激和你的相遇相识,你让我明白有些感情即使可能没有结果,也值得被真诚地表达出来,"谢谢你出现在我的世界里,哪怕只是短暂地照亮过”。<span style="color: #FF0000;">"未来的路,无论你走向何方,都愿你被岁月温柔以待"</span> 。我也深信你会遇见更多温暖的人,经历更多美好的故事,而你的优秀与真诚,也必将照亮更灿烂的生命旅程。</span><br>
</font>
</div>
</div>
<div id="clock-box">
<font color="#666666"><span class="ai" style="color: #FF0000;">潘可爱</span>,我们相识已过去了</font>
<div id="clock"></div>
</div>
<canvas id="canvas"></canvas>
</div>
</div>
<script>
// 工具函数 - 替代 Jscex.Async.sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 工具函数 - 替代动画帧等待
function nextFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
(async function () {
const canvas = $('#canvas');
if (!canvas[0].getContext) {
$("#error").show();
return;
}
const width = canvas.width();
const height = canvas.height();
canvas.attr("width", width);
canvas.attr("height", height);
const opts = {
seed: {
x: width / 2 - 20,
color: "rgb(190, 26, 37)",
scale: 2
},
branch: [
[535, 680, 570, 250, 500, 200, 30, 100, [
[540, 500, 455, 417, 340, 400, 13, 100, [
[450, 435, 434, 430, 394, 395, 2, 40]
]],
[550, 445, 600, 356, 680, 345, 12, 100, [
[578, 400, 648, 409, 661, 426, 3, 80]
]],
[539, 281, 537, 248, 534, 217, 3, 40],
[546, 397, 413, 247, 328, 244, 9, 80, [
[427, 286, 383, 253, 371, 205, 2, 40],
[498, 345, 435, 315, 395, 330, 4, 60]
]],
[546, 357, 608, 252, 678, 221, 6, 100, [
[590, 293, 646, 277, 648, 271, 2, 80]
]]
]]
],
bloom: {
num: 700,
width: 1080,
height: 650,
},
footer: {
width: 1200,
height: 5,
speed: 5,
}
};
const tree = new Tree(canvas[0], width, height, opts);
const seed = tree.seed;
const foot = tree.footer;
let hold = 1;
// 事件处理函数
const handleClick = async (e) => {
$(".music")[0].play();
const offset = canvas.offset();
const x = e.pageX - offset.left;
const y = e.pageY - offset.top;
if (seed.hover(x, y)) {
hold = 0;
canvas.off("click", handleClick);
canvas.off("mousemove", handleMouseMove);
canvas.removeClass('hand');
}
};
const handleMouseMove = (e) => {
const offset = canvas.offset();
const x = e.pageX - offset.left;
const y = e.pageY - offset.top;
canvas.toggleClass('hand', seed.hover(x, y));
};
canvas.on("click", handleClick)
.on("mousemove", handleMouseMove);
// 动画函数
async function seedAnimate() {
seed.draw();
// 等待点击
while (hold) {
await sleep(10);
}
// 种子缩放
while (seed.canScale()) {
seed.scale(0.95);
await sleep(10);
}
// 种子移动
while (seed.canMove()) {
seed.move(0, 2);
foot.draw();
await sleep(10);
}
}
async function growAnimate() {
do {
tree.grow();
await sleep(10);
} while (tree.canGrow());
}
async function flowAnimate() {
do {
tree.flower(1);
await sleep(10);
} while (tree.canFlower());
}
async function moveAnimate() {
tree.snapshot("p1", 240, 0, 610, 680);
while (tree.move("p1", 500, 0)) {
foot.draw();
await sleep(10);
}
foot.draw();
tree.snapshot("p2", 500, 0, 610, 680);
canvas.parent().css("background", `url(${tree.toDataURL('image/png')})`);
}
async function jumpAnimate() {
const ctx = tree.ctx;
while (true) {
tree.ctx.clearRect(0, 0, width, height);
tree.jump();
foot.draw();
await nextFrame(); // 使用requestAnimationFrame更流畅
}
}
async function textAnimate() {
const together = new Date(2025, 4, 5, 22, 40, 0); // 月份是0-11
$("#code").show().typewriter();
$("#clock-box").fadeIn(500);
while (true) {
timeElapse(together);
await sleep(1000);
}
}
// 主执行流程
try {
await seedAnimate();
await growAnimate();
await flowAnimate();
await moveAnimate();
// 并行执行文字动画和跳跃动画
await Promise.all([
textAnimate(),
jumpAnimate()
]);
} catch (error) {
console.error("动画执行出错:", error);
}
})();
</script>
</body>
</html>

4
jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
like.mp3 Normal file

Binary file not shown.

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类

403
test.html Normal file
View File

@@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Gallery - Center View & Realistic Materials</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
}
canvas {
display: block;
}
#info {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
color: white;
pointer-events: none;
text-shadow: 1px 1px 1px black;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
text-shadow: 1px 1px 1px black;
}
</style>
</head>
<body>
<div id="info">3D Gallery - 拖动鼠标旋转视图,滚轮缩放</div>
<div id="loading">Loading...</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/TextureLoader.js"></script>
<script>
// 初始化场景、相机和渲染器
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 0); // 1.6米高,模拟人眼高度
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// 场景中心点(观察目标)
const centerPoint = new THREE.Vector3(0, 1.6, 0);
// 添加轨道控制器,设置目标为场景中心
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.copy(centerPoint);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 1;
controls.maxDistance = 10;
controls.maxPolarAngle = Math.PI * 0.8; // 限制不能看穿地面
// 添加灯光
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 2, 1);
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0xffffff, 0.5, 10);
pointLight.position.set(0, 3, 0);
scene.add(pointLight);
// 创建画廊展品
const galleryItems = [];
const itemCount = 8;
const radius = 4; // 缩小半径使画作更集中
// 创建纹理加载器
const textureLoader = new THREE.TextureLoader();
// 画框材质
const frameMaterials = [
new THREE.MeshStandardMaterial({ color: 0xc9b38b, metalness: 0.3, roughness: 0.4 }),
new THREE.MeshStandardMaterial({ color: 0x7a6a5f, metalness: 0.4, roughness: 0.6 }),
new THREE.MeshStandardMaterial({ color: 0xe5e5e5, metalness: 0.7, roughness: 0.2 }),
new THREE.MeshStandardMaterial({ color: 0x3a2e1e, metalness: 0.1, roughness: 0.8 })
];
// 创建带画框的照片(自适应图片比例)
function createFramedPhoto(texture, frameMaterial, maxWidth, maxHeight, frameWidth) {
const group = new THREE.Group();
// 计算图片实际比例
const imageRatio = texture.image.width / texture.image.height;
let width, height;
// 根据图片比例调整显示尺寸
if (imageRatio > 1) {
// 横向图片
width = maxWidth;
height = maxWidth / imageRatio;
} else {
// 纵向图片
height = maxHeight;
width = maxHeight * imageRatio;
}
// 照片部分
const photoGeometry = new THREE.PlaneGeometry(width, height);
const photoMaterial = new THREE.MeshStandardMaterial({
map: texture,
side: THREE.DoubleSide,
});
const photo = new THREE.Mesh(photoGeometry, photoMaterial);
group.add(photo);
// 画框部分(根据实际图片尺寸调整)
const frameShape = new THREE.Shape();
const outerWidth = width + frameWidth * 2;
const outerHeight = height + frameWidth * 2;
frameShape.moveTo(-outerWidth / 2, -outerHeight / 2);
frameShape.lineTo(outerWidth / 2, -outerHeight / 2);
frameShape.lineTo(outerWidth / 2, outerHeight / 2);
frameShape.lineTo(-outerWidth / 2, outerHeight / 2);
frameShape.lineTo(-outerWidth / 2, -outerHeight / 2);
const innerPath = new THREE.Path();
innerPath.moveTo(-width / 2, -height / 2);
innerPath.lineTo(width / 2, -height / 2);
innerPath.lineTo(width / 2, height / 2);
innerPath.lineTo(-width / 2, height / 2);
innerPath.lineTo(-width / 2, -height / 2);
frameShape.holes.push(innerPath);
const extrudeSettings = {
steps: 1,
depth: frameWidth / 2,
bevelEnabled: true,
bevelThickness: 0.05,
bevelSize: 0.05,
bevelOffset: 0,
bevelSegments: 8
};
const frameGeometry = new THREE.ExtrudeGeometry(frameShape, extrudeSettings);
const frame = new THREE.Mesh(frameGeometry, frameMaterial);
frame.position.z = -frameWidth / 4;
group.add(frame);
// 添加画框背面
const backGeometry = new THREE.PlaneGeometry(outerWidth, outerHeight);
const backMaterial = new THREE.MeshStandardMaterial({
color: 0x222222,
side: THREE.DoubleSide
});
const back = new THREE.Mesh(backGeometry, backMaterial);
back.position.z = -frameWidth / 2;
group.add(back);
return group;
}
// 加载纹理
const textures = [];
let loadedTextures = 0;
// 加载地面和墙壁纹理
const floorTexture = textureLoader.load('https://threejs.org/examples/textures/hardwood2_diffuse.jpg');
const floorNormalMap = textureLoader.load('https://threejs.org/examples/textures/hardwood2_normal.jpg');
const floorRoughnessMap = textureLoader.load('https://threejs.org/examples/textures/hardwood2_roughness.jpg');
const wallTexture = textureLoader.load('https://threejs.org/examples/textures/brick_diffuse.jpg');
const wallNormalMap = textureLoader.load('https://threejs.org/examples/textures/brick_normal.jpg');
const wallRoughnessMap = textureLoader.load('https://threejs.org/examples/textures/brick_roughness.jpg');
function loadTextures() {
// 加载画作纹理
for (let i = 0; i < itemCount; i++) {
textureLoader.load(
`./${i + 1}.jpg`,
(texture) => {
textures[i] = texture;
loadedTextures++;
if (loadedTextures === itemCount) {
initGallery();
document.getElementById('loading').style.display = 'none';
}
},
undefined,
(err) => {
console.error('Error loading texture', err);
textures[i] = null;
loadedTextures++;
if (loadedTextures === itemCount) {
initGallery();
document.getElementById('loading').style.display = 'none';
}
}
);
}
}
function initGallery() {
// 创建展品
for (let i = 0; i < itemCount; i++) {
const angle = (i / itemCount) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const frameMaterial = frameMaterials[Math.floor(Math.random() * frameMaterials.length)];
const framedPhoto = createFramedPhoto(
textures[i], // 加载的纹理
frameMaterial, // 画框材质
2.0, // 最大宽度
1.5, // 最大高度
0.05 // 画框宽度
);
framedPhoto.position.set(x, 1.6, z); // 画作中心与眼睛平齐
framedPhoto.lookAt(centerPoint);
// 为整个group添加用户数据
framedPhoto.userData = {
id: i,
title: `Artwork ${i + 1}`,
description: `This is a description for artwork ${i + 1}`,
originalPosition: new THREE.Vector3(x, 1.6, z),
originalRotation: framedPhoto.rotation.clone(),
originalScale: new THREE.Vector3(1, 1, 1)
};
// 为group中的每个子对象添加引用
framedPhoto.children.forEach(child => {
child.userData.parentGroup = framedPhoto;
});
scene.add(framedPhoto);
galleryItems.push(framedPhoto);
}
// 添加地面 - 使用真实木质纹理
const floorGeometry = new THREE.CircleGeometry(radius + 2, 64);
const floorMaterial = new THREE.MeshStandardMaterial({
map: floorTexture,
normalMap: floorNormalMap,
roughnessMap: floorRoughnessMap,
roughness: 0.8,
metalness: 0.1,
side: THREE.DoubleSide
});
// 调整纹理重复
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
floorTexture.repeat.set(4, 4);
floorNormalMap.wrapS = floorNormalMap.wrapT = THREE.RepeatWrapping;
floorNormalMap.repeat.set(4, 4);
floorRoughnessMap.wrapS = floorRoughnessMap.wrapT = THREE.RepeatWrapping;
floorRoughnessMap.repeat.set(4, 4);
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.position.y = 0;
scene.add(floor);
// 添加墙壁 - 使用真实砖墙纹理
const wallHeight = 3.5;
const wallGeometry = new THREE.CylinderGeometry(radius + 2, radius + 2, wallHeight, 64, 1, true);
const wallMaterial = new THREE.MeshStandardMaterial({
map: wallTexture,
normalMap: wallNormalMap,
roughnessMap: wallRoughnessMap,
roughness: 0.7,
metalness: 0,
side: THREE.DoubleSide
});
// 调整纹理重复
wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping;
wallTexture.repeat.set(2, 1);
wallNormalMap.wrapS = wallNormalMap.wrapT = THREE.RepeatWrapping;
wallNormalMap.repeat.set(2, 1);
wallRoughnessMap.wrapS = wallRoughnessMap.wrapT = THREE.RepeatWrapping;
wallRoughnessMap.repeat.set(2, 1);
const walls = new THREE.Mesh(wallGeometry, wallMaterial);
walls.position.y = wallHeight / 2;
scene.add(walls);
// 添加聚光灯照射每幅画
for (let i = 0; i < itemCount; i++) {
const item = galleryItems[i];
const spotLight = new THREE.SpotLight(0xffffff, 0.8, 10, Math.PI / 6, 0.5);
spotLight.position.set(
item.userData.originalPosition.x * 1.5,
item.userData.originalPosition.y + 1.5,
item.userData.originalPosition.z * 1.5
);
spotLight.target.position.copy(item.position);
scene.add(spotLight);
scene.add(spotLight.target);
// 添加灯光辅助(调试用)
// const helper = new THREE.SpotLightHelper(spotLight);
// scene.add(helper);
}
}
// 加载纹理
loadTextures();
// 交互系统
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedItem = null;
// 鼠标移动事件处理
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (selectedItem) {
selectedItem.position.copy(selectedItem.userData.originalPosition);
selectedItem.rotation.copy(selectedItem.userData.originalRotation);
selectedItem.scale.copy(selectedItem.userData.originalScale);
selectedItem = null;
}
for (let i = 0; i < intersects.length; i++) {
const intersected = intersects[i].object;
if (intersected.userData.parentGroup) {
const parentGroup = intersected.userData.parentGroup;
parentGroup.scale.set(1.1, 1.1, 1.1);
const direction = new THREE.Vector3().subVectors(
parentGroup.position,
centerPoint
).normalize();
parentGroup.position.add(direction.multiplyScalar(0.3));
parentGroup.lookAt(centerPoint);
selectedItem = parentGroup;
break;
}
}
}
function onClick() {
if (selectedItem) {
console.log('Selected:', selectedItem.userData);
alert(`Selected: ${selectedItem.userData.title}\n${selectedItem.userData.description}`);
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);
window.addEventListener('mousemove', onMouseMove, false);
// window.addEventListener('click', onClick, false);
function animate() {
requestAnimationFrame(animate);
controls.update();
if (!controls.target.equals(centerPoint)) {
controls.target.copy(centerPoint);
}
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>