diff --git a/index.html b/index.html
index f21b774..82d233d 100644
--- a/index.html
+++ b/index.html
@@ -11,6 +11,92 @@
Superfice – Präzision beginnt beim Aufmaß
+
diff --git a/public/llms.txt b/public/llms.txt
new file mode 100644
index 0000000..4d1135e
--- /dev/null
+++ b/public/llms.txt
@@ -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.
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..d5c1692
--- /dev/null
+++ b/public/robots.txt
@@ -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
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..ad35b4c
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,18 @@
+
+
+
+ https://www.superfice.de/
+ weekly
+ 1.0
+
+
+ https://www.superfice.de/impressum
+ yearly
+ 0.3
+
+
+ https://www.superfice.de/datenschutz
+ yearly
+ 0.3
+
+
diff --git a/src/App.jsx b/src/App.jsx
index 7c6ca00..f3a9d8d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -33,7 +33,7 @@ function HomePage() {
}
export default function App() {
- const [customCursor, setCustomCursor] = useState(true)
+ const [customCursor, setCustomCursor] = useState(false)
return (
<>
diff --git a/src/components/BathViewer/BathViewer.jsx b/src/components/BathViewer/BathViewer.jsx
index 293545e..473355d 100644
--- a/src/components/BathViewer/BathViewer.jsx
+++ b/src/components/BathViewer/BathViewer.jsx
@@ -423,6 +423,7 @@ export default function BathViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -476,6 +477,7 @@ export default function BathViewer({ onClose }) {
}
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -499,8 +501,8 @@ export default function BathViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }
diff --git a/src/components/BedroomViewer/BedroomViewer.jsx b/src/components/BedroomViewer/BedroomViewer.jsx
index 94e5835..27eb2bd 100644
--- a/src/components/BedroomViewer/BedroomViewer.jsx
+++ b/src/components/BedroomViewer/BedroomViewer.jsx
@@ -383,6 +383,7 @@ export default function BedroomViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
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)
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length===1) {
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('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown)
diff --git a/src/components/ClosetViewer/ClosetViewer.jsx b/src/components/ClosetViewer/ClosetViewer.jsx
index 0ba0765..8688168 100644
--- a/src/components/ClosetViewer/ClosetViewer.jsx
+++ b/src/components/ClosetViewer/ClosetViewer.jsx
@@ -220,6 +220,7 @@ export default function ClosetViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
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)
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length===1) {
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('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown)
diff --git a/src/components/CostCalculator/CostCalculator.jsx b/src/components/CostCalculator/CostCalculator.jsx
index 6c31899..1b28ff9 100644
--- a/src/components/CostCalculator/CostCalculator.jsx
+++ b/src/components/CostCalculator/CostCalculator.jsx
@@ -262,7 +262,7 @@ export default function CostCalculator() {
{
setPlz(e.target.value)
diff --git a/src/components/CustomCursor/CustomCursor.jsx b/src/components/CustomCursor/CustomCursor.jsx
index 8880801..9e875ee 100644
--- a/src/components/CustomCursor/CustomCursor.jsx
+++ b/src/components/CustomCursor/CustomCursor.jsx
@@ -127,9 +127,6 @@ export default function CustomCursor({ enabled = true }) {
isHover = !!el && (el.matches(HOVER_SEL) || !!el.closest(HOVER_SEL))
}
- // Hide native cursor
- document.documentElement.style.cursor = 'none'
-
document.addEventListener('mousemove', onMouseMove, { passive: true })
function render() {
@@ -222,7 +219,6 @@ export default function CustomCursor({ enabled = true }) {
cancelAnimationFrame(rafId)
window.removeEventListener('resize', resize)
document.removeEventListener('mousemove', onMouseMove)
- document.documentElement.style.cursor = ''
canvas.remove()
}
}, [enabled])
diff --git a/src/components/Footer/Footer.css b/src/components/Footer/Footer.css
index 9ece0d6..f511675 100644
--- a/src/components/Footer/Footer.css
+++ b/src/components/Footer/Footer.css
@@ -27,7 +27,7 @@
}
.footer-logo-svg {
- height: 26px;
+ height: 48px;
width: auto;
}
diff --git a/src/components/KitchenViewer/KitchenViewer.jsx b/src/components/KitchenViewer/KitchenViewer.jsx
index 2ebda06..9f5f67f 100644
--- a/src/components/KitchenViewer/KitchenViewer.jsx
+++ b/src/components/KitchenViewer/KitchenViewer.jsx
@@ -442,6 +442,7 @@ export default function KitchenViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -495,6 +496,7 @@ export default function KitchenViewer({ onClose }) {
}
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -518,8 +520,8 @@ export default function KitchenViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }
diff --git a/src/components/Navigation/Navigation.css b/src/components/Navigation/Navigation.css
index 0371393..58e15ba 100644
--- a/src/components/Navigation/Navigation.css
+++ b/src/components/Navigation/Navigation.css
@@ -31,7 +31,7 @@
}
.nav-logo-svg {
- height: 28px;
+ height: 40px;
width: auto;
}
diff --git a/src/components/RoomViewer/RoomViewer.jsx b/src/components/RoomViewer/RoomViewer.jsx
index bd8b834..22e5f04 100644
--- a/src/components/RoomViewer/RoomViewer.jsx
+++ b/src/components/RoomViewer/RoomViewer.jsx
@@ -492,6 +492,7 @@ export default function RoomViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -545,6 +546,7 @@ export default function RoomViewer({ onClose }) {
}
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -568,8 +570,8 @@ export default function RoomViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }
diff --git a/src/components/SpaceBackground/SpaceBackground.jsx b/src/components/SpaceBackground/SpaceBackground.jsx
index cfae04d..f1acf9a 100644
--- a/src/components/SpaceBackground/SpaceBackground.jsx
+++ b/src/components/SpaceBackground/SpaceBackground.jsx
@@ -117,11 +117,14 @@ export default function SpaceBackground() {
// ── Desktop: full animated version ──────────────────────────
const ctx = canvas.getContext('2d', { alpha: false })
- // Performance tier: low-end = ≤4 logical cores (older/budget hardware)
- const isLowEnd = typeof navigator.hardwareConcurrency === 'number' && navigator.hardwareConcurrency <= 4
- const FRAME_MS = isLowEnd ? 48 : 32 // ~20 fps vs ~30 fps
- const SHARD_N = isLowEnd ? 12 : SHARD_COUNT
- const NEBULA_N = isLowEnd ? 4 : NEBULAS.length
+ // Performance tiers based on logical CPU cores
+ // low-end: ≤4 cores | mid: 5–8 cores | high: 9+ cores
+ const cores = typeof navigator.hardwareConcurrency === 'number' ? navigator.hardwareConcurrency : 8
+ const isLowEnd = cores <= 4
+ 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 time = 0
@@ -143,7 +146,7 @@ export default function SpaceBackground() {
function initStars() {
stars = []
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++) {
const r = layer.rMin + Math.random() * (layer.rMax - layer.rMin)
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.fillStyle = col; ctx.fill()
- // Soft halo on brighter stars
- if (!isLowEnd && s.layer === 2 && s.r > 1.5) {
+ // Soft halo on brighter stars (high-end only)
+ 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)
halo.addColorStop(0, STAR_PALETTES[s.palIdx](tw * 0.18))
halo.addColorStop(1, 'rgba(0,0,0,0)')
@@ -190,8 +193,8 @@ export default function SpaceBackground() {
ctx.fillStyle = halo; ctx.fill()
}
- // Diffraction spikes on select bright stars
- if (!isLowEnd && s.spike) {
+ // Diffraction spikes on select bright stars (high-end only)
+ if (!isLowEnd && !isMid && s.spike) {
const spikeLen = s.r * 5.5
const sa = tw * 0.28
ctx.strokeStyle = STAR_PALETTES[s.palIdx](sa)
@@ -214,12 +217,17 @@ export default function SpaceBackground() {
const cy = n.py * H + oy
const rad = n.pr * Math.min(W, H)
const pa = n.a * (0.70 + 0.30 * Math.sin(time * 0.22 + 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.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(1, `rgba(${n.cr},${n.cg},${n.cb},0)`)
- ctx.fillStyle = g
+ // 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)
+ 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.75, `rgba(${n.cr},${n.cg},${n.cb},${pa * 0.12})`)
+ g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`)
+ 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()
}
}
@@ -256,13 +264,19 @@ export default function SpaceBackground() {
const len = Math.hypot(s.vx, s.vy)
const tailX = s.x - (s.vx / len) * s.length
const tailY = s.y - (s.vy / len) * s.length
- const g = ctx.createLinearGradient(s.x, s.y, tailX, tailY)
- 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.50, `rgba(140,80,220,${s.alpha * 0.25})`)
- g.addColorStop(1, `rgba(80,40,160,0)`)
+ // 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)
+ 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.50, `rgba(140,80,220,${s.alpha * 0.25})`)
+ 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.strokeStyle = g; ctx.lineWidth = s.thick; ctx.stroke()
+ ctx.strokeStyle = s._grad; ctx.lineWidth = s.thick; ctx.stroke()
// bright head dot
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()
@@ -272,27 +286,34 @@ export default function SpaceBackground() {
}
// ── Glass shards / space debris ──────────────────────────────
+ function makeShard(x, y) {
+ const size = 14 + Math.random() * 58
+ const sides = 3 + Math.floor(Math.random() * 4)
+ const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)]
+ const baseAlpha = 0.32 + Math.random() * 0.32
+ return {
+ x: x ?? Math.random() * W,
+ y: 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,
+ verts: buildVerts(size, sides),
+ size, col,
+ alpha: 0,
+ alphaTarget: baseAlpha,
+ baseAlpha,
+ svx: 0, svy: 0,
+ phase: Math.random() * Math.PI * 2,
+ floatSpeed: 0.20 + Math.random() * 0.45,
+ age: Math.floor(Math.random() * 2400), // stagger initial ages
+ maxAge: 2800 + Math.floor(Math.random() * 2400), // 90–165 s at 30fps
+ dying: false,
+ }
+ }
+
function initShards() {
- shards = Array.from({ length: SHARD_N }, () => {
- const size = 14 + Math.random() * 58
- const sides = 3 + Math.floor(Math.random() * 4)
- const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)]
- const baseAlpha = 0.28 + Math.random() * 0.30
- return {
- 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,
- verts: buildVerts(size, sides),
- size, col,
- alpha: baseAlpha,
- alphaTarget: baseAlpha,
- svx: 0, svy: 0,
- phase: Math.random() * Math.PI * 2,
- floatSpeed: 0.20 + Math.random() * 0.45,
- }
- })
+ shards = Array.from({ length: SHARD_N }, () => makeShard())
}
function drawShard(s) {
@@ -328,6 +349,18 @@ export default function SpaceBackground() {
const infSq = SHARD_RADIUS * SHARD_RADIUS
for (let i = 0; i < shards.length; 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
if (mouse.x > -1000) {
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.svy += Math.sin(ang) * force * 0.065
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
@@ -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.x += s.vx + s.svx; s.y += s.vy + s.svy; s.rot += s.rotV
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
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
diff --git a/src/components/StairViewer/StairViewer.jsx b/src/components/StairViewer/StairViewer.jsx
index 2c13443..e663aef 100644
--- a/src/components/StairViewer/StairViewer.jsx
+++ b/src/components/StairViewer/StairViewer.jsx
@@ -325,6 +325,7 @@ export default function StairViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
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)
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length===1) {
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('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown)
diff --git a/src/components/TerraceViewer/TerraceViewer.jsx b/src/components/TerraceViewer/TerraceViewer.jsx
index 704f861..4013dcb 100644
--- a/src/components/TerraceViewer/TerraceViewer.jsx
+++ b/src/components/TerraceViewer/TerraceViewer.jsx
@@ -285,6 +285,7 @@ export default function TerraceViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
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)
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length===1) {
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('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key==='Escape') onClose() }
window.addEventListener('keydown', onKeyDown)
diff --git a/src/components/WireframeViewer/WireframeViewer.jsx b/src/components/WireframeViewer/WireframeViewer.jsx
index c0dc729..54137ef 100644
--- a/src/components/WireframeViewer/WireframeViewer.jsx
+++ b/src/components/WireframeViewer/WireframeViewer.jsx
@@ -139,6 +139,7 @@ export default function WireframeViewer({ onClose }) {
let firstFrame = true
function loop(ts) {
state.rafId = requestAnimationFrame(loop)
+ if (document.hidden) return
if (ts - state.lastTs < 32) return
state.lastTs = ts
if (state.autoRotate && !state.dragging) state.rotY += 0.006
@@ -192,6 +193,7 @@ export default function WireframeViewer({ onClose }) {
}
}
const onTouchMove = (e) => {
+ e.preventDefault()
if (!state.dragging) return
if (e.touches.length === 1) {
state.rotY += (e.touches[0].clientX - lastTX) * 0.008
@@ -215,8 +217,8 @@ export default function WireframeViewer({ onClose }) {
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('wheel', onWheel, { passive: false })
- canvas.addEventListener('touchstart', onTouchStart, { passive: true })
- canvas.addEventListener('touchmove', onTouchMove, { passive: true })
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false })
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false })
canvas.addEventListener('touchend', onTouchEnd)
const onKeyDown = (e) => { if (e.key === 'Escape') onClose() }
diff --git a/src/utils/api.js b/src/utils/api.js
index 0e98a3d..dc2642a 100644
--- a/src/utils/api.js
+++ b/src/utils/api.js
@@ -1,11 +1,39 @@
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) {
- const res = await fetch(`${API_BASE}/api/distance.php?plz=${encodeURIComponent(plz)}`)
- if (!res.ok) throw new Error('Netzwerkfehler')
+ // Search near Peitz so closest German result comes first
+ 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()
- if (data.error) throw new Error(data.error)
- return data.distance
+ // Find German postcode feature whose name matches the PLZ
+ 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) {
diff --git a/vite.config.js b/vite.config.js
index ca239cd..ef543e1 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -19,6 +19,16 @@ export default defineConfig({
output: {
manualChunks: {
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',
+ ],
},
},
},