système de spirales/lianes
This commit is contained in:
parent
0cbb10cd90
commit
650cd6eab2
|
|
@ -27,6 +27,7 @@ body {
|
|||
padding-bottom: 0;
|
||||
margin-top: 0px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: start;
|
||||
|
|
@ -79,6 +80,8 @@ body {
|
|||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
max-width: 550px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.index-header .now {
|
||||
|
|
@ -393,6 +396,226 @@ body {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* === VINE ASCII ANIMATION === */
|
||||
#vine-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: calc(100vw - 640px);
|
||||
height: 100vh;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#vine-container:hover {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.vine-char {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vine-char:hover {
|
||||
color: #111 !important;
|
||||
}
|
||||
|
||||
.vine-flower {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.vine-flower:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.vine-flower:hover {
|
||||
filter: brightness(1.4);
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
#vine-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flowerFall {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 0.95;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100px) rotate(60deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leafFall {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg) scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(80px) rotate(90deg) scale(0.6);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flowerPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.9;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.falling-flower {
|
||||
animation: flowerFall 2.5s ease-in forwards;
|
||||
}
|
||||
|
||||
.falling-leaf {
|
||||
animation: leafFall 2s ease-in forwards;
|
||||
}
|
||||
|
||||
.vine-flower {
|
||||
animation: flowerPulse 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
#vine-help-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #111;
|
||||
background: transparent;
|
||||
font-family: 'IM Fell English', serif;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
color: #111;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#vine-help-btn:hover {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#vine-flower-counter {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
right: 10px;
|
||||
font-family: 'IM Fell English', serif;
|
||||
font-size: 13px;
|
||||
color: hsl(180, 60%, 70%);
|
||||
text-align: center;
|
||||
width: 22px;
|
||||
line-height: 1;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#vine-help-panel {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 10px;
|
||||
width: 260px;
|
||||
background: #fff;
|
||||
border: 1px solid #111;
|
||||
padding: 0.8rem 1rem;
|
||||
z-index: 10;
|
||||
font-family: 'IM Fell English', serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #111;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#vine-help-panel .close {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 6px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #111;
|
||||
font-family: 'IM Fell English', serif;
|
||||
}
|
||||
|
||||
#vine-help-panel .close:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#vine-help-panel h3 {
|
||||
margin: 0 0 0.6rem 0;
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
#vine-help-panel ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#vine-help-panel li {
|
||||
margin-bottom: 0.4rem;
|
||||
padding-left: 0.8rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#vine-help-panel li::before {
|
||||
content: '—';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#vine-help-panel li kbd {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0 3px;
|
||||
background: #f9f9f9;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* === SWIRL (page d'accueil) === */
|
||||
#swirl-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.swirl-char {
|
||||
display: inline-block;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#vine-container,
|
||||
#swirl-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* === DETAILS / SUMMARY === */
|
||||
summary {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
124
assets/swirl.js
Normal file
124
assets/swirl.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
(function () {
|
||||
const CHARS = ['~', '-', '_', '·', '°', '*', 'o', '+', '×', '=', '≈', '∿', '◦'];
|
||||
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
class Swirl {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.cx = container.offsetWidth / 2 || window.innerWidth / 2;
|
||||
this.cy = container.offsetHeight / 2 || window.innerHeight / 2;
|
||||
this.elements = [];
|
||||
this.frameCount = 0;
|
||||
this.angle = 0;
|
||||
this.radius = 2;
|
||||
this.hue = Math.random() * 360;
|
||||
this.hueSpeed = 0.25;
|
||||
this.angleStep = 0.18;
|
||||
this.radiusStep = 0.55;
|
||||
this.globalRotation = 0;
|
||||
|
||||
this.loop = this.loop.bind(this);
|
||||
requestAnimationFrame(this.loop);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.cx = container.offsetWidth / 2 || window.innerWidth / 2;
|
||||
this.cy = container.offsetHeight / 2 || window.innerHeight / 2;
|
||||
});
|
||||
}
|
||||
|
||||
createChar(x, y, hue, scale) {
|
||||
const color = `hsl(${hue}, 60%, 68%)`;
|
||||
const el = document.createElement('span');
|
||||
el.textContent = pick(CHARS);
|
||||
el.classList.add('swirl-char');
|
||||
el.style.cssText =
|
||||
`position:absolute;` +
|
||||
`left:${x}px;top:${y}px;` +
|
||||
`font-family:"Courier New",Courier,monospace;` +
|
||||
`font-size:15px;line-height:1;` +
|
||||
`color:${color};` +
|
||||
`pointer-events:none;user-select:none;` +
|
||||
`opacity:0.85;`;
|
||||
el.style.transform = `scale(${scale}) rotate(${Math.random() * 360}deg)`;
|
||||
el.style.display = 'inline-block';
|
||||
|
||||
this.container.appendChild(el);
|
||||
this.elements.push({
|
||||
el,
|
||||
born: this.frameCount,
|
||||
maxAge: 900 + Math.floor(Math.random() * 400),
|
||||
});
|
||||
}
|
||||
|
||||
ageElements() {
|
||||
if (this.frameCount % 12 !== 0) return;
|
||||
const now = this.frameCount;
|
||||
let pruneCount = 0;
|
||||
const maxPrune = 12;
|
||||
for (let i = 0; i < this.elements.length && pruneCount < maxPrune; i++) {
|
||||
const item = this.elements[i];
|
||||
const age = now - item.born;
|
||||
if (age >= item.maxAge) {
|
||||
item.el.remove();
|
||||
this.elements.splice(i, 1);
|
||||
i--;
|
||||
pruneCount++;
|
||||
} else if (age > item.maxAge - 150) {
|
||||
const fade = 1 - (age - (item.maxAge - 150)) / 150;
|
||||
item.el.style.opacity = Math.max(0, fade * 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.frameCount++;
|
||||
|
||||
// Rotation globale du tourbillon
|
||||
this.globalRotation += 0.002;
|
||||
|
||||
// Évolution de la couleur
|
||||
this.hue = (this.hue + this.hueSpeed) % 360;
|
||||
|
||||
// Ajouter un nouveau caractère à la spirale
|
||||
if (this.frameCount % 2 === 0) {
|
||||
this.angle += this.angleStep;
|
||||
// L'espacement augmente avec le rayon (spirale qui s'écarte)
|
||||
const dynamicStep = this.radiusStep + (this.radius / 100) * 0.18;
|
||||
this.radius += dynamicStep;
|
||||
|
||||
const effectiveAngle = this.angle + this.globalRotation;
|
||||
const x = this.cx + Math.cos(effectiveAngle) * this.radius;
|
||||
const y = this.cy + Math.sin(effectiveAngle) * this.radius * 0.6;
|
||||
|
||||
// Scale qui grandit avec le rayon
|
||||
const scale = 0.6 + (this.radius / 600) * 0.6;
|
||||
|
||||
this.createChar(x, y, this.hue, scale);
|
||||
}
|
||||
|
||||
// Quand le rayon dépasse l'écran, on recommence plus petit
|
||||
const maxRadius = Math.max(this.cx, this.cy) + 80;
|
||||
if (this.radius > maxRadius) {
|
||||
this.radius = 8 + Math.random() * 12;
|
||||
this.hue = (this.hue + 60 + Math.random() * 120) % 360;
|
||||
}
|
||||
|
||||
this.ageElements();
|
||||
}
|
||||
|
||||
loop() {
|
||||
this.update();
|
||||
requestAnimationFrame(this.loop);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('swirl-container');
|
||||
if (container) {
|
||||
new Swirl(container);
|
||||
}
|
||||
});
|
||||
})();
|
||||
539
assets/vine.js
Normal file
539
assets/vine.js
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
(function () {
|
||||
const CHARS = {
|
||||
down: ['|', '¦', '¡', 'l', 'I', '1'],
|
||||
downRight: ['\\', '╲'],
|
||||
downLeft: ['/', '╱'],
|
||||
leaf: ['.', 'o', '*', "'", '"', '·', '°', '`', '¨', '+'],
|
||||
flower: ['✿', '❀', '✾', '❁', '⚘'],
|
||||
debris: ['°', '·', '¨', '`', ','],
|
||||
};
|
||||
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
function charForAngle(angle) {
|
||||
const deg = angle * 180 / Math.PI;
|
||||
if (deg >= 60 && deg <= 120) return pick(CHARS.down);
|
||||
if (deg < 60) return pick(CHARS.downRight);
|
||||
return pick(CHARS.downLeft);
|
||||
}
|
||||
|
||||
class Vine {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.width = container.offsetWidth || window.innerWidth;
|
||||
this.height = container.offsetHeight || window.innerHeight;
|
||||
this.tips = [];
|
||||
this.elements = [];
|
||||
this.flowers = [];
|
||||
this.frameCount = 0;
|
||||
this.minStep = 24;
|
||||
this.flowerCount = 0;
|
||||
|
||||
this.startVine();
|
||||
this.startVine();
|
||||
this.startVine();
|
||||
this.startVine();
|
||||
|
||||
this.createHelpUI();
|
||||
|
||||
this.loop = this.loop.bind(this);
|
||||
requestAnimationFrame(this.loop);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.width = container.offsetWidth || window.innerWidth;
|
||||
this.height = container.offsetHeight || window.innerHeight;
|
||||
});
|
||||
|
||||
// Interactions
|
||||
this.container.addEventListener('click', (e) => this.handleClick(e));
|
||||
this.container.addEventListener('dblclick', (e) => this.handleDoubleClick(e));
|
||||
this.container.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleRightClick(e);
|
||||
});
|
||||
}
|
||||
|
||||
startVine() {
|
||||
if (this.tips.length >= 12) return;
|
||||
this.tips.push({
|
||||
x: 40 + Math.random() * Math.max(0, this.width - 80),
|
||||
y: -5,
|
||||
angle: Math.PI / 2 + (Math.random() - 0.5) * 0.25,
|
||||
hue: Math.random() * 360,
|
||||
hueSpeed: (Math.random() - 0.5) * 0.3,
|
||||
generation: 0,
|
||||
age: 0,
|
||||
alive: true,
|
||||
nextBranch: 140 + Math.random() * 140,
|
||||
speed: 0.7 + Math.random() * 0.3,
|
||||
distSinceLastChar: 0,
|
||||
});
|
||||
}
|
||||
|
||||
createHelpUI() {
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'vine-help-btn';
|
||||
btn.textContent = '?';
|
||||
this.container.appendChild(btn);
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'vine-help-panel';
|
||||
panel.style.display = 'none';
|
||||
|
||||
const close = document.createElement('button');
|
||||
close.className = 'close';
|
||||
close.innerHTML = '×';
|
||||
close.setAttribute('aria-label', 'fermer');
|
||||
panel.appendChild(close);
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.innerHTML = this.animText('jardinage');
|
||||
panel.appendChild(title);
|
||||
|
||||
const items = [
|
||||
{ k: 'clic', a: 'sur tige', d: 'coupe la liane' },
|
||||
{ k: 'double-clic', a: 'sur tige', d: 'plante une fleur' },
|
||||
{ k: 'clic droit', a: 'sur tige', d: 'plante une fleur' },
|
||||
{ k: 'clic', a: 'sur fleur', d: "l'attrape" },
|
||||
{ k: 'clic', a: 'sur fond', d: 'dévie la racine' },
|
||||
];
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
items.forEach((it) => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML =
|
||||
'<kbd>' + it.k + '</kbd> ' +
|
||||
it.a + ' <span style="opacity:0.5">→</span> ' + it.d;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
panel.appendChild(ul);
|
||||
|
||||
this.container.appendChild(panel);
|
||||
|
||||
const counter = document.createElement('div');
|
||||
counter.id = 'vine-flower-counter';
|
||||
counter.textContent = '0';
|
||||
this.counterEl = counter;
|
||||
this.container.appendChild(counter);
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
close.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
panel.style.display = 'none';
|
||||
});
|
||||
|
||||
panel.addEventListener('click', (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
animText(text) {
|
||||
return text.split('').map((c, i) => {
|
||||
if (c === ' ') return ' ';
|
||||
const delayColor = (i * 0.3).toFixed(1);
|
||||
const delayLevite = (Math.random() * 4).toFixed(2);
|
||||
const duree = (3 + Math.random() * 3).toFixed(2);
|
||||
return `<span style="display:inline-block;animation:colorshift 12s ease-in-out infinite,levite ${duree}s ease-in-out infinite;animation-delay:-${delayColor}s,-${delayLevite}s">${c}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ===== INTERACTIONS =====
|
||||
|
||||
handleClick(e) {
|
||||
const target = e.target;
|
||||
|
||||
// 1. Clic sur une fleur vivante -> attraper
|
||||
if (target.classList.contains('vine-flower') && !target.classList.contains('falling-flower')) {
|
||||
this.catchFlower(target);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Clic sur caractère de tige (pas feuille) -> couper
|
||||
if (target.classList.contains('vine-char') && !target.classList.contains('vine-leaf')) {
|
||||
this.cutVine(parseFloat(target.style.left), parseFloat(target.style.top));
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Clic sur fond vide -> dévier la racine la plus proche
|
||||
if (target === this.container) {
|
||||
this.redirectVine(e.offsetX, e.offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick(e) {
|
||||
const target = e.target;
|
||||
// Double-clic sur caractère de tige -> planter une fleur
|
||||
if (target.classList.contains('vine-char')) {
|
||||
const x = parseFloat(target.style.left);
|
||||
const y = parseFloat(target.style.top);
|
||||
this.plantFlower(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
handleRightClick(e) {
|
||||
const target = e.target;
|
||||
// Clic droit sur caractère de tige -> planter une fleur
|
||||
if (target.classList.contains('vine-char')) {
|
||||
const x = parseFloat(target.style.left);
|
||||
const y = parseFloat(target.style.top);
|
||||
this.plantFlower(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
findClosestTip(x, y, onlyRoots = false) {
|
||||
let closest = null;
|
||||
let closestDist = Infinity;
|
||||
for (const tip of this.tips) {
|
||||
if (onlyRoots && tip.generation > 0) continue;
|
||||
const dx = tip.x - x;
|
||||
const dy = tip.y - y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < closestDist) {
|
||||
closestDist = dist;
|
||||
closest = tip;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
cutVine(clickX, clickY) {
|
||||
const tip = this.findClosestTip(clickX, clickY);
|
||||
if (!tip) return;
|
||||
|
||||
// Effet de débris visuels
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.createDebris(clickX + (Math.random() - 0.5) * 20, clickY + (Math.random() - 0.5) * 20, tip.hue);
|
||||
}
|
||||
|
||||
// Créer deux nouvelles lianes qui divergent
|
||||
const hue = tip.hue;
|
||||
const hueSpeed = tip.hueSpeed;
|
||||
|
||||
this.tips.push({
|
||||
x: clickX,
|
||||
y: clickY,
|
||||
angle: Math.PI / 2 - 0.3 - Math.random() * 0.4,
|
||||
hue,
|
||||
hueSpeed: hueSpeed * (0.8 + Math.random() * 0.4),
|
||||
generation: 0,
|
||||
age: 0,
|
||||
alive: true,
|
||||
nextBranch: 60 + Math.random() * 80,
|
||||
speed: 0.9 + Math.random() * 0.4,
|
||||
distSinceLastChar: 0,
|
||||
});
|
||||
|
||||
this.tips.push({
|
||||
x: clickX,
|
||||
y: clickY,
|
||||
angle: Math.PI / 2 + 0.3 + Math.random() * 0.4,
|
||||
hue,
|
||||
hueSpeed: hueSpeed * (0.8 + Math.random() * 0.4),
|
||||
generation: 0,
|
||||
age: 0,
|
||||
alive: true,
|
||||
nextBranch: 60 + Math.random() * 80,
|
||||
speed: 0.9 + Math.random() * 0.4,
|
||||
distSinceLastChar: 0,
|
||||
});
|
||||
|
||||
// Tuer l'ancienne liane
|
||||
tip.alive = false;
|
||||
}
|
||||
|
||||
plantFlower(x, y) {
|
||||
const tip = this.findClosestTip(x, y);
|
||||
const hue = tip ? tip.hue : Math.random() * 360;
|
||||
this.createFlower(x, y, hue);
|
||||
}
|
||||
|
||||
catchFlower(el) {
|
||||
const idx = this.flowers.findIndex((f) => f.el === el);
|
||||
if (idx === -1) return;
|
||||
const flower = this.flowers[idx];
|
||||
if (flower.dying) return;
|
||||
|
||||
flower.dying = true;
|
||||
flower.el.classList.remove('vine-flower');
|
||||
flower.el.classList.add('falling-flower');
|
||||
flower.el.style.animationDuration = '0.6s';
|
||||
flower.el.style.animationTimingFunction = 'ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
if (flower.el.parentNode) flower.el.remove();
|
||||
}, 700);
|
||||
this.flowers.splice(idx, 1);
|
||||
|
||||
this.flowerCount++;
|
||||
if (this.counterEl) {
|
||||
this.counterEl.textContent = this.flowerCount;
|
||||
}
|
||||
}
|
||||
|
||||
redirectVine(clickX, clickY) {
|
||||
const root = this.findClosestTip(clickX, clickY, true);
|
||||
if (!root) return;
|
||||
|
||||
const dx = clickX - root.x;
|
||||
const dy = clickY - root.y;
|
||||
let targetAngle = Math.atan2(Math.abs(dy), dx);
|
||||
targetAngle = Math.max(0.3, Math.min(Math.PI - 0.3, targetAngle));
|
||||
|
||||
// Transition douce : on s'approche progressivement de l'angle cible
|
||||
const diff = targetAngle - root.angle;
|
||||
root.angle += diff * 0.4;
|
||||
root.speed = Math.min(root.speed * 1.3, 2.5);
|
||||
}
|
||||
|
||||
createDebris(x, y, hue) {
|
||||
const color = this.getColor(hue, y, false);
|
||||
const el = document.createElement('span');
|
||||
el.textContent = pick(CHARS.debris);
|
||||
el.style.cssText =
|
||||
`position:absolute;left:${x}px;top:${y}px;` +
|
||||
`font-family:"Courier New",Courier,monospace;` +
|
||||
`font-size:12px;line-height:1;color:${color};` +
|
||||
`pointer-events:none;user-select:none;` +
|
||||
`opacity:0.8;`;
|
||||
el.style.display = 'inline-block';
|
||||
this.container.appendChild(el);
|
||||
this.elements.push({
|
||||
el,
|
||||
born: this.frameCount,
|
||||
maxAge: 60 + Math.floor(Math.random() * 40),
|
||||
});
|
||||
}
|
||||
|
||||
// ===== LOGIQUE EXISTANTE =====
|
||||
|
||||
getColor(hue, y, isFlower) {
|
||||
const drifted = (hue + y * 0.04 + Math.random() * 6) % 360;
|
||||
const sat = isFlower ? 68 : 58;
|
||||
const lit = isFlower ? 74 : 68;
|
||||
return `hsl(${drifted}, ${sat}%, ${lit}%)`;
|
||||
}
|
||||
|
||||
createChar(x, y, angle, hue, isLeaf) {
|
||||
const color = this.getColor(hue, y, false);
|
||||
const el = document.createElement('span');
|
||||
el.textContent = isLeaf ? pick(CHARS.leaf) : charForAngle(angle);
|
||||
el.classList.add('vine-char');
|
||||
if (isLeaf) el.classList.add('vine-leaf');
|
||||
el.style.cssText =
|
||||
`position:absolute;left:${x}px;top:${y}px;` +
|
||||
`font-family:"Courier New",Courier,monospace;` +
|
||||
`font-size:17px;line-height:1;color:${color};` +
|
||||
`text-shadow:0 0 3px currentColor;` +
|
||||
`pointer-events:auto;user-select:none;` +
|
||||
`opacity:${isLeaf ? 0.55 : 0.85};`;
|
||||
|
||||
const scale = 0.9 + Math.random() * 0.12;
|
||||
const rot = (Math.random() - 0.5) * 5;
|
||||
el.style.transform = `scale(${scale}) rotate(${rot}deg)`;
|
||||
el.style.display = 'inline-block';
|
||||
|
||||
this.container.appendChild(el);
|
||||
const lifeRatio = Math.min(1, y / Math.max(1, this.height));
|
||||
const maxAge = Math.floor(1800 - lifeRatio * 1000 + Math.random() * 300);
|
||||
this.elements.push({
|
||||
el,
|
||||
born: this.frameCount,
|
||||
maxAge,
|
||||
});
|
||||
}
|
||||
|
||||
createFlower(x, y, hue) {
|
||||
if (this.flowers.length >= 8) return;
|
||||
const color = this.getColor(hue, y, true);
|
||||
const el = document.createElement('span');
|
||||
el.textContent = pick(CHARS.flower);
|
||||
el.classList.add('vine-flower');
|
||||
el.style.cssText =
|
||||
`position:absolute;left:${x}px;top:${y}px;` +
|
||||
`font-family:"Courier New",Courier,monospace;` +
|
||||
`font-size:18px;line-height:1;color:${color};` +
|
||||
`text-shadow:0 0 5px currentColor;` +
|
||||
`pointer-events:auto;user-select:none;` +
|
||||
`opacity:0.9;display:inline-block;`;
|
||||
|
||||
this.container.appendChild(el);
|
||||
this.flowers.push({
|
||||
el,
|
||||
x, y,
|
||||
life: 30 + Math.random() * 40,
|
||||
dying: false,
|
||||
});
|
||||
}
|
||||
|
||||
updateFlowers() {
|
||||
for (let i = this.flowers.length - 1; i >= 0; i--) {
|
||||
const f = this.flowers[i];
|
||||
if (f.dying) continue;
|
||||
f.life -= 1;
|
||||
if (f.life <= 0) {
|
||||
f.dying = true;
|
||||
f.el.classList.remove('vine-flower');
|
||||
f.el.classList.add('falling-flower');
|
||||
setTimeout(() => {
|
||||
if (f.el.parentNode) f.el.remove();
|
||||
}, 2600);
|
||||
this.flowers.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ageElements() {
|
||||
if (this.frameCount % 10 !== 0) return;
|
||||
const now = this.frameCount;
|
||||
let pruneCount = 0;
|
||||
const maxPrune = 18;
|
||||
for (let i = 0; i < this.elements.length && pruneCount < maxPrune; i++) {
|
||||
const item = this.elements[i];
|
||||
const age = now - item.born;
|
||||
if (age >= item.maxAge) {
|
||||
if (item.el.classList.contains('vine-leaf') && !item.falling) {
|
||||
item.falling = true;
|
||||
item.el.classList.add('falling-leaf');
|
||||
setTimeout(() => {
|
||||
if (item.el.parentNode) item.el.remove();
|
||||
}, 2100);
|
||||
} else {
|
||||
item.el.remove();
|
||||
}
|
||||
this.elements.splice(i, 1);
|
||||
i--;
|
||||
pruneCount++;
|
||||
} else if (age > item.maxAge - 120 && !item.falling) {
|
||||
const fade = 1 - (age - (item.maxAge - 120)) / 120;
|
||||
item.el.style.opacity = Math.max(0, fade * 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.frameCount++;
|
||||
if (this.frameCount % 3 !== 0) return;
|
||||
|
||||
this.updateFlowers();
|
||||
this.ageElements();
|
||||
|
||||
const newTips = [];
|
||||
|
||||
for (let i = 0; i < this.tips.length; i++) {
|
||||
const tip = this.tips[i];
|
||||
tip.age++;
|
||||
|
||||
tip.hue = (tip.hue + tip.hueSpeed) % 360;
|
||||
if (tip.hue < 0) tip.hue += 360;
|
||||
|
||||
tip.angle += (Math.random() - 0.5) * 0.025;
|
||||
if (tip.angle < 0.3) tip.angle = 0.3;
|
||||
if (tip.angle > Math.PI - 0.3) tip.angle = Math.PI - 0.3;
|
||||
|
||||
const dx = Math.cos(tip.angle);
|
||||
const dy = Math.sin(tip.angle);
|
||||
|
||||
const step = tip.speed * 1.6;
|
||||
tip.x += dx * step;
|
||||
tip.y += dy * step;
|
||||
tip.distSinceLastChar += step;
|
||||
|
||||
if (tip.distSinceLastChar >= this.minStep) {
|
||||
tip.distSinceLastChar = 0;
|
||||
this.createChar(tip.x, tip.y, tip.angle, tip.hue, false);
|
||||
}
|
||||
|
||||
if (tip.generation > 0 && Math.random() < 0.015) {
|
||||
const lx = tip.x + (Math.random() - 0.5) * 14;
|
||||
const ly = tip.y + (Math.random() - 0.5) * 14;
|
||||
this.createChar(lx, ly, tip.angle, tip.hue, true);
|
||||
}
|
||||
|
||||
if (
|
||||
tip.age >= tip.nextBranch &&
|
||||
tip.generation < 2
|
||||
) {
|
||||
tip.nextBranch = tip.age + 140 + Math.random() * 160;
|
||||
const side = Math.random() > 0.5 ? 1 : -1;
|
||||
const spread = 0.35 + Math.random() * 0.55;
|
||||
newTips.push({
|
||||
x: tip.x,
|
||||
y: tip.y,
|
||||
angle: tip.angle + side * spread,
|
||||
hue: tip.hue,
|
||||
hueSpeed: tip.hueSpeed,
|
||||
generation: tip.generation + 1,
|
||||
age: 0,
|
||||
alive: true,
|
||||
nextBranch: 120 + Math.random() * 140,
|
||||
speed: tip.speed * (0.75 + Math.random() * 0.15),
|
||||
distSinceLastChar: 0,
|
||||
});
|
||||
|
||||
this.createFlower(
|
||||
tip.x + (Math.random() - 0.5) * 8,
|
||||
tip.y + (Math.random() - 0.5) * 8,
|
||||
tip.hue
|
||||
);
|
||||
}
|
||||
|
||||
if (tip.generation >= 1 && Math.random() < 0.008) {
|
||||
this.createFlower(
|
||||
tip.x + (Math.random() - 0.5) * 6,
|
||||
tip.y + (Math.random() - 0.5) * 6,
|
||||
tip.hue
|
||||
);
|
||||
}
|
||||
|
||||
// Branches : meurent quand sortent
|
||||
if (tip.generation > 0) {
|
||||
if (
|
||||
tip.y > this.height + 15 ||
|
||||
tip.x < -20 ||
|
||||
tip.x > this.width + 20
|
||||
) {
|
||||
tip.alive = false;
|
||||
}
|
||||
}
|
||||
// Racines : respawn immédiat quand touchent le bas
|
||||
else if (tip.y > this.height + 15) {
|
||||
tip.y = -5;
|
||||
tip.x = 40 + Math.random() * Math.max(0, this.width - 80);
|
||||
tip.angle = Math.PI / 2 + (Math.random() - 0.5) * 0.25;
|
||||
tip.hue = Math.random() * 360;
|
||||
tip.hueSpeed = (Math.random() - 0.5) * 0.3;
|
||||
tip.age = 0;
|
||||
tip.nextBranch = 140 + Math.random() * 140;
|
||||
tip.distSinceLastChar = 0;
|
||||
// Chance de faire apparaître une nouvelle racine (max 6 racines)
|
||||
if (this.tips.filter((t) => t.generation === 0).length < 6 && Math.random() < 0.35) {
|
||||
this.startVine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tips = this.tips.filter((t) => t.alive).concat(newTips);
|
||||
|
||||
// Respawn de secours si jamais il y a trop peu de tips
|
||||
if (this.tips.filter((t) => t.generation === 0).length < 3 && Math.random() < 0.5) {
|
||||
this.startVine();
|
||||
}
|
||||
}
|
||||
|
||||
loop() {
|
||||
this.update();
|
||||
requestAnimationFrame(this.loop);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('vine-container');
|
||||
if (container) {
|
||||
new Vine(container);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -40,6 +40,8 @@
|
|||
<img class="thanks" src="/assets/draws/thxforreading.png" alt="merci d'avoir lu">
|
||||
</div>
|
||||
</div>
|
||||
<div id="vine-container"></div>
|
||||
<script src="../../../assets/main.js"></script>
|
||||
<script src="../../../assets/vine.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@
|
|||
<img class="thanks" src="/assets/draws/thxforreading.png" alt="merci d'avoir lu">
|
||||
</div>
|
||||
</div>
|
||||
<div id="vine-container"></div>
|
||||
<script src="../../../assets/main.js"></script>
|
||||
<script src="../../../assets/vine.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@
|
|||
<img class="thanks" src="/assets/draws/thxforreading.png" alt="merci d'avoir lu">
|
||||
</div>
|
||||
</div>
|
||||
<div id="vine-container"></div>
|
||||
<script src="../../../assets/main.js"></script>
|
||||
<script src="../../../assets/vine.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@
|
|||
<img class="thanks" src="../../../assets/draws/thxforreading.png" alt="merci d'avoir lu">
|
||||
</div>
|
||||
</div>
|
||||
<div id="vine-container"></div>
|
||||
<script src="../../../assets/main.js"></script>
|
||||
<script src="../../../assets/vine.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@
|
|||
</div>
|
||||
<img src="assets/draws/ewen.png" class="perso" alt="">
|
||||
</div>
|
||||
<div id="swirl-container"></div>
|
||||
<script src="assets/main.js"></script>
|
||||
<script src="assets/swirl.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in a new issue