๐ index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Paths We Choose</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
<script src="/libraries/p5.min.js"></script>
</head>
<body>
<div id="intro">
<h1>The Paths We Choose</h1>
<p>
We move forward,<br />
guided by invisible branches of possibility.
</p>
</div>
<div id="quote"></div>
<script src="script.js"></script>
<script>
const quotes = [
"Not all who wander are lost.",
"Every choice becomes a direction.",
"Life is made of small departures.",
"You are always becoming.",
"The journey shapes the traveler.",
"Some paths reveal themselves only after you walk them.",
"Forward is the only honest direction.",
"We arrive at ourselves slowly.",
"Each step carries a quiet consequence.",
"You are made of every road you refused.",
"Time does not rush, we do.",
"Even hesitation is movement.",
"You are walking through your own future.",
"Be gentle with unfinished versions of yourself.",
"Nothing changes until you move.",
"The present is a crossing.",
"All destinations begin as decisions.",
"Your pace is part of your story.",
"What you seek is often seeking you.",
"Becoming is louder than being.",
"Every moment leaves a footprint.",
"You are never standing still.",
"Some doors open inward.",
"The long way teaches more.",
"You are shaped by what you carry.",
"Change does not ask permission.",
"Where you pause matters.",
"You are already in motion.",
"Silence is also a direction.",
"Every arrival contains another departure.",
"Trust the slow unfolding.",
"You learn by continuing.",
"The path listens to your feet.",
"Growth is rarely dramatic.",
"You move, therefore you become.",
"What remains is what you chose.",
"The future is built quietly.",
"You are writing yourself forward.",
"Nothing is wasted in walking.",
"Time bends around decisions.",
"You carry yesterday into tomorrow.",
"Every turn changes you.",
"Movement is memory.",
"You are the sum of crossings.",
"Even stillness travels.",
"Each direction leaves a trace.",
"You inherit every step.",
"Life happens between choices.",
"Your shadow walks ahead sometimes.",
"The road remembers you.",
"Be patient with distance.",
"You are learning how to arrive.",
"Every branch holds a maybe.",
"Forward is not always fast.",
"You unfold by moving.",
];
const quoteEl = document.getElementById("quote");
let quoteIndex = 0;
function showNextQuote() {
quoteEl.style.opacity = 0;
setTimeout(() => {
quoteEl.textContent = `" ${quotes[quoteIndex]} "`;
quoteEl.style.opacity = 1;
quoteIndex = (quoteIndex + 1) % quotes.length;
}, 1500);
}
setTimeout(() => {
document.getElementById("intro").classList.add("hide");
}, 5000);
setTimeout(() => {
showNextQuote();
setInterval(showNextQuote, 8000);
}, 6000);
</script>
</body>
</html>
๐ sketch.js
const SPEED = 1.5;
const BRANCH_COUNT_MIN = 50;
const BRANCH_COUNT_MAX = 100;
const BRANCH_SPREAD = 0.01;
const BRANCH_LENGTH_MIN = 40;
const BRANCH_LENGTH_MAX = 150;
const BRANCH_CURVE_STRENGTH = 0.08;
const BRANCH_GROWTH_SPEED_MIN = 1;
const BRANCH_GROWTH_SPEED_MAX = 4;
const BRANCH_FADE_DISTANCE = 500;
const BRANCH_FADE_SPEED_MIN = 2;
const BRANCH_FADE_SPEED_MAX = 5;
const BRANCH_FADE_DELAY_MIN = 5;
const BRANCH_FADE_DELAY_MAX = 10;
const FOLLOW_SMOOTHNESS = 0.5;
const SELECTION_THRESHOLD = 18;
let pos;
let vel;
let noiseOffset = 0;
let pastTrail = [];
let activeBranches = [];
let selectedBranch = null;
let isFollowingPath = false;
function setup() {
createCanvas(windowWidth, windowHeight);
pos = createVector(width / 2, height / 2);
vel = p5.Vector.fromAngle(0).mult(SPEED);
spawnNewBranches();
}
function draw() {
background(0);
pos.add(vel);
pastTrail.push(pos.copy());
for (let branch of activeBranches) {
branch.update(pos);
}
if (!selectedBranch || selectedBranch.points.length === 0) {
let closestBranch = null;
let minDist = Infinity;
for (let branch of activeBranches) {
if (branch.points.length > 0) {
let d = p5.Vector.dist(pos, branch.points[0]);
if (d < minDist) {
minDist = d;
closestBranch = branch;
}
}
}
if (closestBranch && minDist < SELECTION_THRESHOLD) {
selectedBranch = closestBranch;
isFollowingPath = true;
for (let branch of activeBranches) {
if (branch !== selectedBranch) {
branch.startFading();
}
}
}
}
if (selectedBranch && selectedBranch.points.length > 0) {
let target = selectedBranch.points[0];
let distToTarget = p5.Vector.dist(pos, target);
if (distToTarget < 3) {
selectedBranch.points.shift();
if (selectedBranch.points.length === 0) {
activeBranches = activeBranches.filter((b) => b.opacity > 0);
selectedBranch = null;
isFollowingPath = false;
spawnNewBranches();
}
} else {
let desired = p5.Vector.sub(target, pos);
desired.setMag(SPEED);
vel.lerp(desired, FOLLOW_SMOOTHNESS);
}
}
activeBranches = activeBranches.filter((b) => b.opacity > 0);
translate(width / 2 - pos.x, height / 2 - pos.y);
noFill();
stroke(255, 120);
strokeWeight(1);
beginShape();
for (let p of pastTrail) {
vertex(p.x, p.y);
}
endShape();
pastTrail = pastTrail.filter(
(p) =>
p.x > pos.x - width * 2 &&
p.x < pos.x + width * 2 &&
p.y > pos.y - height * 2 &&
p.y < pos.y + height * 2,
);
for (let branch of activeBranches) {
branch.show();
}
noStroke();
fill(255);
ellipse(pos.x, pos.y, 10);
}
function spawnNewBranches() {
let branchCount = floor(random(BRANCH_COUNT_MIN, BRANCH_COUNT_MAX + 1));
let baseAngle = vel.heading();
for (let i = 0; i < branchCount; i++) {
let spreadAngle = map(i, 0, branchCount - 1, -BRANCH_SPREAD, BRANCH_SPREAD);
activeBranches.push(new Branch(pos.copy(), baseAngle + spreadAngle));
}
}
class Branch {
constructor(startPos, angle) {
this.points = [];
this.opacity = 255;
this.growing = true;
this.growthIndex = 0;
this.maxPoints = floor(random(BRANCH_LENGTH_MIN, BRANCH_LENGTH_MAX));
this.isFading = false;
this.fadeDelay = 0;
this.fadeSpeed = random(BRANCH_FADE_SPEED_MIN, BRANCH_FADE_SPEED_MAX);
this.fullPath = [];
let dir = p5.Vector.fromAngle(angle);
let current = startPos.copy();
let localNoise = random(1000);
for (let i = 0; i < this.maxPoints; i++) {
let noiseVal = noise(localNoise) * 2 - 1;
dir.rotate(noiseVal * BRANCH_CURVE_STRENGTH);
localNoise += 0.05;
current = current.copy().add(dir.copy().mult(3));
this.fullPath.push(current);
}
}
update(currentPos) {
if (this.growing && this.growthIndex < this.fullPath.length) {
let growthSpeed = floor(
random(BRANCH_GROWTH_SPEED_MIN, BRANCH_GROWTH_SPEED_MAX),
);
for (
let i = 0;
i < growthSpeed && this.growthIndex < this.fullPath.length;
i++
) {
this.points.push(this.fullPath[this.growthIndex]);
this.growthIndex++;
}
}
if (this.isFading) {
if (this.fadeDelay > 0) {
this.fadeDelay--;
} else {
this.opacity = max(0, this.opacity - this.fadeSpeed);
}
} else {
if (this.points.length > 0) {
let distToFirst = p5.Vector.dist(currentPos, this.points[0]);
if (distToFirst > BRANCH_FADE_DISTANCE) {
this.isFading = true;
}
}
}
}
startFading() {
this.isFading = true;
this.fadeDelay = floor(
random(BRANCH_FADE_DELAY_MIN, BRANCH_FADE_DELAY_MAX),
);
}
show() {
if (this.points.length < 2) return;
noFill();
stroke(255, this.opacity * 0.6);
strokeWeight(1.5);
beginShape();
curveVertex(this.points[0].x, this.points[0].y);
for (let p of this.points) {
curveVertex(p.x, p.y);
}
if (this.points.length > 0) {
let last = this.points[this.points.length - 1];
curveVertex(last.x, last.y);
}
endShape();
if (this.growing && this.points.length > 0 && !this.isFading) {
let end = this.points[this.points.length - 1];
noStroke();
fill(255, this.opacity * 0.4);
ellipse(end.x, end.y, 4);
}
}
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
๐ style.css
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: black;
font-family: "Cormorant Garamond", serif;
}
canvas {
position: fixed;
top: 0;
left: 0;
z-index: 0;
}
#intro {
position: fixed;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: black;
color: white;
transition: opacity 2.5s ease;
}
#intro.hide {
opacity: 0;
pointer-events: none;
}
#intro h1 {
font-size: 56px;
font-weight: 500;
letter-spacing: 0.04em;
margin-bottom: 24px;
}
#intro p {
font-size: 20px;
opacity: 0.7;
text-align: center;
line-height: 1.6;
}
#quote {
position: fixed;
bottom: 40px;
width: 100%;
text-align: center;
z-index: 1;
color: white;
font-size: 30px;
letter-spacing: 0.03em;
opacity: 0;
transition: opacity 2s ease;
pointer-events: none;
}