Initialisieren
This commit is contained in:
53
lib/confetti.ts
Normal file
53
lib/confetti.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
export const CONFETTI_COLORS = [
|
||||
"#2f4a3a",
|
||||
"#b4633f",
|
||||
"#d9b36a",
|
||||
"#7c9077",
|
||||
"#ffffff",
|
||||
];
|
||||
|
||||
/** Dezenter Burst bei Ergebnissen. */
|
||||
export function popConfetti() {
|
||||
confetti({
|
||||
particleCount: 70,
|
||||
spread: 60,
|
||||
startVelocity: 32,
|
||||
scalar: 0.9,
|
||||
origin: { y: 0.55 },
|
||||
colors: CONFETTI_COLORS,
|
||||
disableForReducedMotion: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Großer Gewinner-Regen mit zwei Seitenkanonen. */
|
||||
export function celebrateConfetti() {
|
||||
const defaults: confetti.Options = {
|
||||
spread: 70,
|
||||
startVelocity: 42,
|
||||
colors: CONFETTI_COLORS,
|
||||
disableForReducedMotion: true,
|
||||
};
|
||||
confetti({ ...defaults, particleCount: 120, origin: { y: 0.55 } });
|
||||
setTimeout(
|
||||
() =>
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount: 60,
|
||||
angle: 60,
|
||||
origin: { x: 0, y: 0.7 },
|
||||
}),
|
||||
150,
|
||||
);
|
||||
setTimeout(
|
||||
() =>
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount: 60,
|
||||
angle: 120,
|
||||
origin: { x: 1, y: 0.7 },
|
||||
}),
|
||||
250,
|
||||
);
|
||||
}
|
||||
8
lib/gsap.ts
Normal file
8
lib/gsap.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
}
|
||||
|
||||
export { gsap, ScrollTrigger };
|
||||
17
lib/motion.ts
Normal file
17
lib/motion.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Zentrale Motion-Tokens (aus dem Landing-Page-Animations-Skill).
|
||||
* Micro: 120–220ms · UI: 300–600ms · Story: 800–1400ms
|
||||
*/
|
||||
export const DUR = {
|
||||
micro: 0.18,
|
||||
ui: 0.45,
|
||||
story: 1.1,
|
||||
} as const;
|
||||
|
||||
export const EASE = "power3.out";
|
||||
|
||||
/** Langer, physikalisch wirkender Auslauf (Glücksrad). */
|
||||
export const wheelEase = (t: number) => 1 - Math.pow(1 - t, 5);
|
||||
|
||||
/** Münz-Auslauf: dramatisches Slow-down am Ende. */
|
||||
export const coinEase = (t: number) => 1 - Math.pow(1 - t, 4);
|
||||
78
lib/og-image.tsx
Normal file
78
lib/og-image.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
export const ogImageSize = { width: 1200, height: 630 };
|
||||
|
||||
type OgImageContentProps = {
|
||||
visualSrc: string;
|
||||
};
|
||||
|
||||
/** Gemeinsames Layout für Open-Graph- und X/Twitter-Vorschauen. */
|
||||
export function OgImageContent({ visualSrc }: OgImageContentProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#f7f4ed",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- next/image is unsupported inside next/og's ImageResponse (Satori) rendering */}
|
||||
<img
|
||||
src={visualSrc}
|
||||
alt=""
|
||||
width={1200}
|
||||
height={630}
|
||||
style={{ display: "flex", position: "absolute", inset: 0, width: "100%", height: "100%" }}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: 705,
|
||||
backgroundImage: "linear-gradient(90deg, rgba(247,244,237,0.98) 0%, rgba(247,244,237,0.92) 78%, rgba(247,244,237,0) 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
flexDirection: "column",
|
||||
width: 650,
|
||||
padding: "63px 0 58px 76px",
|
||||
color: "#111412",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", fontSize: 40, fontWeight: 700, letterSpacing: -1.5 }}>
|
||||
<span>Entscheid</span>
|
||||
<span style={{ color: "#0d7a57", fontFamily: "Georgia, serif", fontStyle: "italic", fontWeight: 400 }}>omat</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", marginTop: 78, fontSize: 66, lineHeight: 1.04, fontWeight: 700, letterSpacing: -3.2 }}>
|
||||
<span>Lass den</span>
|
||||
<span>
|
||||
<span style={{ color: "#0d7a57", fontFamily: "Georgia, serif", fontStyle: "italic", fontWeight: 400, marginRight: 16 }}>Zufall</span>entscheiden.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", marginTop: 27, color: "#4c5c54", fontSize: 24, lineHeight: 1.35 }}>
|
||||
Münze werfen, Glücksrad drehen, würfeln & mehr.
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", marginTop: "auto", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", padding: "12px 20px", borderRadius: 999, backgroundColor: "#0d7a57", color: "#ffffff", fontSize: 18, fontWeight: 700, letterSpacing: 1.2 }}>
|
||||
7 TOOLS · KOSTENLOS · OHNE ANMELDUNG
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", position: "absolute", right: 540, bottom: 34, color: "#405249", fontSize: 16, fontWeight: 700, letterSpacing: 0.4 }}>
|
||||
entscheidomat.com
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
lib/sounds.ts
Normal file
122
lib/sounds.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Prozedurale Sound-Engine auf Basis der WebAudio API.
|
||||
* Keine Audio-Dateien nötig – alles wird live synthetisiert.
|
||||
*/
|
||||
|
||||
export type SoundName = "tick" | "coin" | "dice" | "pop" | "win" | "whoosh";
|
||||
|
||||
let ctx: AudioContext | null = null;
|
||||
|
||||
function ac(): AudioContext | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
if (!ctx) {
|
||||
const AC =
|
||||
window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext?: typeof AudioContext })
|
||||
.webkitAudioContext;
|
||||
if (!AC) return null;
|
||||
ctx = new AC();
|
||||
}
|
||||
if (ctx.state === "suspended") {
|
||||
ctx.resume().catch(() => {});
|
||||
}
|
||||
return ctx;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function tone(
|
||||
c: AudioContext,
|
||||
freq: number,
|
||||
type: OscillatorType,
|
||||
dur: number,
|
||||
peak = 0.2,
|
||||
when = 0,
|
||||
slideTo?: number,
|
||||
) {
|
||||
const t0 = c.currentTime + when;
|
||||
const osc = c.createOscillator();
|
||||
const gain = c.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.setValueAtTime(freq, t0);
|
||||
if (slideTo) {
|
||||
osc.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur);
|
||||
}
|
||||
gain.gain.setValueAtTime(0, t0);
|
||||
gain.gain.linearRampToValueAtTime(peak, t0 + 0.008);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
||||
osc.connect(gain).connect(c.destination);
|
||||
osc.start(t0);
|
||||
osc.stop(t0 + dur + 0.05);
|
||||
}
|
||||
|
||||
function noiseBurst(
|
||||
c: AudioContext,
|
||||
dur: number,
|
||||
peak: number,
|
||||
filterFreq: number,
|
||||
when = 0,
|
||||
sweepTo?: number,
|
||||
) {
|
||||
const t0 = c.currentTime + when;
|
||||
const len = Math.max(1, Math.floor(c.sampleRate * dur));
|
||||
const buffer = c.createBuffer(1, len, c.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
|
||||
|
||||
const src = c.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
const filter = c.createBiquadFilter();
|
||||
filter.type = "bandpass";
|
||||
filter.frequency.setValueAtTime(filterFreq, t0);
|
||||
filter.Q.value = 1.1;
|
||||
if (sweepTo) {
|
||||
filter.frequency.exponentialRampToValueAtTime(sweepTo, t0 + dur);
|
||||
}
|
||||
const gain = c.createGain();
|
||||
gain.gain.setValueAtTime(0, t0);
|
||||
gain.gain.linearRampToValueAtTime(peak, t0 + 0.01);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
||||
src.connect(filter).connect(gain).connect(c.destination);
|
||||
src.start(t0);
|
||||
}
|
||||
|
||||
export function playSound(name: SoundName) {
|
||||
const c = ac();
|
||||
if (!c) return;
|
||||
try {
|
||||
switch (name) {
|
||||
case "tick":
|
||||
tone(c, 1900, "square", 0.035, 0.07);
|
||||
break;
|
||||
case "coin":
|
||||
// Metallischer Doppelton + Glitzer
|
||||
tone(c, 2093, "triangle", 0.55, 0.16);
|
||||
tone(c, 2637, "triangle", 0.45, 0.1, 0.012);
|
||||
tone(c, 3520, "sine", 0.3, 0.05, 0.02);
|
||||
break;
|
||||
case "dice":
|
||||
// Prasselndes Klackern
|
||||
[0, 0.09, 0.21, 0.31].forEach((w, i) =>
|
||||
noiseBurst(c, 0.05, 0.2 - i * 0.03, 1600 + Math.random() * 1000, w),
|
||||
);
|
||||
break;
|
||||
case "pop":
|
||||
tone(c, 420, "sine", 0.14, 0.22, 0, 880);
|
||||
break;
|
||||
case "whoosh":
|
||||
noiseBurst(c, 0.55, 0.1, 500, 0, 2600);
|
||||
break;
|
||||
case "win":
|
||||
// Kleine Fanfare C5–E5–G5–C6
|
||||
[523.25, 659.25, 783.99, 1046.5].forEach((f, i) =>
|
||||
tone(c, f, "triangle", 0.4, 0.13, i * 0.09),
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Sound ist Dekoration – niemals crashen
|
||||
}
|
||||
}
|
||||
10
lib/tool-pages.ts
Normal file
10
lib/tool-pages.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/** Dedizierte SEO-Landingpages der Tools, für konsistentes internes Linking. */
|
||||
export const TOOL_PAGES = [
|
||||
{ slug: "ja-nein-generator", label: "Ja-Nein-Generator", lastModified: "2026-07-23" },
|
||||
{ slug: "gluecksrad", label: "Glücksrad", lastModified: "2026-07-23" },
|
||||
{ slug: "wuerfel-online", label: "Würfel Online", lastModified: "2026-07-23" },
|
||||
{ slug: "namen-auslosen", label: "Namen-Auslosung", lastModified: "2026-07-23" },
|
||||
{ slug: "muenze-werfen", label: "Münze werfen", lastModified: "2026-07-23" },
|
||||
{ slug: "zufallszahl-generator", label: "Zufallszahl-Generator", lastModified: "2026-07-23" },
|
||||
{ slug: "magic-8-ball", label: "Magic 8-Ball", lastModified: "2026-07-23" },
|
||||
] as const;
|
||||
Reference in New Issue
Block a user