Einen Drachenflug mit naturwissenschaftlichen Kennzahlen simulieren

Vibe Coding ist einer der beliebtesten Anwendungsfälle, wenn es um den Einsatz von KI-Tools wie Gemini, Claude und ChatGPT geht. Über einen User-Prompt generiert das Sprachmodell in Kürze einen umfangreichen Code und startet das Programm in der eigenen Chatumgebung. Im folgenden Beispiel habe ich den Gemini-Chat (Free Plan) über die Canvas-Funktion genutzt, um einen Drachenflug in unterschiedlichen Wetterszenarien zu simulieren. 

Als Prompt wurde der vollständige Code einer bereits entwickelten Anwendung kopiert und in die Promptleiste eingefügt. Der Code ist umfangreich, aber mittlerweile lassen sich auch große Codemengen problemlos per Copy & Paste hineinkopieren. Die Canvas-Funktion wird links unten über das Plus-Symbol aktiviert. Das ist wichtig!

An dieser Stelle möchte ich ausdrücklich erwähnen, dass dieser Code ausschließlich in sicheren Umgebungen wie dem Gemini‑Chat ausgeführt werden sollte. Bitte fügt diesen nicht „blind“ über einen HTML-Block in eure Webseite ein. Das kann gut gehen oder eben auch nicht 😉

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interaktive Flugdrachen-Simulation</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
        body {
            font-family: 'Inter', sans-serif;
        }
        /* Custom styling for sleek sliders */
        input[type="range"] {
            -webkit-appearance: none;
            appearance: none;
            background: #e2e8f0;
            border-radius: 9999px;
            height: 8px;
        }
        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            background: #3b82f6;
            cursor: pointer;
            transition: transform 0.1s ease;
        }
        input[type="range"]::-webkit-slider-thumb:hover {
            transform: scale(1.2);
        }
    </style>
</head>
<body class="bg-slate-900 text-slate-100 min-h-screen flex flex-col justify-between overflow-x-hidden">

    <!-- Header -->
    <header class="bg-slate-800/80 backdrop-blur-md border-b border-slate-700/50 p-4 sticky top-0 z-10">
        <div class="max-w-6xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4">
            <div class="flex items-center gap-3">
                <span class="text-3xl">🪁</span>
                <div>
                    <h1 class="text-xl font-bold tracking-tight text-white">Auf & Ab: Flugdrachen-Simulation</h1>
                    <p class="text-xs text-slate-400">Physikalische Strömungs- und Seilsimulation</p>
                </div>
            </div>
            <div id="weather-badge" class="px-3 py-1.5 rounded-full text-xs font-semibold flex items-center gap-2 bg-blue-500/10 text-blue-400 border border-blue-500/20 transition-all duration-500">
                <span id="weather-icon">☀️</span>
                <span id="weather-text">Sonnig & Windstill</span>
            </div>
        </div>
    </header>

    <!-- Main Content Container -->
    <main class="flex-grow max-w-6xl w-full mx-auto p-4 flex flex-col lg:flex-row gap-6 items-stretch">
        
        <!-- Simulation Canvas Area -->
        <div class="flex-grow bg-slate-950 rounded-2xl border border-slate-800 relative shadow-2xl overflow-hidden min-h-[450px] lg:min-h-[550px] flex flex-col">
            <!-- Canvas -->
            <canvas id="simulationCanvas" class="w-full h-full block cursor-grab active:cursor-grabbing"></canvas>
            
            <!-- Real-time HUD / Telemetrie -->
            <div class="absolute top-4 left-4 bg-slate-900/90 backdrop-blur-md border border-slate-700/60 rounded-xl p-3 text-xs space-y-2 pointer-events-none min-w-[180px]">
                <div class="font-semibold text-slate-300 border-b border-slate-700/50 pb-1 mb-1.5 flex justify-between items-center">
                    <span>Telemetrie</span>
                    <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
                </div>
                <div class="flex justify-between">
                    <span class="text-slate-400">Höhe:</span>
                    <span id="hud-altitude" class="font-mono font-bold text-sky-400">0 m</span>
                </div>
                <div class="flex justify-between">
                    <span class="text-slate-400">Windgeschwindigkeit:</span>
                    <span id="hud-wind" class="font-mono font-bold text-amber-400">0 km/h</span>
                </div>
                <div class="flex justify-between">
                    <span class="text-slate-400">Leinenlänge:</span>
                    <span id="hud-rope" class="font-mono font-bold text-indigo-400">0 m</span>
                </div>
                <div class="flex justify-between">
                    <span class="text-slate-400">Seilspannung:</span>
                    <span id="hud-tension" class="font-mono font-bold text-rose-400">Normal</span>
                </div>
            </div>
        </div>

        <!-- Sidebar / Control Panel -->
        <div class="w-full lg:w-80 flex flex-col gap-4 shrink-0">
            
            <!-- Quick Weather Selector -->
            <div class="bg-slate-800/60 backdrop-blur-md border border-slate-700/50 rounded-2xl p-5 space-y-4">
                <h2 class="text-md font-bold text-white flex items-center gap-2">
                    <span>☁️</span> Wetter-Steuerung
                </h2>
                
                <!-- Auto Weather Toggle -->
                <label class="flex items-center justify-between cursor-pointer bg-slate-900/40 p-3 rounded-xl border border-slate-700/30 hover:bg-slate-900/60 transition">
                    <span class="text-sm font-medium text-slate-300">Automatischer Wechsel</span>
                    <div class="relative">
                        <input id="autoWeather" type="checkbox" checked class="sr-only peer">
                        <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:width-5 after:transition-all peer-checked:bg-blue-600"></div>
                    </div>
                </label>

                <!-- Windstärke Slider -->
                <div class="space-y-2">
                    <div class="flex justify-between text-xs font-semibold text-slate-300">
                        <span>Windstärke (Manuell)</span>
                        <span id="windVal" class="text-amber-400 font-mono">15 km/h</span>
                    </div>
                    <input id="windSlider" type="range" min="0" max="65" value="15" class="w-full">
                    <div class="flex justify-between text-[10px] text-slate-500">
                        <span>Windstill</span>
                        <span>Mäßig</span>
                        <span>Sturm (Max)</span>
                    </div>
                </div>
            </div>

            <!-- Rope & Flight Controls -->
            <div class="bg-slate-800/60 backdrop-blur-md border border-slate-700/50 rounded-2xl p-5 space-y-4 flex-grow flex flex-col justify-between">
                <div class="space-y-4">
                    <h2 class="text-md font-bold text-white flex items-center gap-2">
                        <span>🧵</span> Leinen-Steuerung
                    </h2>

                    <!-- Schnurlänge Slider -->
                    <div class="space-y-2">
                        <div class="flex justify-between text-xs font-semibold text-slate-300">
                            <span>Schnurlänge</span>
                            <span id="ropeVal" class="text-indigo-400 font-mono">150 m</span>
                        </div>
                        <input id="ropeSlider" type="range" min="60" max="320" value="150" class="w-full">
                        <div class="flex justify-between text-[10px] text-slate-500">
                            <span>Kurz</span>
                            <span>Mittel</span>
                            <span>Lang (Max)</span>
                        </div>
                    </div>

                    <!-- Instant Action Buttons -->
                    <div class="grid grid-cols-2 gap-2 pt-2">
                        <button id="btnShorten" class="bg-slate-700 hover:bg-slate-600 active:bg-slate-500 text-xs font-semibold py-2.5 px-3 rounded-xl transition flex items-center justify-center gap-1.5 border border-slate-600/30">
                            🔽 Einholen
                        </button>
                        <button id="btnLengthen" class="bg-slate-700 hover:bg-slate-600 active:bg-slate-500 text-xs font-semibold py-2.5 px-3 rounded-xl transition flex items-center justify-center gap-1.5 border border-slate-600/30">
                            🔼 Auslassen
                        </button>
                    </div>
                </div>

                <!-- Simulation Info/Tips -->
                <div class="bg-slate-900/40 border border-slate-700/30 rounded-xl p-3 text-xs text-slate-400 space-y-2.5 mt-4">
                    <p class="font-semibold text-slate-300 flex items-center gap-1">
                        💡 Tipp:
                    </p>
                    <p class="leading-relaxed">
                        Nutze die Regler, um die Windverhältnisse und die Schnur anzupassen. Der Drachen verhält sich dank integrierter Strömungsphysik völlig dynamisch und gleicht Böen flexibel aus.
                    </p>
                    <div class="pt-2 border-t border-slate-700/30 text-[10px] flex justify-between items-center text-slate-500">
                        <span>Physik-Tick: 60Hz</span>
                        <span>Seilsicher: Ja</span>
                    </div>
                </div>
            </div>
            
        </div>
    </main>

    <!-- Footer -->
    <footer class="bg-slate-950/80 border-t border-slate-800 py-4 px-6 text-center text-xs text-slate-500 mt-6">
        <p>© 2026 Interaktive Drachen-Simulation. Entwickelt mit HTML5 Canvas & moderner Strömungsdynamik.</p>
    </footer>

    <!-- Simulation Script -->
    <script>
        // DOM Elements
        const canvas = document.getElementById('simulationCanvas');
        const ctx = canvas.getContext('2d');
        const windSlider = document.getElementById('windSlider');
        const windVal = document.getElementById('windVal');
        const ropeSlider = document.getElementById('ropeSlider');
        const ropeVal = document.getElementById('ropeVal');
        const autoWeatherCheckbox = document.getElementById('autoWeather');
        const btnShorten = document.getElementById('btnShorten');
        const btnLengthen = document.getElementById('btnLengthen');
        const weatherBadge = document.getElementById('weather-badge');
        const weatherIcon = document.getElementById('weather-icon');
        const weatherText = document.getElementById('weather-text');

        // HUD Elements
        const hudAltitude = document.getElementById('hud-altitude');
        const hudWind = document.getElementById('hud-wind');
        const hudRope = document.getElementById('hud-rope');
        const hudTension = document.getElementById('hud-tension');

        // State & Physics Variables
        let width = canvas.width = canvas.offsetWidth;
        let height = canvas.height = canvas.offsetHeight;

        // Position of the woman holding the string (anchor point)
        const anchor = {
            x: 180,
            y: height - 100
        };

        // Kite State Object
        const kite = {
            x: 400,
            y: height - 350,
            vx: 0,
            vy: 0,
            angle: -Math.PI / 6,
            targetAngle: -Math.PI / 6,
            tail: [], // Points for the tail animation
            tailLength: 15,
            width: 50,
            height: 70,
            stability: 0.15 // dampening factor
        };

        // Initialize Kite Tail
        for (let i = 0; i < kite.tailLength; i++) {
            kite.tail.push({ x: kite.x, y: kite.y });
        }

        // Environment Variables
        let windSpeed = 15; // Current target/actual wind speed
        let actualWindSpeed = 15; // Interpolated for smooth transitions
        let autoWeatherTime = 0; // Timer for auto cycling
        let targetRopeLength = 150;
        let actualRopeLength = 150;

        // Environment Particles (Wind lines, rain)
        const particles = [];
        const maxParticles = 60;
        for (let i = 0; i < maxParticles; i++) {
            particles.push({
                x: Math.random() * width,
                y: Math.random() * (height - 120),
                length: 10 + Math.random() * 30,
                speed: 2 + Math.random() * 5
            });
        }

        // Handle Resize
        window.addEventListener('resize', () => {
            width = canvas.width = canvas.offsetWidth;
            height = canvas.height = canvas.offsetHeight;
            anchor.y = height - 100;
        });

        // Setup Controls Listeners
        windSlider.addEventListener('input', (e) => {
            windSpeed = parseFloat(e.target.value);
            windVal.textContent = `${Math.round(windSpeed)} km/h`;
            if (windSpeed > 0) {
                autoWeatherCheckbox.checked = false;
            }
        });

        ropeSlider.addEventListener('input', (e) => {
            targetRopeLength = parseFloat(e.target.value);
            ropeVal.textContent = `${Math.round(targetRopeLength)} m`;
        });

        btnShorten.addEventListener('click', () => {
            let newVal = Math.max(parseFloat(ropeSlider.min), targetRopeLength - 30);
            targetRopeLength = newVal;
            ropeSlider.value = newVal;
            ropeVal.textContent = `${Math.round(newVal)} m`;
        });

        btnLengthen.addEventListener('click', () => {
            let newVal = Math.min(parseFloat(ropeSlider.max), targetRopeLength + 30);
            targetRopeLength = newVal;
            ropeSlider.value = newVal;
            ropeVal.textContent = `${Math.round(newVal)} m`;
        });

        // Weather configuration mapping
        const weatherConfigs = [
            { threshold: 5, name: "Windstill", icon: "☀️", bg: ["#1e3a8a", "#0284c7"], textClass: "text-blue-400", badgeClass: "bg-blue-500/10 border-blue-500/20" },
            { threshold: 18, name: "Leichte Brise", icon: "🌤️", bg: ["#0f172a", "#0284c7"], textClass: "text-sky-400", badgeClass: "bg-sky-500/10 border-sky-500/20" },
            { threshold: 38, name: "Frischer Wind", icon: "💨", bg: ["#0f172a", "#475569"], textClass: "text-amber-400", badgeClass: "bg-amber-500/10 border-amber-500/20" },
            { threshold: 99, name: "Sturmböen", icon: "⛈️", bg: ["#020617", "#1e293b"], textClass: "text-rose-400", badgeClass: "bg-rose-500/10 border-rose-500/20" }
        ];

        function updateWeatherUI(speed) {
            const config = weatherConfigs.find(c => speed <= c.threshold) || weatherConfigs[weatherConfigs.length - 1];
            weatherIcon.textContent = config.icon;
            weatherText.textContent = `${config.name} (${Math.round(speed)} km/h)`;
            
            // Clear class lists & update
            weatherBadge.className = `px-3 py-1.5 rounded-full text-xs font-semibold flex items-center gap-2 border transition-all duration-500 ${config.textClass} ${config.badgeClass}`;
        }

        // Background Sky Gradient Dynamic Interpolation based on Wind Speed
        function getSkyGradient() {
            // Find current weather color profile
            const currentConfig = weatherConfigs.find(c => actualWindSpeed <= c.threshold) || weatherConfigs[weatherConfigs.length - 1];
            
            const gradient = ctx.createLinearGradient(0, 0, 0, height);
            gradient.addColorStop(0, currentConfig.bg[0]);
            gradient.addColorStop(1, currentConfig.bg[1]);
            return gradient;
        }

        // Main Simulation/Physics Loop
        function update(time) {
            // 1. Handle Automatic Weather Changes
            if (autoWeatherCheckbox.checked) {
                autoWeatherTime += 0.002;
                // Periodic wave generating organic wind fluctuations (range 4 to 58 km/h)
                let baseWind = 28 + Math.sin(autoWeatherTime) * 20 + Math.cos(autoWeatherTime * 2.5) * 8;
                windSpeed = Math.max(3, Math.min(62, baseWind));
                windSlider.value = Math.round(windSpeed);
                windVal.textContent = `${Math.round(windSpeed)} km/h`;
            }

            // Smoothly interpolate actual values for physics comfort
            actualWindSpeed += (windSpeed - actualWindSpeed) * 0.05;
            actualRopeLength += (targetRopeLength - actualRopeLength) * 0.05;

            updateWeatherUI(actualWindSpeed);

            // Update HUD values
            const altitude = Math.max(0, Math.round((anchor.y - kite.y) / 2));
            hudAltitude.textContent = `${altitude} m`;
            hudWind.textContent = `${Math.round(actualWindSpeed)} km/h`;
            hudRope.textContent = `${Math.round(actualRopeLength)} m`;

            // Calculate current distance and vectors
            const dx = kite.x - anchor.x;
            const dy = kite.y - anchor.y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            // Determine Seilspannung (Rope Tension) based on distance
            if (distance > actualRopeLength - 5) {
                hudTension.textContent = actualWindSpeed > 45 ? "Sehr Stark" : "Stark";
                hudTension.className = actualWindSpeed > 45 ? "font-mono font-bold text-rose-500" : "font-mono font-bold text-amber-500";
            } else if (distance < actualRopeLength - 40) {
                hudTension.textContent = "Durchhängend";
                hudTension.className = "font-mono font-bold text-sky-400";
            } else {
                hudTension.textContent = "Optimal";
                hudTension.className = "font-mono font-bold text-emerald-400";
            }

            // 2. Physics of the Kite
            // A. Gravity
            const gravity = 0.22;
            kite.vy += gravity;

            // B. Wind Forces (Aerodynamic Lift and Drag)
            // Wind pushes horizontally.
            // Lift is created by the kite's angle of attack and wind velocity squared.
            const windForceX = actualWindSpeed * 0.15;
            // Turbulence simulation: Organic fluctuation in wind direction/intensity
            const turbulence = Math.sin(time * 0.005) * (actualWindSpeed * 0.04) + (Math.random() - 0.5) * (actualWindSpeed * 0.05);
            
            kite.vx += windForceX + turbulence;

            // Lift: depends on angle of the kite vs horizontal wind flow
            const attackAngle = kite.angle;
            const liftForce = Math.max(0, Math.cos(attackAngle) * actualWindSpeed * 0.28);
            kite.vy -= liftForce;

            // Apply drag/friction (air resistance)
            kite.vx *= 0.93;
            kite.vy *= 0.93;

            // Apply speeds to positions
            kite.x += kite.vx;
            kite.y += kite.vy;

            // C. Constraint: The Rope
            const nextDx = kite.x - anchor.x;
            const nextDy = kite.y - anchor.y;
            const nextDist = Math.sqrt(nextDx * nextDx + nextDy * nextDy);

            // If the kite tries to exceed the rope length, pull it back (rigidbody constraint)
            if (nextDist > actualRopeLength) {
                const ratio = actualRopeLength / nextDist;
                // Constrain position exactly to the sphere of length
                kite.x = anchor.x + nextDx * ratio;
                kite.y = anchor.y + nextDy * ratio;

                // Project velocity vector onto tangent of the rope limit
                // This simulates the rigid string pulling back on movement
                const nx = nextDx / nextDist; // Normal vector
                const ny = nextDy / nextDist;
                
                // Normal velocity component
                const velNormal = kite.vx * nx + kite.vy * ny;
                
                // If moving outwards, kill that velocity component and bounce slightly
                if (velNormal > 0) {
                    kite.vx -= velNormal * nx * 1.05; 
                    kite.vy -= velNormal * ny * 1.05;
                }
            }

            // D. **GUARANTEE CONSTRAINT**: Keep Kite securely in view/canvas bounds
            // The kite cannot fly off-screen! Soft boundaries apply gentle force, hard boundaries clamp.
            const margin = 60;
            // Left & Right Limit
            if (kite.x < margin) {
                kite.x = margin;
                kite.vx = Math.abs(kite.vx) * 0.2;
            } else if (kite.x > width - margin) {
                kite.x = width - margin;
                kite.vx = -Math.abs(kite.vx) * 0.2;
            }

            // Top & Bottom limit
            if (kite.y < margin) {
                kite.y = margin;
                kite.vy = Math.abs(kite.vy) * 0.2;
            } else if (kite.y > anchor.y - 40) { // Limit how close to the ground it can get
                kite.y = anchor.y - 40;
                kite.vy = -Math.abs(kite.vy) * 0.2;
                // Add minor aerodynamic bounce from ground thermal
                kite.vy -= 1.5;
            }

            // E. Dynamic Angle of the Kite
            // The kite aligns with the relative wind flow and string tension
            const stringAngle = Math.atan2(kite.y - anchor.y, kite.x - anchor.x);
            // Kite angle is a blend of the rope pull direction and aerodynamic lift alignment
            kite.targetAngle = stringAngle + Math.PI/2 + (actualWindSpeed * 0.003) - 0.2;
            
            // Flutter effect (vibration under high wind)
            const flutter = Math.sin(time * 0.08) * (actualWindSpeed * 0.0018);
            kite.angle += (kite.targetAngle - kite.angle) * kite.stability + flutter;

            // F. Tail Physics (Chain / Verlet-like follow-up)
            // First tail joint follows kite anchor position
            const tailBaseX = kite.x - Math.sin(kite.angle) * (kite.height / 2);
            const tailBaseY = kite.y + Math.cos(kite.angle) * (kite.height / 2);
            
            kite.tail[0] = { x: tailBaseX, y: tailBaseY };

            // Tail segments flow with gravity and wind
            const tailSpacing = 12;
            for (let i = 1; i < kite.tailLength; i++) {
                const prev = kite.tail[i - 1];
                const curr = kite.tail[i];
                
                // Wind pushes tail segments right, gravity pulls down
                curr.x += actualWindSpeed * 0.12 + Math.sin(time * 0.05 + i) * 0.8;
                curr.y += gravity * 4;

                // Keep tail segment constrained to preceding segment
                const tdx = curr.x - prev.x;
                const tdy = curr.y - prev.y;
                const dist = Math.sqrt(tdx * tdx + tdy * tdy);
                
                if (dist > tailSpacing) {
                    const ratio = tailSpacing / dist;
                    curr.x = prev.x + tdx * ratio;
                    curr.y = prev.y + tdy * ratio;
                }
            }

            // G. Update environment wind particles
            particles.forEach(p => {
                p.x += p.speed * (0.8 + actualWindSpeed * 0.15);
                // Slightly diagonal wind direction based on force
                p.y += (actualWindSpeed * 0.01) + Math.sin(time * 0.001 + p.x * 0.01) * 0.1;

                if (p.x > width + 50) {
                    p.x = -50;
                    p.y = Math.random() * (height - 150);
                }
            });
        }

        // Draw helper functions
        function drawScene(time) {
            // A. Clear and Sky Background
            ctx.fillStyle = getSkyGradient();
            ctx.fillRect(0, 0, width, height);

            // B. Draw Wind/Rain Particles
            ctx.strokeStyle = actualWindSpeed > 45 ? 'rgba(200, 220, 255, 0.25)' : 'rgba(255, 255, 255, 0.15)';
            ctx.lineWidth = actualWindSpeed > 45 ? 1.5 : 1;
            
            particles.forEach(p => {
                ctx.beginPath();
                ctx.moveTo(p.x, p.y);
                // If sturmy, make lines look like heavy rain/gusts
                if (actualWindSpeed > 45) {
                    ctx.lineTo(p.x + p.length * 1.2, p.y + p.length * 0.4);
                } else {
                    ctx.lineTo(p.x + p.length, p.y);
                }
                ctx.stroke();
            });

            // C. Draw the Sun or Storm Clouds
            if (actualWindSpeed < 30) {
                // Bright Sun
                const sunOpacity = Math.max(0.1, 1 - (actualWindSpeed / 30));
                ctx.fillStyle = `rgba(253, 224, 71, ${sunOpacity})`;
                ctx.shadowColor = `rgba(253, 224, 71, 0.5)`;
                ctx.shadowBlur = 30;
                ctx.beginPath();
                ctx.arc(width - 120, 100, 50, 0, Math.PI * 2);
                ctx.fill();
                ctx.shadowBlur = 0; // Reset shadow
            }
            if (actualWindSpeed > 20) {
                // Storm Clouds rising from top left / hovering
                const cloudOpacity = Math.min(0.85, (actualWindSpeed - 15) / 45);
                ctx.fillStyle = `rgba(100, 116, 139, ${cloudOpacity})`;
                ctx.beginPath();
                ctx.arc(100, 80, 70, 0, Math.PI * 2);
                ctx.arc(200, 100, 90, 0, Math.PI * 2);
                ctx.arc(280, 90, 60, 0, Math.PI * 2);
                ctx.fill();
            }

            // D. Draw Green Grass Hills (Ground)
            ctx.fillStyle = '#166534'; // Dark green
            ctx.beginPath();
            ctx.ellipse(width / 3, height + 80, width * 0.7, 180, 0, 0, Math.PI * 2);
            ctx.fill();

            ctx.fillStyle = '#15803d'; // Medium green
            ctx.beginPath();
            ctx.ellipse(width * 0.7, height + 90, width * 0.8, 170, 0, 0, Math.PI * 2);
            ctx.fill();

            // E. Draw Woman Holding the Kite
            // Base anchor coordinates are anchor.x, anchor.y
            const womanColor = '#f1f5f9';
            ctx.strokeStyle = '#020617';
            ctx.lineWidth = 2.5;

            // Draw stylized woman silhouette/illustration
            const wx = anchor.x;
            const wy = anchor.y;

            // Hair/Head
            ctx.fillStyle = '#f59e0b'; // Blonde hair
            ctx.beginPath();
            ctx.arc(wx - 5, wy + 20, 11, 0, Math.PI * 2);
            ctx.fill();

            ctx.fillStyle = '#ffedd5'; // Skin
            ctx.beginPath();
            ctx.arc(wx - 2, wy + 23, 8, 0, Math.PI * 2);
            ctx.fill();

            // Body / Dress (Wind animation on dress)
            ctx.fillStyle = '#ec4899'; // Pink dress
            ctx.beginPath();
            ctx.moveTo(wx - 5, wy + 31);
            ctx.lineTo(wx - 15 + Math.sin(time * 0.04) * (actualWindSpeed * 0.1), wy + 75); // Flutters in wind
            ctx.lineTo(wx + 10, wy + 75);
            ctx.lineTo(wx + 5, wy + 31);
            ctx.closePath();
            ctx.fill();

            // Left Arm holding rope (drawn pointing up towards the kite)
            const angleToKite = Math.atan2(kite.y - wy, kite.x - wx);
            const handX = wx + Math.cos(angleToKite) * 22;
            const handY = wy + 34 + Math.sin(angleToKite) * 22;

            ctx.strokeStyle = '#ffedd5'; // Skin color arm
            ctx.lineWidth = 4;
            ctx.lineCap = 'round';
            ctx.beginPath();
            ctx.moveTo(wx + 2, wy + 34);
            ctx.lineTo(handX, handY);
            ctx.stroke();

            // Anchor point updated to exact hand position
            const actualAnchorX = handX;
            const actualAnchorY = handY;

            // F. Draw the Kite String (Rope)
            // Modelled as a beautiful quadratic curve depending on wind strength.
            // If wind is high, rope is straight. If low, it sags downwards.
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.75)';
            ctx.lineWidth = 1.8;
            ctx.beginPath();
            ctx.moveTo(actualAnchorX, actualAnchorY);

            // Compute midpoint and apply downward sag based on wind deficiency
            const midX = (actualAnchorX + kite.x) / 2;
            const midY = (actualAnchorY + kite.y) / 2;
            const ropeSag = Math.max(0, (50 - actualWindSpeed) * (actualRopeLength * 0.004)); // Sag increases with low wind and long rope
            
            // Quadratic curve sag control point
            const controlX = midX;
            const controlY = midY + ropeSag;

            ctx.quadraticCurveTo(controlX, controlY, kite.x, kite.y);
            ctx.stroke();

            // G. Draw the Kite Tail
            ctx.lineWidth = 3;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';
            ctx.strokeStyle = '#f59e0b'; // Amber tail string
            
            ctx.beginPath();
            ctx.moveTo(kite.tail[0].x, kite.tail[0].y);
            for (let i = 1; i < kite.tailLength; i++) {
                ctx.lineTo(kite.tail[i].x, kite.tail[i].y);
            }
            ctx.stroke();

            // Draw pretty bows/ribbons on the tail
            for (let i = 2; i < kite.tailLength; i += 3) {
                const pt = kite.tail[i];
                // Rotate bow perpendicular to tail direction
                const nextPt = kite.tail[i - 1] || pt;
                const segmentAngle = Math.atan2(pt.y - nextPt.y, pt.x - nextPt.x);
                
                ctx.save();
                ctx.translate(pt.x, pt.y);
                ctx.rotate(segmentAngle);
                
                // Color alternates
                ctx.fillStyle = i % 2 === 0 ? '#3b82f6' : '#ef4444'; // Blue and Red bows
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.ellipse(-4, -6, 4, 8, Math.PI / 4, 0, Math.PI * 2);
                ctx.ellipse(-4, 6, 4, 8, -Math.PI / 4, 0, Math.PI * 2);
                ctx.fill();

                // Center knot
                ctx.fillStyle = '#ffffff';
                ctx.beginPath();
                ctx.arc(0, 0, 2, 0, Math.PI * 2);
                ctx.fill();
                
                ctx.restore();
            }

            // H. Draw the Kite Structure
            ctx.save();
            ctx.translate(kite.x, kite.y);
            ctx.rotate(kite.angle);

            // Shadow for depth effect
            ctx.shadowColor = 'rgba(0, 0, 0, 0.35)';
            ctx.shadowBlur = 15;
            ctx.shadowOffsetY = 10;

            const kw = kite.width;
            const kh = kite.height;

            // Draw Colorful Segments (Classic Diamond)
            // Top-Left Segment (Red)
            ctx.fillStyle = '#ef4444';
            ctx.beginPath();
            ctx.moveTo(0, -kh/2);
            ctx.lineTo(-kw/2, -kh/10);
            ctx.lineTo(0, -kh/10);
            ctx.closePath();
            ctx.fill();

            // Top-Right Segment (Yellow)
            ctx.fillStyle = '#eab308';
            ctx.beginPath();
            ctx.moveTo(0, -kh/2);
            ctx.lineTo(kw/2, -kh/10);
            ctx.lineTo(0, -kh/10);
            ctx.closePath();
            ctx.fill();

            // Bottom-Left Segment (Blue)
            ctx.fillStyle = '#3b82f6';
            ctx.beginPath();
            ctx.moveTo(0, -kh/10);
            ctx.lineTo(-kw/2, -kh/10);
            ctx.lineTo(0, kh/2);
            ctx.closePath();
            ctx.fill();

            // Bottom-Right Segment (Green)
            ctx.fillStyle = '#10b981';
            ctx.beginPath();
            ctx.moveTo(0, -kh/10);
            ctx.lineTo(kw/2, -kh/10);
            ctx.lineTo(0, kh/2);
            ctx.closePath();
            ctx.fill();

            // Reset shadow
            ctx.shadowColor = 'transparent';
            ctx.shadowBlur = 0;
            ctx.shadowOffsetY = 0;

            // Wooden Spars (Cross framework)
            ctx.strokeStyle = '#78350f'; // Dark amber wood
            ctx.lineWidth = 2.5;
            
            // Vertical spar
            ctx.beginPath();
            ctx.moveTo(0, -kh/2);
            ctx.lineTo(0, kh/2);
            ctx.stroke();

            // Horizontal spar
            ctx.beginPath();
            ctx.moveTo(-kw/2, -kh/10);
            ctx.lineTo(kw/2, -kh/10);
        

Der untere Screenshot zeigt die fertig programmierte Simulation im Split-Screen-Modus. Auf der linken Seite kann ich einen Chat nutzen, um mit Folgeprompts die Animation über Version 1, 2, 3 bis hin zu Version X anzupassen. Auf der rechten Seite wird die interaktive Anwendung dargestellt, die automatisch startet und über regelmäßige Wetteranpassungen einzelne Szenarien durchspielt.

Der von einer Person gehaltene Drache verändert je nach Bedingung seinen Flugstil und gleichzeitig ändern sich oben links die Kennzahlen wie „Flughöhe“ oder „Spannung“. Aktuell ist schönes Wetter und der Drache fliegt bei moderater Flughöhe „entspannt“ durch die Luft.

Klicke ich oberhalb der Simulation auf den Button „Code“, so wird mir der vollständig generierte Programmiercode angezeigt, auf dem diese Simulation basiert. Über den Button „Vorschau“ gelangt man wieder zurück zur Animation. Hier ließe sich problemlos hin und her wechseln. Auch ein manuelles Anpassen des Codes ist so möglich.

Mittlerweile haben sich die Wetterbedingungen im Gemini-Chat rasant geändert und nach einem Wolkenaufzug fängt es zusätzlich an zu regnen und zu stürmen. Der Drachenflug  wird so „hektischer“ und benötigt mehr Kraftanstrengung seitens des Schnurhalters. Aber wie viel Kraft benötige ich nun, um den Drachen sicher festhalten zu können? 

Hierzu habe ich rechts unten ein Feld integriert, das über die Kennzahl „Hebelkraft“ die maximale Kraft berechnet. Dies ergibt sich aus der angreifenden Windkraft im Schwerpunkt des Drachens und der Schnurlänge. Im Handumdrehen wechsle ich mit einem Prompt von einer Simulation, die so ungefähr für den Grundschulbereich geeignet ist, hin zum ersten Semester für Bauingenieurinnen & Bauingenieure und der Technischen Mechanik (TM 1). Genau hier zeigt das Vibe Coding seine Stärken! Der Code lässt sich je nach Use Case beliebig anpassen.

Abschließend zeigt sich wieder die Sonne und die Person hat nun ihren Drachen eingeholt, um wohl erstmal ein Eis essen zu gehen. Auf der linken Seite im Chat habe ich noch einen Folgeprompt ausgeführt, da die Zoom-Einstellungen nicht optimal waren bzw. ich unnötigerweise hoch- und runterscrollen musste, um die gesamte Animation einsehen zu können. Solche Kleinigkeiten bezüglich Design und Funktionalität können jederzeit im Rahmen eines Vibe-Codings vorgenommen werden. Viel Erfolg!

FOLGEPROMPT: Nun stelle die Simulation rechts so dar, dass sie bei Browser-Zoom 100 % in ein Fenster passt und dass ich diese nicht hoch und runter scrollen muss. Aktuell ist die Simulation etwas zu hoch. Sonst bleibt alles gleich.

Autor

matthias kindt

Matthias Kindt auf Linkedin