|
|
| Zeile 4: |
Zeile 4: |
| <p id="btc-remaining-text" style="margin-top:10px; font-weight:bold;">Lade Daten…</p> | | <p id="btc-remaining-text" style="margin-top:10px; font-weight:bold;">Lade Daten…</p> |
| </div> | | </div> |
|
| |
| <script>
| |
| (function() {
| |
| const canvas = document.getElementById('bitcoinHourglass');
| |
| if (!canvas) return;
| |
| const ctx = canvas.getContext('2d');
| |
| const totalSupply = 21000000;
| |
| let circulating = 0;
| |
| let remaining = 0;
| |
| let animationParticles = [];
| |
|
| |
| async function fetchCirculatingSupply() {
| |
| try {
| |
| const response = await fetch('https://api.coingecko.com/api/v3/coins/bitcoin');
| |
| if (!response.ok) throw new Error('Netzwerkfehler');
| |
| const data = await response.json();
| |
| circulating = data.market_data.circulating_supply;
| |
| remaining = Math.max(totalSupply - circulating, 0);
| |
| document.getElementById('btc-remaining-text').textContent =
| |
| 'Noch ' + remaining.toLocaleString('de-DE') + ' BTC';
| |
| } catch (err) {
| |
| document.getElementById('btc-remaining-text').textContent = 'Fehler beim Laden der Daten';
| |
| console.error(err);
| |
| }
| |
| }
| |
|
| |
| function drawHourglassFrame() {
| |
| ctx.clearRect(0, 0, canvas.width, canvas.height);
| |
| ctx.strokeStyle = '#333';
| |
| ctx.lineWidth = 2;
| |
|
| |
| // Obere Kammer (Dreieck nach unten)
| |
| ctx.beginPath();
| |
| ctx.moveTo(20, 20);
| |
| ctx.lineTo(canvas.width - 20, 20);
| |
| ctx.lineTo(canvas.width/2 + 20, canvas.height/2 - 20);
| |
| ctx.lineTo(canvas.width/2 - 20, canvas.height/2 - 20);
| |
| ctx.closePath();
| |
| ctx.stroke();
| |
|
| |
| // Untere Kammer (Dreieck nach oben)
| |
| ctx.beginPath();
| |
| ctx.moveTo(20, canvas.height - 20);
| |
| ctx.lineTo(canvas.width - 20, canvas.height - 20);
| |
| ctx.lineTo(canvas.width/2 + 20, canvas.height/2 + 20);
| |
| ctx.lineTo(canvas.width/2 - 20, canvas.height/2 + 20);
| |
| ctx.closePath();
| |
| ctx.stroke();
| |
|
| |
| // Verbindungslinien
| |
| ctx.beginPath();
| |
| ctx.moveTo(canvas.width/2 - 20, canvas.height/2 - 20);
| |
| ctx.lineTo(canvas.width/2 - 20, canvas.height/2 + 20);
| |
| ctx.moveTo(canvas.width/2 + 20, canvas.height/2 - 20);
| |
| ctx.lineTo(canvas.width/2 + 20, canvas.height/2 + 20);
| |
| ctx.stroke();
| |
| }
| |
|
| |
| function drawSandLevels() {
| |
| const ratio = remaining / totalSupply;
| |
| const maxHeight = (canvas.height/2 - 30);
| |
| const upperHeight = maxHeight * ratio;
| |
| const lowerHeight = maxHeight * (1 - ratio);
| |
|
| |
| // Obere Kammer füllen
| |
| if (upperHeight > 0) {
| |
| ctx.fillStyle = '#f2c94c';
| |
| ctx.beginPath();
| |
| const topY = 20;
| |
| const baseY = (canvas.height/2 - 20);
| |
| const currentY = baseY - upperHeight;
| |
| const interpolation = (currentY - topY) / (baseY - topY);
| |
| const leftX = 20 + (canvas.width/2 - 20 - 20) * (1 - interpolation);
| |
| const rightX = canvas.width - 20 - (canvas.width/2 - 20 - 20) * (1 - interpolation);
| |
|
| |
| ctx.moveTo(leftX, currentY);
| |
| ctx.lineTo(rightX, currentY);
| |
| ctx.lineTo(canvas.width/2 + 20, baseY);
| |
| ctx.lineTo(canvas.width/2 - 20, baseY);
| |
| ctx.closePath();
| |
| ctx.fill();
| |
| }
| |
|
| |
| // Untere Kammer füllen
| |
| if (lowerHeight > 0) {
| |
| ctx.fillStyle = '#f2c94c';
| |
| ctx.beginPath();
| |
| const bottomY = canvas.height - 20;
| |
| const baseY2 = (canvas.height/2 + 20);
| |
| const currentY2 = baseY2 + lowerHeight;
| |
| const interpolation2 = (currentY2 - baseY2) / (bottomY - baseY2);
| |
| const leftX2 = 20 + (canvas.width/2 - 20 - 20) * interpolation2;
| |
| const rightX2 = canvas.width - 20 - (canvas.width/2 - 20 - 20) * interpolation2;
| |
|
| |
| ctx.moveTo(canvas.width/2 - 20, baseY2);
| |
| ctx.lineTo(canvas.width/2 + 20, baseY2);
| |
| ctx.lineTo(rightX2, currentY2);
| |
| ctx.lineTo(leftX2, currentY2);
| |
| ctx.closePath();
| |
| ctx.fill();
| |
| }
| |
| }
| |
|
| |
| class Particle {
| |
| constructor() {
| |
| this.reset();
| |
| }
| |
| reset() {
| |
| this.x = canvas.width/2 + (Math.random() * 20 - 10);
| |
| this.y = canvas.height/2 - 20;
| |
| this.size = Math.random() * 3 + 2;
| |
| this.vy = Math.random() * 2 + 1;
| |
| this.alpha = 1;
| |
| }
| |
| update() {
| |
| this.y += this.vy;
| |
| if (this.y > canvas.height/2 + 20) {
| |
| this.alpha -= 0.05;
| |
| }
| |
| if (this.alpha <= 0) {
| |
| this.reset();
| |
| }
| |
| }
| |
| draw() {
| |
| ctx.fillStyle = 'rgba(242,201,76,' + this.alpha + ')';
| |
| ctx.beginPath();
| |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
| |
| ctx.fill();
| |
| }
| |
| }
| |
|
| |
| function initParticles(count) {
| |
| for (let i = 0; i < count; i++) {
| |
| animationParticles.push(new Particle());
| |
| }
| |
| }
| |
|
| |
| function animateSand() {
| |
| drawHourglassFrame();
| |
| drawSandLevels();
| |
| animationParticles.forEach(p => {
| |
| p.update();
| |
| p.draw();
| |
| });
| |
| requestAnimationFrame(animateSand);
| |
| }
| |
|
| |
| async function init() {
| |
| await fetchCirculatingSupply();
| |
| initParticles(50);
| |
| animateSand();
| |
| setInterval(async () => {
| |
| await fetchCirculatingSupply();
| |
| }, 5 * 60 * 1000);
| |
| }
| |
|
| |
| init();
| |
| })();
| |
| </script>
| |