/* 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 (
{fuel.note}
{fuel.name}