xxxxxxxxxx
688
let myFont; // Font
let flock;
let wordData; // 存储JSON依存关系和词向量数据
let wires = []; // 存储电线位置
let song; // 音乐变量
let maxBoidsPerWire = 8; // 每根电线上最多词鸟数量
let handPose; // MediaPipe手部识别
let video; // 摄像头
let hands = []; // 手部识别结果
let connections; // 手部骨架连接
let pinchThreshold = 90; // 捏合阈值(像素)
let lastPinchTime = 0; // 上次捏合时间
let pinchCooldown = 5; // 捏合冷却时间(毫秒)
let wireNoiseValues = []; // 电线噪声值
function preload() {
// 加载字体,确保字体文件路径正确
myFont = loadFont('UNEXPECTED RUSTY TYPEWRITER.OTF');
// 加载音乐
soundFormats('mp3');
song = loadSound('Fujii Kaze - Close To You.mp3');
// 加载HandPose模型
handPose = ml5.handPose();
// 尝试加载JSON数据
try {
wordData = loadJSON('close_to_you_vectors_dependencies.json');
} catch (error) {
console.log('JSON数据加载失败,使用默认词汇');
}
}
function setup() {
// 自适应设置 - 将画布调整为设备屏幕宽度的90%
let canvasWidth =800; // 控制最大宽度为540
let canvasHeight = 800; // 16:9比例,类似抖音竖屏
createCanvas(canvasWidth, canvasHeight);
// 设置摄像头
video = createCapture(VIDEO);
video.size(canvasWidth, canvasHeight);
video.hide(); // 隐藏原始视频元素
// 开始检测手势
handPose.detectStart(video, gotHands);
// 获取手部骨架连接信息
connections = handPose.getConnections();
// 播放音乐并循环
song.setVolume(0.5); // 设置音量为50%
song.loop(); // 循环播放
textFont(myFont); // 应用加载的字体
textSize(canvasWidth * 0.02); // 根据画布大小设置文字大小
textAlign(CENTER, CENTER); // 居中文本
// 创建5根电线,竖屏版本,均匀分布
let wireSpacing = canvasHeight / 6; // 确保电线均匀分布
for (let i = 0; i < 5; i++) {
wires.push(wireSpacing + i * wireSpacing);
wireNoiseValues.push(random(1000)); // 为每根电线初始化一个噪声种子
}
// 准备词汇
let words;
if (wordData && wordData.words) {
words = wordData.words.map(w => w.text);
} else {
// 使用默认歌词
let poetry = "Why do birds suddenly appear Every time you are near? Just like me, they long to be Close to you.Why do stars fall down from the sky Every time you walk by? Just like me, they long to be Close to you.On the day that you were born the angels got together And decided to create a dream come true So they sprinkled moon dust in your hair of gold. And starlight in your eyes of blue.That is why all the girls in town Follow you all around Just like me, they long to be Close to you. On the day that you were born, the angels got together And decided to create a dream come true So they sprinkled moon dust in your hair of gold. And starlight in your eyes of blueThat is why all the girls in town Follow you all around Just like me, they long to be Close to you Just like me, they long to be. Close to you.Wa, close to you Wa, close to you Ha, close to you La, close to you";
words = poetry.split(" ");
}
flock = new Flock(words);
// 初始化初始boid数量为屏幕大小的适当比例
let initialBoidCount = floor(canvasWidth * canvasHeight / 10000); // 根据画布大小调整初始boid数量
// 初始化一些boids
for (let i = 0; i < initialBoidCount; i++) {
let wordText, wordId, deps, x, y;
// 如果有JSON数据,使用词向量位置和依存关系
if (wordData && wordData.words && wordData.words.length > 0) {
let wordObj = wordData.words[i % wordData.words.length];
wordText = wordObj.text;
wordId = wordObj.id;
deps = wordObj.dependencies;
// 使用t-SNE坐标作为初始位置(如果有),但缩放到当前画布大小
if (wordObj.initial_position) {
// 假设原始t-SNE映射到1080x1920的画布
x = map(wordObj.initial_position.x, 0, 1080, 0, canvasWidth);
y = map(wordObj.initial_position.y, 0, 1920, 0, canvasHeight);
} else {
x = random(width);
y = random(height);
}
} else {
// 使用默认值
wordText = words[i % words.length];
wordId = "word_" + i;
deps = [];
x = random(width);
y = random(height);
}
let b = new Boid(x, y, wordText, wordId, deps);
// 找最近的电线
let closestWire = 0;
let minDist = abs(y - wires[0]);
for (let j = 1; j < wires.length; j++) {
let d = abs(y - wires[j]);
if (d < minDist) {
minDist = d;
closestWire = j;
}
}
b.preferredWire = closestWire;
flock.addBoid(b);
}
}
function gotHands(results) {
hands = results;
}
function draw() {
// 黑白视频背景
push();
translate(width, 0);
scale(-1, 1); // 水平翻转,使其像镜子
tint(255, 80); // 30%不透明度
image(video, 0, 0, width, height);
filter(GRAY); // 黑白滤镜
pop();
// 绘制电线
drawWires();
// 更新和显示词群
flock.run();
// 显示电线上形成的句子
displayWireSentences();
// 检查每根电线上的词鸟数量
enforceBoidLimits();
// 处理手势
handleHandGestures();
}
// 处理手势
function handleHandGestures() {
if (hands.length > 0) {
const hand = hands[0];
// 获取拇指和食指的关键点
const thumbTip = hand.keypoints[4]; // 拇指指尖
const indexTip = hand.keypoints[8]; // 食指指尖
// 计算拇指和食指之间的距离
const distance = dist(
thumbTip.x, thumbTip.y,
indexTip.x, indexTip.y
);
// 在拇指和食指位置画小圆,方便调试
push();
noFill();
stroke(255, 0, 0);
// 水平翻转坐标显示
ellipse(width - thumbTip.x, thumbTip.y, 10);
ellipse(width - indexTip.x, indexTip.y, 10);
line(width - thumbTip.x, thumbTip.y, width - indexTip.x, indexTip.y);
pop();
// 如果距离小于阈值,且超过冷却时间,创建新的词鸟
if (distance < pinchThreshold && (millis() - lastPinchTime > pinchCooldown)) {
lastPinchTime = millis();
// 确定位置(水平翻转坐标)
let x = width - indexTip.x;
let y = indexTip.y;
// 决定添加哪个词
let wordText, wordId, deps;
if (wordData && wordData.words) {
// 如果有JSON数据,随机选择一个词及其依存关系
let randomIndex = floor(random(wordData.words.length));
let wordObj = wordData.words[randomIndex];
wordText = wordObj.text;
wordId = wordObj.id;
deps = wordObj.dependencies;
} else {
// 使用默认词汇
let words = flock.words;
let numBoids = flock.boids.length;
wordText = words[numBoids % words.length];
wordId = "word_" + numBoids;
deps = [];
}
// 找最近的电线
let closestWire = 0;
let minDist = abs(y - wires[0]);
for (let i = 1; i < wires.length; i++) {
let d = abs(y - wires[i]);
if (d < minDist) {
minDist = d;
closestWire = i;
}
}
let newBoid = new Boid(x, y, wordText, wordId, deps);
newBoid.preferredWire = closestWire;
flock.addBoid(newBoid);
}
}
}
// 限制每根电线上的词鸟数量
function enforceBoidLimits() {
// 计算每根电线上的词鸟数量
let boidsOnWire = Array(wires.length).fill(0);
for (let i = 0; i < flock.boids.length; i++) {
let boid = flock.boids[i];
if (boid.isOnWire) {
boidsOnWire[boid.preferredWire]++;
}
}
// 如果某根电线上的词鸟超过限制,让一些起飞
for (let i = 0; i < flock.boids.length; i++) {
let boid = flock.boids[i];
if (boid.isOnWire && boidsOnWire[boid.preferredWire] > maxBoidsPerWire) {
// 有20%概率让这个词鸟起飞
if (random() < 0.2) {
boid.isOnWire = false;
boid.velocity.y = random(-1, 1);
boidsOnWire[boid.preferredWire]--;
// 如果已经达到限制,就不再处理
if (boidsOnWire[boid.preferredWire] <= maxBoidsPerWire) {
break;
}
}
}
}
}
function drawWires() {
stroke(255); // 白色电线
strokeWeight(1);
for (let i = 0; i < wires.length; i++) {
// 使用Perlin噪声创建手绘效果
beginShape();
noFill();
// 每帧更新噪声值
wireNoiseValues[i] += 0.01;
// 绘制带噪声的电线
for (let x = 0; x < width; x += 5) {
// 使用噪声计算y偏移
let noiseVal = noise(x * 0.01, wireNoiseValues[i]);
let y = wires[i] + map(noiseVal, 0, 1, -3, 3); // 垂直偏移范围为 -3 到 3 像素
vertex(x, y);
}
endShape();
}
}
// 显示电线上形成的句子
function displayWireSentences() {
// 为每根电线收集单词
for (let i = 0; i < wires.length; i++) {
let wireWords = [];
// 查找在这根电线上的所有boid
for (let j = 0; j < flock.boids.length; j++) {
let boid = flock.boids[j];
if (boid.isOnWire && boid.preferredWire === i && boid.position.x > 0 && boid.position.x < width) {
wireWords.push({
text: boid.text,
x: boid.position.x
});
}
}
// 按x坐标排序
wireWords.sort((a, b) => a.x - b.x);
// 构建句子
let sentence = wireWords.map(w => w.text).join(" ");
// 显示句子
if (sentence.length > 0) {
fill(255); // 白色文字
noStroke();
textAlign(CENTER);
textSize(width * 0.015);
text(sentence, width/2, wires[i] - 15);
}
}
// 恢复文本设置
textAlign(CENTER, CENTER);
textSize(width * 0.02);
}
function Flock(words) {
this.boids = [];
this.words = words;
}
Flock.prototype.addBoid = function(b) {
this.boids.push(b);
};
Flock.prototype.run = function() {
for (let i = 0; i < this.boids.length; i++) {
this.boids[i].run(this.boids);
}
};
function Boid(x, y, text, id, dependencies) {
this.acceleration = createVector(0, 0);
this.velocity = createVector(random(-1, 1), random(-1, 1));
this.position = createVector(x, y);
this.r = 1.0;
this.maxspeed = 2;
this.maxforce = 0.1;
this.text = text;
this.id = id || "word_" + text;
this.dependencies = dependencies || [];
this.preferredWire = 0; // 偏好的电线索引
this.isOnWire = false; // 是否停留在电线上
this.stayOnWireChance = 0.2; // 停留在电线上的概率
this.takeFlight = 0.01; // 从电线起飞的概率
}
Boid.prototype.run = function(boids) {
// 检查是否应该停留在电线上
if (!this.isOnWire && abs(this.position.y - wires[this.preferredWire]) < 5 && random() < this.stayOnWireChance) {
// 检查当前电线上的词鸟数量
let count = 0;
for (let i = 0; i < boids.length; i++) {
if (boids[i].isOnWire && boids[i].preferredWire === this.preferredWire) {
count++;
}
}
// 只有在电线上的词鸟数量未达到限制时才停留
if (count < maxBoidsPerWire) {
this.isOnWire = true;
this.velocity.y = 0; // 停止垂直运动
this.position.y = wires[this.preferredWire]; // 精确停在电线上
}
}
// 检查是否应该从电线起飞
if (this.isOnWire && random() < this.takeFlight) {
this.isOnWire = false;
this.velocity.y = random(-1, 1); // 给一个随机的垂直速度
}
// 应用行为
if (this.isOnWire) {
this.flockOnWire(boids);
} else {
this.flock(boids);
}
this.update();
this.borders();
this.render();
};
Boid.prototype.flockOnWire = function(boids) {
let sep = this.separate(boids);
let dep = this.dependencyAlignment(boids);
// 在电线上时增强依存关系对齐,减弱其他力
sep.mult(1.2);
dep.mult(2.0);
// 应用力
this.applyForce(sep);
this.applyForce(dep);
};
Boid.prototype.flock = function(boids) {
let sep = this.separate(boids); // Separation
let ali = this.align(boids); // Alignment
let coh = this.cohesion(boids); // Cohesion
let dep = this.dependencyAlignment(boids); // 依存关系对齐
let wir = this.seekWire(); // 电线吸引力
sep.mult(1.5);
ali.mult(1.0);
coh.mult(1.0);
dep.mult(5); // 依存关系权重
wir.mult(0.1); // 弱电线吸引力
this.applyForce(sep);
this.applyForce(ali);
this.applyForce(coh);
this.applyForce(dep);
this.applyForce(wir);
};
Boid.prototype.dependencyAlignment = function(boids) {
let steer = createVector(0, 0);
let count = 0;
// 如果没有依存关系数据,返回零向量
if (!this.dependencies || this.dependencies.length === 0) {
return steer;
}
// 检查与其他boid的依存关系
for (let i = 0; i < boids.length; i++) {
let other = boids[i];
if (other === this) continue;
// 查找与other之间的依存关系
for (let j = 0; j < this.dependencies.length; j++) {
let dep = this.dependencies[j];
// 如果找到依存关系
if (dep.target === other.id) {
let d = p5.Vector.dist(this.position, other.position);
if (d > 0 && d < 150) { // 依存关系作用范围比普通对齐更大
// 应用依存关系强度
let force = other.velocity.copy();
let strength = dep.strength || 1.0;
force.mult(strength);
steer.add(force);
count++;
// 如果两个词都在电线上且有依存关系,它们会更靠近
if (this.isOnWire && other.isOnWire && this.preferredWire === other.preferredWire) {
let cohesionForce = p5.Vector.sub(other.position, this.position);
cohesionForce.y = 0; // 保持在电线上
cohesionForce.normalize();
cohesionForce.mult(this.maxspeed * strength * 0.5);
let cohSteer = p5.Vector.sub(cohesionForce, this.velocity);
cohSteer.limit(this.maxforce);
steer.add(cohSteer);
}
}
}
}
}
if (count > 0) {
steer.div(count);
steer.normalize();
steer.mult(this.maxspeed);
steer.sub(this.velocity);
steer.limit(this.maxforce);
}
return steer;
};
Boid.prototype.seekWire = function() {
// 寻找偏好的电线
let targetY = wires[this.preferredWire];
let desired = createVector(this.position.x, targetY);
desired = p5.Vector.sub(desired, this.position);
desired.normalize();
desired.mult(this.maxspeed);
let steer = p5.Vector.sub(desired, this.velocity);
steer.limit(this.maxforce);
return steer;
};
Boid.prototype.applyForce = function(force) {
// 如果在电线上,忽略垂直力
if (this.isOnWire) {
force.y = 0;
}
this.acceleration.add(force);
};
Boid.prototype.update = function() {
this.velocity.add(this.acceleration);
// 如果在电线上,保持水平运动
if (this.isOnWire) {
this.velocity.y = 0;
}
this.velocity.limit(this.maxspeed);
this.position.add(this.velocity);
this.acceleration.mult(0);
};
Boid.prototype.separate = function(boids) {
let desiredSeparation = 30.0;
let steer = createVector(0, 0);
let count = 0;
for (let i = 0; i < boids.length; i++) {
let d = p5.Vector.dist(this.position, boids[i].position);
if ((d > 0) && (d < desiredSeparation)) {
let diff = p5.Vector.sub(this.position, boids[i].position);
diff.normalize();
diff.div(d);
steer.add(diff);
count++;
}
}
if (count > 0) {
steer.div(count);
}
if (steer.mag() > 0) {
steer.normalize();
steer.mult(this.maxspeed);
steer.sub(this.velocity);
steer.limit(this.maxforce);
}
return steer;
};
Boid.prototype.align = function(boids) {
let neighborDist = 50;
let sum = createVector(0, 0);
let count = 0;
for (let i = 0; i < boids.length; i++) {
let d = p5.Vector.dist(this.position, boids[i].position);
if ((d > 0) && (d < neighborDist)) {
sum.add(boids[i].velocity);
count++;
}
}
if (count > 0) {
sum.div(count);
sum.normalize();
sum.mult(this.maxspeed);
let steer = p5.Vector.sub(sum, this.velocity);
steer.limit(this.maxforce);
return steer;
} else {
return createVector(0, 0);
}
};
Boid.prototype.cohesion = function(boids) {
let neighborDist = 50;
let sum = createVector(0, 0);
let count = 0;
for (let i = 0; i < boids.length; i++) {
let d = p5.Vector.dist(this.position, boids[i].position);
if ((d > 0) && (d < neighborDist)) {
sum.add(boids[i].position);
count++;
}
}
if (count > 0) {
sum.div(count);
return this.seek(sum);
} else {
return createVector(0, 0);
}
};
Boid.prototype.seek = function(target) {
let desired = p5.Vector.sub(target, this.position);
desired.normalize();
desired.mult(this.maxspeed);
let steer = p5.Vector.sub(desired, this.velocity);
steer.limit(this.maxforce);
return steer;
};
Boid.prototype.render = function() {
// 使用Shiffman方法获取速度的方向角度
let heading = this.velocity.heading();
fill(255); // 白色文字
noStroke();
push();
translate(this.position.x, this.position.y);
// 根据需要调整文本方向
rotate(heading); // 文本沿着运动方向
text(this.text, 0, 0);
pop();
};
Boid.prototype.borders = function() {
if (this.position.x < -this.r) this.position.x = width + this.r;
if (this.position.y < -this.r) this.position.y = height + this.r;
if (this.position.x > width + this.r) this.position.x = -this.r;
if (this.position.y > height + this.r) this.position.y = -this.r;
};
// 保留鼠标拖拽功能作为备用
function mouseDragged() {
// 使用鼠标位置确定最近的电线
let closestWire = 0;
let minDist = abs(mouseY - wires[0]);
for (let i = 1; i < wires.length; i++) {
let d = abs(mouseY - wires[i]);
if (d < minDist) {
minDist = d;
closestWire = i;
}
}
// 决定添加哪个词
let wordText, wordId, deps;
if (wordData && wordData.words) {
// 如果有JSON数据,随机选择一个词及其依存关系
let randomIndex = floor(random(wordData.words.length));
let wordObj = wordData.words[randomIndex];
wordText = wordObj.text;
wordId = wordObj.id;
deps = wordObj.dependencies;
} else {
// 使用默认词汇
let words = flock.words;
let numBoids = flock.boids.length;
wordText = words[numBoids % words.length];
wordId = "word_" + numBoids;
deps = [];
}
let newBoid = new Boid(mouseX, mouseY, wordText, wordId, deps);
newBoid.preferredWire = closestWire;
flock.addBoid(newBoid);
}
// 添加音乐控制功能
function keyPressed() {
// 按空格键暂停/播放音乐
if (key === ' ') {
if (song.isPlaying()) {
song.pause();
} else {
song.play();
}
}
}
// 窗口大小改变时调整画布
function windowResized() {
let canvasWidth = min(540, windowWidth * 0.9);
let canvasHeight = canvasWidth * 16/9;
resizeCanvas(canvasWidth, canvasHeight);
// 更新摄像头大小
video.size(canvasWidth, canvasHeight);
// 重新计算电线位置
wires = [];
let wireSpacing = canvasHeight / 6;
for (let i = 0; i < 5; i++) {
wires.push(wireSpacing + i * wireSpacing);
}
// 调整文字大小
textSize(canvasWidth * 0.02);
}