Inspiration : When facing a full page of text, have you ever thought about erasing the text by hand, and then the focus will naturally remain on the paper and automatically highlighted? This project brings this idea to real life as an interactive digital experience
A midterm project that combines: particle motion, class, spring, noise, GUI:
1. The characters can be moved with the mouse, and the movement is natural and realistic.
2. Some random characters will change color after being touched, and these characters are connected by springs. Because the movement trajectory and color are different from other characters, they will stand out in the later mouse movement.
3. There are particle rearrangement and code reset functions
4. Add sound effects when the character particles are touched, and the particles and springs have artistic effects
5. The background has ink smudges and dark corners
Step 1
At this stage, the character particles are arranged:
and character particles and mouse interactive movement:
Step 2 Spring class
At this stage, code randomly selects characters, changes colors, and connects them with springs, but the spring length is fixed and the characters are all connected to each other, which looks messy.
Step 3
By putting the selected characters into a new array, this code makes the spring length proportional to the initial position of the selected characters:
and makes two adjacent characters connected.
Step 4 Mousepressed
This code makes the characters scatter when the mouse is clicked once, and rearranged when the mouse is clicked a second time.
Step 5 Artistic Effects
Red halo
Pulsating
Glowing, twisted springs
Step 6 Blood particals
Step 7 Ink block
+dark corner
First version
Second version
Step 8 GUI(customize)
Code
let params = {
fps: 0,
textInput: "",
};
let dust = [];
let fprint;
let scl;
let scatter = false;
let clickCount = 0;
let num = 800;
let rectWidth = 450;
let rectHeight = 450;
let spacing = 20;
let chars = ['መ', 'ሠ', 'ረ', 'ሰ', 'ሸ', 'ቀ', 'በ', 'የ', 'ሀ', 'ለ', 'ሐ', 'ደ', 'ጀ', 'ገ', 'ጠ', 'ጨ', 'ጰ', 'ጸ', 'ፀ', 'ቨ', 'ተ', 'ቸ', 'ኀ', 'ነ', 'ኘ', 'አ', 'ከ', 'ኸ', 'ወ', 'ዐ', 'ዘ', 'ዠ', 'ፈ', 'ፐ'
];
let springs = [];
let coloredDustArray = [];
let inkBlots = [];
let bloodParticles = [];
let noiseOffset = 0;
let darkCorners;
let selectSound;
let rearrangeSound;
function preload() {
selectSound = [
loadSound("Sound/NOCsound2.mp3"),
loadSound("Sound/NOCsound3.mp3")
];
rearrangeSound = loadSound("Sound/NOCsound4.mp3");
// volume
for (let sound of selectSound) {
sound.setVolume(0.5);
}
rearrangeSound.setVolume(0.5);
}
function setup() {
createCanvas(800, 800);
setupGUI();
// FYI: pane.hidden = false;
scl = height / 600;
fprint = createGraphics(width, height);
fprint.background(60);
// DARK CORNER
darkCorners = createGraphics(width, height);
createDarkCorners();
imageMode(CENTER);
makeDust();
createInkBlots();
}
function updateGUI() {
params.fps = frameRate();
pane.refresh();
}
function draw() {
updateGUI();
background(50);
noiseOffset += 0.005;
push();
translate(width / 2, height / 2);
// ink
for (let blot of inkBlots) {
push();
noStroke();
fill(0, blot.alpha);
// noise
beginShape();
for (let a = 0; a < TWO_PI; a += 0.1) {
let xoff = map(cos(a), -1, 1, 0, 3) + blot.noiseOffset;
let yoff = map(sin(a), -1, 1, 0, 3) + blot.noiseOffset + noiseOffset;
let r = blot.size * (0.5 + 0.5 * noise(xoff, yoff));
let x = blot.x + r * cos(a);
let y = blot.y + r * sin(a);
vertex(x, y);
}
endShape(CLOSE);
pop();
}
// blink??
if (random(1) < 0.05) {
push();
noStroke();
fill(100, 0, 0, random(5, 15));
rect(-width / 2, -height / 2, width, height);
pop();
}
scale(scl);
// spring
for (let spring of springs) {
spring.update();
spring.display();
}
// word
for (let d of dust) {
if (d.colored) {
// select+glowing
push();
for (let i = 3; i > 0; i--) {
let pulseIntensity = 50 + 30 * sin(frameCount * 0.05 + d.id * 0.1);
fill(200, 0, 0, pulseIntensity / i);
textSize(20 + i * 2);
text(d.char, d.pos.x, d.pos.y);
}
let bloodRed = color(255, 0, 0);
let darkRed = color(120, 0, 0); // 更暗的红色
let pulseValue = sin(frameCount * 0.1 + d.id * 0.2) * 0.5 + 0.5;
let charColor = lerpColor(darkRed, bloodRed, pulseValue);
fill(charColor);
textSize(20);
text(d.char, d.pos.x, d.pos.y);
// blood on words
if (random(1) < 0.005) {
for (let i = 0; i < 2; i++) {
bloodParticles.push({
pos: createVector(d.pos.x, d.pos.y),
vel: createVector(random(-0.5, 0.5), random(0.5, 2)),
size: random(2, 5),
alpha: random(150, 200),
});
}
}
pop();
} else {
fill(0);
textSize(20);
textAlign(CENTER, CENTER);
text(d.char, d.pos.x, d.pos.y);
}
if (frameCount > d.id) {
d.pos.lerp(d.tpos, 0.1);
}
if (d.vel) {
d.vel.add(d.acc || createVector(0, 0));
d.pos.add(d.vel);
d.vel.mult(0.98);
d.acc = createVector(0, 0);
}
}
for (let i = bloodParticles.length - 1; i >= 0; i--) {
let p = bloodParticles[i];
p.pos.add(p.vel);
p.vel.mult(0.98);
p.vel.add(createVector(0, 0.05));
// blood partical
noStroke();
fill(150, 0, 0, p.alpha);
ellipse(p.pos.x, p.pos.y, p.size, p.size);
// lifespan
if (p.pos.y > height / (2 * scl) || p.alpha < 5) {
bloodParticles.splice(i, 1);
} else {
p.alpha *= 0.98;
}
}
pop();
// darkCorner
image(darkCorners, width / 2, height / 2);
}
function createDarkCorners() {
darkCorners.noStroke();
let gradient = darkCorners.drawingContext.createRadialGradient(
width / 2,
height / 2,
300, // center
width / 2,
height / 2,
width / 1.5 // diagram
);
gradient.addColorStop(0, "rgba(0,0,0,0)");
gradient.addColorStop(1, "rgba(0,0,0,0.85)");
darkCorners.drawingContext.fillStyle = gradient;
darkCorners.rect(0, 0, width, height);
}
function createInkBlots() {
for (let i = 0; i < 15; i++) {
inkBlots.push({
x: random(-width / 2, width / 2),
y: random(-height / 2, height / 2),
size: random(80, 200),
alpha: random(20, 50),
noiseScale: random(0.01, 0.05),
noiseOffset: random(1000),
});
}
}
//!!!
function resetEverything() {
dust = [];
springs = [];
coloredDustArray = [];
inkBlots = [];
bloodParticles = [];
fprint.background(60);
makeDust();
createInkBlots();
scatter = false;
clickCount = 0;
frameCount = 0;
}
function makeDust() {
let cols = Math.floor(rectWidth / spacing);
let rows = Math.floor(rectHeight / spacing);
let xOffset = (-cols * spacing) / 2;
let yOffset = (-rows * spacing) / 2;
for (let i = 0; i < num; i++) {
let col = i % cols;
let r = Math.floor(i / cols);
if (r >= rows) break;
let x = xOffset + col * spacing;
let y = yOffset + r * spacing;
let randomChar = random(chars);
dust.push({
char: randomChar,
pos: createVector(x, y),
tpos: createVector(x, y),
opos: createVector(x, y),
id: random(60),
vel: createVector(0, 0),
acc: createVector(0, 0),
mass: 1,
});
}
}
function mousePressed() {
if (mouseX > width) return;
clickCount = (clickCount + 1) % 3; //count
if (clickCount === 0) {
//the third time
resetEverything();
return;
}
rearrangeSound.play();
scatter = !scatter;
frameCount = 0;
if (scatter) {
for (let d of dust) {
let pos = p5.Vector.random2D();
pos.mult(random(height / (3 * scl), height / scl));
d.tpos.x = d.opos.x + pos.x;
d.tpos.y = d.opos.y + pos.y;
}
} else {
for (let d of dust) {
d.tpos.x = d.opos.x;
d.tpos.y = d.opos.y;
}
}
}
function mouseMoved() {
for (let d of dust) {
let mpos = createVector(mouseX - width / 2, mouseY - height / 2);
let delta = mpos.sub(d.pos);
let farness = delta.mag();
if (farness < 50 * scl) {
let move = createVector(movedX, movedY + random(-25, 25)).mult(
10 / max(2, farness)
);
d.tpos.add(move);
if (!d.colored && random(1) < 0.005 && coloredDustArray.length < 20) {
//choose
d.colored = true;
d.color = color(255, 0, 0);
let randomSoundIndex = floor(random(selectSound.length));
selectSound[randomSoundIndex].play();
// blood partical explode
for (let i = 0; i < 15; i++) {
bloodParticles.push({
pos: createVector(d.pos.x, d.pos.y),
vel: p5.Vector.random2D().mult(random(1, 3)),
size: random(2, 8),
alpha: random(150, 255),
});
}
if (coloredDustArray.length > 0) {
let prevDust = coloredDustArray[coloredDustArray.length - 1];
let distance = p5.Vector.dist(d.pos, prevDust.pos)+10;
springs.push(new Spring(prevDust, d, distance, 0.005));
}
coloredDustArray.push(d);
// 20 then connect
if (coloredDustArray.length === 20) {
let firstDust = coloredDustArray[0];
let lastDust = coloredDustArray[coloredDustArray.length - 1];
let distance = p5.Vector.dist(firstDust.pos, lastDust.pos);
springs.push(new Spring(firstDust, lastDust, distance, 0.01));
}
}
}
}
}
class Spring {
constructor(a, b, restLength, stiffness) {
this.bobA = a;
this.bobB = b;
this.len = restLength;
this.k = stiffness;
}
update() {
let force = p5.Vector.sub(this.bobB.pos, this.bobA.pos);
let distance = force.mag();
let stretch = distance - this.len;
force.normalize();
force.mult(stretch * this.k);
if (this.bobA.acc) {
this.bobA.acc.add(force);
this.bobB.acc.sub(force);
}
}
display() {
push();
// pulsing spring
let pulseValue = sin(frameCount * 0.1) * 0.5 + 0.5;
let alpha = lerp(120, 190, pulseValue);
stroke(150, 0, 0, alpha);
strokeWeight(random(1, 1.5));
beginShape();
vertex(this.bobA.pos.x, this.bobA.pos.y);
// twisting
for (let t = 0.2; t < 0.8; t += 0.2) {
let x = lerp(this.bobA.pos.x, this.bobB.pos.x, t);
let y = lerp(this.bobA.pos.y, this.bobB.pos.y, t);
let noiseVal = noise(x * 0.05, y * 0.05, frameCount * 0.002);
let offsetX = map(noiseVal, 0, 1, -3, 3) * sin(t * PI);
let offsetY = map(noiseVal, 0, 1, -3, 3) * sin(t * PI);
vertex(x + offsetX, y + offsetY);
}
vertex(this.bobB.pos.x, this.bobB.pos.y);
endShape();
// blood from spring
if (random(1) < 0.01) {
let midPoint = p5.Vector.lerp(
createVector(this.bobA.pos.x, this.bobA.pos.y),
createVector(this.bobB.pos.x, this.bobB.pos.y),
random(0.3, 0.7)
);
bloodParticles.push({
pos: createVector(midPoint.x, midPoint.y),
vel: createVector(random(-0.2, 0.2), random(0.5, 1.5)),
size: random(2, 4),
alpha: random(150, 200),
});
}
pop();
}
}
function setupGUI() {
pane = new Pane();
// pane.addBinding(params, "fps");
const helpFolder = pane.addFolder({
title: 'How to play^^',
expanded: true //!!
});
helpFolder.addButton({
title: "Click once: Scatter"
});
helpFolder.addButton({
title: "Click twice: Regroup"
});
helpFolder.addButton({
title: "Click three times: Reset"
});
pane.addBinding(params, "textInput", {
label: "Ur content",
});
pane
.addButton({
label: " ",
title: "Cast!",
})
.on("click", function () {
let str = params.textInput; //?
console.log(str);
//logic:
// remove spaces
// get an array of characters
// replace "chars" with the character
// double-check length
// reset
chars = processString(str);
resetEverything();
});
}
function processString(inputStr) {
const noSpacesStr = inputStr.replace(/\s/g, "");
if (noSpacesStr.length === 0) {
return [];
}
const resultArray = [];
const strLength = noSpacesStr.length;
for (let i = 0; i < 34; i++) {
const randomIndex = Math.floor(Math.random() * strLength);
resultArray.push(noSpacesStr.charAt(randomIndex));
}
return resultArray;
}