first commit
This commit is contained in:
99
default.css
Normal file
99
default.css
Normal 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
56
functions.js
Normal 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
228
index.html
Normal 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
4
jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
626
love.js
Normal file
626
love.js
Normal 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
403
test.html
Normal 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>
|
||||||
Reference in New Issue
Block a user