FeedGine launch
This commit is contained in:
196
scripts/add/send.php
Normal file
196
scripts/add/send.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Feedgine Web API Handler
|
||||
* ALL SENSITIVE DATA IS IN config.php — NOT HERE
|
||||
* Handles: contact forms, KI chat, cookie consent, tracking events
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/config.php';
|
||||
|
||||
// ============================================================
|
||||
// SECURITY HEADERS
|
||||
// ============================================================
|
||||
header('Content-Type: application/json');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
|
||||
// CORS
|
||||
$allowedOrigins = unserialize(ALLOWED_ORIGINS);
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if (in_array($origin, $allowedOrigins)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
} else {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
}
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
function sendResponse($success, $message, $data = null, $statusCode = 200) {
|
||||
http_response_code($statusCode);
|
||||
$response = ['success' => $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);
|
||||
}
|
||||
256
scripts/cursor.js
Normal file
256
scripts/cursor.js
Normal file
@@ -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();
|
||||
});
|
||||
85
scripts/feed-calculator.js
Normal file
85
scripts/feed-calculator.js
Normal file
@@ -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();
|
||||
}
|
||||
})();
|
||||
131
scripts/feedgine.js
Normal file
131
scripts/feedgine.js
Normal file
@@ -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 →'; }
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
531
scripts/hex-background.js
Normal file
531
scripts/hex-background.js
Normal file
@@ -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 };
|
||||
})();
|
||||
194
scripts/ki-chat-bubble.js
Normal file
194
scripts/ki-chat-bubble.js
Normal file
@@ -0,0 +1,194 @@
|
||||
// KI Chat Bubble — Flowise embed (Feedgine-branded)
|
||||
// To remove: delete this file and its <script> tag in index.html.
|
||||
|
||||
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js";
|
||||
|
||||
// Read from config - on localhost talk directly to Flowise, on prod use proxy.
|
||||
const isLocal = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
||||
|
||||
Chatbot.init({
|
||||
chatflowid: isLocal ? "d63d3d02-b5fa-482c-9161-c21c615fb625" : "chat",
|
||||
apiHost: isLocal ? "https://flowise.profice.de" : window.location.origin,
|
||||
theme: {
|
||||
button: {
|
||||
backgroundColor: "#05050d",
|
||||
right: 24,
|
||||
bottom: 24,
|
||||
size: "medium",
|
||||
iconColor: "#ff6600"
|
||||
},
|
||||
chatWindow: {
|
||||
showTitle: true,
|
||||
title: "Feedgine Assistent",
|
||||
titleBackgroundColor: "#05050d",
|
||||
titleTextColor: "#e8e8f2",
|
||||
welcomeMessage: "Hallo! Ich bin der KI-Assistent von Feedgine.\n\nIch beantworte Ihre Fragen rund um Profit Intelligence, POAS-Optimierung und Server-Side Tracking.",
|
||||
backgroundColor: "#0d0d1a",
|
||||
fontSize: 15,
|
||||
showAgentMessages: true,
|
||||
poweredByTextColor: "#0d0d1a",
|
||||
botMessage: {
|
||||
backgroundColor: "rgba(255,255,255,0.07)",
|
||||
textColor: "#e8e8f2",
|
||||
showAvatar: true,
|
||||
avatarSrc: "/images/icons/KI.png"
|
||||
},
|
||||
userMessage: {
|
||||
backgroundColor: "rgba(255,102,0,0.18)",
|
||||
textColor: "#e8e8f2",
|
||||
showAvatar: false
|
||||
},
|
||||
textInput: {
|
||||
placeholder: "Ihre Nachricht...",
|
||||
backgroundColor: "rgba(255,255,255,0.04)",
|
||||
textColor: "#e8e8f2",
|
||||
sendButtonColor: "#ff6600"
|
||||
},
|
||||
footer: {
|
||||
textColor: "#8888a8",
|
||||
text: "KI-System · Keine sensiblen Daten eingeben.",
|
||||
company: " ",
|
||||
companyLink: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Inject orange border around the chat header via shadow DOM
|
||||
function injectHeaderStyle() {
|
||||
const flowise = document.querySelector('flowise-chatbot');
|
||||
if (!flowise?.shadowRoot) return false;
|
||||
|
||||
const shadow = flowise.shadowRoot;
|
||||
if (shadow.querySelector('#feedgine-header-applied')) return true;
|
||||
|
||||
const all = [...shadow.querySelectorAll('*')];
|
||||
const header = all.find(el => {
|
||||
const bg = getComputedStyle(el).backgroundColor;
|
||||
return bg === 'rgb(5, 5, 13)' && el.textContent.includes('Feedgine Assistent');
|
||||
});
|
||||
|
||||
if (!header) return false;
|
||||
|
||||
header.style.borderTop = '1px solid rgba(255,102,0,0.5)';
|
||||
header.style.borderLeft = '1px solid rgba(255,102,0,0.5)';
|
||||
header.style.borderRight = '1px solid rgba(255,102,0,0.5)';
|
||||
header.style.borderBottom = '1px solid rgba(255,102,0,0.25)';
|
||||
header.style.boxShadow = '0 0 10px rgba(255,102,0,0.28), 0 0 24px rgba(255,102,0,0.10)';
|
||||
header.style.boxSizing = 'border-box';
|
||||
|
||||
const chatWindow = header.parentElement;
|
||||
if (chatWindow) {
|
||||
chatWindow.style.borderBottomLeftRadius = '12px';
|
||||
chatWindow.style.borderBottomRightRadius = '12px';
|
||||
chatWindow.style.border = '1px solid rgba(255,102,0,0.25)';
|
||||
chatWindow.style.boxShadow = '0 0 30px rgba(255,102,0,0.08)';
|
||||
chatWindow.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
const marker = document.createElement('span');
|
||||
marker.id = 'feedgine-header-applied';
|
||||
marker.style.display = 'none';
|
||||
shadow.appendChild(marker);
|
||||
return true;
|
||||
}
|
||||
|
||||
let attempts = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (injectHeaderStyle() || ++attempts > 20) clearInterval(interval);
|
||||
}, 300);
|
||||
|
||||
// Keep shadow DOM cursor in sync with the main-page cursor toggle.
|
||||
// CSS from the outer document cannot penetrate shadow DOM, so we inject
|
||||
// a <style> tag directly and update it whenever body's class changes.
|
||||
function updateShadowCursor(shadow) {
|
||||
let style = shadow.querySelector('#feedgine-cursor-style');
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = 'feedgine-cursor-style';
|
||||
shadow.appendChild(style);
|
||||
}
|
||||
style.textContent = document.body.classList.contains('system-cursor')
|
||||
? ''
|
||||
: '* { cursor: none !important; }';
|
||||
}
|
||||
|
||||
function initShadowCursorSync() {
|
||||
const host = document.querySelector('flowise-chatbot');
|
||||
if (!host?.shadowRoot) return false;
|
||||
|
||||
updateShadowCursor(host.shadowRoot);
|
||||
|
||||
new MutationObserver(() => updateShadowCursor(host.shadowRoot))
|
||||
.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let cursorAttempts = 0;
|
||||
const cursorInterval = setInterval(() => {
|
||||
if (initShadowCursorSync() || ++cursorAttempts > 20) clearInterval(cursorInterval);
|
||||
}, 300);
|
||||
|
||||
// Global helper: open the Flowise chat and automatically send a message.
|
||||
// Called by CTA buttons across the page.
|
||||
window.openChatWithMessage = function (message) {
|
||||
// Defer by one tick so Flowise's own click-outside handler runs first
|
||||
// (otherwise it would close the chat immediately after we open it).
|
||||
setTimeout(() => {
|
||||
const chatbot = document.querySelector('flowise-chatbot');
|
||||
if (!chatbot || !chatbot.shadowRoot) return;
|
||||
|
||||
const shadow = chatbot.shadowRoot;
|
||||
|
||||
// Open chat window if not yet visible
|
||||
if (!shadow.querySelector('textarea')) {
|
||||
const toggleBtn = shadow.querySelector('button[part="button"]') || shadow.querySelector('button');
|
||||
if (toggleBtn) toggleBtn.click();
|
||||
}
|
||||
|
||||
// Poll until the textarea is available, then fill + send via Enter key
|
||||
let tries = 0;
|
||||
const poll = setInterval(() => {
|
||||
if (++tries > 30) { clearInterval(poll); return; }
|
||||
|
||||
const ta = shadow.querySelector('textarea');
|
||||
if (!ta) return;
|
||||
clearInterval(poll);
|
||||
|
||||
// Use execCommand('insertText') — fires a real, trusted InputEvent
|
||||
// that Solid.js signal tracking actually picks up (unlike synthetic events).
|
||||
ta.focus();
|
||||
ta.select();
|
||||
document.execCommand('delete', false);
|
||||
document.execCommand('insertText', false, message);
|
||||
|
||||
// Find and click the send button.
|
||||
// Walk UP from the textarea; the send button is a sibling in the
|
||||
// same input-row container. Fall back to the last non-toggle button
|
||||
// in the entire shadow DOM (send button is always last in source order).
|
||||
setTimeout(() => {
|
||||
const toggleBtn = shadow.querySelector('button[part="button"]');
|
||||
|
||||
let sendBtn = null;
|
||||
let el = ta;
|
||||
for (let i = 0; i < 6 && !sendBtn; i++) {
|
||||
el = el.parentElement;
|
||||
if (!el) break;
|
||||
const candidate = [...el.querySelectorAll('button')]
|
||||
.find(b => b !== toggleBtn);
|
||||
if (candidate) sendBtn = candidate;
|
||||
}
|
||||
|
||||
// Fallback: the send button is the last button in the shadow DOM
|
||||
if (!sendBtn) {
|
||||
const all = [...shadow.querySelectorAll('button')]
|
||||
.filter(b => b !== toggleBtn);
|
||||
sendBtn = all[all.length - 1] || null;
|
||||
}
|
||||
|
||||
if (sendBtn) sendBtn.click();
|
||||
}, 300);
|
||||
}, 150);
|
||||
}, 200); // 200 ms lets the click-outside event fully propagate before we open
|
||||
};
|
||||
64
scripts/scroll-header.js
Normal file
64
scripts/scroll-header.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Scroll Header — adapted from Profice WebSite
|
||||
* Handles sticky header state changes on scroll.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const topBanner = document.querySelector('.top-banner');
|
||||
if (!topBanner) return;
|
||||
|
||||
const scrollThreshold = 50;
|
||||
const rafDelay = 8;
|
||||
|
||||
let isScrolled = false;
|
||||
let lastScrollY = 0;
|
||||
let rafId = null;
|
||||
let lastUpdateTime = 0;
|
||||
let lastScrollTime = 0;
|
||||
|
||||
function calculateVelocity(currentScrollY, currentTime) {
|
||||
if (lastScrollTime === 0) { lastScrollTime = currentTime; return 0; }
|
||||
const timeDelta = currentTime - lastScrollTime;
|
||||
const scrollDelta = Math.abs(currentScrollY - lastScrollY);
|
||||
lastScrollTime = currentTime;
|
||||
return scrollDelta / timeDelta;
|
||||
}
|
||||
|
||||
function updateHeaderState(scrolled, velocity = 0) {
|
||||
if (scrolled === isScrolled) return;
|
||||
if (velocity > 5) {
|
||||
topBanner.classList.add('fast-scroll');
|
||||
} else {
|
||||
topBanner.classList.remove('fast-scroll');
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
if (scrolled) {
|
||||
topBanner.classList.add('scrolled');
|
||||
} else {
|
||||
topBanner.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
isScrolled = scrolled;
|
||||
}
|
||||
|
||||
function handleScroll(currentTime) {
|
||||
if (currentTime - lastUpdateTime < rafDelay) {
|
||||
rafId = requestAnimationFrame(handleScroll);
|
||||
return;
|
||||
}
|
||||
lastUpdateTime = currentTime;
|
||||
|
||||
const currentScrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const velocity = calculateVelocity(currentScrollY, currentTime);
|
||||
|
||||
updateHeaderState(currentScrollY > scrollThreshold, velocity);
|
||||
lastScrollY = currentScrollY;
|
||||
rafId = null;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!rafId) rafId = requestAnimationFrame(handleScroll);
|
||||
}, { passive: true });
|
||||
|
||||
// Initial state
|
||||
if (window.pageYOffset > scrollThreshold) updateHeaderState(true);
|
||||
});
|
||||
Reference in New Issue
Block a user