evaluation-guidebook / app /src /content /embeds /banner-molecules.html
tfrere's picture
tfrere HF Staff
Clean repository - remove missing LFS files
6afedde
raw
history blame
22.3 kB
<div class="molecular-space"
style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;position:relative;overflow:hidden;"></div>
<script>
(() => {
const ensureAnime = (cb) => {
if (window.anime && typeof window.anime === 'function') return cb();
let s = document.getElementById('anime-cdn-script');
if (!s) {
s = document.createElement('script');
s.id = 'anime-cdn-script';
s.src = 'https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js';
document.head.appendChild(s);
}
const onReady = () => { if (window.anime && typeof window.anime === 'function') cb(); };
s.addEventListener('load', onReady, { once: true });
if (window.anime) onReady();
};
const bootstrap = () => {
const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
const container = (mount && mount.querySelector && mount.querySelector('.molecular-space')) || document.querySelector('.molecular-space');
if (!container) return;
if (container.dataset) {
if (container.dataset.mounted === 'true') return;
container.dataset.mounted = 'true';
}
const canvas = document.createElement('canvas');
canvas.style.display = 'block';
canvas.style.width = '100%';
canvas.style.height = '100%';
container.appendChild(canvas);
const ctx = canvas.getContext('2d');
const getColors = () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
return {
atoms: {
C: isDark ? 'rgba(144, 144, 144, 0.9)' : 'rgba(90, 90, 90, 0.85)',
N: isDark ? 'rgba(78, 165, 183, 0.9)' : 'rgba(50, 130, 160, 0.85)',
O: isDark ? 'rgba(232, 137, 171, 0.9)' : 'rgba(220, 80, 130, 0.85)',
H: isDark ? 'rgba(255, 255, 255, 0.8)' : 'rgba(240, 240, 240, 0.75)',
P: isDark ? 'rgba(206, 192, 250, 0.9)' : 'rgba(138, 100, 220, 0.85)',
S: isDark ? 'rgba(255, 200, 50, 0.9)' : 'rgba(230, 180, 30, 0.85)',
},
bond: isDark ? 'rgba(206, 192, 250, 0.3)' : 'rgba(138, 100, 220, 0.35)',
glow: isDark ? 'rgba(206, 192, 250, 0.3)' : 'rgba(138, 100, 220, 0.25)',
};
};
let colors = getColors();
const observer = new MutationObserver(() => {
colors = getColors();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
let atoms = [];
let bonds = [];
let molecules = [];
let width, height;
const atomSizes = { H: 6, C: 12, N: 11, O: 11, P: 13, S: 13 };
// Définitions de vraies molécules
const moleculeTemplates = {
water: {
name: "H₂O",
atoms: [
{ type: 'O', pos: [0, 0] },
{ type: 'H', pos: [-15, -12] },
{ type: 'H', pos: [15, -12] }
],
bonds: [[0, 1, 1], [0, 2, 1]]
},
co2: {
name: "CO₂",
atoms: [
{ type: 'C', pos: [0, 0] },
{ type: 'O', pos: [-25, 0] },
{ type: 'O', pos: [25, 0] }
],
bonds: [[0, 1, 2], [0, 2, 2]]
},
methane: {
name: "CH₄",
atoms: [
{ type: 'C', pos: [0, 0] },
{ type: 'H', pos: [0, -18] },
{ type: 'H', pos: [17, 9] },
{ type: 'H', pos: [-17, 9] },
{ type: 'H', pos: [0, 12] }
],
bonds: [[0, 1, 1], [0, 2, 1], [0, 3, 1], [0, 4, 1]]
},
ammonia: {
name: "NH₃",
atoms: [
{ type: 'N', pos: [0, 0] },
{ type: 'H', pos: [0, -16] },
{ type: 'H', pos: [-14, 8] },
{ type: 'H', pos: [14, 8] }
],
bonds: [[0, 1, 1], [0, 2, 1], [0, 3, 1]]
},
benzene: {
name: "C₆H₆",
atoms: [
{ type: 'C', pos: [20, 0] },
{ type: 'C', pos: [10, 17] },
{ type: 'C', pos: [-10, 17] },
{ type: 'C', pos: [-20, 0] },
{ type: 'C', pos: [-10, -17] },
{ type: 'C', pos: [10, -17] },
{ type: 'H', pos: [36, 0] },
{ type: 'H', pos: [18, 31] },
{ type: 'H', pos: [-18, 31] },
{ type: 'H', pos: [-36, 0] },
{ type: 'H', pos: [-18, -31] },
{ type: 'H', pos: [18, -31] }
],
bonds: [
[0, 1, 1], [1, 2, 2], [2, 3, 1],
[3, 4, 2], [4, 5, 1], [5, 0, 2],
[0, 6, 1], [1, 7, 1], [2, 8, 1],
[3, 9, 1], [4, 10, 1], [5, 11, 1]
]
},
ethanol: {
name: "C₂H₅OH",
atoms: [
{ type: 'C', pos: [-15, 0] },
{ type: 'C', pos: [15, 0] },
{ type: 'O', pos: [30, 15] },
{ type: 'H', pos: [42, 20] },
{ type: 'H', pos: [-25, -12] },
{ type: 'H', pos: [-25, 12] },
{ type: 'H', pos: [-5, 12] },
{ type: 'H', pos: [25, -12] },
{ type: 'H', pos: [5, -12] }
],
bonds: [
[0, 1, 1], [1, 2, 1], [2, 3, 1],
[0, 4, 1], [0, 5, 1], [0, 6, 1],
[1, 7, 1], [1, 8, 1]
]
},
acetone: {
name: "(CH₃)₂CO",
atoms: [
{ type: 'C', pos: [0, 0] },
{ type: 'O', pos: [0, -25] },
{ type: 'C', pos: [-25, 10] },
{ type: 'C', pos: [25, 10] },
{ type: 'H', pos: [-35, 0] },
{ type: 'H', pos: [-30, 22] },
{ type: 'H', pos: [-15, 18] },
{ type: 'H', pos: [35, 0] },
{ type: 'H', pos: [30, 22] },
{ type: 'H', pos: [15, 18] }
],
bonds: [
[0, 1, 2], [0, 2, 1], [0, 3, 1],
[2, 4, 1], [2, 5, 1], [2, 6, 1],
[3, 7, 1], [3, 8, 1], [3, 9, 1]
]
},
glucose: {
name: "C₆H₁₂O₆",
atoms: [
{ type: 'C', pos: [0, -20] },
{ type: 'C', pos: [20, -10] },
{ type: 'C', pos: [20, 10] },
{ type: 'C', pos: [0, 20] },
{ type: 'C', pos: [-20, 10] },
{ type: 'O', pos: [-20, -10] },
{ type: 'O', pos: [0, -35] },
{ type: 'H', pos: [10, -35] },
{ type: 'O', pos: [35, -15] },
{ type: 'H', pos: [45, -10] },
{ type: 'O', pos: [35, 15] },
{ type: 'H', pos: [45, 10] }
],
bonds: [
[0, 1, 1], [1, 2, 1], [2, 3, 1],
[3, 4, 1], [4, 5, 1], [5, 0, 1],
[0, 6, 1], [6, 7, 1],
[1, 8, 1], [8, 9, 1],
[2, 10, 1], [10, 11, 1]
]
}
};
const resize = () => {
width = container.clientWidth || 800;
height = Math.max(260, Math.round(width / 3));
canvas.width = width;
canvas.height = height;
initMolecules();
};
const initMolecules = () => {
atoms = [];
bonds = [];
molecules = [];
// Sélectionner 3-4 molécules aléatoires
const templateNames = Object.keys(moleculeTemplates);
const numMolecules = 3 + Math.floor(Math.random() * 2);
const selectedTemplates = [];
for (let i = 0; i < numMolecules; i++) {
const template = moleculeTemplates[templateNames[Math.floor(Math.random() * templateNames.length)]];
selectedTemplates.push(template);
}
selectedTemplates.forEach((template, idx) => {
createMoleculeFromTemplate(template, idx, numMolecules);
});
// Animations initiales
atoms.forEach((atom, i) => {
anime({
targets: atom,
scale: 1,
opacity: atom.targetOpacity,
duration: 800,
delay: i * 30,
easing: 'easeOutElastic(1, .6)'
});
});
bonds.forEach((bond, i) => {
anime({
targets: bond,
opacity: bond.targetOpacity,
duration: 600,
delay: 400 + i * 20,
easing: 'easeOutQuad'
});
});
// Cycle: changer de molécules
setInterval(() => {
changeMolecules();
}, 7000 + Math.random() * 3000);
};
const createMoleculeFromTemplate = (template, moleculeIdx, totalMolecules) => {
const marginX = width * 0.12;
const marginY = height * 0.25;
const centerX = marginX + (moleculeIdx / (totalMolecules - 1 || 1)) * (width - 2 * marginX);
const centerY = marginY + (Math.random() - 0.5) * (height - 2 * marginY) * 0.5 + height / 2;
const scale = 1 + Math.random() * 0.3;
const moleculeAtoms = [];
// Créer les atomes
template.atoms.forEach(atomDef => {
const atom = {
x: centerX + atomDef.pos[0] * scale,
y: centerY + atomDef.pos[1] * scale,
baseX: centerX + atomDef.pos[0] * scale,
baseY: centerY + atomDef.pos[1] * scale,
type: atomDef.type,
size: atomSizes[atomDef.type],
scale: 0,
opacity: 0,
targetOpacity: 0.85 + Math.random() * 0.15,
vibrationPhase: Math.random() * Math.PI * 2,
vibrationSpeed: 0.015 + Math.random() * 0.01,
vibrationAmplitude: 0.8 + Math.random() * 1.2,
moleculeIdx: moleculeIdx
};
atoms.push(atom);
moleculeAtoms.push(atom);
});
molecules.push({
atoms: moleculeAtoms,
centerX: centerX,
centerY: centerY,
rotation: 0,
rotationSpeed: (Math.random() - 0.5) * 0.004,
name: template.name
});
// Créer les liaisons
template.bonds.forEach(bondDef => {
bonds.push({
from: moleculeAtoms[bondDef[0]],
to: moleculeAtoms[bondDef[1]],
strength: bondDef[2],
opacity: 0,
targetOpacity: 0.5 + Math.random() * 0.2,
vibrationPhase: Math.random() * Math.PI * 2
});
});
};
const changeMolecules = () => {
// Dissoudre une molécule aléatoire
if (molecules.length === 0) return;
const moleculeToRemove = molecules[Math.floor(Math.random() * molecules.length)];
// Fade out atomes
moleculeToRemove.atoms.forEach(atom => {
anime({
targets: atom,
scale: 0,
opacity: 0,
duration: 600,
easing: 'easeInQuad',
complete: () => {
const idx = atoms.indexOf(atom);
if (idx > -1) atoms.splice(idx, 1);
}
});
});
// Fade out liaisons
const bondsToRemove = bonds.filter(b =>
moleculeToRemove.atoms.includes(b.from) || moleculeToRemove.atoms.includes(b.to)
);
bondsToRemove.forEach(bond => {
anime({
targets: bond,
opacity: 0,
duration: 400,
easing: 'easeInQuad',
complete: () => {
const idx = bonds.indexOf(bond);
if (idx > -1) bonds.splice(idx, 1);
}
});
});
molecules.splice(molecules.indexOf(moleculeToRemove), 1);
// Créer nouvelle molécule
setTimeout(() => {
const templateNames = Object.keys(moleculeTemplates);
const template = moleculeTemplates[templateNames[Math.floor(Math.random() * templateNames.length)]];
const newIdx = molecules.length;
createMoleculeFromTemplate(template, newIdx, molecules.length + 1);
// Animer la nouvelle molécule
const newMolecule = molecules[molecules.length - 1];
newMolecule.atoms.forEach((atom, i) => {
anime({
targets: atom,
scale: 1,
opacity: atom.targetOpacity,
duration: 800,
delay: i * 30,
easing: 'easeOutElastic(1, .6)'
});
});
const newBonds = bonds.filter(b => newMolecule.atoms.includes(b.from));
newBonds.forEach((bond, i) => {
anime({
targets: bond,
opacity: bond.targetOpacity,
duration: 600,
delay: i * 20,
easing: 'easeOutQuad'
});
});
}, 700);
};
const update = () => {
atoms.forEach(atom => {
atom.vibrationPhase += atom.vibrationSpeed;
const vx = Math.cos(atom.vibrationPhase) * atom.vibrationAmplitude;
const vy = Math.sin(atom.vibrationPhase * 1.3) * atom.vibrationAmplitude;
atom.x = atom.baseX + vx;
atom.y = atom.baseY + vy;
});
molecules.forEach(molecule => {
molecule.rotation += molecule.rotationSpeed;
molecule.atoms.forEach(atom => {
const dx = atom.baseX - molecule.centerX;
const dy = atom.baseY - molecule.centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
const currentAngle = Math.atan2(dy, dx);
const newAngle = currentAngle + molecule.rotationSpeed;
atom.baseX = molecule.centerX + Math.cos(newAngle) * dist;
atom.baseY = molecule.centerY + Math.sin(newAngle) * dist;
});
});
bonds.forEach(bond => {
bond.vibrationPhase += 0.015;
});
};
const draw = () => {
ctx.clearRect(0, 0, width, height);
// Draw bonds
bonds.forEach(bond => {
if (bond.opacity < 0.01) return;
const from = bond.from;
const to = bond.to;
const vibration = Math.sin(bond.vibrationPhase) * 0.2;
const rgb = colors.bond.match(/[\d.]+/g);
ctx.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${bond.opacity})`;
ctx.lineWidth = 2 + vibration;
if (bond.strength === 2) {
const dx = to.x - from.x;
const dy = to.y - from.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const offsetX = -dy / dist * 2.5;
const offsetY = dx / dist * 2.5;
ctx.beginPath();
ctx.moveTo(from.x + offsetX, from.y + offsetY);
ctx.lineTo(to.x + offsetX, to.y + offsetY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(from.x - offsetX, from.y - offsetY);
ctx.lineTo(to.x - offsetX, to.y - offsetY);
ctx.stroke();
} else if (bond.strength === 3) {
const dx = to.x - from.x;
const dy = to.y - from.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const offsetX = -dy / dist * 3;
const offsetY = dx / dist * 3;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(from.x + offsetX, from.y + offsetY);
ctx.lineTo(to.x + offsetX, to.y + offsetY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(from.x - offsetX, from.y - offsetY);
ctx.lineTo(to.x - offsetX, to.y - offsetY);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
}
});
// Draw atoms
atoms.forEach(atom => {
if (atom.opacity < 0.01) return;
const finalSize = atom.size * atom.scale;
if (atom.scale > 1.1) {
const glowRadius = finalSize * 2;
const gradient = ctx.createRadialGradient(atom.x, atom.y, 0, atom.x, atom.y, glowRadius);
gradient.addColorStop(0, colors.glow.replace(/[\d.]+\)$/, `${atom.opacity * 0.4})`));
gradient.addColorStop(1, colors.glow.replace(/[\d.]+\)$/, '0)'));
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(atom.x, atom.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = colors.atoms[atom.type];
ctx.beginPath();
ctx.arc(atom.x, atom.y, finalSize, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = `rgba(255, 255, 255, ${atom.opacity * 0.3})`;
ctx.lineWidth = 1.5;
ctx.stroke();
const highlightGradient = ctx.createRadialGradient(
atom.x - finalSize * 0.3, atom.y - finalSize * 0.3, 0,
atom.x, atom.y, finalSize
);
highlightGradient.addColorStop(0, `rgba(255, 255, 255, ${atom.opacity * 0.5})`);
highlightGradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
ctx.fillStyle = highlightGradient;
ctx.beginPath();
ctx.arc(atom.x, atom.y, finalSize, 0, Math.PI * 2);
ctx.fill();
});
update();
requestAnimationFrame(draw);
};
if (window.ResizeObserver) {
const ro = new ResizeObserver(resize);
ro.observe(container);
} else {
window.addEventListener('resize', resize);
}
resize();
draw();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => ensureAnime(bootstrap), { once: true });
} else {
ensureAnime(bootstrap);
}
})();
</script>