style fix. calculator fix. Optimisation
This commit is contained in:
86
index.html
86
index.html
@@ -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
36
public/llms.txt
Normal 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
26
public/robots.txt
Normal 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
18
public/sitemap.xml
Normal 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>
|
||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-logo-svg {
|
.footer-logo-svg {
|
||||||
height: 26px;
|
height: 48px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-logo-svg {
|
.nav-logo-svg {
|
||||||
height: 28px;
|
height: 40px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
|||||||
@@ -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: 5–8 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))
|
||||||
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad)
|
// Cache gradient — nebulas drift slowly, only rebuild when moved >6px or alpha shifts
|
||||||
g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`)
|
const moved = Math.hypot(cx - (n._gcx ?? cx + 99), cy - (n._gcy ?? cy + 99))
|
||||||
g.addColorStop(0.40, `rgba(${n.cr},${n.cg},${n.cb},${pa * 0.40})`)
|
if (!n._grad || moved > 6 || Math.abs(pa - (n._gpa ?? -1)) > 0.008) {
|
||||||
g.addColorStop(0.75, `rgba(${n.cr},${n.cg},${n.cb},${pa * 0.12})`)
|
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad)
|
||||||
g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`)
|
g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`)
|
||||||
ctx.fillStyle = g
|
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()
|
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
|
||||||
const g = ctx.createLinearGradient(s.x, s.y, tailX, tailY)
|
// Cache gradient — recreate only when position or alpha shifts noticeably
|
||||||
g.addColorStop(0, `rgba(255,255,255,${s.alpha * 0.95})`)
|
const alphaDelta = Math.abs(s.alpha - (s._gAlpha ?? -1))
|
||||||
g.addColorStop(0.12, `rgba(220,200,255,${s.alpha * 0.65})`)
|
const posDelta = Math.hypot(s.x - (s._gX ?? s.x + 99), s.y - (s._gY ?? s.y + 99))
|
||||||
g.addColorStop(0.50, `rgba(140,80,220,${s.alpha * 0.25})`)
|
if (!s._grad || alphaDelta > 0.06 || posDelta > 8) {
|
||||||
g.addColorStop(1, `rgba(80,40,160,0)`)
|
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.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 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() {
|
function initShards() {
|
||||||
shards = Array.from({ length: SHARD_N }, () => {
|
shards = Array.from({ length: SHARD_N }, () => makeShard())
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user