1020 lines
28 KiB
Plaintext
1020 lines
28 KiB
Plaintext
// 粒子类
|
|
class Particle {
|
|
constructor(x, y, targetX, targetY) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.targetX = targetX;
|
|
this.targetY = targetY;
|
|
this.life = 1.0;
|
|
this.speed = random(0.02, 0.05);
|
|
this.size = random(2, 4);
|
|
this.alpha = random(150, 255);
|
|
}
|
|
|
|
update() {
|
|
this.x = lerp(this.x, this.targetX, this.speed);
|
|
this.y = lerp(this.y, this.targetY, this.speed);
|
|
|
|
let dist = distance(this.x, this.y, this.targetX, this.targetY);
|
|
if (dist < 5) {
|
|
this.life -= 0.05;
|
|
}
|
|
}
|
|
|
|
display() {
|
|
push();
|
|
noStroke();
|
|
fill(100, 200, 255, this.alpha * this.life);
|
|
circle(this.x, this.y, this.size);
|
|
pop();
|
|
}
|
|
|
|
isDead() {
|
|
return this.life <= 0;
|
|
}
|
|
}
|
|
|
|
function distance(x1, y1, x2, y2) {
|
|
return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
|
|
}
|
|
|
|
// 文字对象类
|
|
class FloatingText {
|
|
constructor(x, y, radius) {
|
|
this.centerX = x;
|
|
this.centerY = y;
|
|
this.maxRadius = radius;
|
|
|
|
let angle = random(TWO_PI);
|
|
let r = random(this.maxRadius * 0.9);
|
|
this.x = this.centerX + cos(angle) * r;
|
|
this.y = this.centerY + sin(angle) * r;
|
|
|
|
this.targetX = this.x;
|
|
this.targetY = this.y;
|
|
|
|
this.speedX = random(-0.3, 0.3);
|
|
this.speedY = random(-0.3, 0.3);
|
|
|
|
this.noiseOffsetX = random(1000);
|
|
this.noiseOffsetY = random(1000);
|
|
|
|
this.updateText();
|
|
|
|
this.baseSize = random(20, 32);
|
|
this.size = this.baseSize;
|
|
this.targetSize = this.baseSize;
|
|
|
|
this.baseAlpha = random(50, 150);
|
|
this.alpha = this.baseAlpha;
|
|
this.targetAlpha = this.baseAlpha;
|
|
|
|
this.changeTimer = random(1, 2);
|
|
this.changeCounter = 0;
|
|
|
|
// 造句相关
|
|
this.isCandidate = false;
|
|
this.isSelected = false;
|
|
this.isFinal = false;
|
|
this.isEliminated = false;
|
|
|
|
// 概率相关
|
|
this.probability = 0;
|
|
this.targetProbability = 0;
|
|
|
|
// 候选字变化
|
|
this.candidateChars = [];
|
|
this.candidateIndex = 0;
|
|
this.candidateChangeTimer = 0;
|
|
|
|
// 抖动效果
|
|
this.shakeAmount = 0;
|
|
}
|
|
|
|
updateText() {
|
|
const chars = [
|
|
'梦', '想', '希', '望', '光', '影', '星', '月', '云', '风',
|
|
'诗', '歌', '舞', '画', '音', '色', '情', '爱', '心', '灵',
|
|
'天', '地', '人', '和', '美', '真', '善', '雅', '韵', '意',
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
];
|
|
|
|
let numChars = random() > 0.6 ? 2 : 1;
|
|
this.text = '';
|
|
for (let i = 0; i < numChars; i++) {
|
|
this.text += random(chars);
|
|
}
|
|
}
|
|
|
|
setCandidate(candidateChars, initialProbability) {
|
|
this.isCandidate = true;
|
|
this.candidateChars = candidateChars;
|
|
this.candidateIndex = 0;
|
|
this.text = candidateChars[0];
|
|
this.candidateChangeTimer = 0;
|
|
this.probability = initialProbability;
|
|
this.targetProbability = initialProbability;
|
|
|
|
// 根据初始概率设置大小
|
|
this.updateSizeByProbability();
|
|
this.shakeAmount = 3; // 不确定时抖动
|
|
}
|
|
|
|
updateSizeByProbability() {
|
|
// 概率越高,字越大
|
|
let sizeMultiplier = map(this.targetProbability, 0, 100, 1.2, 2.0);
|
|
this.targetSize = this.baseSize * sizeMultiplier;
|
|
this.targetAlpha = map(this.targetProbability, 0, 100, 150, 255);
|
|
}
|
|
|
|
setProbability(prob) {
|
|
this.targetProbability = prob;
|
|
this.updateSizeByProbability();
|
|
|
|
// 概率高的时候抖动减少
|
|
this.shakeAmount = map(prob, 0, 100, 5, 0);
|
|
}
|
|
|
|
eliminate() {
|
|
this.isEliminated = true;
|
|
this.targetAlpha = 0;
|
|
this.targetSize = this.baseSize * 0.5;
|
|
}
|
|
|
|
setSelected(targetX, targetY, isFirst) {
|
|
this.isCandidate = false;
|
|
this.isSelected = true;
|
|
this.shakeAmount = 0;
|
|
|
|
let dx = targetX - this.x;
|
|
let dy = targetY - this.y;
|
|
let maxMove = 40;
|
|
|
|
if (abs(dx) > maxMove || abs(dy) > maxMove) {
|
|
let angle = atan2(dy, dx);
|
|
this.targetX = this.x + cos(angle) * maxMove;
|
|
this.targetY = this.y + sin(angle) * maxMove;
|
|
} else {
|
|
this.targetX = targetX;
|
|
this.targetY = targetY;
|
|
}
|
|
|
|
if (isFirst) {
|
|
this.targetSize = 55;
|
|
this.targetAlpha = 255;
|
|
} else {
|
|
this.targetSize = 42;
|
|
this.targetAlpha = 255;
|
|
}
|
|
}
|
|
|
|
cancelCandidate() {
|
|
this.isCandidate = false;
|
|
this.isEliminated = false;
|
|
this.probability = 0;
|
|
this.targetProbability = 0;
|
|
this.targetSize = this.baseSize;
|
|
this.targetAlpha = this.baseAlpha;
|
|
this.candidateChars = [];
|
|
this.shakeAmount = 0;
|
|
this.updateText();
|
|
}
|
|
|
|
finalize() {
|
|
this.isFinal = true;
|
|
this.shakeAmount = 0;
|
|
}
|
|
|
|
reset() {
|
|
this.isCandidate = false;
|
|
this.isSelected = false;
|
|
this.isFinal = false;
|
|
this.isEliminated = false;
|
|
this.probability = 0;
|
|
this.targetProbability = 0;
|
|
this.targetSize = this.baseSize;
|
|
this.targetAlpha = this.baseAlpha;
|
|
this.size = this.baseSize;
|
|
this.alpha = this.baseAlpha;
|
|
this.candidateChars = [];
|
|
this.shakeAmount = 0;
|
|
this.updateText();
|
|
}
|
|
|
|
update() {
|
|
// 候选字缓慢变化
|
|
if (this.isCandidate && this.candidateChars.length > 1 && !this.isEliminated) {
|
|
this.candidateChangeTimer++;
|
|
if (this.candidateChangeTimer >= 12) {
|
|
this.candidateIndex = (this.candidateIndex + 1) % this.candidateChars.length;
|
|
this.text = this.candidateChars[this.candidateIndex];
|
|
this.candidateChangeTimer = 0;
|
|
}
|
|
}
|
|
|
|
// 概率平滑过渡
|
|
this.probability = lerp(this.probability, this.targetProbability, 0.1);
|
|
|
|
if (this.isSelected || this.isCandidate) {
|
|
this.x = lerp(this.x, this.targetX, 0.08);
|
|
this.y = lerp(this.y, this.targetY, 0.08);
|
|
this.size = lerp(this.size, this.targetSize, 0.1);
|
|
this.alpha = lerp(this.alpha, this.targetAlpha, 0.1);
|
|
} else {
|
|
let noiseX = noise(this.noiseOffsetX) * 2 - 1;
|
|
let noiseY = noise(this.noiseOffsetY) * 2 - 1;
|
|
|
|
this.x += noiseX * 0.5 + this.speedX;
|
|
this.y += noiseY * 0.5 + this.speedY;
|
|
|
|
this.noiseOffsetX += 0.01;
|
|
this.noiseOffsetY += 0.01;
|
|
|
|
let dx = this.x - this.centerX;
|
|
let dy = this.y - this.centerY;
|
|
let dist = sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist > this.maxRadius) {
|
|
let angle = atan2(dy, dx);
|
|
this.x = this.centerX + cos(angle) * this.maxRadius;
|
|
this.y = this.centerY + sin(angle) * this.maxRadius;
|
|
this.speedX *= -0.8;
|
|
this.speedY *= -0.8;
|
|
}
|
|
|
|
this.changeCounter++;
|
|
if (this.changeCounter >= this.changeTimer) {
|
|
this.updateText();
|
|
this.changeCounter = 0;
|
|
this.changeTimer = random(5, 10);
|
|
}
|
|
}
|
|
}
|
|
|
|
display() {
|
|
push();
|
|
|
|
// 计算抖动偏移
|
|
let shakeX = random(-this.shakeAmount, this.shakeAmount);
|
|
let shakeY = random(-this.shakeAmount, this.shakeAmount);
|
|
|
|
// 根据概率显示彩色光晕
|
|
if (this.isCandidate && !this.isEliminated) {
|
|
let hue = map(this.probability, 0, 100, 200, 0); // 蓝色到红色
|
|
let glowSize = map(this.probability, 0, 100, 20, 50);
|
|
|
|
noStroke();
|
|
fill(hue, 200, 255, 30);
|
|
circle(this.x + shakeX, this.y + shakeY, this.size + glowSize);
|
|
|
|
// 边框
|
|
stroke(hue, 200, 255, this.alpha * 0.5);
|
|
strokeWeight(2);
|
|
noFill();
|
|
circle(this.x + shakeX, this.y + shakeY, this.size + 15);
|
|
}
|
|
|
|
// 显示文字
|
|
fill(255, this.alpha);
|
|
noStroke();
|
|
textAlign(CENTER, CENTER);
|
|
textSize(this.size);
|
|
text(this.text, this.x + shakeX, this.y + shakeY);
|
|
|
|
// 显示概率
|
|
if (this.isCandidate && !this.isEliminated && this.probability > 0) {
|
|
textSize(12);
|
|
fill(255, 200);
|
|
text(int(this.probability) + '%', this.x + shakeX, this.y + shakeY + this.size * 0.6);
|
|
}
|
|
|
|
pop();
|
|
}
|
|
}
|
|
|
|
let floatingTexts = [];
|
|
let numTexts = 60;
|
|
let circleRadius = 280;
|
|
|
|
// 造句相关
|
|
let targetSentence = "我想要被看到";
|
|
let sentenceProgress = 0;
|
|
let selectedTexts = [];
|
|
let candidateTexts = [];
|
|
let sentenceStarted = false;
|
|
let sentenceComplete = false;
|
|
|
|
// 粒子系统
|
|
let particles = [];
|
|
|
|
// 预测状态机
|
|
let predictionState = 'idle'; // idle, round1, round2, round3, confirming
|
|
let stateTimer = 0;
|
|
|
|
function setup() {
|
|
createCanvas(windowWidth, windowHeight);
|
|
|
|
// 自动创建词云
|
|
for (let i = 0; i < numTexts; i++) {
|
|
floatingTexts.push(new FloatingText(width / 2, height / 2, circleRadius));
|
|
}
|
|
}
|
|
|
|
function draw() {
|
|
background(0);
|
|
|
|
// 更新和显示所有文字
|
|
for (let text of floatingTexts) {
|
|
text.update();
|
|
text.display();
|
|
}
|
|
|
|
// 更新和显示粒子
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
particles[i].update();
|
|
particles[i].display();
|
|
if (particles[i].isDead()) {
|
|
particles.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// 绘制已确定字符的连线
|
|
if (selectedTexts.length > 1) {
|
|
stroke(255, 180);
|
|
strokeWeight(2);
|
|
for (let i = 0; i < selectedTexts.length - 1; i++) {
|
|
let current = selectedTexts[i];
|
|
let next = selectedTexts[i + 1];
|
|
if (next.isFinal) {
|
|
line(current.x, current.y, next.x, next.y);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 发射粒子(在预测阶段)
|
|
if ((predictionState === 'round1' || predictionState === 'round2' || predictionState === 'round3')
|
|
&& selectedTexts.length > 0 && frameCount % 3 === 0) {
|
|
let lastSelected = selectedTexts[selectedTexts.length - 1];
|
|
|
|
for (let candidate of candidateTexts) {
|
|
if (!candidate.text.isEliminated) {
|
|
// 粒子密度根据概率
|
|
let particleCount = map(candidate.text.probability, 0, 100, 0.5, 3);
|
|
if (random() < particleCount / 3) {
|
|
particles.push(new Particle(
|
|
lastSelected.x,
|
|
lastSelected.y,
|
|
candidate.text.x,
|
|
candidate.text.y
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 显示状态提示
|
|
if (sentenceStarted && !sentenceComplete) {
|
|
displayStateHint();
|
|
}
|
|
|
|
// 预测过程
|
|
if (sentenceStarted && sentenceProgress < targetSentence.length) {
|
|
handlePrediction();
|
|
}
|
|
}
|
|
|
|
function displayStateHint() {
|
|
push();
|
|
textAlign(LEFT, TOP);
|
|
textSize(14);
|
|
fill(150, 200, 255, 200);
|
|
noStroke();
|
|
|
|
let hint = '';
|
|
switch(predictionState) {
|
|
case 'round1':
|
|
hint = '正在分析可能性...';
|
|
break;
|
|
case 'round2':
|
|
hint = '匹配语法结构...';
|
|
break;
|
|
case 'round3':
|
|
hint = '计算最终概率...';
|
|
break;
|
|
case 'confirming':
|
|
hint = '确认结果中...';
|
|
break;
|
|
}
|
|
|
|
if (hint) {
|
|
text(hint, 30, 30);
|
|
}
|
|
pop();
|
|
}
|
|
|
|
function handlePrediction() {
|
|
stateTimer++;
|
|
|
|
switch(predictionState) {
|
|
case 'idle':
|
|
startPrediction();
|
|
break;
|
|
|
|
case 'round1':
|
|
// 第一轮:6-7个候选,概率20-40%(持续0.8秒)
|
|
if (stateTimer >= 48) {
|
|
eliminateRound1();
|
|
predictionState = 'round2';
|
|
stateTimer = 0;
|
|
}
|
|
break;
|
|
|
|
case 'round2':
|
|
// 第二轮:3-4个候选,概率40-60%(持续0.7秒)
|
|
if (stateTimer >= 42) {
|
|
eliminateRound2();
|
|
predictionState = 'round3';
|
|
stateTimer = 0;
|
|
}
|
|
break;
|
|
|
|
case 'round3':
|
|
// 第三轮:2个候选竞争,概率60-80%(持续1秒)
|
|
if (stateTimer >= 60) {
|
|
predictionState = 'confirming';
|
|
stateTimer = 0;
|
|
confirmPrediction();
|
|
}
|
|
break;
|
|
|
|
case 'confirming':
|
|
// 确认阶段:正确答案概率飙升到95%+(持续0.5秒)
|
|
if (stateTimer >= 30) {
|
|
finalizePrediction();
|
|
predictionState = 'idle';
|
|
stateTimer = 0;
|
|
sentenceProgress++;
|
|
|
|
if (sentenceProgress >= targetSentence.length) {
|
|
sentenceComplete = true;
|
|
setTimeout(() => {
|
|
resetSentence();
|
|
}, 1500);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function startPrediction() {
|
|
let correctChar = targetSentence[sentenceProgress];
|
|
|
|
// 如果是第一个字,直接显示
|
|
if (sentenceProgress === 0) {
|
|
let availableTexts = floatingTexts.filter(t => !t.isSelected);
|
|
let selectedText = random(availableTexts);
|
|
|
|
let startX = width / 2 - (targetSentence.length * 45) / 2;
|
|
selectedText.text = correctChar;
|
|
selectedText.setSelected(startX, height / 2, true);
|
|
selectedText.finalize();
|
|
selectedTexts.push(selectedText);
|
|
|
|
sentenceProgress++;
|
|
predictionState = 'idle';
|
|
stateTimer = 0;
|
|
return;
|
|
}
|
|
|
|
// 生成候选字(6-7个)
|
|
let candidates = [correctChar];
|
|
const possibleChars = ['要', '爱', '见', '到', '你', '他', '她', '们', '的', '了', '吗', '呢', '想', '看', '被', '着', '给', '和', '在'];
|
|
|
|
let numCandidates = int(random(6, 8));
|
|
while (candidates.length < numCandidates) {
|
|
let distractor = random(possibleChars);
|
|
if (!candidates.includes(distractor)) {
|
|
candidates.push(distractor);
|
|
}
|
|
}
|
|
|
|
// 找到可用的文字对象
|
|
candidateTexts = [];
|
|
let availableTexts = floatingTexts.filter(t => !t.isSelected && !t.isCandidate);
|
|
|
|
for (let i = 0; i < candidates.length && i < availableTexts.length; i++) {
|
|
let text = availableTexts[i];
|
|
let charVariations = [candidates[i]];
|
|
|
|
// 添加变化字符
|
|
for (let j = 0; j < 2; j++) {
|
|
let variation = random(possibleChars);
|
|
if (!charVariations.includes(variation)) {
|
|
charVariations.push(variation);
|
|
}
|
|
}
|
|
charVariations.push(candidates[i]);
|
|
|
|
// 初始概率随机分配
|
|
let initialProb = random(15, 35);
|
|
text.setCandidate(charVariations, initialProb);
|
|
|
|
candidateTexts.push({
|
|
text: text,
|
|
correctChar: candidates[i],
|
|
isCorrect: candidates[i] === correctChar
|
|
});
|
|
}
|
|
|
|
predictionState = 'round1';
|
|
stateTimer = 0;
|
|
}
|
|
|
|
function eliminateRound1() {
|
|
// 淘汰概率最低的3个
|
|
candidateTexts.sort((a, b) => {
|
|
if (a.isCorrect) return 1; // 确保正确答案不被淘汰
|
|
if (b.isCorrect) return -1;
|
|
return a.text.probability - b.text.probability;
|
|
});
|
|
|
|
let toEliminate = min(3, candidateTexts.length - 3);
|
|
for (let i = 0; i < toEliminate; i++) {
|
|
candidateTexts[i].text.eliminate();
|
|
}
|
|
|
|
// 剩余候选概率上升到40-60%
|
|
for (let i = toEliminate; i < candidateTexts.length; i++) {
|
|
let newProb = random(40, 60);
|
|
if (candidateTexts[i].isCorrect) {
|
|
newProb = random(50, 65); // 正确答案稍高
|
|
}
|
|
candidateTexts[i].text.setProbability(newProb);
|
|
}
|
|
}
|
|
|
|
function eliminateRound2() {
|
|
// 移除已淘汰的
|
|
candidateTexts = candidateTexts.filter(c => !c.text.isEliminated);
|
|
|
|
// 再淘汰1-2个
|
|
candidateTexts.sort((a, b) => {
|
|
if (a.isCorrect) return 1;
|
|
if (b.isCorrect) return -1;
|
|
return a.text.probability - b.text.probability;
|
|
});
|
|
|
|
let toEliminate = min(candidateTexts.length - 2, 2);
|
|
for (let i = 0; i < toEliminate; i++) {
|
|
candidateTexts[i].text.eliminate();
|
|
}
|
|
|
|
// 剩余2个候选概率上升到60-80%
|
|
for (let i = toEliminate; i < candidateTexts.length; i++) {
|
|
let newProb = random(60, 75);
|
|
if (candidateTexts[i].isCorrect) {
|
|
newProb = random(70, 82); // 正确答案更高
|
|
}
|
|
candidateTexts[i].text.setProbability(newProb);
|
|
}
|
|
}
|
|
|
|
function confirmPrediction() {
|
|
candidateTexts = candidateTexts.filter(c => !c.text.isEliminated);
|
|
|
|
// 正确答案概率飙升到95%+
|
|
for (let candidate of candidateTexts) {
|
|
if (candidate.isCorrect) {
|
|
candidate.text.setProbability(random(95, 99));
|
|
} else {
|
|
candidate.text.setProbability(random(5, 15));
|
|
candidate.text.eliminate();
|
|
}
|
|
}
|
|
}
|
|
|
|
function finalizePrediction() {
|
|
let correctText = null;
|
|
|
|
for (let candidate of candidateTexts) {
|
|
if (candidate.isCorrect) {
|
|
correctText = candidate.text;
|
|
} else {
|
|
candidate.text.cancelCandidate();
|
|
}
|
|
}
|
|
|
|
if (correctText) {
|
|
let startX = width / 2 - (targetSentence.length * 45) / 2;
|
|
let targetX = startX + sentenceProgress * 45;
|
|
let targetY = height / 2;
|
|
|
|
correctText.setSelected(targetX, targetY, false);
|
|
|
|
setTimeout(() => {
|
|
correctText.finalize();
|
|
}, 300);
|
|
|
|
selectedTexts.push(correctText);
|
|
}
|
|
|
|
candidateTexts = [];
|
|
}
|
|
|
|
function resetSentence() {
|
|
sentenceStarted = false;
|
|
sentenceComplete = false;
|
|
sentenceProgress = 0;
|
|
predictionState = 'idle';
|
|
stateTimer = 0;
|
|
particles = [];
|
|
|
|
for (let text of selectedTexts) {
|
|
text.reset();
|
|
}
|
|
selectedTexts = [];
|
|
candidateTexts = [];
|
|
}
|
|
|
|
function windowResized() {
|
|
resizeCanvas(windowWidth, windowHeight);
|
|
for (let text of floatingTexts) {
|
|
text.centerX = width / 2;
|
|
text.centerY = height / 2;
|
|
}
|
|
}
|
|
|
|
function mousePressed() {
|
|
if (!sentenceStarted && !sentenceComplete) {
|
|
sentenceStarted = true;
|
|
predictionState = 'idle';
|
|
stateTimer = 0;
|
|
}
|
|
}
|
|
能把这个的背景变成
|
|
// 文字粒子类
|
|
class TextParticle {
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.vx = random(-0.5, 0.5);
|
|
this.vy = random(-0.5, 0.5);
|
|
this.char = this.randomChar();
|
|
this.alpha = random(40, 180);
|
|
this.baseAlpha = this.alpha;
|
|
this.size = random(25, 35);
|
|
this.changeTimer = random(10, 30);
|
|
this.connections = [];
|
|
this.connectionTimer = 0;
|
|
this.isConnected = false;
|
|
this.attractionTimer = 0;
|
|
}
|
|
|
|
randomChar() {
|
|
const chars = '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严';
|
|
return chars[floor(random(chars.length))];
|
|
}
|
|
|
|
update(offsetX, offsetY) {
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
|
|
// 轻微漂浮
|
|
this.vx += random(-0.1, 0.1);
|
|
this.vy += random(-0.1, 0.1);
|
|
|
|
// 限制速度
|
|
this.vx = constrain(this.vx, -1, 1);
|
|
this.vy = constrain(this.vy, -1, 1);
|
|
|
|
// 边界处理
|
|
const boundary = 800;
|
|
if (this.x < -boundary) this.x = -boundary;
|
|
if (this.x > boundary) this.x = boundary;
|
|
if (this.y < -boundary) this.y = -boundary;
|
|
if (this.y > boundary) this.y = boundary;
|
|
|
|
// 只有在没有连接时才变换文字
|
|
if (!this.isConnected) {
|
|
this.changeTimer--;
|
|
if (this.changeTimer <= 0) {
|
|
this.char = this.randomChar();
|
|
this.changeTimer = random(2, 4);
|
|
}
|
|
}
|
|
|
|
// 更新吸引计时器
|
|
if (this.attractionTimer > 0) {
|
|
this.attractionTimer--;
|
|
}
|
|
}
|
|
|
|
display(offsetX, offsetY) {
|
|
push();
|
|
|
|
// 如果有连接,显示更明显:放大
|
|
let displaySize = this.size;
|
|
if (this.isConnected) {
|
|
displaySize = this.size * 1.3;
|
|
}
|
|
|
|
// 绘制文字
|
|
fill(255, this.alpha);
|
|
noStroke();
|
|
textSize(displaySize);
|
|
textAlign(CENTER, CENTER);
|
|
text(this.char, this.x + offsetX, this.y + offsetY);
|
|
pop();
|
|
}
|
|
|
|
// 计算到其他粒子的距离
|
|
distTo(other) {
|
|
return dist(this.x, this.y, other.x, other.y);
|
|
}
|
|
|
|
// 检查粒子是否在遮罩范围内(考虑offset)
|
|
isInMask(offsetX, offsetY, centerX, centerY, radius) {
|
|
let screenX = this.x + offsetX;
|
|
let screenY = this.y + offsetY;
|
|
let d = dist(screenX, screenY, centerX, centerY);
|
|
return d < radius;
|
|
}
|
|
}
|
|
|
|
// ========== 文字密度调节区域 ==========
|
|
const PARTICLE_COUNT = 250;
|
|
// =====================================
|
|
|
|
// ========== 连线粗细调节区域 ==========
|
|
const LINE_THICKNESS = 2.5;
|
|
// =====================================
|
|
|
|
// 全局变量
|
|
let particles = [];
|
|
let offsetX = 0, offsetY = 0;
|
|
let lastMouseX = 0, lastMouseY = 0;
|
|
let isDragging = false;
|
|
let maskRadius = 250;
|
|
let phraseGroups = [];
|
|
|
|
// 有意义的词组和短句(2-4字,像潜意识的想法)
|
|
const meaningfulPhrases = [
|
|
'我想', '可以', '不行', '为什么',
|
|
'去哪', '做梦', '忘记', '记得',
|
|
'爱你', '讨厌', '喜欢', '害怕',
|
|
'自由', '困住', '离开', '回来',
|
|
'明天', '昨天', '现在', '以后',
|
|
'真的', '假的', '也许', '一定',
|
|
'孤独', '热闹', '安静', '吵闹',
|
|
'快乐', '悲伤', '愤怒', '平静',
|
|
'梦想', '现实', '理想', '幻想',
|
|
'逃避', '面对', '接受', '拒绝',
|
|
'开始', '结束', '继续', '放弃',
|
|
'找到', '失去', '得到', '放下',
|
|
'相信', '怀疑', '确定', '迷茫',
|
|
'成长', '退缩', '前进', '后退',
|
|
'温暖', '冷漠', '善良', '残忍',
|
|
'希望', '绝望', '勇气', '懦弱',
|
|
'坚持', '动摇', '改变', '守护',
|
|
'理解', '误解', '清楚', '混乱',
|
|
'简单', '复杂', '容易', '困难',
|
|
'靠近', '远离', '拥抱', '推开',
|
|
'说谎', '诚实', '隐藏', '坦白',
|
|
'醒来', '睡去', '清醒', '迷糊',
|
|
'选择', '犹豫', '决定', '后悔',
|
|
'珍惜', '浪费', '把握', '错过',
|
|
'等待', '追逐', '寻找', '遇见',
|
|
'重要', '无聊', '有趣', '平凡',
|
|
'特别', '普通', '独特', '相同',
|
|
'永远', '瞬间', '长久', '短暂',
|
|
'完整', '破碎', '圆满', '遗憾',
|
|
'美好', '糟糕', '幸福', '痛苦',
|
|
'想起', '遗忘', '回忆', '未来',
|
|
'过去', '此刻', '那时', '现在',
|
|
'或许', '肯定', '否定', '承认',
|
|
'否认', '相遇', '别离', '重逢',
|
|
'陌生', '熟悉', '新鲜', '厌倦',
|
|
'期待', '失望', '满足', '渴望',
|
|
'需要', '多余', '必须', '随意',
|
|
'认真', '敷衍', '真心', '假意',
|
|
'明白', '糊涂', '聪明', '愚蠢',
|
|
'清醒', '沉醉', '冷静', '疯狂'
|
|
];
|
|
|
|
function setup() {
|
|
createCanvas(800, 600);
|
|
textFont('Arial');
|
|
|
|
// 创建粒子
|
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
|
particles.push(new TextParticle(
|
|
random(-600, 600),
|
|
random(-600, 600)
|
|
));
|
|
}
|
|
}
|
|
|
|
function draw() {
|
|
background(20, 25, 30);
|
|
|
|
// 更新粒子
|
|
for (let p of particles) {
|
|
p.update(offsetX, offsetY);
|
|
}
|
|
|
|
// 建立和断开连接
|
|
updateConnections();
|
|
|
|
// 应用轻微的吸引力
|
|
applyConnectionForces();
|
|
|
|
// 开始圆形遮罩
|
|
push();
|
|
drawingContext.save();
|
|
drawingContext.beginPath();
|
|
drawingContext.arc(width / 2, height / 2, maskRadius, 0, TWO_PI);
|
|
drawingContext.clip();
|
|
|
|
// 绘制连接线
|
|
stroke(120, 180, 255, 180);
|
|
strokeWeight(LINE_THICKNESS);
|
|
for (let group of phraseGroups) {
|
|
for (let i = 0; i < group.particles.length - 1; i++) {
|
|
let p1 = group.particles[i];
|
|
let p2 = group.particles[i + 1];
|
|
line(
|
|
p1.x + offsetX, p1.y + offsetY,
|
|
p2.x + offsetX, p2.y + offsetY
|
|
);
|
|
}
|
|
}
|
|
|
|
// 绘制粒子
|
|
for (let p of particles) {
|
|
p.display(offsetX, offsetY);
|
|
}
|
|
|
|
drawingContext.restore();
|
|
pop();
|
|
|
|
// 绘制圆形边界
|
|
push();
|
|
noFill();
|
|
stroke(100, 150, 255, 150);
|
|
strokeWeight(2);
|
|
circle(width / 2, height / 2, maskRadius * 2);
|
|
pop();
|
|
|
|
// 绘制提示信息
|
|
fill(255, 100);
|
|
noStroke();
|
|
textSize(14);
|
|
textAlign(LEFT);
|
|
text('拖拽鼠标查看不同区域', 10, 20);
|
|
text('当前文字数量: ' + PARTICLE_COUNT, 10, 40);
|
|
}
|
|
|
|
// 更新连接关系
|
|
function updateConnections() {
|
|
// 更新现有短语组的计时器
|
|
for (let i = phraseGroups.length - 1; i >= 0; i--) {
|
|
let group = phraseGroups[i];
|
|
group.timer--;
|
|
|
|
if (group.timer <= 0) {
|
|
// 时间到,解除连接
|
|
for (let p of group.particles) {
|
|
p.isConnected = false;
|
|
p.connections = [];
|
|
p.alpha = p.baseAlpha;
|
|
}
|
|
phraseGroups.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// 提高连接频率:随机建立新的短语连接
|
|
if (random() < 0.08) {
|
|
// 选择一个随机短语
|
|
let phrase = random(meaningfulPhrases);
|
|
let phraseLength = phrase.length;
|
|
|
|
// 筛选在遮罩范围内且未连接的粒子
|
|
let centerX = width / 2;
|
|
let centerY = height / 2;
|
|
let availableParticles = particles.filter(p =>
|
|
!p.isConnected && p.isInMask(offsetX, offsetY, centerX, centerY, maskRadius - 50)
|
|
);
|
|
|
|
// 如果遮罩内粒子不够,放宽范围
|
|
if (availableParticles.length < phraseLength) {
|
|
availableParticles = particles.filter(p =>
|
|
!p.isConnected && p.isInMask(offsetX, offsetY, centerX, centerY, maskRadius + 100)
|
|
);
|
|
}
|
|
|
|
if (availableParticles.length < phraseLength) return;
|
|
|
|
// 选择一个起始粒子
|
|
let startParticle = random(availableParticles);
|
|
let selectedParticles = [startParticle];
|
|
|
|
// 移除已选择的粒子
|
|
availableParticles = availableParticles.filter(p => p !== startParticle);
|
|
|
|
// 选择附近的其他粒子
|
|
for (let i = 1; i < phraseLength; i++) {
|
|
if (availableParticles.length === 0) break;
|
|
|
|
// 找到距离最后选择的粒子较近的粒子
|
|
let lastSelected = selectedParticles[selectedParticles.length - 1];
|
|
let nearbyParticles = availableParticles.filter(p =>
|
|
lastSelected.distTo(p) < 200
|
|
);
|
|
|
|
if (nearbyParticles.length > 0) {
|
|
let nextParticle = random(nearbyParticles);
|
|
selectedParticles.push(nextParticle);
|
|
availableParticles = availableParticles.filter(p => p !== nextParticle);
|
|
} else {
|
|
// 如果没有附近的,就随机选择
|
|
let nextParticle = random(availableParticles);
|
|
selectedParticles.push(nextParticle);
|
|
availableParticles = availableParticles.filter(p => p !== nextParticle);
|
|
}
|
|
}
|
|
|
|
if (selectedParticles.length === phraseLength) {
|
|
// 设置每个粒子的文字为短语中的字,并提高透明度
|
|
for (let i = 0; i < phraseLength; i++) {
|
|
selectedParticles[i].char = phrase[i];
|
|
selectedParticles[i].isConnected = true;
|
|
selectedParticles[i].attractionTimer = 20;
|
|
selectedParticles[i].alpha = random(190, 210);
|
|
}
|
|
|
|
// 创建连接
|
|
for (let i = 0; i < phraseLength - 1; i++) {
|
|
selectedParticles[i].connections = [selectedParticles[i + 1]];
|
|
}
|
|
|
|
// 添加到短语组
|
|
phraseGroups.push({
|
|
particles: selectedParticles,
|
|
timer: random(60, 120)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 应用轻微的吸引力
|
|
function applyConnectionForces() {
|
|
for (let group of phraseGroups) {
|
|
for (let i = 0; i < group.particles.length - 1; i++) {
|
|
let p1 = group.particles[i];
|
|
let p2 = group.particles[i + 1];
|
|
|
|
// 只在吸引计时器大于0时应用力
|
|
if (p1.attractionTimer > 0 || p2.attractionTimer > 0) {
|
|
let dx = p2.x - p1.x;
|
|
let dy = p2.y - p1.y;
|
|
let d = sqrt(dx * dx + dy * dy);
|
|
|
|
if (d > 0 && d > 60) {
|
|
let force = 0.15;
|
|
p1.vx += (dx / d) * force;
|
|
p1.vy += (dy / d) * force;
|
|
p2.vx -= (dx / d) * force;
|
|
p2.vy -= (dy / d) * force;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 鼠标按下
|
|
function mousePressed() {
|
|
let d = dist(mouseX, mouseY, width / 2, height / 2);
|
|
if (d < maskRadius) {
|
|
isDragging = true;
|
|
lastMouseX = mouseX;
|
|
lastMouseY = mouseY;
|
|
}
|
|
}
|
|
|
|
// 鼠标拖拽
|
|
function mouseDragged() {
|
|
if (isDragging) {
|
|
let dx = mouseX - lastMouseX;
|
|
let dy = mouseY - lastMouseY;
|
|
offsetX += dx;
|
|
offsetY += dy;
|
|
lastMouseX = mouseX;
|
|
lastMouseY = mouseY;
|
|
}
|
|
}
|
|
|
|
// 鼠标释放
|
|
function mouseReleased() {
|
|
isDragging = false;
|
|
}这样的么 |