commit 32b6ceba8074b2900f4aedea38a058580b1c8632 Author: Ihor_Zhekov Date: Wed Apr 22 10:57:37 2026 +0200 FeedGine launch diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..cc05b4d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "Bash(wc -l \"/c/projekt/Profice WebSite\"/style/*.css \"/c/projekt/Profice WebSite\"/scripts/*.js)", + "Bash(find '/c/projekt/Profice WebSite' -name *.js -o -name *.css)", + "Bash(xargs wc:*)", + "Bash(grep -E \"\\\\.html$\")", + "Bash(cp 'C:/projekt/Profice WebSite/images/logo/logo-01-complete.png' C:/Users/mvita/Desktop/Alles/FEEDGINE/images/logo/)", + "Bash(cp 'C:/projekt/Profice WebSite/images/logo/Appicon 1024X1024-01.png' C:/Users/mvita/Desktop/Alles/FEEDGINE/images/logo/)", + "Bash(cp 'C:/projekt/Profice WebSite/images/icons/spider.png' C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/)", + "Bash(cp \"C:/projekt/Profice WebSite/images/icons/email_white.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")", + "Bash(cp \"C:/projekt/Profice WebSite/images/icons/instagram.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")", + "Bash(cp \"C:/projekt/Profice WebSite/images/icons/facebook.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")", + "Bash(cp \"C:/projekt/Profice WebSite/images/icons/KI.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")", + "Bash(cp \"C:/projekt/Profice WebSite/images/additional/cursor.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/additional/\")", + "Bash(cp \"C:/projekt/Profice WebSite/scripts/hex-background.js\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/scripts/\")", + "Bash(cp \"C:/projekt/Profice WebSite/scripts/feed-calculator.js\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/scripts/\")", + "Bash(cp:*)" + ] + } +} diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..6acd11f --- /dev/null +++ b/.htaccess @@ -0,0 +1,51 @@ +# Feedgine — Apache Configuration + +# Security: disable directory listing +Options -Indexes + +# Flowise chat proxy +RewriteEngine On +RewriteRule ^api/v1/prediction/(.*)$ scripts/add/flowise-proxy.php?path=$1 [L,QSA] + +# HTTPS redirect (enable in production) +# RewriteCond %{HTTPS} off +# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + +# GZIP compression + + AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json + AddOutputFilterByType DEFLATE image/svg+xml font/ttf font/woff font/woff2 + + +# Browser caching + + ExpiresActive On + ExpiresByType text/css "access plus 1 month" + ExpiresByType application/javascript "access plus 1 month" + ExpiresByType image/png "access plus 1 month" + ExpiresByType image/jpeg "access plus 1 month" + ExpiresByType image/svg+xml "access plus 1 month" + ExpiresByType font/ttf "access plus 1 month" + ExpiresByType font/woff2 "access plus 1 month" + ExpiresByType text/html "access plus 1 hour" + + +# Security headers + + Header always set X-Content-Type-Options "nosniff" + Header always set X-XSS-Protection "1; mode=block" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + # X-Frame-Options set per-page where needed + + +# Protect config file + + Order allow,deny + Deny from all + + +# Protect data directory + + Order allow,deny + Deny from all + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5de458d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +COPY . /usr/share/nginx/html + +EXPOSE 80 diff --git a/config.php b/config.php new file mode 100644 index 0000000..32241d0 --- /dev/null +++ b/config.php @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + Feedgine — Profit Intelligence Platform by Profice + + + + + + + + + + + + + + + + + + + + + + +
+ Problem + Lösung + Module + Pricing + Kalkulator + ↗ KI Systeme (Profice) + Profit-Analyse buchen → +
+
+ + +
+
+
+
Profit Intelligence Platform
+

ROAS lügt.
Profit zählt.

+

+ Feedgine verbindet deine JTL-Wawi-Margen mit Google Shopping, Meta und Microsoft Ads. Ergebnis bei lkw-teile24.de: +31 % profitabler Umsatz, −23 % verschwendetes Budget. +

+ +
+ +
+
+
+
+
+ LIVE · feedgine.engine +
+
+ Produkt A — Werkzeug + POAS 4.2 SKALIEREN +
+
+ Produkt B — Zubehör + POAS 1.1 PRÜFEN +
+
+ Produkt C — Sonder + POAS 0.6 PAUSIEREN +
+
+ Produkt D — OEM-Teil + POAS 3.8 +BUDGET +
+ +
+
+
+ + +
+
+
Live-Referenz: lkw-teile24.de — 230.000 Produkte · JTL-Wawi · Google PMax
+
+
230k
+
Produkte live in Referenz-Deployment lkw-teile24.de
+
+
+
97%
+
Tracking-Coverage durch Server Side Tracking
+
+
+
−23%
+
Ads-Budget auf unprofitable Produkte nach POAS-Umstellung
+
+
+
+31%
+
Umsatz mit profitablen Produkten im selben Zeitraum
+
+
+
+ +
+ + +
+
Das Problem
+

Dein ROAS lügt dich an.

+

Klassische Tools zeigen dir Umsatz — nicht Marge. Du weißt nicht welche Produkte wirklich Geld verdienen, und welche dein Budget verbrennen.

+ +
+
+
01 — PROBLEM
+
+ +
+
Feed-Tools kennen keinen EK
+

Channable, DataFeedWatch & Co. syndizieren Daten. Sie wissen nichts über deine Einkaufspreise, Versandkosten oder echte Marge pro Produkt.

+
+
+
02 — PROBLEM
+
+ +
+
Tracking bricht täglich
+

Adblocker, ITP, Cookie-Verweigerer. Client-Side Tracking verliert 20–40% der Conversions. Du optimierst auf Phantomdaten.

+
+
+
03 — PROBLEM
+
+ +
+
ROAS ≠ Profitabilität
+

ROAS 5 auf einem Produkt mit 8% Marge. Minus. Du weißt es nicht — und Google bietet weiter drauf. Das ist das eigentliche Problem.

+
+
+
+ +
+ + +
+
Die Lösung
+

Profit-Intelligenz,
nicht Feed-Sync.

+ +
+
+
+ 01 +
+
Datenbasis aus deinem WaWi
+

Feedgine liest EK, Versandkosten und alle relevanten Margen-Felder direkt aus JTL-Wawi. Keine manuelle Pflege.

+
+
+
+ 02 +
+
Bereinigung & Margenkalkulation
+

Jedes Produkt bekommt eine echte Deckungsbeitragsberechnung. Daten werden bereinigt, normiert und POAS-tauglich gemacht.

+
+
+
+ 03 +
+
POAS Custom Labels
+

Produkte werden automatisch in Profit-Tiers eingeteilt. Google Shopping und PMax bieten auf Basis echter Marge — nicht Umsatz.

+
+
+
+ 04 +
+
Server Side Tracking
+

Cookie-optional. First-Party. Tracking-Coverage über 95% — unabhängig von Browser, Adblocker oder Consent-Rate.

+
+
+
+ 05 +
+
Multi-Channel Ausgabe
+

Google Shopping, Meta CAPI und Microsoft Ads — täglich aktualisiert, ohne Drittanbieter-Abo.

+
+
+
+ +
+
Profit-Analyse · Live
+
+
Top-Seller → WerkzeugePOAS 4.2
+
+
+
+
Mittelfeld → ZubehörPOAS 2.8
+
+
+
+
Verlust → SonderartikelPOAS 0.6
+
+
+
+
Tracking vorher61%
+
Tracking nachher (SST)97%
+
Ads-Budget eingespart−23%
+
Profit. Umsatz+31%
+
+
+ Referenz: lkw-teile24.de
230.000 Produkte · JTL-Wawi · Google PMax +
+
+
+
+ +
+ + +
+
Was enthalten ist
+

Drei Module.
Ein System.

+

Kein Flickenteppich aus Einzellösungen. Feedgine verbindet Profitdaten, Feed-Intelligence und Tracking in einer Pipeline.

+ +
+
+
01
+
MODUL 01 — PROFIT ENGINE
+
Margenkalkulation
+

EK, Versand, Gebühren — alles berücksichtigt. POAS Custom Labels für Google Shopping und PMax auf Basis echter Deckungsbeiträge.

+
    +
  • EK-Daten aus JTL-Wawi / MSSQL
  • +
  • Automatische DB-Berechnung
  • +
  • POAS-Tiering (Skala 1–5)
  • +
  • Täglicher Update-Zyklus
  • +
+
+
+
02
+
MODUL 02 — FEED INTELLIGENCE
+
Datenpipeline
+

Datenbereinigungs-Pipeline für Google, Meta und Microsoft. Kein Channable. Direkt aus deiner Datenbasis — täglich, automatisch.

+
    +
  • Google Shopping / PMax
  • +
  • Meta Product Catalog (CAPI)
  • +
  • Microsoft Shopping Ads
  • +
  • OE/OEM Nummer-Handling
  • +
+
+
+
03
+
MODUL 03 — SERVER SIDE TRACKING
+
First-Party Tracking
+

Cookie-optionales Tracking auf EU-Infrastruktur. Kein Datenverlust durch Adblocker oder ITP. DSGVO-konform by design.

+
    +
  • Google Ads Conversions (enhanced)
  • +
  • Meta Conversion API
  • +
  • GA4 Server Container
  • +
  • Consent Mode v2 ready
  • +
+
+
+
+ +
+ + +
+
Marktvergleich
+

Was andere kosten.
Was du bekommst.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureKlassische AgenturFeedgine
Echte Margenkalkulation (EK)
POAS Custom LabelsManuell✓ Automatisch
Server Side TrackingExtra Kosten✓ Enthalten
Multi-Channel (Google + Meta + MS)Teilweise
JTL-Wawi Integration✓ Nativ
EU-Hosting / DSGVOTeilweise✓ 100%
Monatliche Kosten€2.000–5.000+ab €999
+
+
+ +
+ + +
+
Pricing
+

Kein Lock-in.
Kein Bullshit.

+

Managed oder Self-Hosted — du entscheidest. Alle Preise netto zzgl. MwSt.

+ +
+ +
+
Feedgine Setup
+
€4.999
+
einmalig
+
    +
  • Feed + POAS Setup
  • +
  • Tracking Fix
  • +
  • Margensteuerung
  • +
+ Setup anfragen → +
+
+
Feedgine Betrieb
+
ab €1.500
+
/Monat
+
    +
  • Monitoring
  • +
  • Optimierung
  • +
  • Skalierung
  • +
+ Starten → +
+
+
+ +
+ + +
+
Zielgruppe
+

Feedgine ist gebaut für
E-Commerce mit Ambition.

+ +
+
+
PROFIL 01
+
JTL-Shop mit >10k Produkten
+

Du nutzt JTL-Wawi und hast einen aktiven Google-Shopping-Feed. Du weißt, dass dein Feed besser sein könnte — aber dir fehlt die Marge-Transparenz.

+
+
+
PROFIL 02
+
Adspend >€5k/Monat
+

Du investierst signifikant in Google Ads oder Meta — und willst endlich wissen, welche Produkte echten Profit liefern und welche Budget verbrennen.

+
+
+
PROFIL 03
+
ROAS-Reporting reicht nicht mehr
+

Du hast das Gefühl, dass deine Agentur auf Umsatz optimiert statt auf Gewinn. Du brauchst Transparenz — keine Dashboards mit Vanity-Metriken.

+
+
+
+ +
+ + +
+
+
Kalkulator
+

Ihr persönlicher Gewinn-Rechner

+

Geben Sie Ihre Zahlen ein — und sehen Sie sofort, wie viel mehr Profit durch optimierte Feed-Steuerung möglich ist.

+ +
+
+

Ihre Kennzahlen

+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
Bisheriger Profit
+
5.000 €
+
Vor der Feed-Optimierung
+
+
+
Neuer Profit
+
+
Mit optimiertem Feed
+
+
+
Ihr Hebel
+
+
+
+
+
Mehr Gewinn — gleiches Budget
+
+
+
+
+
+ + +
+
Bereit?
+

Wisse endlich, welche
Produkte Geld verdienen.

+

Kein Sales-Funnel. Kein Massen-Onboarding. Wir arbeiten mit ausgewählten E-Commerce-Betreibern — direkt, konkret, auf Augenhöhe.

+ Profit-Analyse buchen +
+ + + + + + + + + + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..51acb5d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,25 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Redirect /index.html to / + location = /index.html { + return 301 /; + } + + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + gzip on; + gzip_types text/plain text/css application/javascript image/svg+xml; +} diff --git a/scripts/add/send.php b/scripts/add/send.php new file mode 100644 index 0000000..e33c296 --- /dev/null +++ b/scripts/add/send.php @@ -0,0 +1,196 @@ + $success, 'message' => $message, 'timestamp' => date('c')]; + if ($data !== null && (!USE_PRODUCTION || DEBUG_MODE)) { + $response['data'] = $data; + } + echo json_encode($response); + exit(); +} + +function sanitizeInput($input) { + return htmlspecialchars(trim($input ?? ''), ENT_QUOTES, 'UTF-8'); +} + +function getClientIP() { + foreach (['HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR'] as $key) { + if (!empty($_SERVER[$key])) { + $ip = explode(',', $_SERVER[$key])[0]; + return filter_var(trim($ip), FILTER_VALIDATE_IP) ?: 'unknown'; + } + } + return 'unknown'; +} + +function checkRateLimit($ip) { + $file = __DIR__ . '/data/rate_limits.json'; + $limits = file_exists($file) ? json_decode(file_get_contents($file), true) : []; + $now = time(); + // Clean old entries + foreach ($limits as $k => $v) { + if ($now - $v['first'] > RATE_LIMIT_WINDOW) unset($limits[$k]); + } + if (!isset($limits[$ip])) { + $limits[$ip] = ['count' => 1, 'first' => $now]; + } else { + $limits[$ip]['count']++; + if ($limits[$ip]['count'] > RATE_LIMIT_REQUESTS) { + file_put_contents($file, json_encode($limits)); + return false; + } + } + file_put_contents($file, json_encode($limits)); + return true; +} + +function sendToWebhook($data, $webhookUrl) { + $payload = json_encode($data); + if (function_exists('curl_init')) { + $ch = curl_init($webhookUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Content-Length: ' . strlen($payload)], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + ]); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + return ['success' => $httpCode >= 200 && $httpCode < 300, 'http_code' => $httpCode, 'response' => $response, 'error' => $error]; + } + // Fallback + $ctx = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\nContent-Length: " . strlen($payload) . "\r\n", + 'content' => $payload, + 'timeout' => 15, + ]]); + $result = @file_get_contents($webhookUrl, false, $ctx); + return ['success' => $result !== false, 'response' => $result, 'method' => 'file_get_contents']; +} + +function storeLead($data) { + $file = __DIR__ . '/data/leads.json'; + $leads = file_exists($file) ? json_decode(file_get_contents($file), true) : []; + $leads[] = array_merge($data, ['timestamp' => date('c'), 'ip' => getClientIP()]); + if (count($leads) > 200) $leads = array_slice($leads, -200); + file_put_contents($file, json_encode($leads, JSON_PRETTY_PRINT)); +} + +// ============================================================ +// REQUEST HANDLERS +// ============================================================ + +function handleContactForm($data) { + $name = sanitizeInput($data['name'] ?? ''); + $contact = sanitizeInput($data['contact'] ?? ''); + $message = sanitizeInput($data['message'] ?? ''); + $company = sanitizeInput($data['company'] ?? ''); + + if (empty($name) || empty($contact)) { + sendResponse(false, 'Name und Kontakt sind Pflichtfelder.', null, 400); + } + + $payload = ['source' => 'feedgine.de', 'name' => $name, 'contact' => $contact, + 'message' => $message, 'company' => $company, 'timestamp' => date('c'), 'ip' => getClientIP()]; + storeLead($payload); + sendToWebhook($payload, FEEDGINE_WEBHOOK_URL); + sendResponse(true, 'Anfrage erfolgreich übermittelt. Wir melden uns in Kürze.'); +} + +function handleChatMessage($data) { + $message = sanitizeInput($data['message'] ?? ''); + $session_id = sanitizeInput($data['session_id'] ?? ''); + + if (empty($message)) { + sendResponse(false, 'Nachricht darf nicht leer sein.', null, 400); + } + + $payload = ['message' => $message, 'session_id' => $session_id, 'source' => 'feedgine.de']; + $result = sendToWebhook($payload, KI_CHAT_WEBHOOK_URL); + $botReply = 'Vielen Dank für Ihre Nachricht. Unser Team meldet sich schnellstmöglich.'; + + if ($result['success'] && !empty($result['response'])) { + $decoded = json_decode($result['response'], true); + foreach (['message','output','text','response','answer','result'] as $key) { + if (!empty($decoded[$key])) { $botReply = $decoded[$key]; break; } + } + } + sendResponse(true, 'OK', ['message' => $botReply, 'session_id' => $session_id]); +} + +function handleCookieConsent($data) { + $file = __DIR__ . '/data/cookie_consent.json'; + $records = file_exists($file) ? json_decode(file_get_contents($file), true) : []; + $records[] = ['consent' => $data, 'timestamp' => date('c'), 'ip' => getClientIP()]; + if (count($records) > 1000) $records = array_slice($records, -1000); + file_put_contents($file, json_encode($records)); + sendResponse(true, 'Consent gespeichert.'); +} + +// ============================================================ +// ROUTER +// ============================================================ + +$ip = getClientIP(); +if (!checkRateLimit($ip)) { + sendResponse(false, 'Zu viele Anfragen. Bitte warten.', null, 429); +} + +$raw = file_get_contents('php://input'); +$body = json_decode($raw, true); + +if (json_last_error() !== JSON_ERROR_NONE || empty($body)) { + sendResponse(false, 'Ungültige Anfrage.', null, 400); +} + +$type = sanitizeInput($body['type'] ?? ''); + +switch ($type) { + case 'contact': handleContactForm($body); break; + case 'chat': handleChatMessage($body); break; + case 'cookie_consent': handleCookieConsent($body); break; + default: sendResponse(false, "Unbekannter Typ: $type", null, 400); +} diff --git a/scripts/cursor.js b/scripts/cursor.js new file mode 100644 index 0000000..d527cbe --- /dev/null +++ b/scripts/cursor.js @@ -0,0 +1,256 @@ +// cursor.js — Venom/Spider Cursor · Space Edition (white + neon) + +document.addEventListener("DOMContentLoaded", function () { + if (window.matchMedia("(pointer: coarse)").matches) return; + + /* ── CONFIG ── */ + const CONFIG = { + tentacleCount: 10, + triggerDist: 8, + maxLength: 300, + connectionDist: 150, + prediction: 3.5, + + // Idle: cold silver-white + idleStroke: { r: 210, g: 228, b: 255 }, + idleGlow: { r: 160, g: 200, b: 255 }, + + // Hover: neon orange (matches site accent) + hoverStroke: { r: 255, g: 102, b: 0 }, + hoverGlow: { r: 255, g: 80, b: 0 }, + }; + + /* ── toggle ── */ + const toggleBtn = document.getElementById('cursorToggle'); + const body = document.body; + + let isCursorDisabled = localStorage.getItem('venomCursorDisabled') !== 'false'; + + function updateCursorState() { + if (isCursorDisabled) { + body.classList.add('system-cursor'); + document.documentElement.style.cursor = ''; + if (toggleBtn) { + toggleBtn.classList.remove('active'); + const icon = toggleBtn.querySelector('.cursor-icon'); + if (icon && icon.tagName === 'IMG') { + icon.src = 'images/icons/spider.png'; + icon.alt = 'Spider Cursor'; + } + } + } else { + body.classList.remove('system-cursor'); + document.documentElement.style.cursor = 'none'; + if (toggleBtn) { + toggleBtn.classList.add('active'); + const icon = toggleBtn.querySelector('.cursor-icon'); + if (icon && icon.tagName === 'IMG') { + icon.src = 'images/additional/cursor.png'; + icon.alt = 'Custom Cursor'; + } + } + } + } + + updateCursorState(); + + if (toggleBtn) { + toggleBtn.addEventListener('click', () => { + isCursorDisabled = !isCursorDisabled; + localStorage.setItem('venomCursorDisabled', isCursorDisabled); + updateCursorState(); + }); + } + + /* ── canvas ── */ + const canvas = document.createElement('canvas'); + canvas.id = 'venom-cursor'; + document.body.appendChild(canvas); + const ctx = canvas.getContext('2d'); + + function resize() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + resize(); + window.addEventListener('resize', resize); + + /* ── state ── */ + const mouse = { x: 0, y: 0 }; + const oldMouse = { x: 0, y: 0 }; + let isHover = false; + let colorT = 0; // 0 = idle, 1 = hover (lerped) + + const tentacles = []; + let rotation = 0; // reticle ring slow spin + + /* ── hover selector ── */ + const HOVER_SEL = [ + 'a', 'button', '[role="button"]', 'select', 'label', + '.nav-link', '.btn-primary', '.btn-ghost', '.nav-demo-btn', + '.price-cta', '.prob-card', '.module-card', '.how-step', + '.dropdown-item', '.mobile-nav-link', '.mobile-nav-cta', + '[onclick]', '.clickable' + ].join(','); + + document.addEventListener('mousemove', (e) => { + mouse.x = e.clientX; + mouse.y = e.clientY; + const el = document.elementFromPoint(e.clientX, e.clientY); + isHover = !!el && (el.matches(HOVER_SEL) || !!el.closest(HOVER_SEL)); + }, { passive: true }); + + /* ── colour helper ── */ + function lerpColor(a, b, t) { + return { + r: a.r + (b.r - a.r) * t | 0, + g: a.g + (b.g - a.g) * t | 0, + b: a.b + (b.b - a.b) * t | 0, + }; + } + + /* ── Tentacle ── */ + class Tentacle { + constructor(targetX, targetY) { + this.anchor = { x: targetX, y: targetY }; + this.dead = false; + this.dist = 0; + this.age = 0; + } + + update() { + const dx = mouse.x - this.anchor.x; + const dy = mouse.y - this.anchor.y; + this.dist = Math.sqrt(dx * dx + dy * dy); + this.age++; + if (this.dist > CONFIG.maxLength) this.dead = true; + } + + draw(stroke, glow) { + if (this.dead) return; + + const tension = Math.min(this.dist / CONFIG.maxLength, 1); + + // Fade in over first 10 frames + const fadeIn = Math.min(this.age / 10, 1); + const lineAlpha = fadeIn * (1 - tension * 0.85) * 0.70; + const dotAlpha = fadeIn * (1 - tension) * 0.90; + const lw = Math.max(0.2, 1.1 * (1 - tension * 0.80)); + + // Glow pass (wide, soft) + ctx.save(); + ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},${lineAlpha * 0.55})`; + ctx.shadowBlur = 6; + ctx.beginPath(); + ctx.moveTo(mouse.x, mouse.y); + ctx.lineTo(this.anchor.x, this.anchor.y); + ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${lineAlpha})`; + ctx.lineWidth = lw; + ctx.lineCap = 'round'; + ctx.stroke(); + ctx.restore(); + + // Anchor dot + ctx.beginPath(); + ctx.arc(this.anchor.x, this.anchor.y, 1.8 * (1 - tension), 0, Math.PI * 2); + ctx.fillStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${dotAlpha})`; + ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},${dotAlpha * 0.6})`; + ctx.shadowBlur = 4; + ctx.fill(); + ctx.shadowBlur = 0; + } + } + + /* ── render ── */ + function render() { + if (isCursorDisabled) { + requestAnimationFrame(render); + return; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Lerp color T + colorT += (isHover ? 1 : 0 - colorT) * 0.12; + + const stroke = lerpColor(CONFIG.idleStroke, CONFIG.hoverStroke, colorT); + const glow = lerpColor(CONFIG.idleGlow, CONFIG.hoverGlow, colorT); + + /* spawn tentacle on movement */ + const moved = Math.hypot(mouse.x - oldMouse.x, mouse.y - oldMouse.y); + if (moved > CONFIG.triggerDist) { + const vx = mouse.x - oldMouse.x; + const vy = mouse.y - oldMouse.y; + const tx = mouse.x + vx * CONFIG.prediction + (Math.random() - 0.5) * 60; + const ty = mouse.y + vy * CONFIG.prediction + (Math.random() - 0.5) * 60; + tentacles.push(new Tentacle(tx, ty)); + oldMouse.x = mouse.x; + oldMouse.y = mouse.y; + } + if (tentacles.length > CONFIG.tentacleCount) tentacles.shift(); + + /* update + draw tentacles */ + for (let i = tentacles.length - 1; i >= 0; i--) { + tentacles[i].update(); + if (tentacles[i].dead) tentacles.splice(i, 1); + else tentacles[i].draw(stroke, glow); + } + + /* web connections between close anchor pairs */ + for (let i = 0; i < tentacles.length; i++) { + for (let j = i + 1; j < tentacles.length; j++) { + const a = tentacles[i].anchor; + const b = tentacles[j].anchor; + const d = Math.hypot(a.x - b.x, a.y - b.y); + if (d < CONFIG.connectionDist) { + const alpha = (1 - d / CONFIG.connectionDist) * 0.28; + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${alpha})`; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + } + } + + /* cursor shape — segmented reticle ring */ + rotation += isHover ? 0.040 : 0.016; + + const ringR = isHover ? 9 : 7; + const gapHalf = 0.28; // gap half-angle in radians at each cardinal point + const segments = [ + [rotation + gapHalf, rotation + Math.PI * 0.5 - gapHalf], + [rotation + Math.PI * 0.5 + gapHalf, rotation + Math.PI - gapHalf], + [rotation + Math.PI + gapHalf, rotation + Math.PI * 1.5 - gapHalf], + [rotation + Math.PI * 1.5 + gapHalf, rotation + Math.PI * 2.0 - gapHalf], + ]; + + ctx.save(); + ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},0.75)`; + ctx.shadowBlur = isHover ? 14 : 8; + ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},0.90)`; + ctx.lineWidth = isHover ? 1.4 : 1.1; + ctx.lineCap = 'round'; + for (const [start, end] of segments) { + ctx.beginPath(); + ctx.arc(mouse.x, mouse.y, ringR, start, end); + ctx.stroke(); + } + ctx.restore(); + + // Center dot — exact cursor tip + ctx.save(); + ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},0.90)`; + ctx.shadowBlur = isHover ? 10 : 6; + ctx.beginPath(); + ctx.arc(mouse.x, mouse.y, isHover ? 2.2 : 1.6, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},1)`; + ctx.fill(); + ctx.restore(); + + requestAnimationFrame(render); + } + + render(); +}); diff --git a/scripts/feed-calculator.js b/scripts/feed-calculator.js new file mode 100644 index 0000000..2ff638b --- /dev/null +++ b/scripts/feed-calculator.js @@ -0,0 +1,85 @@ +/** + * Gewinn-Calculator — Feed Page + * Real-time profit calculator with synchronized sliders and number inputs. + */ +(function () { + function fmt(n) { + return n.toLocaleString('de-DE', { maximumFractionDigits: 0 }) + ' €'; + } + + function fmtPct(n) { + return (n >= 0 ? '+' : '') + n.toLocaleString('de-DE', { maximumFractionDigits: 0 }) + ' %'; + } + + var WASTE = 0.20; // Anteil Budget-Fresser — fixed at 20% + + function calc() { + var B = +document.getElementById('sl-budget').value; + var U = +document.getElementById('sl-umsatz').value; + var m = +document.getElementById('sl-marge').value / 100; + var w = WASTE; + + var P_alt = U * m - B; + var roas_core = U / (B * (1 - w)); + var U_neu = U + B * w * roas_core; + var P_neu = U_neu * m - B; + var wachstum = P_alt !== 0 ? ((P_neu - P_alt) / Math.abs(P_alt)) * 100 : 0; + var hebel = P_neu - P_alt; + + document.getElementById('out-profit-alt').textContent = fmt(Math.round(P_alt)); + document.getElementById('out-profit-neu').textContent = fmt(Math.round(P_neu)); + document.getElementById('out-hebel-eur').textContent = (hebel >= 0 ? '+' : '') + fmt(Math.round(hebel)).replace(' €', '') + ' €'; + document.getElementById('out-hebel-pct').textContent = fmtPct(Math.round(wachstum)); + } + + function syncFromSlider(slId, numId, lblId, unit) { + var sl = document.getElementById(slId); + var num = document.getElementById(numId); + var lbl = document.getElementById(lblId); + + function updateLbl(v) { + if (unit === '€') { + lbl.textContent = Number(v).toLocaleString('de-DE') + ' €'; + } else { + lbl.textContent = v + ' %'; + } + } + + sl.addEventListener('input', function () { + num.value = sl.value; + updateLbl(sl.value); + calc(); + }); + + num.addEventListener('input', function () { + var v = Math.min(+num.max, Math.max(+num.min, +num.value || +num.min)); + sl.value = v; + updateLbl(v); + calc(); + }); + + num.addEventListener('change', function () { + var v = Math.min(+num.max, Math.max(+num.min, +num.value || +num.min)); + num.value = v; + sl.value = v; + updateLbl(v); + calc(); + }); + } + + function init() { + if (!document.getElementById('sl-budget')) return; + + syncFromSlider('sl-budget', 'num-budget', 'lbl-budget', '€'); + syncFromSlider('sl-umsatz', 'num-umsatz', 'lbl-umsatz', '€'); + syncFromSlider('sl-marge', 'num-marge', 'lbl-marge', '%'); + + calc(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/scripts/feedgine.js b/scripts/feedgine.js new file mode 100644 index 0000000..5e4cc73 --- /dev/null +++ b/scripts/feedgine.js @@ -0,0 +1,131 @@ +/** + * feedgine.js — Main page interactions + * - Scroll animations (IntersectionObserver) + * - Bar animation for the profit visual + * - Terminal pulse effect + * - Smooth scroll for anchor links + */ +(function () { + 'use strict'; + + // ---- SCROLL ANIMATIONS ---- + const obs = new IntersectionObserver((entries) => { + entries.forEach(e => { + if (e.isIntersecting) { + e.target.classList.add('visible'); + obs.unobserve(e.target); + } + }); + }, { threshold: 0.1 }); + + document.querySelectorAll('.animate-in').forEach(el => obs.observe(el)); + + // ---- BAR ANIMATION ---- + const barObs = new IntersectionObserver((entries) => { + entries.forEach(e => { + if (e.isIntersecting) { + e.target.querySelectorAll('.poas-fill').forEach(bar => { + const w = bar.style.width; + bar.style.width = '0%'; + setTimeout(() => { bar.style.width = w; }, 150); + }); + barObs.unobserve(e.target); + } + }); + }, { threshold: 0.3 }); + + document.querySelectorAll('.how-visual').forEach(el => barObs.observe(el)); + + // ---- TERMINAL PULSE ---- + setInterval(() => { + document.querySelectorAll('.t-val').forEach(v => { + v.style.transition = 'opacity 0.3s'; + v.style.opacity = '0.4'; + setTimeout(() => { v.style.opacity = '1'; }, 300); + }); + }, 3500); + + // ---- HAMBURGER MENU ---- + const hamburger = document.getElementById('hamburger'); + const mobileNav = document.getElementById('mobileNav'); + + function closeMobileNav() { + if (!hamburger || !mobileNav) return; + hamburger.classList.remove('open'); + hamburger.setAttribute('aria-expanded', 'false'); + mobileNav.classList.remove('open'); + mobileNav.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + } + + if (hamburger && mobileNav) { + hamburger.addEventListener('click', () => { + const isOpen = hamburger.classList.contains('open'); + if (isOpen) { + closeMobileNav(); + } else { + hamburger.classList.add('open'); + hamburger.setAttribute('aria-expanded', 'true'); + mobileNav.classList.add('open'); + mobileNav.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + } + }); + + // Close on any nav link click + mobileNav.querySelectorAll('a').forEach(a => { + a.addEventListener('click', closeMobileNav); + }); + + // Close on overlay click or Escape key + document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMobileNav(); }); + } + + // ---- SMOOTH SCROLL ---- + document.querySelectorAll('a[href^="#"]').forEach(a => { + a.addEventListener('click', (e) => { + const href = a.getAttribute('href'); + if (!href || href === '#') return; + const target = document.querySelector(href); + if (!target) return; + e.preventDefault(); + const offset = 100; + const top = target.getBoundingClientRect().top + window.pageYOffset - offset; + window.scrollTo({ top, behavior: 'smooth' }); + }); + }); + + // ---- CONTACT FORM SUBMIT ---- + const form = document.getElementById('contactForm'); + if (form) { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const btn = form.querySelector('button[type="submit"]'); + if (btn) { btn.disabled = true; btn.textContent = 'Wird gesendet…'; } + + const data = { + type: 'contact', + name: form.querySelector('[name="name"]')?.value || '', + contact: form.querySelector('[name="contact"]')?.value || '', + message: form.querySelector('[name="message"]')?.value || '', + company: form.querySelector('[name="company"]')?.value || '', + }; + + try { + const res = await fetch('scripts/add/send.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + const json = await res.json(); + const msg = form.querySelector('.form-success'); + if (msg) { msg.style.display = 'block'; } + form.reset(); + } catch (err) { + console.error('Form error:', err); + } finally { + if (btn) { btn.disabled = false; btn.textContent = 'Demo anfragen →'; } + } + }); + } +})(); diff --git a/scripts/hex-background.js b/scripts/hex-background.js new file mode 100644 index 0000000..45d50a3 --- /dev/null +++ b/scripts/hex-background.js @@ -0,0 +1,531 @@ +/** + * Space Background — Glass Shards · Animated Nebula · Drifting Stars · Shooting Stars + * No hex grid. Cursor scatters floating glass fragments. + */ + +(function () { + 'use strict'; + + let canvas, ctx; + let W = 0, H = 0; + let mouse = { x: -2000, y: -2000 }; + let rafId; + let initialized = false; + let time = 0; + + /* ────────────────────────────────────────────── + NEBULA BLOBS (animated radial gradients) — dimmed + ────────────────────────────────────────────── */ + const NEBULAS = [ + { px: 0.12, py: 0.25, pr: 0.50, cr: 255, cg: 80, cb: 0, a: 0.040, spx: 0.00014, spy: 0.00007, phase: 0.0 }, + { px: 0.82, py: 0.55, pr: 0.55, cr: 0, cg: 255, cb: 136, a: 0.028, spx:-0.00009, spy: 0.00011, phase: 2.1 }, + { px: 0.50, py: 0.88, pr: 0.42, cr: 0, cg: 180, cb: 255, a: 0.022, spx: 0.00007, spy:-0.00009, phase: 4.3 }, + { px: 0.72, py: 0.12, pr: 0.38, cr: 160, cg: 0, cb: 255, a: 0.016, spx:-0.00010, spy: 0.00005, phase: 1.5 }, + { px: 0.35, py: 0.65, pr: 0.32, cr: 255, cg: 140, cb: 0, a: 0.016, spx: 0.00005, spy:-0.00006, phase: 3.7 }, + ]; + + function drawNebulas() { + for (let i = 0; i < NEBULAS.length; i++) { + const n = NEBULAS[i]; + const ox = Math.sin(time * n.spx * 800 + n.phase) * W * 0.10; + const oy = Math.cos(time * n.spy * 800 + n.phase + 1.2) * H * 0.10; + const cx = n.px * W + ox; + const cy = n.py * H + oy; + const rad = n.pr * Math.min(W, H); + const pa = n.a * (0.65 + 0.35 * Math.sin(time * 0.28 + n.phase)); + + const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad); + g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`); + g.addColorStop(0.45,`rgba(${n.cr},${n.cg},${n.cb},${pa * 0.35})`); + g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`); + ctx.fillStyle = g; + ctx.beginPath(); + ctx.arc(cx, cy, rad, 0, Math.PI * 2); + ctx.fill(); + } + } + + /* ────────────────────────────────────────────── + DRIFTING STARS — dimmed halos + ────────────────────────────────────────────── */ + const STAR_COUNT = 220; + let stars = []; + + function initStars() { + stars = []; + for (let i = 0; i < STAR_COUNT; i++) { + stars.push({ + x: Math.random() * W, + y: Math.random() * H, + r: 0.35 + Math.random() * 1.65, + vx: (Math.random() - 0.5) * 0.055, + vy: 0.018 + Math.random() * 0.055, + phase: Math.random() * Math.PI * 2, + twinkleSpeed: 0.4 + Math.random() * 1.6, + // 0=white 1=neon-green 2=neon-teal + type: Math.random() < 0.78 ? 0 : (Math.random() < 0.5 ? 1 : 2), + }); + } + } + + function drawStars() { + for (let i = 0; i < stars.length; i++) { + const s = stars[i]; + const tw = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(time * s.twinkleSpeed + s.phase)); + + s.x += s.vx; + s.y += s.vy; + if (s.y > H + 2) { s.y = -2; s.x = Math.random() * W; } + if (s.x < -2) s.x = W + 2; + if (s.x > W + 2) s.x = -2; + + const clr = s.type === 0 + ? `rgba(255,255,255,${tw * 0.70})` + : s.type === 1 + ? `rgba(0,255,136,${tw * 0.50})` + : `rgba(0,220,255,${tw * 0.50})`; + + ctx.beginPath(); + ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); + ctx.fillStyle = clr; + ctx.fill(); + + // Halo only on larger stars, dimmed + if (s.r > 1.1) { + const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 3.5); + const ha = tw * (s.type === 0 ? 0.14 : 0.10); + halo.addColorStop(0, s.type === 0 + ? `rgba(255,255,255,${ha})` + : s.type === 1 + ? `rgba(0,255,136,${ha})` + : `rgba(0,220,255,${ha})`); + halo.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.beginPath(); + ctx.arc(s.x, s.y, s.r * 3.5, 0, Math.PI * 2); + ctx.fillStyle = halo; + ctx.fill(); + } + } + } + + /* ────────────────────────────────────────────── + SHOOTING STARS + ────────────────────────────────────────────── */ + const MAX_SHOOTING_STARS = 3; + let shootingStars = []; + let nextShootingStarAt = 0; // time value when next star spawns + + function spawnShootingStar() { + // Spawn from top or right edge, travel down-left or down-right + const fromRight = Math.random() < 0.5; + const startX = fromRight ? W * (0.5 + Math.random() * 0.6) : W * Math.random() * 0.7; + const startY = Math.random() * H * 0.45; + const angle = (Math.PI / 4) + (Math.random() - 0.5) * 0.6; // ~45° downward + const speed = 6 + Math.random() * 9; + const length = 80 + Math.random() * 160; + + shootingStars.push({ + x: startX, + y: startY, + vx: Math.cos(angle) * speed * (fromRight ? -1 : 1), + vy: Math.sin(angle) * speed, + length, + alpha: 0, + fadeIn: true, + life: 0, + maxLife: (length / speed) * 1.6, // frames to live + }); + } + + function updateDrawShootingStars() { + // Possibly spawn a new one + if (shootingStars.length < MAX_SHOOTING_STARS && time > nextShootingStarAt) { + spawnShootingStar(); + // Next star between 4–14 seconds of time units (time += 0.016/frame) + nextShootingStarAt = time + 4 + Math.random() * 10; + } + + for (let i = shootingStars.length - 1; i >= 0; i--) { + const s = shootingStars[i]; + s.life++; + + // Fade in quickly, fade out near end + if (s.life < 8) { + s.alpha = s.life / 8; + } else if (s.life > s.maxLife - 10) { + s.alpha = Math.max(0, (s.maxLife - s.life) / 10); + } else { + s.alpha = 1; + } + + s.x += s.vx; + s.y += s.vy; + + // Draw trail + const tailX = s.x - (s.vx / Math.hypot(s.vx, s.vy)) * s.length; + const tailY = s.y - (s.vy / Math.hypot(s.vx, s.vy)) * s.length; + + const grad = ctx.createLinearGradient(s.x, s.y, tailX, tailY); + grad.addColorStop(0, `rgba(255,255,255,${s.alpha * 0.90})`); + grad.addColorStop(0.15,`rgba(220,235,255,${s.alpha * 0.55})`); + grad.addColorStop(1, `rgba(180,210,255,0)`); + + ctx.beginPath(); + ctx.moveTo(s.x, s.y); + ctx.lineTo(tailX, tailY); + ctx.strokeStyle = grad; + ctx.lineWidth = 1.5; + ctx.stroke(); + + // Bright head dot + ctx.beginPath(); + ctx.arc(s.x, s.y, 1.4, 0, Math.PI * 2); + ctx.fillStyle = `rgba(255,255,255,${s.alpha * 0.95})`; + ctx.fill(); + + // Remove if expired or off-screen + if (s.life >= s.maxLife || s.x < -50 || s.x > W + 50 || s.y > H + 50) { + shootingStars.splice(i, 1); + } + } + } + + /* ────────────────────────────────────────────── + GLASS SHARDS — fixed physics + ────────────────────────────────────────────── */ + const SHARD_COUNT = 30; + const SHARD_RADIUS = 230; // mouse influence radius + const SCATTER_FORCE = 3.0; + const ROT_V_MAX = 0.018; // cap on rotation speed + + const SHARD_COLORS = [ + { r: 255, g: 102, b: 0 }, // neon orange + { r: 0, g: 255, b: 136 }, // neon green + { r: 0, g: 220, b: 255 }, // neon teal + { r: 255, g: 255, b: 255 }, // white + { r: 255, g: 180, b: 0 }, // amber + ]; + + let shards = []; + + function buildVerts(size, sides) { + const verts = []; + const base = Math.random() * Math.PI * 2; + for (let i = 0; i < sides; i++) { + const a = base + (Math.PI * 2 * i / sides) + (Math.random() - 0.5) * 0.65; + const r = size * (0.55 + Math.random() * 0.45); + verts.push([Math.cos(a) * r, Math.sin(a) * r]); + } + return verts; + } + + function initShards() { + shards = []; + for (let i = 0; i < SHARD_COUNT; i++) { + const size = 11 + Math.random() * 42; + const sides = 3 + Math.floor(Math.random() * 3); // 3–5 sides + const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)]; + shards.push({ + x: Math.random() * W, + y: Math.random() * H, + vx: (Math.random() - 0.5) * 0.22, + vy: (Math.random() - 0.5) * 0.22, + rot: Math.random() * Math.PI * 2, + rotV: (Math.random() - 0.5) * 0.003, // very gentle initial spin + verts: buildVerts(size, sides), + size, col, + alpha: 0.18 + Math.random() * 0.24, + alphaTarget: 0.18 + Math.random() * 0.24, + svx: 0, svy: 0, + phase: Math.random() * Math.PI * 2, + floatSpeed: 0.20 + Math.random() * 0.45, // slower float + }); + } + } + + function drawShard(s) { + ctx.save(); + ctx.translate(s.x, s.y); + ctx.rotate(s.rot); + + const { r, g, b } = s.col; + const al = s.alpha; + + ctx.beginPath(); + ctx.moveTo(s.verts[0][0], s.verts[0][1]); + for (let i = 1; i < s.verts.length; i++) ctx.lineTo(s.verts[i][0], s.verts[i][1]); + ctx.closePath(); + + // Glass fill + const fill = ctx.createLinearGradient(-s.size, -s.size, s.size * 0.6, s.size * 0.6); + fill.addColorStop(0, `rgba(${r},${g},${b},${al * 0.18})`); + fill.addColorStop(0.45,`rgba(255,255,255,${al * 0.08})`); + fill.addColorStop(1, `rgba(${r},${g},${b},${al * 0.03})`); + ctx.fillStyle = fill; + ctx.fill(); + + // Neon outline — dimmed + ctx.shadowColor = `rgba(${r},${g},${b},0.45)`; + ctx.shadowBlur = 7; + ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.70})`; + ctx.lineWidth = 1.0; + ctx.stroke(); + + // Soft outer glow — dimmed + ctx.shadowBlur = 14; + ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.18})`; + ctx.lineWidth = 2.0; + ctx.stroke(); + ctx.shadowBlur = 0; + + // Inner highlight + if (s.verts.length >= 2) { + ctx.beginPath(); + ctx.moveTo(s.verts[0][0] * 0.55, s.verts[0][1] * 0.55); + ctx.lineTo(s.verts[1][0] * 0.55, s.verts[1][1] * 0.55); + ctx.strokeStyle = `rgba(255,255,255,${al * 0.40})`; + ctx.lineWidth = 0.8; + ctx.shadowBlur = 3; + ctx.shadowColor = 'rgba(255,255,255,0.3)'; + ctx.stroke(); + ctx.shadowBlur = 0; + } + + ctx.restore(); + } + + function updateShards() { + const infSq = SHARD_RADIUS * SHARD_RADIUS; + + for (let i = 0; i < shards.length; i++) { + const s = shards[i]; + + // Gentle float bob + s.vy += Math.sin(time * s.floatSpeed + s.phase) * 0.002; + + // Mouse scatter + if (mouse.x > -1000) { + const dx = s.x - mouse.x; + const dy = s.y - mouse.y; + const distSq = dx * dx + dy * dy; + + if (distSq < infSq) { + const dist = Math.sqrt(distSq); + const force = (1 - dist / SHARD_RADIUS) * SCATTER_FORCE; + const ang = Math.atan2(dy, dx); + s.svx += Math.cos(ang) * force * 0.065; + s.svy += Math.sin(ang) * force * 0.065; + // Small nudge to rotation — NOT multiplicative + s.rotV += (Math.random() - 0.5) * 0.004 * (1 - dist / SHARD_RADIUS); + // Brighten on interaction + s.alphaTarget = Math.min(0.75, s.alphaTarget + 0.03); + } + } + + // Rotation damping — keeps shards from spinning endlessly + s.rotV *= 0.96; + // Hard cap on rotation speed + if (s.rotV > ROT_V_MAX) s.rotV = ROT_V_MAX; + if (s.rotV < -ROT_V_MAX) s.rotV = -ROT_V_MAX; + + // Scatter velocity damping + s.svx *= 0.92; + s.svy *= 0.92; + s.vx *= 0.998; + s.vy *= 0.998; + + // Apply + s.x += s.vx + s.svx; + s.y += s.vy + s.svy; + s.rot += s.rotV; + + // Fade alpha toward target + s.alpha += (s.alphaTarget - s.alpha) * 0.025; + // Slowly restore target + const base = 0.18; + if (s.alphaTarget > base) s.alphaTarget -= 0.006; + + // Edge wrap + const m = s.size + 10; + if (s.x < -m) s.x = W + m; + if (s.x > W + m) s.x = -m; + if (s.y < -m) s.y = H + m; + if (s.y > H + m) s.y = -m; + } + } + + /* ────────────────────────────────────────────── + CURSOR AURA — dimmed + ────────────────────────────────────────────── */ + function drawCursorAura() { + const r = 130 + Math.sin(time * 1.8) * 22; + const aura = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, r); + aura.addColorStop(0, 'rgba(255,102,0,0.04)'); + aura.addColorStop(0.5, 'rgba(255,102,0,0.012)'); + aura.addColorStop(1, 'rgba(255,102,0,0)'); + ctx.fillStyle = aura; + ctx.beginPath(); + ctx.arc(mouse.x, mouse.y, r, 0, Math.PI * 2); + ctx.fill(); + } + + /* ────────────────────────────────────────────── + MAIN LOOP + ────────────────────────────────────────────── */ + function animate() { + if (!initialized) return; + + time += 0.016; + + ctx.clearRect(0, 0, W, H); + + drawNebulas(); + drawStars(); + updateDrawShootingStars(); + updateShards(); + for (let i = 0; i < shards.length; i++) drawShard(shards[i]); + if (mouse.x > -1000) drawCursorAura(); + + rafId = requestAnimationFrame(animate); + } + + /* ────────────────────────────────────────────── + MOBILE — one-shot static render + ────────────────────────────────────────────── */ + function initMobileStatic() { + const c = document.getElementById('hexCanvas'); + if (!c) return; + const dpr = Math.min(window.devicePixelRatio || 1, 2); + const w = window.innerWidth; + const h = window.innerHeight; + c.width = w * dpr; + c.height = h * dpr; + c.style.width = w + 'px'; + c.style.height = h + 'px'; + const cx = c.getContext('2d'); + cx.scale(dpr, dpr); + + // Nebula blobs — dimmed + const g1 = cx.createRadialGradient(w*0.15, h*0.3, 0, w*0.15, h*0.3, w*0.5); + g1.addColorStop(0, 'rgba(255,80,0,0.04)'); g1.addColorStop(1, 'rgba(255,80,0,0)'); + cx.fillStyle = g1; cx.fillRect(0, 0, w, h); + + const g2 = cx.createRadialGradient(w*0.82, h*0.65, 0, w*0.82, h*0.65, w*0.45); + g2.addColorStop(0, 'rgba(0,255,136,0.03)'); g2.addColorStop(1, 'rgba(0,255,136,0)'); + cx.fillStyle = g2; cx.fillRect(0, 0, w, h); + + const g3 = cx.createRadialGradient(w*0.5, h*0.85, 0, w*0.5, h*0.85, w*0.4); + g3.addColorStop(0, 'rgba(0,180,255,0.025)'); g3.addColorStop(1, 'rgba(0,180,255,0)'); + cx.fillStyle = g3; cx.fillRect(0, 0, w, h); + + // Stars + for (let i = 0; i < 130; i++) { + const sx = Math.random() * w; + const sy = Math.random() * h; + const sr = 0.35 + Math.random() * 1.5; + const sa = 0.20 + Math.random() * 0.55; + cx.beginPath(); + cx.arc(sx, sy, sr, 0, Math.PI * 2); + cx.fillStyle = `rgba(255,255,255,${sa})`; + cx.fill(); + } + + // Glass shards + const MCOLS = [[255,102,0],[0,255,136],[0,220,255],[255,255,255],[255,180,0]]; + for (let i = 0; i < 14; i++) { + const sx = Math.random() * w; + const sy = Math.random() * h; + const ss = 10 + Math.random() * 35; + const sides = 3 + Math.floor(Math.random() * 3); + const [cr,cg,cb] = MCOLS[Math.floor(Math.random() * MCOLS.length)]; + cx.save(); + cx.translate(sx, sy); + cx.rotate(Math.random() * Math.PI * 2); + cx.beginPath(); + for (let j = 0; j < sides; j++) { + const a = (Math.PI * 2 * j / sides) + (Math.random()-0.5)*0.6; + const r = ss * (0.55 + Math.random() * 0.45); + j === 0 ? cx.moveTo(Math.cos(a)*r, Math.sin(a)*r) + : cx.lineTo(Math.cos(a)*r, Math.sin(a)*r); + } + cx.closePath(); + cx.fillStyle = `rgba(${cr},${cg},${cb},0.04)`; + cx.strokeStyle = `rgba(${cr},${cg},${cb},0.38)`; + cx.lineWidth = 1; + cx.fill(); + cx.shadowColor = `rgba(${cr},${cg},${cb},0.45)`; + cx.shadowBlur = 6; + cx.stroke(); + cx.shadowBlur = 0; + cx.restore(); + } + } + + /* ────────────────────────────────────────────── + INIT + ────────────────────────────────────────────── */ + function init() { + if (window.innerWidth <= 768 || ('ontouchstart' in window)) { + initMobileStatic(); + return; + } + + canvas = document.getElementById('hexCanvas'); + if (!canvas) return; + + ctx = canvas.getContext('2d', { alpha: true }); + resize(); + + window.addEventListener('resize', debounce(resize, 200)); + document.addEventListener('mousemove', e => { + mouse.x = e.clientX; + mouse.y = e.clientY; + }, { passive: true }); + document.addEventListener('mouseleave', () => { + mouse.x = -2000; + mouse.y = -2000; + }); + + // First shooting star after a short delay + nextShootingStarAt = 3 + Math.random() * 5; + + initialized = true; + animate(); + } + + function resize() { + const dpr = Math.min(window.devicePixelRatio || 1, 2); + W = window.innerWidth; + H = window.innerHeight; + canvas.width = W * dpr; + canvas.height = H * dpr; + canvas.style.width = W + 'px'; + canvas.style.height = H + 'px'; + canvas.style.position = 'fixed'; + canvas.style.top = '0'; + canvas.style.left = '0'; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(dpr, dpr); + initStars(); + initShards(); + } + + function debounce(fn, ms) { + let t; + return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; + } + + function destroy() { + if (rafId) cancelAnimationFrame(rafId); + initialized = false; + stars = []; shards = []; shootingStars = []; + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + window.HexBackground = { init, destroy }; +})(); diff --git a/scripts/ki-chat-bubble.js b/scripts/ki-chat-bubble.js new file mode 100644 index 0000000..0c77cc2 --- /dev/null +++ b/scripts/ki-chat-bubble.js @@ -0,0 +1,194 @@ +// KI Chat Bubble — Flowise embed (Feedgine-branded) +// To remove: delete this file and its