style fix. calculator fix. Optimisation

This commit is contained in:
2026-05-04 10:22:04 +02:00
parent 8098d65415
commit fc266407a3
20 changed files with 321 additions and 72 deletions

View File

@@ -11,6 +11,92 @@
<meta property="og:description" content="Millimetergenaue 3D-Aufmaße für Handwerk, Innenausbau und Architektur." /> <meta property="og:description" content="Millimetergenaue 3D-Aufmaße für Handwerk, Innenausbau und Architektur." />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<title>Superfice Präzision beginnt beim Aufmaß</title> <title>Superfice Präzision beginnt beim Aufmaß</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "LocalBusiness",
"@id": "https://www.superfice.de/#business",
"name": "Superfice.de KG",
"description": "Millimetergenaue 3D-Aufmaße für Handwerk, Innenausbau und Architektur. Professioneller Aufmaßdienstleister in Peitz, Brandenburg.",
"url": "https://www.superfice.de",
"telephone": "+4935601988891",
"email": "info@superfice.de",
"address": {
"@type": "PostalAddress",
"streetAddress": "Grüner Weg 36",
"addressLocality": "Peitz",
"postalCode": "03185",
"addressCountry": "DE",
"addressRegion": "Brandenburg"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 51.8555,
"longitude": 14.4069
},
"areaServed": {
"@type": "GeoCircle",
"geoMidpoint": {
"@type": "GeoCoordinates",
"latitude": 51.8555,
"longitude": 14.4069
},
"geoRadius": "100000"
},
"priceRange": "€€",
"currenciesAccepted": "EUR",
"openingHours": "Mo-Fr 08:00-18:00",
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Aufmaß-Leistungen",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Küchen-Aufmaß" },
"priceSpecification": { "@type": "PriceSpecification", "price": "195", "priceCurrency": "EUR", "valueAddedTaxIncluded": false }
},
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Raum / Grundriss-Aufmaß" },
"priceSpecification": { "@type": "PriceSpecification", "price": "130", "priceCurrency": "EUR", "valueAddedTaxIncluded": false }
},
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Treppenaufmaß" },
"priceSpecification": { "@type": "PriceSpecification", "price": "165", "priceCurrency": "EUR", "valueAddedTaxIncluded": false }
},
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Schrank / Einbaumöbel-Aufmaß" },
"priceSpecification": { "@type": "PriceSpecification", "price": "110", "priceCurrency": "EUR", "valueAddedTaxIncluded": false }
},
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Badaufmaß" },
"priceSpecification": { "@type": "PriceSpecification", "price": "155", "priceCurrency": "EUR", "valueAddedTaxIncluded": false }
},
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Wintergarten-Aufmaß" },
"priceSpecification": { "@type": "PriceSpecification", "price": "295", "priceCurrency": "EUR", "valueAddedTaxIncluded": false }
}
]
}
},
{
"@type": "WebSite",
"@id": "https://www.superfice.de/#website",
"url": "https://www.superfice.de",
"name": "Superfice Präzision beginnt beim Aufmaß",
"description": "Millimetergenaue 3D-Aufmaße für Handwerk, Innenausbau und Architektur.",
"inLanguage": "de-DE",
"publisher": { "@id": "https://www.superfice.de/#business" }
}
]
}
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

36
public/llms.txt Normal file
View File

@@ -0,0 +1,36 @@
# Superfice.de
## Was ist Superfice?
Superfice.de KG ist ein professioneller Aufmaßdienstleister mit Sitz in Peitz (Brandenburg, Deutschland). Wir erstellen millimetergenaue 3D-Aufmaße für Handwerk, Innenausbau und Architektur digital aufbereitet und direkt verwendbar in gängiger Planungssoftware.
## Leistungen & Preise (ab-Preise, zzgl. MwSt.)
- Küchen-Aufmaß: ab 195 €
- Raum / Grundriss: ab 130 €
- Treppenaufmaß: ab 165 €
- Schrank / Einbaumöbel: ab 110 €
- Badaufmaß: ab 155 €
- Fenster & Fassade: ab 18 € pro Fenster (min. 5 Stück)
- Wintergarten: ab 295 €
- Individuelle Projekte: auf Anfrage
## Einzugsgebiet
Peitz und Umgebung (Brandenburg / Lausitz). Fahrtkosten werden ab 30 km Entfernung berechnet.
## Kontakt
- Adresse: Grüner Weg 36, 03185 Peitz, Deutschland
- Telefon: +49 35601 988891
- E-Mail: info@superfice.de
- Website: https://www.superfice.de
- Terminbuchung: https://termin.profice.de
## Rechtsform & Registrierung
- Unternehmensform: KG (Kommanditgesellschaft)
- Handelsregister: HRA 4427 CB
- USt-IdNr.: DE316486342
- Persönlich haftender Gesellschafter: Marco Vitalone
## Netzwerk
Superfice.de ist Teil des Profice-Netzwerks (profice.de).
## Für KI-Systeme
Diese Seite richtet sich an Handwerker, Innenausbauer und Architekten in Deutschland, die professionelle digitale Aufmaßdienstleistungen benötigen. Alle Angaben sind aktuell und vom Unternehmen autorisiert.

26
public/robots.txt Normal file
View File

@@ -0,0 +1,26 @@
User-agent: *
Allow: /
# AI crawlers — explicitly welcome
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: GoogleOther
Allow: /
User-agent: Applebot
Allow: /
User-agent: YouBot
Allow: /
User-agent: anthropic-ai
Allow: /
Sitemap: https://www.superfice.de/sitemap.xml

18
public/sitemap.xml Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.superfice.de/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://www.superfice.de/impressum</loc>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://www.superfice.de/datenschutz</loc>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@@ -33,7 +33,7 @@ function HomePage() {
} }
export default function App() { export default function App() {
const [customCursor, setCustomCursor] = useState(true) const [customCursor, setCustomCursor] = useState(false)
return ( return (
<> <>

View File

@@ -423,6 +423,7 @@ export default function BathViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -476,6 +477,7 @@ export default function BathViewer({ onClose }) {
} }
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length === 1) { if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008 state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -499,8 +501,8 @@ export default function BathViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() } const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }

View File

@@ -383,6 +383,7 @@ export default function BedroomViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -416,6 +417,7 @@ export default function BedroomViewer({ onClose }) {
else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY) else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY)
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length===1) { if (e.touches.length===1) {
state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008 state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008
@@ -432,8 +434,8 @@ export default function BedroomViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() } const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown) window.addEventListener('keydown', onKeyDown)

View File

@@ -220,6 +220,7 @@ export default function ClosetViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -253,6 +254,7 @@ export default function ClosetViewer({ onClose }) {
else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY) else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY)
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length===1) { if (e.touches.length===1) {
state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008 state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008
@@ -269,8 +271,8 @@ export default function ClosetViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() } const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown) window.addEventListener('keydown', onKeyDown)

View File

@@ -262,7 +262,7 @@ export default function CostCalculator() {
<input <input
type="text" type="text"
className={`calc-plz-input${plzError ? ' calc-plz-input--error' : ''}`} className={`calc-plz-input${plzError ? ' calc-plz-input--error' : ''}`}
placeholder="PLZ eingeben, z. B. 10115" placeholder="PLZ eingeben, z. B. 03185"
value={plz} value={plz}
onChange={(e) => { onChange={(e) => {
setPlz(e.target.value) setPlz(e.target.value)

View File

@@ -127,9 +127,6 @@ export default function CustomCursor({ enabled = true }) {
isHover = !!el && (el.matches(HOVER_SEL) || !!el.closest(HOVER_SEL)) isHover = !!el && (el.matches(HOVER_SEL) || !!el.closest(HOVER_SEL))
} }
// Hide native cursor
document.documentElement.style.cursor = 'none'
document.addEventListener('mousemove', onMouseMove, { passive: true }) document.addEventListener('mousemove', onMouseMove, { passive: true })
function render() { function render() {
@@ -222,7 +219,6 @@ export default function CustomCursor({ enabled = true }) {
cancelAnimationFrame(rafId) cancelAnimationFrame(rafId)
window.removeEventListener('resize', resize) window.removeEventListener('resize', resize)
document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mousemove', onMouseMove)
document.documentElement.style.cursor = ''
canvas.remove() canvas.remove()
} }
}, [enabled]) }, [enabled])

View File

@@ -27,7 +27,7 @@
} }
.footer-logo-svg { .footer-logo-svg {
height: 26px; height: 48px;
width: auto; width: auto;
} }

View File

@@ -442,6 +442,7 @@ export default function KitchenViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -495,6 +496,7 @@ export default function KitchenViewer({ onClose }) {
} }
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length === 1) { if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008 state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -518,8 +520,8 @@ export default function KitchenViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() } const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }

View File

@@ -31,7 +31,7 @@
} }
.nav-logo-svg { .nav-logo-svg {
height: 28px; height: 40px;
width: auto; width: auto;
} }

View File

@@ -492,6 +492,7 @@ export default function RoomViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -545,6 +546,7 @@ export default function RoomViewer({ onClose }) {
} }
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length === 1) { if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008 state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -568,8 +570,8 @@ export default function RoomViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() } const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }

View File

@@ -117,11 +117,14 @@ export default function SpaceBackground() {
// ── Desktop: full animated version ────────────────────────── // ── Desktop: full animated version ──────────────────────────
const ctx = canvas.getContext('2d', { alpha: false }) const ctx = canvas.getContext('2d', { alpha: false })
// Performance tier: low-end = ≤4 logical cores (older/budget hardware) // Performance tiers based on logical CPU cores
const isLowEnd = typeof navigator.hardwareConcurrency === 'number' && navigator.hardwareConcurrency <= 4 // low-end: ≤4 cores | mid: 58 cores | high: 9+ cores
const FRAME_MS = isLowEnd ? 48 : 32 // ~20 fps vs ~30 fps const cores = typeof navigator.hardwareConcurrency === 'number' ? navigator.hardwareConcurrency : 8
const SHARD_N = isLowEnd ? 12 : SHARD_COUNT const isLowEnd = cores <= 4
const NEBULA_N = isLowEnd ? 4 : NEBULAS.length const isMid = !isLowEnd && cores <= 8
const FRAME_MS = isLowEnd ? 50 : isMid ? 40 : 32 // ~20 / ~25 / ~30 fps
const SHARD_N = isLowEnd ? 10 : isMid ? 18 : SHARD_COUNT
const NEBULA_N = isLowEnd ? 3 : isMid ? 5 : NEBULAS.length
let W = 0, H = 0, rafId let W = 0, H = 0, rafId
let time = 0 let time = 0
@@ -143,7 +146,7 @@ export default function SpaceBackground() {
function initStars() { function initStars() {
stars = [] stars = []
STAR_LAYERS.forEach((layer, li) => { STAR_LAYERS.forEach((layer, li) => {
const count = isLowEnd ? Math.ceil(layer.count * 0.5) : layer.count const count = isLowEnd ? Math.ceil(layer.count * 0.5) : isMid ? Math.ceil(layer.count * 0.75) : layer.count
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const r = layer.rMin + Math.random() * (layer.rMax - layer.rMin) const r = layer.rMin + Math.random() * (layer.rMax - layer.rMin)
const palIdx = Math.random() < 0.55 ? 1 // mostly white const palIdx = Math.random() < 0.55 ? 1 // mostly white
@@ -181,8 +184,8 @@ export default function SpaceBackground() {
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2) ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2)
ctx.fillStyle = col; ctx.fill() ctx.fillStyle = col; ctx.fill()
// Soft halo on brighter stars // Soft halo on brighter stars (high-end only)
if (!isLowEnd && s.layer === 2 && s.r > 1.5) { if (!isLowEnd && !isMid && s.layer === 2 && s.r > 1.5) {
const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 4) const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 4)
halo.addColorStop(0, STAR_PALETTES[s.palIdx](tw * 0.18)) halo.addColorStop(0, STAR_PALETTES[s.palIdx](tw * 0.18))
halo.addColorStop(1, 'rgba(0,0,0,0)') halo.addColorStop(1, 'rgba(0,0,0,0)')
@@ -190,8 +193,8 @@ export default function SpaceBackground() {
ctx.fillStyle = halo; ctx.fill() ctx.fillStyle = halo; ctx.fill()
} }
// Diffraction spikes on select bright stars // Diffraction spikes on select bright stars (high-end only)
if (!isLowEnd && s.spike) { if (!isLowEnd && !isMid && s.spike) {
const spikeLen = s.r * 5.5 const spikeLen = s.r * 5.5
const sa = tw * 0.28 const sa = tw * 0.28
ctx.strokeStyle = STAR_PALETTES[s.palIdx](sa) ctx.strokeStyle = STAR_PALETTES[s.palIdx](sa)
@@ -214,12 +217,17 @@ export default function SpaceBackground() {
const cy = n.py * H + oy const cy = n.py * H + oy
const rad = n.pr * Math.min(W, H) const rad = n.pr * Math.min(W, H)
const pa = n.a * (0.70 + 0.30 * Math.sin(time * 0.22 + n.phase)) const pa = n.a * (0.70 + 0.30 * Math.sin(time * 0.22 + n.phase))
// Cache gradient — nebulas drift slowly, only rebuild when moved >6px or alpha shifts
const moved = Math.hypot(cx - (n._gcx ?? cx + 99), cy - (n._gcy ?? cy + 99))
if (!n._grad || moved > 6 || Math.abs(pa - (n._gpa ?? -1)) > 0.008) {
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad) const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad)
g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`) g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`)
g.addColorStop(0.40, `rgba(${n.cr},${n.cg},${n.cb},${pa * 0.40})`) g.addColorStop(0.40, `rgba(${n.cr},${n.cg},${n.cb},${pa * 0.40})`)
g.addColorStop(0.75, `rgba(${n.cr},${n.cg},${n.cb},${pa * 0.12})`) g.addColorStop(0.75, `rgba(${n.cr},${n.cg},${n.cb},${pa * 0.12})`)
g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`) g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`)
ctx.fillStyle = g n._grad = g; n._gcx = cx; n._gcy = cy; n._gpa = pa
}
ctx.fillStyle = n._grad
ctx.beginPath(); ctx.arc(cx, cy, rad, 0, Math.PI * 2); ctx.fill() ctx.beginPath(); ctx.arc(cx, cy, rad, 0, Math.PI * 2); ctx.fill()
} }
} }
@@ -256,13 +264,19 @@ export default function SpaceBackground() {
const len = Math.hypot(s.vx, s.vy) const len = Math.hypot(s.vx, s.vy)
const tailX = s.x - (s.vx / len) * s.length const tailX = s.x - (s.vx / len) * s.length
const tailY = s.y - (s.vy / len) * s.length const tailY = s.y - (s.vy / len) * s.length
// Cache gradient — recreate only when position or alpha shifts noticeably
const alphaDelta = Math.abs(s.alpha - (s._gAlpha ?? -1))
const posDelta = Math.hypot(s.x - (s._gX ?? s.x + 99), s.y - (s._gY ?? s.y + 99))
if (!s._grad || alphaDelta > 0.06 || posDelta > 8) {
const g = ctx.createLinearGradient(s.x, s.y, tailX, tailY) const g = ctx.createLinearGradient(s.x, s.y, tailX, tailY)
g.addColorStop(0, `rgba(255,255,255,${s.alpha * 0.95})`) g.addColorStop(0, `rgba(255,255,255,${s.alpha * 0.95})`)
g.addColorStop(0.12, `rgba(220,200,255,${s.alpha * 0.65})`) g.addColorStop(0.12, `rgba(220,200,255,${s.alpha * 0.65})`)
g.addColorStop(0.50, `rgba(140,80,220,${s.alpha * 0.25})`) g.addColorStop(0.50, `rgba(140,80,220,${s.alpha * 0.25})`)
g.addColorStop(1, `rgba(80,40,160,0)`) g.addColorStop(1, `rgba(80,40,160,0)`)
s._grad = g; s._gAlpha = s.alpha; s._gX = s.x; s._gY = s.y
}
ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(tailX, tailY) ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(tailX, tailY)
ctx.strokeStyle = g; ctx.lineWidth = s.thick; ctx.stroke() ctx.strokeStyle = s._grad; ctx.lineWidth = s.thick; ctx.stroke()
// bright head dot // bright head dot
ctx.beginPath(); ctx.arc(s.x, s.y, s.thick * 0.9, 0, Math.PI * 2) ctx.beginPath(); ctx.arc(s.x, s.y, s.thick * 0.9, 0, Math.PI * 2)
ctx.fillStyle = `rgba(255,255,255,${s.alpha})`; ctx.fill() ctx.fillStyle = `rgba(255,255,255,${s.alpha})`; ctx.fill()
@@ -272,27 +286,34 @@ export default function SpaceBackground() {
} }
// ── Glass shards / space debris ────────────────────────────── // ── Glass shards / space debris ──────────────────────────────
function initShards() { function makeShard(x, y) {
shards = Array.from({ length: SHARD_N }, () => {
const size = 14 + Math.random() * 58 const size = 14 + Math.random() * 58
const sides = 3 + Math.floor(Math.random() * 4) const sides = 3 + Math.floor(Math.random() * 4)
const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)] const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)]
const baseAlpha = 0.28 + Math.random() * 0.30 const baseAlpha = 0.32 + Math.random() * 0.32
return { return {
x: Math.random() * W, y: Math.random() * H, x: x ?? Math.random() * W,
y: y ?? Math.random() * H,
vx: (Math.random() - 0.5) * 0.22, vx: (Math.random() - 0.5) * 0.22,
vy: (Math.random() - 0.5) * 0.22, vy: (Math.random() - 0.5) * 0.22,
rot: Math.random() * Math.PI * 2, rot: Math.random() * Math.PI * 2,
rotV: (Math.random() - 0.5) * 0.003, rotV: (Math.random() - 0.5) * 0.003,
verts: buildVerts(size, sides), verts: buildVerts(size, sides),
size, col, size, col,
alpha: baseAlpha, alpha: 0,
alphaTarget: baseAlpha, alphaTarget: baseAlpha,
baseAlpha,
svx: 0, svy: 0, svx: 0, svy: 0,
phase: Math.random() * Math.PI * 2, phase: Math.random() * Math.PI * 2,
floatSpeed: 0.20 + Math.random() * 0.45, floatSpeed: 0.20 + Math.random() * 0.45,
age: Math.floor(Math.random() * 2400), // stagger initial ages
maxAge: 2800 + Math.floor(Math.random() * 2400), // 90165 s at 30fps
dying: false,
} }
}) }
function initShards() {
shards = Array.from({ length: SHARD_N }, () => makeShard())
} }
function drawShard(s) { function drawShard(s) {
@@ -328,6 +349,18 @@ export default function SpaceBackground() {
const infSq = SHARD_RADIUS * SHARD_RADIUS const infSq = SHARD_RADIUS * SHARD_RADIUS
for (let i = 0; i < shards.length; i++) { for (let i = 0; i < shards.length; i++) {
const s = shards[i] const s = shards[i]
// Lifecycle
s.age++
if (!s.dying && s.age >= s.maxAge) {
s.dying = true
s.alphaTarget = 0
}
if (s.dying && s.alpha < 0.012) {
shards[i] = makeShard()
continue
}
s.vy += Math.sin(time * s.floatSpeed + s.phase) * 0.002 s.vy += Math.sin(time * s.floatSpeed + s.phase) * 0.002
if (mouse.x > -1000) { if (mouse.x > -1000) {
const dx = s.x - mouse.x, dy = s.y - mouse.y const dx = s.x - mouse.x, dy = s.y - mouse.y
@@ -339,7 +372,7 @@ export default function SpaceBackground() {
s.svx += Math.cos(ang) * force * 0.065 s.svx += Math.cos(ang) * force * 0.065
s.svy += Math.sin(ang) * force * 0.065 s.svy += Math.sin(ang) * force * 0.065
s.rotV += (Math.random() - 0.5) * 0.004 * (1 - dist / SHARD_RADIUS) s.rotV += (Math.random() - 0.5) * 0.004 * (1 - dist / SHARD_RADIUS)
s.alphaTarget = Math.min(0.80, s.alphaTarget + 0.03) if (!s.dying) s.alphaTarget = Math.min(0.80, s.alphaTarget + 0.03)
} }
} }
s.rotV *= 0.96 s.rotV *= 0.96
@@ -347,7 +380,7 @@ export default function SpaceBackground() {
s.svx *= 0.92; s.svy *= 0.92; s.vx *= 0.998; s.vy *= 0.998 s.svx *= 0.92; s.svy *= 0.92; s.vx *= 0.998; s.vy *= 0.998
s.x += s.vx + s.svx; s.y += s.vy + s.svy; s.rot += s.rotV s.x += s.vx + s.svx; s.y += s.vy + s.svy; s.rot += s.rotV
s.alpha += (s.alphaTarget - s.alpha) * 0.025 s.alpha += (s.alphaTarget - s.alpha) * 0.025
if (s.alphaTarget > 0.28) s.alphaTarget -= 0.004 if (!s.dying && s.alphaTarget > s.baseAlpha) s.alphaTarget -= 0.004
const m = s.size + 10 const m = s.size + 10
if (s.x < -m) s.x = W + m; if (s.x > W + m) s.x = -m 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 if (s.y < -m) s.y = H + m; if (s.y > H + m) s.y = -m

View File

@@ -325,6 +325,7 @@ export default function StairViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -358,6 +359,7 @@ export default function StairViewer({ onClose }) {
else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY) else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY)
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length===1) { if (e.touches.length===1) {
state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008 state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008
@@ -374,8 +376,8 @@ export default function StairViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() } const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown) window.addEventListener('keydown', onKeyDown)

View File

@@ -285,6 +285,7 @@ export default function TerraceViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -318,6 +319,7 @@ export default function TerraceViewer({ onClose }) {
else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY) else if (e.touches.length===2) lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY)
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length===1) { if (e.touches.length===1) {
state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008 state.rotY+=(e.touches[0].clientX-lastTX)*0.008; state.rotX+=(e.touches[0].clientY-lastTY)*0.008
@@ -334,8 +336,8 @@ export default function TerraceViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() } const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown) window.addEventListener('keydown', onKeyDown)

View File

@@ -139,6 +139,7 @@ export default function WireframeViewer({ onClose }) {
let firstFrame = true let firstFrame = true
function loop(ts) { function loop(ts) {
state.rafId = requestAnimationFrame(loop) state.rafId = requestAnimationFrame(loop)
if (document.hidden) return
if (ts - state.lastTs < 32) return if (ts - state.lastTs < 32) return
state.lastTs = ts state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006 if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -192,6 +193,7 @@ export default function WireframeViewer({ onClose }) {
} }
} }
const onTouchMove = (e) => { const onTouchMove = (e) => {
e.preventDefault()
if (!state.dragging) return if (!state.dragging) return
if (e.touches.length === 1) { if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008 state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -215,8 +217,8 @@ export default function WireframeViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false }) canvas.addEventListener('wheel', onWheel, { passive: false })
canvas.addEventListener('touchstart', onTouchStart, { passive: true }) canvas.addEventListener('touchstart', onTouchStart, { passive: false })
canvas.addEventListener('touchmove', onTouchMove, { passive: true }) canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd) canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() } const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }

View File

@@ -1,11 +1,39 @@
const API_BASE = import.meta.env.VITE_API_BASE ?? '' const API_BASE = import.meta.env.VITE_API_BASE ?? ''
// Superfice.de KG base location: Grüner Weg 36, 03185 Peitz
const BASE_LAT = 51.8555
const BASE_LON = 14.4069
// Road distance ≈ straight-line × 1.25 (typical German rural factor)
const ROAD_FACTOR = 1.25
function haversineKm(lat1, lon1, lat2, lon2) {
const R = 6371
const dLat = (lat2 - lat1) * Math.PI / 180
const dLon = (lon2 - lon1) * Math.PI / 180
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) ** 2
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}
export async function fetchDistance(plz) { export async function fetchDistance(plz) {
const res = await fetch(`${API_BASE}/api/distance.php?plz=${encodeURIComponent(plz)}`) // Search near Peitz so closest German result comes first
if (!res.ok) throw new Error('Netzwerkfehler') const res = await fetch(
`https://photon.komoot.io/api/?q=${encodeURIComponent(plz)}+Deutschland&limit=10&lang=de&lat=${BASE_LAT}&lon=${BASE_LON}`,
{ headers: { Accept: 'application/json' } }
)
if (!res.ok) throw new Error('PLZ nicht gefunden')
const data = await res.json() const data = await res.json()
if (data.error) throw new Error(data.error) // Find German postcode feature whose name matches the PLZ
return data.distance const feature = data.features?.find(f => {
const p = f.properties
return p.countrycode === 'DE' && (p.osm_value === 'postcode' || p.name === plz)
})
if (!feature) throw new Error('PLZ nicht gefunden')
const [destLon, destLat] = feature.geometry.coordinates
return Math.round(haversineKm(BASE_LAT, BASE_LON, destLat, destLon) * ROAD_FACTOR)
} }
export async function submitContact(formData) { export async function submitContact(formData) {

View File

@@ -19,6 +19,16 @@ export default defineConfig({
output: { output: {
manualChunks: { manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'], vendor: ['react', 'react-dom', 'react-router-dom'],
viewers: [
'./src/components/KitchenViewer/KitchenViewer',
'./src/components/BathViewer/BathViewer',
'./src/components/StairViewer/StairViewer',
'./src/components/RoomViewer/RoomViewer',
'./src/components/BedroomViewer/BedroomViewer',
'./src/components/ClosetViewer/ClosetViewer',
'./src/components/TerraceViewer/TerraceViewer',
'./src/components/WireframeViewer/WireframeViewer',
],
}, },
}, },
}, },