Inspiration : Under the overwhelming presence of AI, human living space is gradually compressed. But our heart(the essence of humanity) will ultimately returning to and transforming into nature.
A midterm project that combines:
particle motion, class, Particle Systems, Autonomous Agents, to make realistic natural landscape and ml5.js(handpose&body pose)
The last tree
The final structure:
{Code structure}
🧠Issues resolved:
Tree:
Tree growth animation
Adjust the size of the tree and the length of the branches
Let the branches and flowers swing slightly and naturally
The leaves will break away from the swaying trunk, especially the top few
The tree will block the image in the middle of the screen, changing the tree closer to the middle of the screen to be smaller, and the trees closer to the sides of the screen to be larger
Color gradient and thickness gradient of each branch on one tree
heart:
Select a part of the particles to become heart particles
Let the heart particles have natural bumping and a gradient glow effect
Control the heart from going out of the frame
sun:
As the 2000 particles disperse, let the transparency of the heart particles themselves slowly decrease until they disappear. At the same time, the glow effect gradually increases (diffusion), and then increases, and the color also changes from red in the center to orange and yellow at the edge... like a gradually growing sun
The sun's rays can be made more natural, and gradually become transparent at the end of the rays
background:
Png backgroundless images still have a black background in p5js
Let the black background change the color of the background as the particles continue to break away from the image, from black, to black dark blue gradient, to dark blue orange gradient, to orange light blue gradient, to light blue.
cloud:
Make the cloud shape more beautiful
Cloud particles can float up more slowly, and then the particles have a clear process of growing from small to large
The amplitude and speed of cloud particle vibrations are reduced while maintaining its size (radius)
Control the overlap of cloud particles to expand the range of their formation
flower:
Flower growth effect
The flower as a whole swings left and right around the bottom of the stem
petals and leaves:
Petals and leaves have lifespan at the bottom pileup
Leaf veins on the leaves
flocking:
Fix the flocking movement, let them slightly avoid each other and rotate around the heart
Draw an abstract flock of birds instead of triangles
Add a natural effect of "flapping wings" to each bird (triangle), use the noise function (noise()) and the vector rotation updated in each frame to dynamically adjust the distance between the two vertices of the triangle and the fuselage, so as to produce the effect of wings opening and closing when "flapping"
ml5.js:
Fix the left and right movement mirror of ml5.js
Once the position of the hand cannot be detected, the entire image will suddenly appear in the upper left corner, set a loading process and set the default position in the middle of the canvas (but the problem is not solved)
overall:
Unify the color style and initialize the color library for unified management
Painful merging process!!!
The proportion arrangement of each particle (first divided into heaven and earth...)
Code
let human;
let particles = [];
let lostParticles = [];
let heartParticles = [];
let triangleParticles = [];
let cloudParticles = [];
let flowers = [];
let grass = [];
let trees = [];
let borderPadding = 50;
let heartX, heartY;
let cloudCenters = [];
let heartGlowProgress = 0;
let totalParticles = 2000;
let escapedParticles = 0;
let video;
let bodyPose;
let poses = [];
let pose;
let startupDelay = 60; // 启动延迟帧数
let framesSinceStart = 0;
let isStartupComplete = false;
let filteredOffsetX = 0;
let filteredOffsetY = 0;
let filterStrength = 0.2; // 平滑系数
let G = 60;
let TARGET_MASS = 80;
let INNER_RADIUS = 150;
let TANGENT_FACTOR_X = 2;
let TANGENT_FACTOR_Y = 1.0;
let FLAP_MIN = 0.5;
let FLAP_MAX = 1.5;
let heartSound;
let cutSounds = [];
let lastPlayedCutAt = 0;
let cutInterval = 20; // 播放一次音效
let prevEscapedParticles = 0;
let bodyDistance = 0;
let posePosition = { x: 500, y: 300 };
let birdColors = [
[235, 213, 177],
[220, 205, 170],
[220, 160, 140],
[190, 130, 100],
[140, 155, 170],
[170, 170, 170],
];
// sky
let skyColorsTop = [
[0, 0, 0],
[15, 15, 35],
[25, 25, 60],
[50, 40, 80],
[70, 40, 90],
[100, 50, 80],
[180, 120, 100],
[210, 170, 120],
[180, 220, 240],
[200, 230, 250],
];
let skyColorsBottom = [
[5, 5, 20],
[20, 20, 50],
[40, 30, 70],
[80, 50, 90],
[120, 60, 80],
[180, 100, 70],
[220, 180, 100],
[180, 220, 220],
[210, 240, 250],
[215, 226, 198],
];
// 色彩库
const treeLeafColorsRGB = [
[155, 170, 155],
[132, 142, 112],
[163, 179, 164],
[121, 130, 108],
[175, 186, 158],
[213, 170, 142],
[193, 150, 128],
[221, 183, 150],
[176, 120, 94],
[204, 142, 110],
];
const flowerPetalColorsRGB = [
[193, 173, 170],
[220, 200, 167],
[178, 162, 174],
[200, 180, 172],
[183, 160, 158],
[202, 160, 155],
[230, 200, 189],
[210, 180, 172],
[226, 191, 173],
];
const flowerStemColorsRGB = [
[155, 170, 155],
[132, 142, 112],
[163, 179, 164],
[121, 130, 108],
[175, 186, 158],
];
const airPetalColorsRGB = [
[187, 146, 140],
[202, 160, 155],
[230, 200, 189],
[210, 180, 172],
[255, 240, 240],
];
const airLeafColorsRGB = [
[155, 170, 155],
[132, 142, 112],
[163, 179, 164],
[121, 130, 108],
[175, 186, 158],
];
// p5.Color色库
let treeLeafColors = [],
flowerPetalColors = [],
flowerStemColors = [],
airPetalColors = [],
airLeafColors = [];
function preload() {
// load the bodyPose model
bodyPose = ml5.bodyPose();
human = loadImage("img/imgtest3.png");
heartSound = loadSound("sound/heart.mp3");
cutSounds.push(loadSound("sound/cut1.mp3"));
cutSounds.push(loadSound("sound/cut2.mp3"));
cutSounds.push(loadSound("sound/cut3.mp3"));
// cutSounds.push(loadSound('sound/cut4.mp3'));
}
function setup() {
createCanvas(1000, 600);
colorMode(RGB);
video = createCapture(VIDEO);
video.size(640, 480);
video.hide();
// start detecting poses in the webcam video
bodyPose.detectStart(video, gotPoses);
//handPose.detectStart(video, gotHands);
//connections = handPose.getConnections();
// 色库初始化
treeLeafColors = [];
for (let i = 0; i < treeLeafColorsRGB.length; i++) {
treeLeafColors.push(
color(
treeLeafColorsRGB[i][0],
treeLeafColorsRGB[i][1],
treeLeafColorsRGB[i][2]
)
);
}
flowerPetalColors = [];
for (let i = 0; i < flowerPetalColorsRGB.length; i++) {
flowerPetalColors.push(
color(
flowerPetalColorsRGB[i][0],
flowerPetalColorsRGB[i][1],
flowerPetalColorsRGB[i][2]
)
);
}
flowerStemColors = [];
for (let i = 0; i < flowerStemColorsRGB.length; i++) {
flowerStemColors.push(
color(
flowerStemColorsRGB[i][0],
flowerStemColorsRGB[i][1],
flowerStemColorsRGB[i][2]
)
);
}
airPetalColors = [];
for (let i = 0; i < airPetalColorsRGB.length; i++) {
airPetalColors.push(
color(
airPetalColorsRGB[i][0],
airPetalColorsRGB[i][1],
airPetalColorsRGB[i][2]
)
);
}
airLeafColors = [];
for (let i = 0; i < airLeafColorsRGB.length; i++) {
airLeafColors.push(
color(
airLeafColorsRGB[i][0],
airLeafColorsRGB[i][1],
airLeafColorsRGB[i][2]
)
);
}
let scaleFactor = (height * 0.6) / human.height;
human.resize(human.width * scaleFactor, human.height * scaleFactor);
// 心脏位置
heartX = human.width * 0.6;
heartY = human.height * 0.24;
let heartRadius = human.width * 0.06;
// 云朵中心点
for (let i = 0; i < 10; i++) {
cloudCenters.push({
x: random(width * 0.1, width * 0.9),
y: random(height * 0.03, height * 0.15),
radius: random(250, 350), // 云朵半径
});
}
// 初始化粒子
human.loadPixels();
let validPixels = [];
for (let y = 0; y < human.height; y++) {
for (let x = 0; x < human.width; x++) {
let idx = (y * human.width + x) * 4;
let a = human.pixels[idx + 3];
if (a >= 20) {
// 透明度足够的像素
validPixels.push({ x: x, y: y });
}
}
}
// if有效像素太少
if (validPixels.length < totalParticles * 0.5) {
validPixels = [];
for (let y = 0; y < human.height; y++) {
for (let x = 0; x < human.width; x++) {
let idx = (y * human.width + x) * 4;
let a = human.pixels[idx + 3];
if (a >= 5) {
// 降低透明度阈值
validPixels.push({ x: x, y: y });
}
}
}
}
let actualTotalParticles = min(totalParticles, validPixels.length);
// 更新总粒子数
totalParticles = actualTotalParticles;
// 像素生成粒子
validPixels = shuffle(validPixels);
for (let i = 0; i < actualTotalParticles; i++) {
if (i >= validPixels.length) break;
let pixelPos = validPixels[i];
let x = pixelPos.x;
let y = pixelPos.y;
let col = human.get(x, y);
let br = brightness(col);
let h = hue(col);
let distToHeart = dist(x, y, heartX, heartY);
let particle = {
originalX: x,
originalY: y,
x: x,
y: y,
color: col,
brightness: br,
hue: h,
angle: map(h, 0, 255, 0, 90),
size: map(br, 0, 255, 5, 40),
isLost: false,
velocity: createVector(0, 0),
acceleration: createVector(0, 0),
};
if (distToHeart < heartRadius) {
heartParticles.push(particle);
} else {
particles.push(particle);
}
}
heartSound.setVolume(2.0);
heartSound.rate(0.7);
heartSound.loop();
// 音效的音量
for (let sound of cutSounds) {
sound.setVolume(0.1);
}
background(0);
}
function draw() {
if (!isStartupComplete) {
framesSinceStart++;
if (framesSinceStart >= startupDelay) {
isStartupComplete = true;
}
}
let totalPossibleEscaped = particles.length + escapedParticles;
let progressRatio = min(1.0, escapedParticles / (totalParticles * 0.8)); // 80%粒子逃脱
// 更新背景音乐音量
let heartVolume = 1.0 - progressRatio;
heartSound.setVolume(heartVolume);
// 检查是否需要播放剪切音效
if (escapedParticles - prevEscapedParticles >= cutInterval) {
// 播放随机一个剪切音效
let randomCutIndex = floor(random(cutSounds.length));
cutSounds[randomCutIndex].play();
// 更新上次播放的计数
prevEscapedParticles = escapedParticles;
}
// if (frameCount % 60 === 0) {
// console.log("逃逸粒子: " + escapedParticles);
// console.log("总粒子: " + totalPossibleEscaped);
// console.log("进度比率: " + progressRatio);
// console.log("手部闭合度: " + bodyDistance);
// }
let colorIndex = progressRatio * (skyColorsTop.length - 1);
let lowerIndex = floor(colorIndex);
let upperIndex = ceil(colorIndex);
let mixRatio = colorIndex - lowerIndex;
// 清除背景
clear();
// 绘制垂直渐变背景
noStroke();
for (let y = 0; y < height; y++) {
// 计算当前高度的插值位置
let yRatio = y / height;
// 获取顶部颜色
let topR = lerp(
skyColorsTop[lowerIndex][0],
skyColorsTop[upperIndex][0],
mixRatio
);
let topG = lerp(
skyColorsTop[lowerIndex][1],
skyColorsTop[upperIndex][1],
mixRatio
);
let topB = lerp(
skyColorsTop[lowerIndex][2],
skyColorsTop[upperIndex][2],
mixRatio
);
// 获取底部颜色
let bottomR = lerp(
skyColorsBottom[lowerIndex][0],
skyColorsBottom[upperIndex][0],
mixRatio
);
let bottomG = lerp(
skyColorsBottom[lowerIndex][1],
skyColorsBottom[upperIndex][1],
mixRatio
);
let bottomB = lerp(
skyColorsBottom[lowerIndex][2],
skyColorsBottom[upperIndex][2],
mixRatio
);
// 在顶部和底部颜色之间进行插值
let r = lerp(topR, bottomR, yRatio);
let g = lerp(topG, bottomG, yRatio);
let b = lerp(topB, bottomB, yRatio);
// 绘制一条线
stroke(r, g, b);
line(0, y, width, y);
}
// 图像居中
let imageX = (width - human.width) / 2;
let imageY = (height - human.height) / 2;
// 计算边框收缩程度
let borderShrinkFactor = map(bodyDistance, 0, 1, 1, 0.005);
let currentBorderPadding = borderPadding * borderShrinkFactor;
// 计算当前边框边界
let boundaryLeft = imageX - currentBorderPadding;
let boundaryRight = imageX + human.width + currentBorderPadding;
let boundaryTop = imageY - currentBorderPadding;
let boundaryBottom = imageY + human.height + currentBorderPadding;
// 边框
noFill();
stroke(255);
rect(
boundaryLeft,
boundaryTop,
human.width + currentBorderPadding * 2,
human.height + currentBorderPadding * 2
);
let targetOffsetX = 0;
let targetOffsetY = 0;
if (pose && isStartupComplete) {
// 将位置映射到适当的偏移量范围
targetOffsetX = map(posePosition.x, 0, video.width, -width / 4, width / 4);
targetOffsetY = map(
posePosition.y,
0,
video.height,
-height / 4,
height / 4
);
} else if (!isStartupComplete) {
// 在启动延迟期间使用0偏移,保持图像居中
targetOffsetX = 0;
targetOffsetY = 0;
} else {
// 如果没有检测到手,则回退到使用鼠标(但仍应用平滑)
targetOffsetX = (mouseX - width / 2) * 0.995;
targetOffsetY = (mouseY - height / 2) * 0.995;
}
// 平滑偏移量
filteredOffsetX = lerp(filteredOffsetX, targetOffsetX, filterStrength);
filteredOffsetY = lerp(filteredOffsetY, targetOffsetY, filterStrength);
// 最终的有效偏移量
let offsetX = filteredOffsetX;
let offsetY = filteredOffsetY;
// 心脏中心
let heartCenterX = imageX + heartX + offsetX;
let heartCenterY = imageY + heartY + offsetY;
// 心脏粒子
let heartPulse = sin(frameCount * 0.05) * 0.2 + 1; // 脉动因子
let glowIntensity = (sin(frameCount * 0.08) + 1) * 0.5; // 发光强度
heartGlowProgress = min(1.0, escapedParticles / (totalParticles * 0.9)); // 90%粒子逃脱时达到最大效果
for (let p of heartParticles) {
let newX = imageX + p.originalX + offsetX;
let newY = imageY + p.originalY + offsetY;
// 应用脉动效果 - 从心脏中心向外扩散
let distFromHeartCenter = dist(p.originalX, p.originalY, heartX, heartY);
let pulseDelay = distFromHeartCenter * 0.01;
let localPulse = sin(frameCount * 0.1 - pulseDelay) * 0.15 + 1;
// 计算脉动后的新位置
let heartCX = imageX + heartX + offsetX;
let heartCY = imageY + heartY + offsetY;
let vecX = newX - heartCX;
let vecY = newY - heartCY;
newX = heartCX + vecX * localPulse;
newY = heartCY + vecY * localPulse;
newX = constrain(newX, boundaryLeft, boundaryRight);
newY = constrain(newY, boundaryTop, boundaryBottom);
// 绘制发光效果
noStroke();
let expandedGlowSize = (p.size * 2 * (1 + heartGlowProgress * 5)) / 1.8; // 随进度放大
let enhancedGlowIntensity = glowIntensity + heartGlowProgress * 0.8; // 随进度增强
//使用一个估计值代替依赖全局变量
let maxHeartRadius = human.width * 0.8;
// 基于距离心脏中心的距离创建从中心到边缘的渐变
let normalizedDist = constrain(distFromHeartCenter / maxHeartRadius, 0, 1);
// 从红色到橙色到黄色的渐变
let innerColor = color(255, 50, 50); // 红色(中心)
let midColor = color(255, 150, 50); // 橙色
let outerColor = color(255, 230, 100); // 黄色
let glowColor;
if (normalizedDist < 0.5) {
// 从内到中的渐变
glowColor = lerpColor(innerColor, midColor, normalizedDist * 2);
} else {
// 从中到外的渐变
glowColor = lerpColor(midColor, outerColor, (normalizedDist - 0.5) * 2);
}
// 调整透明度
glowColor.setAlpha(50 * enhancedGlowIntensity);
fill(glowColor);
ellipse(newX, newY, expandedGlowSize, expandedGlowSize);
// 绘制主体粒子
let particleOpacity = 255 * (1 - heartGlowProgress * 0.9); // 随进度降低不透明度
fill(red(p.color), green(p.color), blue(p.color), particleOpacity);
push();
translate(newX, newY);
rotate(radians(p.angle));
rect(0, 0, p.size * localPulse, 2);
pop();
}
// 当心脏粒子几乎透明时,添加额外的光晕效果
if (heartGlowProgress > 0.5) {
let sunProgress = map(heartGlowProgress, 0.5, 1, 0, 1);
// 减小太阳大小到原来的1/3
let sunSize = (100 + sunProgress * 150) / 1.5;
// 减小光线长度到原来的1/3
let rayLength = (50 + sunProgress * 200) / 1.5;
// 确保太阳和光线在边界内
let adjustedHeartCenterX = constrain(
heartCenterX,
boundaryLeft,
boundaryRight
);
let adjustedHeartCenterY = constrain(
heartCenterY,
boundaryTop,
boundaryBottom
);
push();
translate(adjustedHeartCenterX, adjustedHeartCenterY);
// 绘制太阳中心光晕
for (let r = 0; r < 5; r++) {
let radius = sunSize * (0.2 + r * 0.2);
let alpha = 120 * (1 - r * 0.2) * sunProgress;
noStroke();
fill(255, 230, 100, alpha);
ellipse(0, 0, radius, radius);
}
for (let a = 0; a < TWO_PI; a += PI / 12) {
// 增加光线数量
let rayX = cos(a) * rayLength;
let rayY = sin(a) * rayLength;
// 使用渐变描边来创建更自然的光线,从中心向外渐渐透明
push();
rotate(a);
noFill();
for (let i = 0; i < 3; i++) {
// 每条光线绘制3层
let alphaBase = 90 * sunProgress;
let thickness = (3 - i) * sunProgress;
// 创建从中心到边缘的渐变
let segments = 10;
for (let j = 0; j < segments; j++) {
let start = (j / segments) * rayLength;
let end = ((j + 1) / segments) * rayLength;
let alphaStart = alphaBase * (1 - j / segments);
let alphaEnd = alphaBase * (1 - (j + 1) / segments);
stroke(255, 230, 100, alphaStart);
strokeWeight(thickness);
line(start, 0, end, 0);
}
}
pop();
}
// 添加一些散射小粒子增强太阳光效果
for (let i = 0; i < 20 * sunProgress; i++) {
let angle = random(TWO_PI);
let dist = random(sunSize * 0.5, rayLength * 0.8);
let particleSize = random(1, 4) * sunProgress;
let alpha = random(30, 80) * sunProgress * (1 - dist / rayLength);
fill(255, 230, 100, alpha);
noStroke();
ellipse(cos(angle) * dist, sin(angle) * dist, particleSize, particleSize);
}
pop();
}
// 正常粒子
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
if (p.isLost) continue;
let newX = imageX + p.originalX + offsetX;
let newY = imageY + p.originalY + offsetY;
if (
newX < boundaryLeft ||
newX > boundaryRight ||
newY < boundaryTop ||
newY > boundaryBottom
) {
p.isLost = true;
escapedParticles++;
let fate = random(1);
if (fate < 0.15) {
// 地面
let splashVelocity = createVector(random(-8, 8), random(2, 5));
lostParticles.push({
x: newX,
y: newY,
color: p.color,
velocity: splashVelocity,
acceleration: createVector(0, 0.2),
type: "ground",
plantType: random(1),
landed: false,
landPosition: { x: 0, y: 0 },
});
} else if (fate < 0.16) {
// 三角形(鸟)
lostParticles.push({
x: newX,
y: newY,
color: p.color,
velocity: createVector(random(-1, 1), random(-2, -1)),
acceleration: createVector(0, -0.01),
type: "toTriangle",
transitionProgress: 0,
size: p.size,
readyForOrbit: false,
});
} else if (fate < 0.25) {
// 花瓣
let petalColor = airPetalColors[int(random(airPetalColors.length))];
lostParticles.push({
x: newX,
y: newY,
color: petalColor,
//花瓣的速度和加速度
velocity: createVector(random(-0.5, 0.5), random(-0.3, 0.1)), // 水平速度范围
acceleration: createVector(0, random(0.005, 0.02)), // 加速度差异
type: "petal",
rotation: random(TWO_PI),
rotationSpeed: random(-0.01, 0.01),
size: random(7, 15),
});
} else if (fate < 0.3) {
// 树叶
let leafColor = airLeafColors[int(random(airLeafColors.length))];
lostParticles.push({
x: newX,
y: newY,
color: leafColor,
// 树叶的参数
velocity: createVector(random(-0.2, 0.2), random(-0.05, 0.02)), // 初始速度
acceleration: createVector(0, 0.004), // 加速度
rotationSpeed: random(-0.01, 0.01), // 旋转速度
swayFactor: random(0.05, 0.15), // 摇摆幅度
type: "leaf",
rotation: random(TWO_PI),
size: random(10, 18),
});
} else {
// 云朵
let closestCloud = cloudCenters[0];
let minDist = dist(newX, newY, closestCloud.x, closestCloud.y);
for (let j = 1; j < cloudCenters.length; j++) {
let d = dist(newX, newY, cloudCenters[j].x, cloudCenters[j].y);
if (d < minDist) {
minDist = d;
closestCloud = cloudCenters[j];
}
}
cloudParticles.push({
x: newX,
y: newY,
targetX: closestCloud.x + random(-120, 120),
targetY: closestCloud.y + random(-30, 30),
color: color(255, 255, 255),
opacity: random(120, 200),
size: random(6, 14),
speed: random(0.01, 0.02),
cloudIndex: cloudCenters.indexOf(closestCloud),
});
}
particles.splice(i, 1);
continue;
}
noStroke();
fill(red(p.color), green(p.color), blue(p.color), 255);
push();
translate(newX, newY);
rotate(radians(p.angle));
rect(0, 0, p.size, 2);
pop();
}
// 失去的粒子
updateLostParticles(lostParticles);
// 三角形粒子
updateTriangleParticles(triangleParticles, heartCenterX, heartCenterY);
// 云朵
updateCloudParticles();
// 植物
updatePlants();
if (pose && false) {
// 设置为true启用调试显示
push();
// 绘制手部连接
for (let i = 0; i < hands.length; i++) {
let hand = hands[i];
for (let j = 0; j < connections.length; j++) {
let pointAIndex = connections[j][0];
let pointBIndex = connections[j][1];
let pointA = hand.keypoints[pointAIndex];
let pointB = hand.keypoints[pointBIndex];
stroke(255, 0, 0, 150);
strokeWeight(2);
line(pointA.x, pointA.y, pointB.x, pointB.y);
}
}
// 绘制关键点
for (let i = 0; i < hands.length; i++) {
let hand = hands[i];
for (let j = 0; j < hand.keypoints.length; j++) {
let keypoint = hand.keypoints[j];
fill(0, 255, 0, 150);
noStroke();
circle(keypoint.x, keypoint.y, 10);
// 高亮显示拇指尖和食指尖
if (j === 4 || j === 8) {
fill(255, 0, 0, 200);
circle(keypoint.x, keypoint.y, 15);
}
}
}
pop();
}
fill(255);
textSize(18);
textAlign(CENTER);
if (!isStartupComplete) {
text(
"初始化中... " + floor((framesSinceStart / startupDelay) * 100) + "%",
width / 2,
30
);
} else if (pose) {
// text("请进入视野范围内", width / 2, 30);
} else {
text("请进入视野范围内", width / 2, 30);
}
}
// 递归生成树结构
function makeTreeStructure(
length,
size,
maxLevel,
level = 0,
growthFactor = 1
) {
let node = {
length: length * (level === 0 ? 1 : growthFactor * 0.6), // 主干原长度,分支增长
size: size,
angle: random(-PI / 6, PI / 6),
children: [],
leaves: [],
};
if (level < maxLevel) {
let branchCount = floor(random(2, 4));
for (let i = 0; i < branchCount; i++) {
let branchLength = length * random(0.7, 0.9); // 长度保留因子
let branchSize = size * random(0.65, 0.85);
let branchAngle = random(-PI / 3, PI / 3);
let child = makeTreeStructure(
branchLength,
branchSize,
maxLevel,
level + 1,
growthFactor
);
child.angle = branchAngle;
node.children.push(child);
}
}
if (level >= 2 && level >= maxLevel - 1) {
let leafCount = floor(random(1, 3));
for (let i = 0; i < leafCount; i++) {
node.leaves.push({
angle: random(-PI / 2, PI / 2),
distance: random(length * 0.5, length),
size: random(8, 14),
});
}
}
return node;
}
// 递归绘制树结构
function drawTreeStructure(
node,
level,
maxLevel,
growthStage,
leafColor,
treeX
) {
if (level > growthStage) return;
push();
// 添加树枝摆动
let swayAmount = 0;
if (level > 0) {
// 只有分支会摆动,主干保持稳定
// 摆动幅度随分支级别增加,越高摆动越明显
let swayFactor = map(level, 1, maxLevel, 0.02, 0.06);
swayAmount =
sin(frameCount * 0.02 + (treeX || 0) * 0.01 + level * 0.5) * swayFactor;
}
// 应用摆动
rotate(node.angle + swayAmount);
stroke(100, 70, 40, 200);
strokeWeight(node.size);
// 添加生长动画
let growthProgress = min(1, (growthStage - level) * 2);
let lengthScale = level === 0 ? 2 : 0.9;
let currentLength = node.length * growthProgress * lengthScale;
let segments = 8; // 分段数量
let segmentLength = currentLength / segments;
for (let i = 0; i < segments; i++) {
// 计算当前段的位置比例
let ratio = i / segments;
// 深棕色到浅棕色的渐变
let darkBrown = color(70, 40, 20);
let lightBrown = color(140, 100, 60);
let segmentColor = lerpColor(darkBrown, lightBrown, ratio);
stroke(segmentColor);
strokeWeight(node.size * (1 - ratio * 0.3)); // 树干粗细稍微变化
// 绘制当前段
line(0, -segmentLength * i, 0, -segmentLength * (i + 1));
}
translate(0, -currentLength);
if (growthProgress >= 0.9) {
// 只在分支长度达到90%时绘制子分支和叶子
// 绘制子分支
for (let child of node.children) {
push();
drawTreeStructure(
child,
level + 1,
maxLevel,
growthStage,
leafColor,
treeX
);
pop();
}
// 绘制叶子
if (node.leaves && node.leaves.length > 0) {
for (let leaf of node.leaves) {
push();
rotate(leaf.angle);
// 减小叶子距离以确保连接到树干
let leafDistance = leaf.distance * 0.5;
translate(0, -leafDistance);
noStroke();
fill(red(leafColor), green(leafColor), blue(leafColor), 180);
let leafSize = node.size * leaf.size;
// 绘制叶子
beginShape();
vertex(0, 0); // 从树干连接点开始
bezierVertex(
leafSize / 3,
-leafSize / 3,
leafSize / 2,
0,
leafSize / 3,
leafSize / 3
);
bezierVertex(
0,
leafSize / 2,
-leafSize / 3,
leafSize / 3,
-leafSize / 2,
0
);
bezierVertex(-leafSize / 3, -leafSize / 3, 0, -leafSize / 2, 0, 0);
endShape(CLOSE);
// 叶脉
stroke(red(leafColor), green(leafColor), blue(leafColor), 100);
strokeWeight(0.5);
line(0, 0, 0, -leafSize / 3);
line(0, 0, leafSize / 3, 0);
line(0, 0, -leafSize / 3, 0);
pop();
}
}
}
pop();
}
function updateLostParticles(particles) {
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.velocity.add(p.acceleration);
p.x += p.velocity.x;
p.y += p.velocity.y;
if (p.type === "ground") {
if (p.y > height - 20 && !p.landed) {
p.landed = true;
p.landPosition = { x: p.x, y: height - 20 };
p.velocity.set(0, 0);
p.acceleration.set(0, 0);
if (p.plantType < 0.05) {
// 花
flowers.push({
x: p.landPosition.x,
y: p.landPosition.y,
color: flowerPetalColors[int(random(flowerPetalColors.length))],
growthStage: 0,
maxGrowth: random(20, 40),
growthSpeed: random(0.2, 0.5),
petalCount: floor(random(5, 10)),
swayFactor: random(0.04, 0.06),
stemColor: flowerStemColors[int(random(flowerStemColors.length))],
});
} else if (p.plantType < 0.1) {
// 草
grass.push({
x: p.landPosition.x,
y: p.landPosition.y,
color: treeLeafColors[int(random(treeLeafColors.length))],
growthStage: 0,
maxGrowth: random(20, 40), // 最大高度
growthSpeed: random(0.3, 0.8),
bladeCount: floor(random(5, 12)), // 草叶数量
swayFactor: random(0.02, 0.08),
width: random(12, 25), // 宽度属性
});
} // 树的生成
else if (p.plantType < 0.155) {
// 树
// 计算距离屏幕中心的距离比例
let distFromCenter = abs(p.landPosition.x - width / 2) / (width / 2);
// 根据距离调整总体树高和分支因素
let maxLevel = floor(random(3, 5));
let startLength = map(distFromCenter, 0, 1, 40, 80); // 初始树干长度
let startSize = map(distFromCenter, 0, 1, 2, 5); // 树干粗细
// 生长系数以获得更高的树
let growthFactor = map(distFromCenter, 0, 1, 1.9, 2.5);
let leafColor = treeLeafColors[int(random(treeLeafColors.length))];
trees.push({
x: p.landPosition.x,
y: p.landPosition.y,
growthStage: 0,
maxLevel: maxLevel,
leafColor: leafColor,
growthFactor: growthFactor, // 保存生长系数
structure: makeTreeStructure(
startLength,
startSize,
maxLevel,
0,
growthFactor
), // 添加生长系数参数
distFromCenter: distFromCenter,
});
}
particles.splice(i, 1);
continue;
}
noStroke();
fill(red(p.color), green(p.color), blue(p.color));
circle(p.x, p.y, 3);
} else if (p.type === "toTriangle") {
p.transitionProgress = min(p.transitionProgress + 0.01, 1);
if (p.transitionProgress >= 1 && p.y < height * 0.4 && !p.readyForOrbit) {
p.readyForOrbit = true;
// 随机选择一对渐变色
let colorIndex = floor(random(0, 3)) * 2; // 0, 2, 或 4
let startColor = birdColors[colorIndex];
let endColor = birdColors[colorIndex + 1];
triangleParticles.push({
pos: createVector(p.x, p.y),
vel: createVector(random(-1, 1), random(-1, 1)),
acc: createVector(0, 0),
color: p.color, // 保留原始颜色用于参考
startColor: color(startColor[0], startColor[1], startColor[2]),
endColor: color(endColor[0], endColor[1], endColor[2]),
colorOffset: random(1), // 颜色偏移量,使每只鸟渐变位置不同
size: map(brightness(p.color), 0, 255, 5, 15),
angle: 0,
mass: 1,
maxSpeed: random(1, 3),
maxSteerForce: 0.08,
separateDistance: 40,
wingFlapSpeed: random(0.03, 0.08), // 翅膀扇动速度
lastUpdateTime: frameCount,
timeOffset: random(1000), // 时间偏移,使每只鸟翅膀扇动不同步
});
particles.splice(i, 1);
continue;
}
noStroke();
fill(red(p.color), green(p.color), blue(p.color));
push();
translate(p.x, p.y);
if (p.transitionProgress < 1) {
let rectAlpha = map(p.transitionProgress, 0, 1, 1, 0);
fill(red(p.color), green(p.color), blue(p.color), 255 * rectAlpha);
rotate(radians(p.angle));
rect(0, 0, p.size * (1 - p.transitionProgress), 2);
let triAlpha = map(p.transitionProgress, 0, 1, 0, 1);
fill(red(p.color), green(p.color), blue(p.color), 255 * triAlpha);
let triSize = map(p.transitionProgress, 0, 1, 2, 6);
triangle(0, 0, -triSize, -triSize * 0.4, -triSize, triSize * 0.4);
} else {
rotate(frameCount * 0.02);
triangle(0, 0, -6, -2.4, -6, 2.4);
}
pop();
} else if (p.type === "petal") {
p.rotation += p.rotationSpeed;
p.velocity.x += sin(frameCount * 0.05) * 0.05;
// 当花瓣接近底部时,有一定概率停留(堆积效果)
if (p.y > height - 30 && !p.settled && random() < 0.3) {
p.settled = true;
p.y = height - random(5, 15); // 稍微上移,形成堆叠感
p.velocity.set(0, 0);
p.acceleration.set(0, 0);
// 轻微偏移,避免完全重叠
p.x += random(-5, 5);
// 降低不透明度,使其稍微融入背景
p.opacity = random(150, 200);
}
noStroke();
fill(red(p.color), green(p.color), blue(p.color), p.opacity || 255);
push();
translate(p.x, p.y);
rotate(p.rotation);
beginShape();
for (let a = 0; a < TWO_PI; a += 0.1) {
let xoff = cos(a) * p.size;
let yoff = sin(a) * p.size * 0.6;
let r = p.size * (1 + 0.3 * sin(a * 2));
vertex((xoff * r) / p.size, (yoff * r) / p.size);
}
endShape(CLOSE);
pop();
// 移除条件:已经超出屏幕且未堆积,或者生命周期结束
if (
(p.y > height && !p.settled) ||
(p.settled && p.life && p.life <= 0)
) {
particles.splice(i, 1);
}
// 如果已堆积,就减少生命值
if (p.settled) {
if (!p.life) p.life = random(300, 600); // 设置生命周期
p.life--;
}
} else if (p.type === "leaf") {
p.rotation += p.rotationSpeed;
p.velocity.x += sin(frameCount * 0.03) * p.swayFactor;
if (p.y > height - 25 && !p.settled && random() < 0.25) {
p.settled = true;
p.y = height - random(5, 20);
p.velocity.set(0, 0);
p.acceleration.set(0, 0);
p.x += random(-8, 8);
p.opacity = random(130, 180);
}
noStroke();
fill(red(p.color), green(p.color), blue(p.color), p.opacity || 255);
push();
translate(p.x, p.y);
rotate(p.rotation);
beginShape();
vertex(0, -p.size / 2);
bezierVertex(
p.size / 2,
-p.size / 4,
p.size / 2,
p.size / 4,
0,
p.size / 2
);
bezierVertex(
-p.size / 2,
p.size / 4,
-p.size / 2,
-p.size / 4,
0,
-p.size / 2
);
endShape(CLOSE);
stroke(red(p.color) * 0.8, green(p.color) * 0.8, blue(p.color) * 0.8);
strokeWeight(0.5);
line(0, -p.size / 2, 0, p.size / 2);
line(0, 0, p.size / 3, 0);
line(0, 0, -p.size / 3, 0);
}
pop();
// 移除条件:超出屏幕且未堆积,或者生命周期结束
if ((p.y > height && !p.settled) || (p.settled && p.life && p.life <= 0)) {
particles.splice(i, 1);
}
// 如果已堆积,就减少生命值
if (p.settled) {
if (!p.life) p.life = random(400, 800);
p.life--;
}
}
}
function updateTriangleParticles(triangles, centerX, centerY) {
for (let i = triangles.length - 1; i >= 0; i--) {
let t = triangles[i];
// 应用分离力 - 避开其他三角形
let sepForce = separate(t, triangles).mult(1.5);
applyForce(t, sepForce);
// 应用轨道运动
applyOrbit(t, createVector(centerX, centerY));
// 更新物理
t.vel.add(t.acc);
t.vel.limit(t.maxSpeed);
t.pos.add(t.vel);
t.acc.mult(0);
// 更新朝向
let targetAngle = t.vel.heading();
t.angle = lerp(t.angle, targetAngle, 0.1);
// 计算鸟的颜色 - 使用正确的渐变色
let progress = (sin(frameCount * 0.01 + t.colorOffset * TWO_PI) + 1) * 0.5;
let birdColor = lerpColor(t.startColor, t.endColor, progress);
// 绘制带翅膀扇动效果的三角形
push();
translate(t.pos.x, t.pos.y);
rotate(t.angle);
noStroke();
fill(birdColor); // 使用渐变颜色
// 翅膀扇动效果
let wingSize = t.size * 2.0; // 保持原尺寸比例
let flap1 = map(
noise((frameCount + t.timeOffset) * t.wingFlapSpeed, t.pos.x / 100),
0,
1,
wingSize * FLAP_MIN,
wingSize * FLAP_MAX
);
let flap2 = map(
noise((frameCount + t.timeOffset + 500) * t.wingFlapSpeed, t.pos.y / 100),
0,
1,
wingSize * FLAP_MIN,
wingSize * FLAP_MAX
);
// 用 flap1、flap2 去画
beginShape();
vertex(0, 0);
vertex(-wingSize, -flap1);
vertex(-wingSize * 0.6, 0);
vertex(-wingSize, flap2);
endShape(CLOSE);
pop();
}
}
// 轨道运动函数 - 引力+切向力
function applyOrbit(boid, target) {
let r = p5.Vector.sub(target, boid.pos);
let d = r.mag();
let dir = r.copy().normalize();
// 排斥 - 当距离过近时
if (d < INNER_RADIUS) {
let repel = dir.copy().mult(-boid.maxSteerForce * 3);
applyForce(boid, repel);
return;
}
// 引力
let strength = (G * boid.mass * TARGET_MASS) / (d * d);
strength = constrain(strength, 0, boid.maxSteerForce);
applyForce(boid, dir.copy().mult(strength));
// 椭圆切向力
let rawTangent = createVector(-dir.y, dir.x);
rawTangent.x *= TANGENT_FACTOR_X;
rawTangent.y *= TANGENT_FACTOR_Y;
let tangent = rawTangent.normalize().mult(strength * 1.2);
applyForce(boid, tangent);
}
function updateCloudParticles() {
for (let i = 0; i < cloudCenters.length; i++) {
cloudCenters[i].x += sin(frameCount * 0.001 + i) * 0.1;
cloudCenters[i].y += cos(frameCount * 0.0008 + i) * 0.05;
cloudCenters[i].x -= 2;
if (cloudCenters[i].x < -500) {
cloudCenters[i].x = width;
for (let p of cloudParticles) {
if (p.cloudIndex == i) {
p.x += width + 200;
}
}
}
}
for (let p of cloudParticles) {
let cloud = cloudCenters[p.cloudIndex];
// 如果没有初始化targetOffset,初始化它们以分散粒子
if (!p.targetOffset) {
p.targetOffset = {
x: random(-cloud.radius * 0.1, cloud.radius * 0.1),
y: random(-cloud.radius * 0.08, cloud.radius * 0.04), // 垂直分布
};
}
// 漂移幅度
let driftX = sin(frameCount * 0.003 + p.x * 0.01) * 8;
let driftY = cos(frameCount * 0.002 + p.y * 0.01) * 4;
// 目标位置
p.targetX = cloud.x + driftX + p.targetOffset.x;
p.targetY = cloud.y + driftY + p.targetOffset.y;
// 移动速度
p.x += (p.targetX - p.x) * (p.speed * 0.3);
p.y += (p.targetY - p.y) * (p.speed * 0.3);
noStroke();
fill(red(p.color), green(p.color), blue(p.color), p.opacity);
ellipse(p.x, p.y, p.size * random(1.8, 2.3), p.size * random(1.6, 2.1)); // 更大的粒子
}
}
function updatePlants() {
// —— 草(保持不变) ——
for (let blade of grass) {
blade.growthStage = min(
blade.growthStage + blade.growthSpeed,
blade.maxGrowth
);
push();
translate(blade.x, blade.y);
for (let i = 0; i < blade.bladeCount; i++) {
let offset = map(i, 0, blade.bladeCount - 1, -blade.width, blade.width);
push();
translate(offset, 0);
let swayAngle =
sin(frameCount * 0.05 + i + blade.x * 0.01) * blade.swayFactor;
rotate(swayAngle - HALF_PI);
let grassHeight =
blade.growthStage * (0.7 + 0.6 * noise(i * 0.5, blade.x * 0.02));
stroke(red(blade.color), green(blade.color), blue(blade.color));
strokeWeight(map(i, 0, blade.bladeCount, 0.8, 2));
noFill();
beginShape();
let cpOffset = random(-5, 5);
vertex(0, 0);
bezierVertex(
cpOffset,
-grassHeight / 3,
cpOffset * 1.5,
(-grassHeight * 2) / 3,
0,
-grassHeight
);
endShape();
if (random() > 0.7) {
stroke(
red(blade.color) * 1.3,
green(blade.color) * 1.3,
blue(blade.color) * 1.3,
100
);
strokeWeight(0.5);
line(0, -grassHeight * 0.3, 0, -grassHeight * 0.7);
}
pop();
}
pop();
}
// —— 树(原来最后,现在提前) ——
for (let tree of trees) {
tree.growthStage = min(tree.growthStage + 0.01, tree.maxLevel);
push();
translate(tree.x, tree.y);
drawTreeStructure(
tree.structure,
0,
tree.maxLevel,
tree.growthStage,
tree.leafColor,
tree.x
);
pop();
}
// —— 花(挪到这里,保证在树之上) ——
for (let flower of flowers) {
flower.growthStage = min(
flower.growthStage + flower.growthSpeed * 0.5,
flower.maxGrowth * 1.5
);
push();
translate(flower.x, flower.y);
stroke(flower.stemColor);
strokeWeight(2);
let stemHeight = flower.growthStage * 1.5;
let stemSwayX =
sin(frameCount * 0.02 + flower.x * 0.01) * flower.swayFactor * stemHeight;
line(0, 0, stemSwayX, -stemHeight);
if (flower.growthStage > flower.maxGrowth * 0.7) {
let bloomSize = map(
flower.growthStage,
flower.maxGrowth * 0.7,
flower.maxGrowth * 1.5,
0,
16
);
translate(stemSwayX, -stemHeight);
noStroke();
fill(255, 255, 0);
ellipse(0, 0, bloomSize);
fill(flower.color);
for (let i = 0; i < flower.petalCount; i++) {
push();
rotate((TWO_PI * i) / flower.petalCount);
ellipse(bloomSize * 0.7, 0, bloomSize * 1.2, bloomSize * 0.8);
pop();
}
}
pop();
}
}
// Flocking behavior functions鸟群运动
function seek(boid, target) {
let desired = p5.Vector.sub(target, boid.pos);
let distance = desired.mag();
desired.normalize();
if (distance < 50) {
let speed = map(distance, 0, 50, 0, boid.maxSpeed);
desired.mult(speed);
} else {
desired.mult(boid.maxSpeed);
}
let steer = p5.Vector.sub(desired, boid.vel);
steer.limit(boid.maxSteerForce);
return steer;
}
function separate(boid, boids) {
let sum = createVector();
let count = 0;
for (let other of boids) {
let d = p5.Vector.dist(boid.pos, other.pos);
if (other !== boid && d < boid.separateDistance) {
let diff = p5.Vector.sub(boid.pos, other.pos).normalize().div(d);
sum.add(diff);
count++;
}
}
if (count > 0) {
sum.div(count).setMag(boid.maxSpeed);
let steer = p5.Vector.sub(sum, boid.vel);
steer.limit(boid.maxSteerForce);
return steer;
}
return createVector(0, 0);
}
function applyForce(boid, force) {
let f = p5.Vector.div(force, boid.mass);
boid.acc.add(f);
}
function keyPressed() {
if (key == "f") {
let fs = fullscreen();
fullscreen(!fs);
}
}
// callback function for when bodyPose outputs data
function gotPoses(results) {
// save the output to the poses variable
poses = results;
// if there are poses, get the first pose!
if (poses.length > 0) {
pose = poses[0];
let midPos = createVector(0,0);
let shoulderL = createVector(pose.left_shoulder.x, pose.left_shoulder.y);
let shoulderR = createVector(pose.right_shoulder.x, pose.right_shoulder.y);
midPos.add(shoulderL);
midPos.add(shoulderR);
midPos.div(2);
posePosition = {
x: video.width - (midPos.x + midPos.x) / 2,
y: (midPos.y + midPos.y) / 2,
};
let distance = shoulderL.dist(shoulderR);
bodyDistance = map(distance, 90, 10, 0, 1);
bodyDistance = constrain(bodyDistance, 0, 1);
} else {
pose = null;
posePosition = {
x: video.width - (500 + 300) / 2,
y: (500 + 300) / 2,
};
}
}
function keyPressed() {
if (key == "f") {
let fs = fullscreen();
fullscreen(!fs);
}else if (key == "r") {
window.location.reload();
}
}