/* Nexaro fuel-scroll section. Motion is tied to blend composition and readout state. */ (function () { if (document.getElementById("fuel-redesign-kf")) return; const style = document.createElement("style"); style.id = "fuel-redesign-kf"; style.textContent = ` @keyframes nxf-wipe { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); } } @keyframes nxf-up { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } } `; document.head.appendChild(style); })(); const FUEL_ITEMS = [ { name: "Diesel B7", spec: "EN 590", tag: "B7", blend: 7, blendLabel: "Biodiesel FAME", baseLabel: "Fossiler Diesel", accent: "199, 145, 58", note: "Straßendiesel mit bis zu 7% Biodiesel, verfügbar in Saisonqualitäten und bei der Beladung zertifiziert.", }, { name: "Benzin E5", spec: "EN 228", tag: "E5", blend: 5, blendLabel: "Ethanol", baseLabel: "Fossiles Benzin", accent: "204, 157, 80", note: "Super 95 mit bis zu 5% Ethanol, verfügbar für ältere Fahrzeuge und leistungsstarke Motoren.", }, { name: "Benzin E10", spec: "EN 228", tag: "E10", blend: 10, blendLabel: "Ethanol", baseLabel: "Fossiles Benzin", accent: "191, 132, 62", note: "Super E10 mit bis zu 10% Ethanol und geringerem fossilem Anteil für den Großteil der aktiven Flotte.", }, { name: "HVO 100", spec: "EN 15940", tag: "HVO", blend: 100, blendLabel: "Erneuerbares HVO", baseLabel: "Fossiler Anteil", accent: "127, 192, 160", note: "Paraffinischer erneuerbarer Diesel mit niedrigerer CO2-Bilanz und Drop-in-Nutzung für kompatible Dieselmotoren.", }, { name: "Heizöl", spec: "DIN 51603-1", tag: "Öl", blend: 0, blendLabel: "Additivpaket", baseLabel: "Schwefelarmes Heizöl", accent: "227, 194, 130", note: "Schwefelarmes Heizöl für Haushalte, Wohnungswirtschaft und gewerbliche Standorte.", }, ]; function getFuelViewportHeight() { return window.visualViewport?.height || document.documentElement.clientHeight || window.innerHeight; } function fuelMix(a, b, t) { return a.map((value, index) => value + (b[index] - value) * t); } function BlendColumn({ stateRef }) { const ref = React.useRef(null); React.useEffect(() => { const canvas = ref.current; const wrap = canvas.parentElement; const dpr = Math.min(2, window.devicePixelRatio || 1); const ctx = canvas.getContext("2d"); const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; let width = 0; let height = 0; let raf; const resize = () => { width = wrap.clientWidth; height = wrap.clientHeight; canvas.width = width * dpr; canvas.height = height * dpr; }; const draw = () => { const state = stateRef.current; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, width, height); const columnWidth = Math.min(width * 0.46, 136); const x = (width - columnWidth) / 2; const top = height * 0.07; const bottom = height - top; const columnHeight = bottom - top; const radius = 5; ctx.strokeStyle = "rgba(255,255,255,0.18)"; ctx.lineWidth = 1; ctx.font = "500 10px Ubuntu, sans-serif"; ctx.textAlign = "right"; ctx.textBaseline = "middle"; for (let index = 0; index <= 10; index += 1) { const y = bottom - (columnHeight * index) / 10; const long = index % 5 === 0; ctx.beginPath(); ctx.moveTo(x - 14, y); ctx.lineTo(x - (long ? 24 : 18), y); ctx.stroke(); if (long) { ctx.fillStyle = "rgba(255,255,255,0.42)"; ctx.fillText(String(index * 10), x - 30, y); } } function tube() { const r = Math.min(radius, columnWidth / 2); ctx.beginPath(); ctx.moveTo(x + r, top); ctx.arcTo(x + columnWidth, top, x + columnWidth, bottom, r); ctx.arcTo(x + columnWidth, bottom, x, bottom, r); ctx.arcTo(x, bottom, x, top, r); ctx.arcTo(x, top, x + columnWidth, top, r); ctx.closePath(); } tube(); ctx.fillStyle = "rgba(255,255,255,0.025)"; ctx.fill(); ctx.save(); tube(); ctx.clip(); const blend = Math.max(0, Math.min(1, state.blend)); const blendTop = bottom - columnHeight * blend; const baseGradient = ctx.createLinearGradient(0, top, 0, bottom); baseGradient.addColorStop(0, "rgba(65, 91, 126, 0.55)"); baseGradient.addColorStop(1, "rgba(15, 32, 65, 0.88)"); if (blend < 0.999) { ctx.fillStyle = baseGradient; ctx.fillRect(x, top, columnWidth, blendTop - top); } const [red, green, blue] = state.rgb; if (blend > 0.001) { const blendGradient = ctx.createLinearGradient(0, blendTop, 0, bottom); blendGradient.addColorStop(0, `rgba(${red},${green},${blue},0.92)`); blendGradient.addColorStop(1, `rgba(${red},${green},${blue},0.58)`); ctx.fillStyle = blendGradient; ctx.fillRect(x, blendTop, columnWidth, bottom - blendTop); } if (blend > 0.001 && blend < 0.999) { ctx.fillStyle = `rgba(${red},${green},${blue},0.92)`; ctx.fillRect(x, blendTop - 1, columnWidth, 2); } if (!reduce) { const scanY = top + ((state.time * 0.12) % 1) * columnHeight; const scan = ctx.createLinearGradient(0, scanY - 24, 0, scanY + 24); scan.addColorStop(0, "rgba(255,255,255,0)"); scan.addColorStop(0.5, "rgba(255,255,255,0.07)"); scan.addColorStop(1, "rgba(255,255,255,0)"); ctx.fillStyle = scan; ctx.fillRect(x, scanY - 24, columnWidth, 48); } const highlight = ctx.createLinearGradient(x, 0, x + columnWidth * 0.38, 0); highlight.addColorStop(0, "rgba(255,255,255,0.1)"); highlight.addColorStop(1, "rgba(255,255,255,0)"); ctx.fillStyle = highlight; ctx.fillRect(x, top, columnWidth * 0.38, columnHeight); ctx.restore(); tube(); ctx.strokeStyle = "rgba(255,255,255,0.24)"; ctx.lineWidth = 1.2; ctx.stroke(); if (!reduce) raf = requestAnimationFrame(draw); }; resize(); const observer = new ResizeObserver(resize); observer.observe(wrap); if (reduce) draw(); else raf = requestAnimationFrame(draw); return () => { cancelAnimationFrame(raf); observer.disconnect(); }; }, []); return ; } function ScrollFuels() { const ref = React.useRef(null); const initialRgb = FUEL_ITEMS[0].accent.split(",").map(Number); const stateRef = React.useRef({ blend: FUEL_ITEMS[0].blend / 100, rgb: initialRgb, time: 0 }); const targetRef = React.useRef({ blend: FUEL_ITEMS[0].blend / 100, rgb: initialRgb }); const stepRef = React.useRef(0); const progressRef = React.useRef(0); const rafRef = React.useRef(0); const [step, setStep] = React.useState(0); const [progress, setProgress] = React.useState(0); const [shownBlend, setShownBlend] = React.useState(FUEL_ITEMS[0].blend); React.useEffect(() => { let raf; const start = performance.now(); const tick = (now) => { const state = stateRef.current; const target = targetRef.current; state.blend += (target.blend - state.blend) * 0.09; state.rgb = fuelMix(state.rgb, target.rgb, 0.09); state.time = (now - start) / 1000; setShownBlend((value) => value + ((target.blend * 100) - value) * 0.12); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, []); const setFuel = React.useCallback((index) => { const item = FUEL_ITEMS[index]; if (stepRef.current !== index) { stepRef.current = index; setStep(index); } targetRef.current = { blend: item.blend / 100, rgb: item.accent.split(",").map(Number), }; }, []); React.useEffect(() => { const apply = () => { const el = ref.current; if (!el) return; const total = el.offsetHeight - getFuelViewportHeight(); if (total <= 20) return; const passed = Math.min(Math.max(-el.getBoundingClientRect().top, 0), total); const p = passed / total; const index = Math.min(FUEL_ITEMS.length - 1, Math.floor(p * FUEL_ITEMS.length)); if (Math.abs(progressRef.current - p) > 0.002) { progressRef.current = p; setProgress(p); } setFuel(index); }; const schedule = () => { if (rafRef.current) return; rafRef.current = window.requestAnimationFrame(() => { rafRef.current = 0; apply(); }); }; window.addEventListener("scroll", schedule, { passive: true }); window.addEventListener("resize", schedule); window.visualViewport?.addEventListener("resize", schedule); apply(); return () => { window.removeEventListener("scroll", schedule); window.removeEventListener("resize", schedule); window.visualViewport?.removeEventListener("resize", schedule); if (rafRef.current) window.cancelAnimationFrame(rafRef.current); }; }, [setFuel]); const goTo = (index) => { const el = ref.current; if (!el) return; const total = el.offsetHeight - getFuelViewportHeight(); setFuel(index); if (total <= 20) return; window.scrollTo({ top: el.offsetTop + ((index + 0.5) / FUEL_ITEMS.length) * total, behavior: "smooth", }); }; const fuel = FUEL_ITEMS[step]; const accent = `rgb(${fuel.accent})`; const titleKey = `title-${step}`; const progressScale = Math.max(0, Math.min(1, progress)); return (
Produkte / Fünf Kraftstoffe, ein Lieferant

{fuel.name}

{fuel.tag} {fuel.spec}

{fuel.note}

{fuel.blend > 0 ? "Erneuerbarer bzw. Bio-Anteil" : "Zusammensetzung"}
{Math.round(shownBlend)} %
Norm {fuel.spec} Zertifiziert
{FUEL_ITEMS.map((item, index) => { const active = index === step; const fill = index < step ? 1 : active ? Math.max(0, Math.min(1, progress * FUEL_ITEMS.length - index)) : 0; return ( ); })}
); } function ReadoutRow({ swatch, label, value }) { return (
{label} {value}
); } window.ScrollFuels = ScrollFuels;