Redesign: new logo, fonts, toon-shaded 3D, scroll animations
Visual overhaul addressing render glitches and adding wow factor. Type: - Display: Fraunces -> Instrument Serif (more elegant, italic-forward) - Body: Inter -> Geist (tighter, more contemporary) - Mono: JetBrains -> Geist Mono Logo: - New SVG mark — pairs a gabled vernacular silhouette with a glass-grid contemporary tower, divided by a ground line. Communicates "2D3D" through architectural language rather than typography. - Component supports mark / full / inverse variants - Favicon + apple-touch-icon updated to match - OG image regenerated with new palette and 3 representative house silhouettes 3D models (houses.ts): - Switched to MeshToonMaterial with a 3-step gradient LUT — flat designer-toy look that's forgiving of low-poly approximations - Reworked geometry across all six typologies for consistent scale and clean composition (real arches via ExtrudeGeometry on cula loggia, proper sasesc roof shapes via extruded slope, contemporary slats sized to volume) - Per-house camera framing - 2D->3D intro animation when scene first becomes visible — house extrudes from a flat plane (on-brand for the 2d3d.ro concept) - Subtle breathing y-translation while idle - Hover speeds up the spin smoothly Animations: - Word-split + staggered reveal for hero headline - IntersectionObserver-based reveal-on-scroll for [data-reveal]/[data-reveal-blur] - Animated number counters (Romanian locale) - Magnetic hover on CTAs and brand mark - Cursor-tracking radial glow in hero - Section dividers with blueprint dimension-line aesthetic - Marquee strip listing typology names in intro - Glow-ring (animated conic gradient) on primary CTAs Palette refined — slightly less saturated terra/wood/sky-mist for a more sophisticated tone. All animations respect prefers-reduced-motion. Off-screen 3D scenes still paused via the prior IntersectionObserver fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.9 KiB |
+14
-4
@@ -1,6 +1,16 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
<rect width="64" height="64" rx="12" fill="#3a1c10"/>
|
<rect width="64" height="64" rx="14" fill="#1f0e08"/>
|
||||||
<path d="M12 44 L32 12 L52 44 Z" fill="#d4703f"/>
|
<rect x="6" y="50" width="52" height="2" fill="#3a1c10"/>
|
||||||
<rect x="14" y="44" width="36" height="8" fill="#5e2d1a"/>
|
<path d="M10 50 L10 30 L22 18 L34 30 L34 50 Z" fill="#d4703f"/>
|
||||||
<circle cx="32" cy="34" r="3" fill="#fae8da"/>
|
<path d="M22 18 L22 38 M22 38 L34 50 M22 38 L10 50" stroke="#3a1c10" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<rect x="18" y="38" width="8" height="12" fill="#3a1c10"/>
|
||||||
|
<rect x="20" y="40" width="2" height="2" fill="#fae8da"/>
|
||||||
|
<rect x="22" y="40" width="2" height="2" fill="#fae8da"/>
|
||||||
|
<rect x="38" y="22" width="18" height="28" fill="#fae8da"/>
|
||||||
|
<path d="M38 22 L47 14 L56 22" fill="#3a1c10"/>
|
||||||
|
<g fill="#3a1c10">
|
||||||
|
<rect x="41" y="26" width="3" height="3"/><rect x="46" y="26" width="3" height="3"/><rect x="51" y="26" width="3" height="3"/>
|
||||||
|
<rect x="41" y="32" width="3" height="3"/><rect x="46" y="32" width="3" height="3"/><rect x="51" y="32" width="3" height="3"/>
|
||||||
|
<rect x="41" y="38" width="3" height="3"/><rect x="46" y="38" width="3" height="3"/><rect x="51" y="38" width="3" height="3"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 114 KiB |
+63
-39
@@ -13,17 +13,17 @@ const svg = `
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0" stop-color="#1f0e08"/>
|
<stop offset="0" stop-color="#190b06"/>
|
||||||
<stop offset="1" stop-color="#3a1c10"/>
|
<stop offset="1" stop-color="#2e160d"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<radialGradient id="sun" cx="0.5" cy="0.35" r="0.6">
|
<radialGradient id="sun" cx="0.5" cy="0.35" r="0.7">
|
||||||
<stop offset="0" stop-color="#f4cfb1" stop-opacity="0.55"/>
|
<stop offset="0" stop-color="#f0c39b" stop-opacity="0.45"/>
|
||||||
<stop offset="1" stop-color="#f4cfb1" stop-opacity="0"/>
|
<stop offset="1" stop-color="#f0c39b" stop-opacity="0"/>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
<linearGradient id="shimmer" x1="0" y1="0" x2="1" y2="0">
|
<linearGradient id="shimmer" x1="0" y1="0" x2="1" y2="0">
|
||||||
<stop offset="0" stop-color="#b35831"/>
|
<stop offset="0" stop-color="#7d361c"/>
|
||||||
<stop offset="0.5" stop-color="#ecae82"/>
|
<stop offset="0.5" stop-color="#e69d6a"/>
|
||||||
<stop offset="1" stop-color="#b35831"/>
|
<stop offset="1" stop-color="#7d361c"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
@@ -31,47 +31,61 @@ const svg = `
|
|||||||
<rect width="${W}" height="${H}" fill="url(#sun)"/>
|
<rect width="${W}" height="${H}" fill="url(#sun)"/>
|
||||||
|
|
||||||
<!-- Subtle grid -->
|
<!-- Subtle grid -->
|
||||||
<g stroke="#fdf6f1" stroke-opacity="0.04">
|
<g stroke="#fbf8f1" stroke-opacity="0.04">
|
||||||
${Array.from({ length: 20 }).map((_, i) => `<line x1="${i * 60}" y1="0" x2="${i * 60}" y2="${H}"/>`).join('')}
|
${Array.from({ length: 20 }).map((_, i) => `<line x1="${i * 60}" y1="0" x2="${i * 60}" y2="${H}"/>`).join('')}
|
||||||
${Array.from({ length: 11 }).map((_, i) => `<line x1="0" y1="${i * 60}" x2="${W}" y2="${i * 60}"/>`).join('')}
|
${Array.from({ length: 11 }).map((_, i) => `<line x1="0" y1="${i * 60}" x2="${W}" y2="${i * 60}"/>`).join('')}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Stylized house silhouettes (3 of them) -->
|
<!-- 3 stylized houses across the ground -->
|
||||||
<g opacity="0.95">
|
<g>
|
||||||
<!-- Maramures -->
|
<!-- Maramures -->
|
||||||
<g transform="translate(150, 380)">
|
<g transform="translate(140, 460)">
|
||||||
<rect x="-50" y="-10" width="100" height="80" fill="#5e3a1a"/>
|
<rect x="-55" y="-10" width="110" height="60" fill="#7a4a23"/>
|
||||||
<polygon points="-65,-10 0,-160 65,-10" fill="#2d1810"/>
|
<rect x="-55" y="50" width="110" height="6" fill="#1f150c"/>
|
||||||
<polygon points="0,-160 0,-185 -7,-160" fill="#2d1810"/>
|
<polygon points="-72,-10 0,-180 72,-10" fill="#2a160b"/>
|
||||||
|
<polygon points="-2,-200 2,-200 0,-180" fill="#2a160b"/>
|
||||||
|
<line x1="-1" y1="-200" x2="-1" y2="-218" stroke="#1f150c" stroke-width="2"/>
|
||||||
|
<line x1="-9" y1="-210" x2="9" y2="-210" stroke="#1f150c" stroke-width="2"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- Cula -->
|
<!-- Cula -->
|
||||||
<g transform="translate(420, 380)">
|
<g transform="translate(420, 460)">
|
||||||
<rect x="-50" y="40" width="100" height="40" fill="#cfbf94"/>
|
<rect x="-55" y="-10" width="110" height="60" fill="#c8b380"/>
|
||||||
<rect x="-55" y="-30" width="110" height="70" fill="#e2d8be"/>
|
<rect x="-60" y="-90" width="120" height="80" fill="#e7d6b3"/>
|
||||||
<rect x="-55" y="-100" width="110" height="70" fill="#e2d8be"/>
|
<polygon points="-66,-90 0,-150 66,-90" fill="#5a3220"/>
|
||||||
<polygon points="-65,-100 0,-160 65,-100" fill="#7e5d36"/>
|
<g fill="#1a0d05">
|
||||||
|
<rect x="-44" y="-50" width="20" height="22"/>
|
||||||
|
<rect x="-10" y="-50" width="20" height="22"/>
|
||||||
|
<rect x="24" y="-50" width="20" height="22"/>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<!-- Contemporary -->
|
<!-- Contemporary -->
|
||||||
<g transform="translate(700, 380)">
|
<g transform="translate(720, 460)">
|
||||||
<rect x="-70" y="-20" width="140" height="100" fill="#f1ecdf"/>
|
<rect x="-90" y="-10" width="180" height="60" fill="#a8a29a"/>
|
||||||
<rect x="40" y="-10" width="60" height="90" fill="#bf9b6f"/>
|
<rect x="-90" y="50" width="180" height="6" fill="#1f150c"/>
|
||||||
<rect x="-65" y="-10" width="80" height="50" fill="#84b6cd" opacity="0.6"/>
|
<rect x="-85" y="-70" width="115" height="60" fill="#f3ecdc"/>
|
||||||
<polygon points="-75,-20 75,-50 75,-20" fill="#3a1c10"/>
|
<rect x="38" y="-50" width="50" height="40" fill="#b58f5d"/>
|
||||||
|
<rect x="-78" y="-58" width="100" height="40" fill="#9ec5d9" opacity="0.7"/>
|
||||||
|
<polygon points="-90,-70 30,-90 30,-70" fill="#2e160d"/>
|
||||||
|
<g fill="#0d0805" opacity="0.85">
|
||||||
|
<rect x="-78" y="-100" width="20" height="3" transform="rotate(-3, -78, -100)"/>
|
||||||
|
<rect x="-50" y="-101" width="20" height="3" transform="rotate(-3, -50, -101)"/>
|
||||||
|
<rect x="-22" y="-102" width="20" height="3" transform="rotate(-3, -22, -102)"/>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title — Instrument Serif look (use serif fallback) -->
|
||||||
<g transform="translate(80, 110)">
|
<g transform="translate(80, 130)">
|
||||||
<text font-family="Georgia, serif" font-size="22" fill="#ecae82" letter-spacing="6">PATRIMONIU CONSTRUIT · 2026</text>
|
<text font-family="'Instrument Serif', Georgia, serif" font-size="24" fill="#e69d6a" letter-spacing="6">PATRIMONIU CONSTRUIT · 2026</text>
|
||||||
<text y="92" font-family="Georgia, serif" font-size="80" font-weight="500" fill="#faf8f3">Arhitectura</text>
|
<text y="100" font-family="'Instrument Serif', Georgia, serif" font-size="92" font-weight="400" fill="#fbf8f1">Arhitectura</text>
|
||||||
<text y="172" font-family="Georgia, serif" font-size="80" font-weight="500" fill="url(#shimmer)" font-style="italic">României</text>
|
<text y="200" font-family="'Instrument Serif', Georgia, serif" font-size="92" font-weight="400" fill="url(#shimmer)" font-style="italic">României</text>
|
||||||
<text y="240" font-family="Georgia, serif" font-size="44" fill="#faf8f3">de la cula la zgârie-nori</text>
|
<text y="270" font-family="'Instrument Serif', Georgia, serif" font-size="36" fill="#fbf8f1" opacity="0.8" font-style="italic">de la cula la zgârie-nori</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Bottom strip -->
|
<!-- Bottom strip -->
|
||||||
<g transform="translate(80, ${H - 70})">
|
<g transform="translate(80, ${H - 60})">
|
||||||
<text font-family="system-ui, sans-serif" font-size="22" fill="#fae8da" opacity="0.8">2d3d.ro</text>
|
<text font-family="'Geist', system-ui, sans-serif" font-weight="500" font-size="20" fill="#f9e3d0" opacity="0.85">2d3d.ro</text>
|
||||||
<text x="${W - 160}" font-family="system-ui, sans-serif" font-size="22" fill="#fae8da" opacity="0.8">de Beletage</text>
|
<text x="${W - 200}" font-family="'Geist', system-ui, sans-serif" font-size="20" fill="#f9e3d0" opacity="0.7">un proiect Beletage</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
@@ -79,13 +93,23 @@ const svg = `
|
|||||||
await sharp(Buffer.from(svg)).png({ quality: 92 }).toFile(join(outDir, 'og.png'));
|
await sharp(Buffer.from(svg)).png({ quality: 92 }).toFile(join(outDir, 'og.png'));
|
||||||
console.log('Generated og.png');
|
console.log('Generated og.png');
|
||||||
|
|
||||||
// Also apple-touch-icon
|
// Also apple-touch-icon — matches favicon design
|
||||||
const ati = `
|
const ati = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">
|
||||||
<rect width="180" height="180" rx="36" fill="#3a1c10"/>
|
<rect width="180" height="180" rx="40" fill="#190b06"/>
|
||||||
<path d="M30 122 L90 30 L150 122 Z" fill="#d4703f"/>
|
<rect x="18" y="142" width="144" height="6" fill="#2e160d"/>
|
||||||
<rect x="36" y="122" width="108" height="22" fill="#5e2d1a"/>
|
<path d="M28 142 L28 84 L62 50 L96 84 L96 142 Z" fill="#d97942"/>
|
||||||
<circle cx="90" cy="92" r="9" fill="#fae8da"/>
|
<path d="M62 50 L62 110 L96 142 M62 110 L28 142" stroke="#2e160d" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<rect x="50" y="110" width="24" height="32" fill="#2e160d"/>
|
||||||
|
<rect x="56" y="116" width="6" height="6" fill="#fae8da"/>
|
||||||
|
<rect x="64" y="116" width="6" height="6" fill="#fae8da"/>
|
||||||
|
<rect x="106" y="62" width="50" height="80" fill="#fae8da"/>
|
||||||
|
<path d="M106 62 L131 40 L156 62" fill="#2e160d"/>
|
||||||
|
<g fill="#2e160d">
|
||||||
|
<rect x="114" y="74" width="8" height="8"/><rect x="127" y="74" width="8" height="8"/><rect x="140" y="74" width="8" height="8"/>
|
||||||
|
<rect x="114" y="92" width="8" height="8"/><rect x="127" y="92" width="8" height="8"/><rect x="140" y="92" width="8" height="8"/>
|
||||||
|
<rect x="114" y="110" width="8" height="8"/><rect x="127" y="110" width="8" height="8"/><rect x="140" y="110" width="8" height="8"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
await sharp(Buffer.from(ati)).resize(180, 180).png().toFile(join(outDir, 'apple-touch-icon.png'));
|
await sharp(Buffer.from(ati)).resize(180, 180).png().toFile(join(outDir, 'apple-touch-icon.png'));
|
||||||
|
|||||||
@@ -5,12 +5,15 @@
|
|||||||
<div class="mx-auto max-w-6xl px-6 lg:px-12">
|
<div class="mx-auto max-w-6xl px-6 lg:px-12">
|
||||||
<div class="grid gap-12 lg:grid-cols-12 lg:items-center">
|
<div class="grid gap-12 lg:grid-cols-12 lg:items-center">
|
||||||
<div class="lg:col-span-7">
|
<div class="lg:col-span-7">
|
||||||
<p class="text-xs font-medium uppercase tracking-[0.3em] text-terra-700">Cine a făcut asta</p>
|
<p class="text-xs font-medium uppercase tracking-[0.32em] text-terra-700" data-reveal>
|
||||||
<h2 class="mt-4 font-display text-4xl font-medium leading-tight text-ink-900 sm:text-5xl lg:text-6xl title-balance">
|
<span class="inline-block h-px w-8 align-middle bg-terra-700 mr-3"></span>
|
||||||
Realizat de <a href="https://beletage.ro" rel="dofollow" class="underline decoration-terra-500 decoration-4 underline-offset-8 hover:decoration-terra-700">Beletage</a>,
|
Cine a făcut asta
|
||||||
|
</p>
|
||||||
|
<h2 class="mt-5 font-display text-4xl font-normal leading-[1.05] text-ink-900 sm:text-5xl lg:text-6xl title-balance" data-reveal data-delay="0.1">
|
||||||
|
Realizat de <a href="https://beletage.ro" rel="dofollow" class="font-display-italic text-terra-600 underline decoration-terra-300 decoration-4 underline-offset-[10px] hover:text-terra-700 hover:decoration-terra-500">Beletage</a>,
|
||||||
birou de arhitectură din Cluj-Napoca.
|
birou de arhitectură din Cluj-Napoca.
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-6 max-w-2xl text-lg leading-relaxed text-ink-800/80 text-pretty">
|
<p class="mt-7 max-w-2xl text-lg leading-relaxed text-ink-800/80 text-pretty" data-reveal data-delay="0.2">
|
||||||
Proiectăm locuințe individuale, ansambluri rezidențiale, spații de birouri și amenajări
|
Proiectăm locuințe individuale, ansambluri rezidențiale, spații de birouri și amenajări
|
||||||
publice. Lucrăm cu BIM, integrăm patrimoniul în soluții contemporane și ținem mereu cont de
|
publice. Lucrăm cu BIM, integrăm patrimoniul în soluții contemporane și ținem mereu cont de
|
||||||
climă, lumină și buget.
|
climă, lumină și buget.
|
||||||
@@ -20,11 +23,12 @@
|
|||||||
ajuns să locuim astăzi. Dacă ai un proiect propriu, hai să-l discutăm.
|
ajuns să locuim astăzi. Dacă ai un proiect propriu, hai să-l discutăm.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-10 flex flex-wrap gap-4">
|
<div class="mt-10 flex flex-wrap gap-4" data-reveal data-delay="0.3">
|
||||||
<a
|
<a
|
||||||
href="https://beletage.ro"
|
href="https://beletage.ro"
|
||||||
rel="dofollow"
|
rel="dofollow"
|
||||||
class="group inline-flex items-center gap-3 rounded-full bg-ink-900 px-7 py-4 text-base font-medium text-bone-50 transition hover:bg-terra-700"
|
data-magnetic="0.3"
|
||||||
|
class="group glow-ring inline-flex items-center gap-3 rounded-full bg-ink-900 px-8 py-4 text-base font-medium text-bone-50 shadow-xl shadow-ink-900/20 transition hover:bg-terra-700"
|
||||||
>
|
>
|
||||||
Vezi portofoliul Beletage
|
Vezi portofoliul Beletage
|
||||||
<span class="transition group-hover:translate-x-1" aria-hidden>→</span>
|
<span class="transition group-hover:translate-x-1" aria-hidden>→</span>
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
<a
|
<a
|
||||||
href="https://beletage.ro/contact"
|
href="https://beletage.ro/contact"
|
||||||
rel="dofollow"
|
rel="dofollow"
|
||||||
|
data-magnetic="0.2"
|
||||||
class="inline-flex items-center gap-2 rounded-full border border-ink-900/20 px-6 py-4 text-base text-ink-900 transition hover:border-ink-900/40 hover:bg-ink-900/5"
|
class="inline-flex items-center gap-2 rounded-full border border-ink-900/20 px-6 py-4 text-base text-ink-900 transition hover:border-ink-900/40 hover:bg-ink-900/5"
|
||||||
>
|
>
|
||||||
Discutăm un proiect
|
Discutăm un proiect
|
||||||
@@ -39,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lg:col-span-5">
|
<div class="lg:col-span-5" data-reveal-blur data-delay="0.2">
|
||||||
<div class="relative overflow-hidden rounded-3xl bg-wood-900 p-8 text-bone-50 shadow-2xl">
|
<div class="relative overflow-hidden rounded-3xl bg-wood-900 p-8 text-bone-50 shadow-2xl">
|
||||||
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(244,207,177,0.2),transparent_60%)]"></div>
|
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(244,207,177,0.2),transparent_60%)]"></div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
import Logo from './Logo.astro';
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '#poveste', label: 'Povestea' },
|
{ href: '#poveste', label: 'Povestea' },
|
||||||
{ href: '#tipologii', label: 'Tipologii' },
|
{ href: '#tipologii', label: 'Tipologii' },
|
||||||
@@ -12,12 +13,18 @@ const navItems = [
|
|||||||
data-header
|
data-header
|
||||||
>
|
>
|
||||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 lg:px-12">
|
<div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 lg:px-12">
|
||||||
<a href="/" class="group flex items-center gap-2 text-bone-50">
|
<a
|
||||||
<span class="grid h-9 w-9 place-items-center rounded-md bg-terra-600/90 font-display text-sm font-bold backdrop-blur transition group-hover:bg-terra-500">2D<span class="text-terra-200">3D</span></span>
|
href="/"
|
||||||
<span class="hidden font-display text-base text-bone-50 sm:inline">Arhitectura României</span>
|
class="group inline-flex items-center gap-3 text-bone-50"
|
||||||
|
data-magnetic="0.15"
|
||||||
|
>
|
||||||
|
<Logo size={36} variant="inverse" class="transition-transform duration-500 group-hover:rotate-[-6deg]" />
|
||||||
|
<span class="hidden font-display text-base text-bone-50/95 sm:inline">
|
||||||
|
<span class="text-terra-200">2D</span><span class="opacity-50">·</span>3D <span class="opacity-60">— Arhitectura României</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="hidden items-center gap-2 md:flex">
|
<nav class="hidden items-center gap-1 md:flex">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
@@ -31,9 +38,10 @@ const navItems = [
|
|||||||
<a
|
<a
|
||||||
href="https://beletage.ro"
|
href="https://beletage.ro"
|
||||||
rel="dofollow"
|
rel="dofollow"
|
||||||
|
data-magnetic="0.3"
|
||||||
class="hidden items-center gap-2 rounded-full bg-bone-50 px-4 py-2 text-sm font-medium text-ink-900 transition hover:bg-terra-200 sm:inline-flex"
|
class="hidden items-center gap-2 rounded-full bg-bone-50 px-4 py-2 text-sm font-medium text-ink-900 transition hover:bg-terra-200 sm:inline-flex"
|
||||||
>
|
>
|
||||||
Beletage.ro <span aria-hidden>→</span>
|
Beletage <span aria-hidden>→</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -44,9 +52,9 @@ const navItems = [
|
|||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
const y = window.scrollY;
|
const y = window.scrollY;
|
||||||
if (y > 80) {
|
if (y > 80) {
|
||||||
header.classList.add('bg-wood-900/80', 'backdrop-blur-md', 'border-b', 'border-bone-50/10', 'shadow-lg');
|
header.classList.add('bg-wood-900/85', 'backdrop-blur-md', 'border-b', 'border-bone-50/10', 'shadow-lg');
|
||||||
} else {
|
} else {
|
||||||
header.classList.remove('bg-wood-900/80', 'backdrop-blur-md', 'border-b', 'border-bone-50/10', 'shadow-lg');
|
header.classList.remove('bg-wood-900/85', 'backdrop-blur-md', 'border-b', 'border-bone-50/10', 'shadow-lg');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('scroll', onScroll, { passive: true });
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
|||||||
+50
-27
@@ -1,10 +1,24 @@
|
|||||||
---
|
---
|
||||||
// Hero with rotating Maramures-style house silhouette behind hero text
|
import Logo from './Logo.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="relative isolate min-h-screen overflow-hidden bg-gradient-to-b from-wood-900 via-wood-800 to-terra-950 text-bone-50">
|
<section
|
||||||
<!-- Background sun -->
|
data-hero-glow
|
||||||
<div class="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_60%_50%_at_50%_30%,rgba(244,207,177,0.18),transparent_70%)]"></div>
|
class="relative isolate min-h-screen overflow-hidden bg-wood-900 text-bone-50"
|
||||||
|
style="--mx:50%; --my:30%"
|
||||||
|
>
|
||||||
|
<!-- Cursor-tracking glow -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 -z-10 transition-opacity duration-700"
|
||||||
|
style="background: radial-gradient(circle 600px at var(--mx) var(--my), rgba(217,121,66,0.28), transparent 60%);"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Static gradient base -->
|
||||||
|
<div class="absolute inset-0 -z-20 bg-gradient-to-b from-wood-900 via-wood-800 to-terra-950"></div>
|
||||||
|
|
||||||
|
<!-- Floating orbs -->
|
||||||
|
<div class="orb -top-32 -left-32 h-96 w-96 bg-terra-500" style="animation-delay: 0s"></div>
|
||||||
|
<div class="orb top-1/3 -right-40 h-[28rem] w-[28rem] bg-sky-mist-700" style="animation-delay: 2s; opacity: .35"></div>
|
||||||
|
|
||||||
<!-- Three.js canvas -->
|
<!-- Three.js canvas -->
|
||||||
<canvas
|
<canvas
|
||||||
@@ -13,62 +27,73 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
<!-- Decorative grid -->
|
<!-- Architectural grid overlay -->
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0 -z-10 opacity-[0.07]"
|
class="pointer-events-none absolute inset-0 -z-10 opacity-[0.05]"
|
||||||
style="background-image:linear-gradient(to right,#fff 1px,transparent 1px),linear-gradient(to bottom,#fff 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse at center,#000 30%,transparent 80%)"
|
style="background-image:linear-gradient(to right,#fff 1px,transparent 1px),linear-gradient(to bottom,#fff 1px,transparent 1px);background-size:80px 80px;mask-image:radial-gradient(ellipse at center,#000 30%,transparent 80%)"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<!-- Top-left brand mark -->
|
||||||
|
<div class="absolute left-6 top-6 lg:left-12 lg:top-8 z-20" data-reveal data-delay="0.2">
|
||||||
|
<Logo size={48} variant="inverse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative mx-auto flex min-h-screen max-w-7xl flex-col justify-center px-6 pb-24 pt-32 lg:px-12">
|
<div class="relative mx-auto flex min-h-screen max-w-7xl flex-col justify-center px-6 pb-24 pt-32 lg:px-12">
|
||||||
<p class="text-sm font-medium uppercase tracking-[0.32em] text-terra-300 animate-[fade-up_0.6s_ease-out_both]">
|
<p class="text-xs font-medium uppercase tracking-[0.4em] text-terra-300" data-reveal>
|
||||||
|
<span class="inline-block h-px w-8 align-middle bg-terra-300 mr-3"></span>
|
||||||
Patrimoniu construit · 2026
|
Patrimoniu construit · 2026
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 class="mt-6 max-w-5xl font-display text-5xl font-medium leading-[1.05] tracking-tight text-bone-50 sm:text-6xl md:text-7xl lg:text-8xl title-balance animate-[fade-up_0.8s_ease-out_0.1s_both]">
|
<h1 class="mt-8 max-w-5xl font-display text-5xl font-normal leading-[0.98] tracking-tight text-bone-50 sm:text-7xl md:text-8xl lg:text-[8.5rem] title-balance">
|
||||||
Arhitectura <span class="shimmer-text">României</span>,
|
<span class="block" data-split>Arhitectura României</span>
|
||||||
de la cula olteană
|
<span class="mt-2 block font-display-italic text-terra-200" data-split>de la cula la zgârie-nori.</span>
|
||||||
la zgârie-norul de azi.
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="mt-8 max-w-2xl text-balance text-lg leading-relaxed text-bone-100/80 sm:text-xl animate-[fade-up_0.8s_ease-out_0.25s_both]">
|
<p class="mt-10 max-w-2xl text-balance text-lg leading-relaxed text-bone-100/80 sm:text-xl" data-reveal-blur data-delay="0.6">
|
||||||
Șase tipologii care au definit cum locuiesc românii.
|
Șase tipologii care au definit cum locuiesc românii.
|
||||||
Modele 3D interactive, povești scurte, materiale, fapte. Scroll-uiește.
|
Fiecare începe ca un plan 2D și se ridică în 3D pe măsură ce dai scroll.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-12 flex flex-wrap items-center gap-4 animate-[fade-up_0.8s_ease-out_0.4s_both]">
|
<div class="mt-12 flex flex-wrap items-center gap-4" data-reveal data-delay="0.9">
|
||||||
<a
|
<a
|
||||||
href="#poveste"
|
href="#poveste"
|
||||||
class="group inline-flex items-center gap-3 rounded-full bg-terra-500 px-7 py-4 text-base font-medium text-bone-50 shadow-xl shadow-terra-900/40 transition hover:bg-terra-400"
|
data-magnetic="0.35"
|
||||||
|
class="group glow-ring inline-flex items-center gap-3 rounded-full bg-terra-500 px-8 py-4 text-base font-medium text-bone-50 shadow-2xl shadow-terra-900/40 transition hover:bg-terra-400"
|
||||||
>
|
>
|
||||||
|
<span class="relative inline-flex h-2 w-2">
|
||||||
|
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-bone-50 opacity-75"></span>
|
||||||
|
<span class="relative inline-flex h-2 w-2 rounded-full bg-bone-50"></span>
|
||||||
|
</span>
|
||||||
Începe povestea
|
Începe povestea
|
||||||
<span class="transition group-hover:translate-y-0.5" aria-hidden>↓</span>
|
<span class="transition group-hover:translate-y-0.5" aria-hidden>↓</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#quiz"
|
href="#quiz"
|
||||||
|
data-magnetic="0.25"
|
||||||
class="inline-flex items-center gap-2 rounded-full border border-bone-50/20 px-6 py-4 text-base text-bone-50 backdrop-blur transition hover:border-bone-50/40 hover:bg-bone-50/5"
|
class="inline-flex items-center gap-2 rounded-full border border-bone-50/20 px-6 py-4 text-base text-bone-50 backdrop-blur transition hover:border-bone-50/40 hover:bg-bone-50/5"
|
||||||
>
|
>
|
||||||
Sari la quiz
|
Sari direct la quiz
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats strip -->
|
<!-- Stats -->
|
||||||
<dl class="mt-20 grid max-w-3xl grid-cols-3 gap-6 border-t border-bone-50/10 pt-8 sm:gap-12 animate-[fade-up_0.8s_ease-out_0.6s_both]">
|
<dl class="mt-20 grid max-w-3xl grid-cols-3 gap-6 border-t border-bone-50/10 pt-8 sm:gap-12" data-reveal data-delay="1.2">
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-xs uppercase tracking-wider text-bone-200/60">Tipologii</dt>
|
<dt class="text-xs uppercase tracking-wider text-bone-200/60">Tipologii</dt>
|
||||||
<dd class="mt-1 font-display text-3xl text-terra-200 sm:text-4xl">6</dd>
|
<dd class="mt-2 font-display text-4xl text-terra-200 sm:text-5xl" data-counter="6" data-counter-duration="1.4">0</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-xs uppercase tracking-wider text-bone-200/60">Ani de istorie</dt>
|
<dt class="text-xs uppercase tracking-wider text-bone-200/60">Ani de istorie</dt>
|
||||||
<dd class="mt-1 font-display text-3xl text-terra-200 sm:text-4xl">800+</dd>
|
<dd class="mt-2 font-display text-4xl text-terra-200 sm:text-5xl" data-counter="800" data-counter-suffix="+" data-counter-duration="1.8">0</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-xs uppercase tracking-wider text-bone-200/60">Sub UNESCO</dt>
|
<dt class="text-xs uppercase tracking-wider text-bone-200/60">Sub UNESCO</dt>
|
||||||
<dd class="mt-1 font-display text-3xl text-terra-200 sm:text-4xl">15</dd>
|
<dd class="mt-2 font-display text-4xl text-terra-200 sm:text-5xl" data-counter="15" data-counter-duration="1.2">0</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scroll indicator -->
|
<!-- Scroll prompt -->
|
||||||
<div class="absolute bottom-8 left-1/2 hidden -translate-x-1/2 sm:block">
|
<div class="absolute bottom-8 left-1/2 hidden -translate-x-1/2 sm:block">
|
||||||
<div class="flex flex-col items-center gap-2 text-bone-200/50">
|
<div class="flex flex-col items-center gap-2 text-bone-200/50">
|
||||||
<span class="text-[10px] uppercase tracking-[0.3em]">Scroll</span>
|
<span class="text-[10px] uppercase tracking-[0.3em]">Scroll</span>
|
||||||
@@ -89,11 +114,9 @@
|
|||||||
};
|
};
|
||||||
resize();
|
resize();
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
// Camera position tweak for hero
|
ctx.camera.position.set(7.5, 5.0, 8);
|
||||||
ctx.camera.position.set(7, 5, 8);
|
ctx.camera.lookAt(0, 2.6, 0);
|
||||||
ctx.camera.lookAt(0, 2.4, 0);
|
|
||||||
|
|
||||||
// Pause hero render loop when scrolled past — saves GPU for the rest of the page
|
|
||||||
const heroSection = canvas.closest('section');
|
const heroSection = canvas.closest('section');
|
||||||
if (heroSection) {
|
if (heroSection) {
|
||||||
const io = new IntersectionObserver(
|
const io = new IntersectionObserver(
|
||||||
|
|||||||
+42
-10
@@ -2,33 +2,65 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
<section id="poveste" class="relative bg-bone-50 py-32 sm:py-40">
|
<section id="poveste" class="relative bg-bone-50 py-32 sm:py-40">
|
||||||
<div class="mx-auto grid max-w-6xl gap-16 px-6 lg:grid-cols-12 lg:px-12">
|
<!-- Decorative blueprint divider above -->
|
||||||
|
<div class="absolute inset-x-0 top-0 mx-auto max-w-6xl px-6 lg:px-12">
|
||||||
|
<div class="divider-blueprint"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-6xl px-6 lg:px-12">
|
||||||
|
<div class="grid gap-16 lg:grid-cols-12">
|
||||||
<div class="lg:col-span-5">
|
<div class="lg:col-span-5">
|
||||||
<p class="text-xs font-medium uppercase tracking-[0.3em] text-terra-700">Despre</p>
|
<p class="text-xs font-medium uppercase tracking-[0.32em] text-terra-700" data-reveal>
|
||||||
<h2 class="mt-4 font-display text-4xl font-medium text-ink-900 sm:text-5xl title-balance">
|
<span class="inline-block h-px w-8 align-middle bg-terra-700 mr-3"></span>
|
||||||
De ce arată casele noastre așa cum arată?
|
Despre proiect
|
||||||
|
</p>
|
||||||
|
<h2 class="mt-5 font-display text-4xl font-normal leading-[1.05] text-ink-900 sm:text-5xl lg:text-6xl title-balance" data-reveal data-delay="0.1">
|
||||||
|
De ce arată casele <span class="font-display-italic text-terra-600">noastre</span> așa cum arată?
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 text-lg leading-relaxed text-ink-800/85 lg:col-span-7 text-pretty">
|
<div class="space-y-7 text-lg leading-relaxed text-ink-800/85 lg:col-span-7 text-pretty">
|
||||||
<p>
|
<p data-reveal>
|
||||||
Pentru că pe aceste pământuri au trecut romanii, slavii, ungurii, sașii, turcii, austriecii și
|
Pentru că pe aceste pământuri au trecut romanii, slavii, ungurii, sașii, turcii, austriecii și
|
||||||
sovieticii — și fiecare a lăsat în urmă un mod de a tăia un acoperiș, de a sparge un zid pentru o
|
sovieticii — și fiecare a lăsat în urmă un mod de a tăia un acoperiș, de a sparge un zid pentru o
|
||||||
fereastră, de a aşeza o casă față de drum.
|
fereastră, de a aşeza o casă față de drum.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p data-reveal data-delay="0.1">
|
||||||
Pentru că în Maramureș plouă șapte luni pe an și acoperișurile s-au înălțat ca să nu țină zăpada.
|
Pentru că în Maramureș plouă șapte luni pe an și acoperișurile s-au înălțat ca să nu țină zăpada.
|
||||||
Pentru că în Oltenia trecea graniță între imperii și casa boierească s-a transformat în cetate.
|
Pentru că în Oltenia trecea graniță între imperii și casa boierească s-a transformat în cetate.
|
||||||
Pentru că în interbelic Bucureștiul a vrut să-și inventeze identitatea — și a făcut-o cu hublouri și balcoane curbate.
|
Pentru că în interbelic Bucureștiul a vrut să-și inventeze identitatea — și a făcut-o cu hublouri și balcoane curbate.
|
||||||
</p>
|
</p>
|
||||||
<p class="font-display text-2xl text-terra-700">
|
<blockquote class="relative -ml-2 border-l-2 border-terra-500 pl-6 font-display text-2xl italic text-terra-700 sm:text-3xl text-balance" data-reveal data-delay="0.2">
|
||||||
Arhitectura este memoria unui popor turnată în beton, lemn și piatră.
|
Arhitectura este memoria unui popor turnată în beton, lemn și piatră.
|
||||||
</p>
|
</blockquote>
|
||||||
<p>
|
<p data-reveal data-delay="0.3">
|
||||||
Acest proiect prezintă șase tipologii care, împreună, povestesc cum am ajuns să locuim astăzi.
|
Acest proiect prezintă șase tipologii care, împreună, povestesc cum am ajuns să locuim astăzi.
|
||||||
Fiecare model este simplificat — păstrează esența formei, ignoră detaliul.
|
Fiecare model este simplificat — păstrează esența formei, ignoră detaliul.
|
||||||
Pentru detaliu, mergi în satul de la munte. Pentru context — citește mai jos.
|
Pentru detaliu, mergi în satul de la munte. Pentru context — citește mai jos.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Marquee of typology names -->
|
||||||
|
<div class="mt-24 marquee" data-reveal data-delay="0.2">
|
||||||
|
<div class="marquee-track font-display text-4xl text-ink-900/15 sm:text-6xl">
|
||||||
|
<span class="flex items-center gap-12">
|
||||||
|
<span>Maramureș</span><span>·</span>
|
||||||
|
<span class="font-display-italic">Cula olteană</span><span>·</span>
|
||||||
|
<span>Casa săsească</span><span>·</span>
|
||||||
|
<span class="font-display-italic">Vila interbelică</span><span>·</span>
|
||||||
|
<span>Blocul comunist</span><span>·</span>
|
||||||
|
<span class="font-display-italic">Contemporan</span><span>·</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-12" aria-hidden="true">
|
||||||
|
<span>Maramureș</span><span>·</span>
|
||||||
|
<span class="font-display-italic">Cula olteană</span><span>·</span>
|
||||||
|
<span>Casa săsească</span><span>·</span>
|
||||||
|
<span class="font-display-italic">Vila interbelică</span><span>·</span>
|
||||||
|
<span>Blocul comunist</span><span>·</span>
|
||||||
|
<span class="font-display-italic">Contemporan</span><span>·</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
size?: number;
|
||||||
|
variant?: 'mark' | 'full' | 'inverse';
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
const { size = 40, variant = 'mark', class: className = '' } = Astro.props;
|
||||||
|
|
||||||
|
// Color tokens (can override with CSS vars on parent)
|
||||||
|
const c1 = 'var(--logo-c1, #d4703f)'; // terra
|
||||||
|
const c2 = 'var(--logo-c2, #fae8da)'; // light
|
||||||
|
const c3 = 'var(--logo-c3, #3a1c10)'; // dark
|
||||||
|
---
|
||||||
|
|
||||||
|
{variant === 'mark' && (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class={`logo-mark ${className}`}
|
||||||
|
aria-label="2D3D"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoGrad" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color={c1} />
|
||||||
|
<stop offset="1" stop-color={c3} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
{/* Base ground line */}
|
||||||
|
<rect x="6" y="50" width="52" height="2" fill={c3} opacity="0.9" />
|
||||||
|
|
||||||
|
{/* Left "house" — gable form (the 2 of 2D) */}
|
||||||
|
<g>
|
||||||
|
<path d="M10 50 L10 30 L22 18 L34 30 L34 50 Z" fill={c1} />
|
||||||
|
{/* depth lines */}
|
||||||
|
<path d="M22 18 L22 38 M22 38 L34 50 M22 38 L10 50" stroke={c3} stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round" opacity="0.85" />
|
||||||
|
<rect x="18" y="38" width="8" height="12" fill={c3} />
|
||||||
|
<rect x="20" y="40" width="2" height="2" fill={c2} />
|
||||||
|
<rect x="22" y="40" width="2" height="2" fill={c2} />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Right "tower" (the 3 of 3D) */}
|
||||||
|
<g>
|
||||||
|
<rect x="38" y="22" width="18" height="28" fill="url(#logoGrad)" />
|
||||||
|
<path d="M38 22 L47 14 L56 22" fill={c3} />
|
||||||
|
{/* window grid */}
|
||||||
|
<g fill={c2} opacity="0.9">
|
||||||
|
<rect x="41" y="26" width="3" height="3" />
|
||||||
|
<rect x="46" y="26" width="3" height="3" />
|
||||||
|
<rect x="51" y="26" width="3" height="3" />
|
||||||
|
<rect x="41" y="32" width="3" height="3" />
|
||||||
|
<rect x="46" y="32" width="3" height="3" />
|
||||||
|
<rect x="51" y="32" width="3" height="3" />
|
||||||
|
<rect x="41" y="38" width="3" height="3" />
|
||||||
|
<rect x="46" y="38" width="3" height="3" />
|
||||||
|
<rect x="51" y="38" width="3" height="3" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Diagonal axis line — represents the 2D→3D transformation */}
|
||||||
|
<path d="M10 56 L56 56" stroke={c3} stroke-width="0.8" opacity="0.4" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === 'full' && (
|
||||||
|
<span class={`logo-full inline-flex items-baseline gap-2 ${className}`}>
|
||||||
|
<Logo size={size} variant="mark" />
|
||||||
|
<span class="font-display text-base tracking-tight" style={`color: ${c3};`}>
|
||||||
|
<span style={`color: ${c1};`}>2D</span><span style="opacity:.4">·</span><span>3D</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === 'inverse' && (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class={`logo-mark ${className}`}
|
||||||
|
aria-label="2D3D"
|
||||||
|
>
|
||||||
|
<rect x="6" y="50" width="52" height="2" fill="#fae8da" opacity="0.9" />
|
||||||
|
<g>
|
||||||
|
<path d="M10 50 L10 30 L22 18 L34 30 L34 50 Z" fill="#ecae82" />
|
||||||
|
<path d="M22 18 L22 38 M22 38 L34 50 M22 38 L10 50" stroke="#1f0e08" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round" opacity="0.85" />
|
||||||
|
<rect x="18" y="38" width="8" height="12" fill="#1f0e08" />
|
||||||
|
<rect x="20" y="40" width="2" height="2" fill="#fae8da" />
|
||||||
|
<rect x="22" y="40" width="2" height="2" fill="#fae8da" />
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="38" y="22" width="18" height="28" fill="#fae8da" />
|
||||||
|
<path d="M38 22 L47 14 L56 22" fill="#1f0e08" />
|
||||||
|
<g fill="#1f0e08" opacity="0.85">
|
||||||
|
<rect x="41" y="26" width="3" height="3" />
|
||||||
|
<rect x="46" y="26" width="3" height="3" />
|
||||||
|
<rect x="51" y="26" width="3" height="3" />
|
||||||
|
<rect x="41" y="32" width="3" height="3" />
|
||||||
|
<rect x="46" y="32" width="3" height="3" />
|
||||||
|
<rect x="51" y="32" width="3" height="3" />
|
||||||
|
<rect x="41" y="38" width="3" height="3" />
|
||||||
|
<rect x="46" y="38" width="3" height="3" />
|
||||||
|
<rect x="51" y="38" width="3" height="3" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
@@ -67,17 +67,21 @@ const typoMap = Object.fromEntries(typologies.map((t) => [t.id, t]));
|
|||||||
|
|
||||||
<div class="relative mx-auto max-w-4xl px-6 lg:px-12">
|
<div class="relative mx-auto max-w-4xl px-6 lg:px-12">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-xs font-medium uppercase tracking-[0.3em] text-terra-300">Joc</p>
|
<p class="text-xs font-medium uppercase tracking-[0.32em] text-terra-300" data-reveal>
|
||||||
<h2 class="mt-4 font-display text-4xl font-medium leading-tight sm:text-5xl title-balance">
|
<span class="inline-block h-px w-8 align-middle bg-terra-300 mr-3"></span>
|
||||||
Ce stil de casă ți s-ar potrivi?
|
Joc interactiv
|
||||||
|
</p>
|
||||||
|
<h2 class="mx-auto mt-5 max-w-3xl font-display text-4xl font-normal leading-[1.05] sm:text-6xl lg:text-7xl title-balance" data-reveal data-delay="0.1">
|
||||||
|
Ce stil de <span class="font-display-italic text-terra-300">casă</span> ți s-ar potrivi?
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mx-auto mt-4 max-w-2xl text-bone-100/70 text-balance">
|
<p class="mx-auto mt-5 max-w-xl text-lg text-bone-100/70 text-balance" data-reveal data-delay="0.2">
|
||||||
5 întrebări, 30 de secunde. La final primești tipologia ta și o poți distribui prietenilor.
|
5 întrebări, 30 de secunde. La final primești tipologia ta și o poți distribui prietenilor.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="mt-12 rounded-3xl border border-bone-50/10 bg-wood-900/50 p-6 backdrop-blur sm:p-10"
|
class="mt-14 rounded-3xl border border-bone-50/10 bg-wood-900/60 p-6 shadow-2xl shadow-black/40 backdrop-blur sm:p-10"
|
||||||
|
data-reveal data-delay="0.3"
|
||||||
data-quiz
|
data-quiz
|
||||||
data-questions={JSON.stringify(questions)}
|
data-questions={JSON.stringify(questions)}
|
||||||
data-typologies={JSON.stringify(typoMap)}
|
data-typologies={JSON.stringify(typoMap)}
|
||||||
|
|||||||
@@ -6,48 +6,63 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
const { typology: t, index } = Astro.props;
|
const { typology: t, index } = Astro.props;
|
||||||
const isEven = index % 2 === 0;
|
const isEven = index % 2 === 0;
|
||||||
|
const num = String(index + 1).padStart(2, '0');
|
||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id={t.id}
|
id={t.id}
|
||||||
class="relative overflow-hidden py-24 sm:py-32"
|
class="relative overflow-hidden py-28 sm:py-36"
|
||||||
style={`background: linear-gradient(180deg, ${t.palette.sky} 0%, ${t.palette.sky}dd 100%);`}
|
style={`background: linear-gradient(180deg, ${t.palette.sky}40 0%, ${t.palette.sky}aa 50%, ${t.palette.sky}40 100%); --typo-accent: ${t.palette.accent}; --typo-sun: ${t.palette.sun}; --typo-sky: ${t.palette.sky};`}
|
||||||
data-typology
|
data-typology
|
||||||
>
|
>
|
||||||
<!-- Era ribbon -->
|
<!-- Era ribbon — large bg lettering -->
|
||||||
<div class="pointer-events-none absolute inset-x-0 top-0 -z-0 select-none overflow-hidden">
|
<div class="pointer-events-none absolute inset-x-0 top-12 -z-0 select-none overflow-hidden">
|
||||||
<p
|
<p
|
||||||
class="whitespace-nowrap font-display text-[18vw] font-black leading-none tracking-tight opacity-[0.06]"
|
class="whitespace-nowrap font-display-italic text-[15vw] font-normal leading-none tracking-tight opacity-[0.07]"
|
||||||
style={`color: ${t.palette.accent};`}
|
style={`color: ${t.palette.accent};`}
|
||||||
>
|
>
|
||||||
{t.era} · {t.region.toUpperCase()}
|
{t.region.toUpperCase()} · {t.era}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-auto grid max-w-7xl items-center gap-12 px-6 lg:grid-cols-2 lg:gap-16 lg:px-12">
|
<div class="mx-auto grid max-w-7xl items-center gap-12 px-6 lg:grid-cols-12 lg:gap-16 lg:px-12">
|
||||||
{/* Canvas — alternates side */}
|
{/* Canvas — alternates side */}
|
||||||
<div class={`relative ${isEven ? 'lg:order-1' : 'lg:order-2'}`}>
|
<div class={`relative lg:col-span-6 ${isEven ? 'lg:order-1' : 'lg:order-2'}`} data-reveal-blur>
|
||||||
<div class="relative aspect-square w-full overflow-hidden rounded-3xl">
|
<div class="relative aspect-square w-full">
|
||||||
|
<!-- Soft sun gradient backdrop -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0"
|
class="absolute inset-4 rounded-[2.5rem]"
|
||||||
style={`background: radial-gradient(circle at 50% 60%, ${t.palette.sun}aa, ${t.palette.sky} 70%);`}
|
style={`background: radial-gradient(circle at 50% 65%, ${t.palette.sun}cc, ${t.palette.sky}88 65%);`}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<!-- Decorative blueprint corner -->
|
||||||
|
<svg class="pointer-events-none absolute left-2 top-2 h-12 w-12 opacity-30" viewBox="0 0 48 48" fill="none">
|
||||||
|
<path d="M2 16 V2 H16" stroke={t.palette.accent} stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="pointer-events-none absolute right-2 bottom-2 h-12 w-12 opacity-30" viewBox="0 0 48 48" fill="none">
|
||||||
|
<path d="M46 32 V46 H32" stroke={t.palette.accent} stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
<canvas
|
<canvas
|
||||||
data-house-canvas
|
data-house-canvas
|
||||||
data-house-id={t.id}
|
data-house-id={t.id}
|
||||||
class="absolute inset-0 h-full w-full cursor-grab active:cursor-grabbing"
|
class="absolute inset-0 h-full w-full cursor-grab active:cursor-grabbing"
|
||||||
aria-label={`Model 3D: ${t.title}`}
|
aria-label={`Model 3D: ${t.title}`}
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
<!-- Drag hint -->
|
<!-- Drag hint -->
|
||||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full bg-ink-900/40 px-3 py-1 text-[11px] font-medium uppercase tracking-wider text-bone-50 backdrop-blur" data-hint>
|
<div class="absolute bottom-6 left-1/2 -translate-x-1/2 rounded-full bg-ink-900/45 px-4 py-1.5 text-[11px] font-medium uppercase tracking-wider text-bone-50 backdrop-blur-md" data-hint>
|
||||||
↻ Trage pentru a roti
|
↻ Trage să rotești · Hover pentru spin
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags + materials -->
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-5 flex flex-wrap items-center gap-2 text-xs">
|
||||||
{t.tags.map((tag) => (
|
{t.tags.map((tag) => (
|
||||||
<span class="rounded-full bg-ink-900/5 px-3 py-1 text-xs font-medium text-ink-800/70">
|
<span
|
||||||
|
class="rounded-full px-3 py-1 font-medium backdrop-blur-sm"
|
||||||
|
style={`background: ${t.palette.accent}15; color: ${t.palette.accent};`}
|
||||||
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -55,46 +70,48 @@ const isEven = index % 2 === 0;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text */}
|
{/* Text */}
|
||||||
<div class={`${isEven ? 'lg:order-2' : 'lg:order-1'}`}>
|
<div class={`lg:col-span-6 ${isEven ? 'lg:order-2' : 'lg:order-1'}`}>
|
||||||
<div class="flex items-baseline gap-4">
|
<div class="flex items-baseline gap-5" data-reveal>
|
||||||
<span class="font-mono text-sm font-medium" style={`color: ${t.palette.accent};`}>
|
<span class="font-display text-5xl font-normal leading-none" style={`color: ${t.palette.accent};`}>
|
||||||
0{index + 1}
|
{num}
|
||||||
</span>
|
</span>
|
||||||
<p class="text-xs font-medium uppercase tracking-[0.25em] text-ink-800/60">
|
<div class="flex-1 border-t border-ink-900/15 pt-1">
|
||||||
|
<p class="text-xs font-medium uppercase tracking-[0.28em] text-ink-800/60">
|
||||||
{t.region} · {t.era}
|
{t.region} · {t.era}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="mt-3 font-display text-4xl font-medium leading-tight text-ink-900 sm:text-5xl title-balance">
|
<h2 class="mt-5 font-display text-4xl font-normal leading-[1.05] text-ink-900 sm:text-5xl lg:text-6xl title-balance" data-reveal data-delay="0.1">
|
||||||
{t.title}
|
{t.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="mt-3 font-display text-xl italic" style={`color: ${t.palette.accent};`}>
|
<p class="mt-3 font-display-italic text-2xl sm:text-3xl text-balance" style={`color: ${t.palette.accent};`} data-reveal data-delay="0.2">
|
||||||
{t.subtitle}
|
{t.subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mt-6 text-lg leading-relaxed text-ink-800/85 text-pretty">
|
<p class="mt-7 text-lg leading-relaxed text-ink-800/85 text-pretty" data-reveal data-delay="0.3">
|
||||||
{t.intro}
|
{t.intro}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mt-4 text-base leading-relaxed text-ink-800/75 text-pretty">
|
<p class="mt-4 text-base leading-relaxed text-ink-800/75 text-pretty" data-reveal data-delay="0.35">
|
||||||
{t.story}
|
{t.story}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Facts -->
|
<!-- Facts grid -->
|
||||||
<dl class="mt-8 grid grid-cols-2 gap-x-8 gap-y-4 border-t border-ink-900/10 pt-6">
|
<dl class="mt-8 grid grid-cols-2 gap-x-8 gap-y-5 border-t border-ink-900/10 pt-7" data-reveal data-delay="0.4">
|
||||||
{t.facts.map((f) => (
|
{t.facts.map((f) => (
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-xs uppercase tracking-wider text-ink-800/50">{f.label}</dt>
|
<dt class="text-[11px] font-medium uppercase tracking-wider text-ink-800/55">{f.label}</dt>
|
||||||
<dd class="mt-1 font-display text-base font-medium text-ink-900">{f.value}</dd>
|
<dd class="mt-1.5 font-display text-lg font-normal text-ink-900 leading-tight">{f.value}</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<!-- Materials -->
|
<!-- Materials line -->
|
||||||
<div class="mt-6">
|
<div class="mt-7 flex flex-wrap items-baseline gap-x-3 gap-y-1" data-reveal data-delay="0.45">
|
||||||
<p class="text-xs uppercase tracking-wider text-ink-800/50">Materiale</p>
|
<span class="text-[11px] font-medium uppercase tracking-wider text-ink-800/55">Materiale</span>
|
||||||
<p class="mt-2 text-sm text-ink-800/80">{t.materials.join(' · ')}</p>
|
<span class="text-sm font-medium text-ink-800/85">{t.materials.join(' · ')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const jsonLd = {
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,700;9..144,900&family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500&display=swap"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Open Graph */}
|
{/* Open Graph */}
|
||||||
|
|||||||
@@ -28,4 +28,6 @@ import { typologies } from '../data/typologies';
|
|||||||
<script>
|
<script>
|
||||||
// Bootstrap all 3D scenes for typology canvases
|
// Bootstrap all 3D scenes for typology canvases
|
||||||
import('../three/bootstrap');
|
import('../three/bootstrap');
|
||||||
|
// Site-wide animations: reveals, counters, magnetic, hero glow
|
||||||
|
import('../scripts/anim');
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* Lightweight site-wide animation primitives:
|
||||||
|
* - Reveal-on-scroll for [data-reveal] / [data-reveal-blur] / .reveal-word
|
||||||
|
* - Number counter for [data-counter]
|
||||||
|
* - Magnetic hover for [data-magnetic]
|
||||||
|
* - Cursor-aware glow for hero
|
||||||
|
*/
|
||||||
|
|
||||||
|
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
function setupReveal() {
|
||||||
|
const targets = document.querySelectorAll<HTMLElement>('[data-reveal], [data-reveal-blur], .reveal-word');
|
||||||
|
if (!targets.length) return;
|
||||||
|
|
||||||
|
if (reduceMotion) {
|
||||||
|
targets.forEach((el) => el.classList.add('in'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.isIntersecting) return;
|
||||||
|
const el = entry.target as HTMLElement;
|
||||||
|
const delay = parseFloat(el.dataset.delay || '0');
|
||||||
|
if (delay) {
|
||||||
|
window.setTimeout(() => el.classList.add('in'), delay * 1000);
|
||||||
|
} else {
|
||||||
|
el.classList.add('in');
|
||||||
|
}
|
||||||
|
io.unobserve(el);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: '-50px 0px', threshold: 0.05 }
|
||||||
|
);
|
||||||
|
targets.forEach((el) => io.observe(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCounters() {
|
||||||
|
const targets = document.querySelectorAll<HTMLElement>('[data-counter]');
|
||||||
|
if (!targets.length) return;
|
||||||
|
|
||||||
|
const animate = (el: HTMLElement) => {
|
||||||
|
const end = parseFloat(el.dataset.counter || '0');
|
||||||
|
const dur = parseFloat(el.dataset.counterDuration || '1.6');
|
||||||
|
const suffix = el.dataset.counterSuffix || '';
|
||||||
|
const prefix = el.dataset.counterPrefix || '';
|
||||||
|
if (reduceMotion) { el.textContent = `${prefix}${end}${suffix}`; return; }
|
||||||
|
const start = performance.now();
|
||||||
|
const tick = (now: number) => {
|
||||||
|
const t = Math.min(1, (now - start) / (dur * 1000));
|
||||||
|
const eased = 1 - Math.pow(1 - t, 3);
|
||||||
|
const value = end * eased;
|
||||||
|
const display = end >= 100 ? Math.round(value).toLocaleString('ro-RO') : value.toFixed(value === Math.floor(value) ? 0 : 1);
|
||||||
|
el.textContent = `${prefix}${display}${suffix}`;
|
||||||
|
if (t < 1) requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) { animate(e.target as HTMLElement); io.unobserve(e.target); }
|
||||||
|
}),
|
||||||
|
{ threshold: 0.5 }
|
||||||
|
);
|
||||||
|
targets.forEach((el) => io.observe(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMagnetic() {
|
||||||
|
if (reduceMotion || matchMedia('(pointer: coarse)').matches) return;
|
||||||
|
const targets = document.querySelectorAll<HTMLElement>('[data-magnetic]');
|
||||||
|
targets.forEach((el) => {
|
||||||
|
const strength = parseFloat(el.dataset.magnetic || '0.4');
|
||||||
|
let raf = 0;
|
||||||
|
let tx = 0, ty = 0;
|
||||||
|
let cx = 0, cy = 0;
|
||||||
|
|
||||||
|
const onMove = (e: PointerEvent) => {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
cx = (e.clientX - (r.left + r.width / 2)) * strength;
|
||||||
|
cy = (e.clientY - (r.top + r.height / 2)) * strength;
|
||||||
|
if (!raf) raf = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
const onLeave = () => {
|
||||||
|
cx = 0; cy = 0;
|
||||||
|
if (!raf) raf = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
const loop = () => {
|
||||||
|
tx += (cx - tx) * 0.18;
|
||||||
|
ty += (cy - ty) * 0.18;
|
||||||
|
el.style.transform = `translate3d(${tx.toFixed(2)}px, ${ty.toFixed(2)}px, 0)`;
|
||||||
|
if (Math.abs(tx - cx) > 0.1 || Math.abs(ty - cy) > 0.1) {
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
} else {
|
||||||
|
raf = 0;
|
||||||
|
if (cx === 0 && cy === 0) el.style.transform = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.addEventListener('pointermove', onMove);
|
||||||
|
el.addEventListener('pointerleave', onLeave);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupHeroGlow() {
|
||||||
|
const hero = document.querySelector<HTMLElement>('[data-hero-glow]');
|
||||||
|
if (!hero || reduceMotion) return;
|
||||||
|
let raf = 0;
|
||||||
|
let tx = 50, ty = 50;
|
||||||
|
let cx = 50, cy = 50;
|
||||||
|
const onMove = (e: PointerEvent) => {
|
||||||
|
const r = hero.getBoundingClientRect();
|
||||||
|
cx = ((e.clientX - r.left) / r.width) * 100;
|
||||||
|
cy = ((e.clientY - r.top) / r.height) * 100;
|
||||||
|
if (!raf) raf = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
const loop = () => {
|
||||||
|
tx += (cx - tx) * 0.08;
|
||||||
|
ty += (cy - ty) * 0.08;
|
||||||
|
hero.style.setProperty('--mx', `${tx}%`);
|
||||||
|
hero.style.setProperty('--my', `${ty}%`);
|
||||||
|
if (Math.abs(tx - cx) > 0.1 || Math.abs(ty - cy) > 0.1) {
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
} else { raf = 0; }
|
||||||
|
};
|
||||||
|
hero.addEventListener('pointermove', onMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupWordSplit() {
|
||||||
|
// Split [data-split] into per-word spans for staggered reveal
|
||||||
|
document.querySelectorAll<HTMLElement>('[data-split]').forEach((el) => {
|
||||||
|
if (el.dataset.splitDone) return;
|
||||||
|
const text = el.textContent || '';
|
||||||
|
el.dataset.splitDone = '1';
|
||||||
|
el.innerHTML = '';
|
||||||
|
const words = text.split(/\s+/);
|
||||||
|
words.forEach((w, i) => {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'reveal-word';
|
||||||
|
span.textContent = w;
|
||||||
|
span.style.transitionDelay = `${i * 60}ms`;
|
||||||
|
el.appendChild(span);
|
||||||
|
if (i < words.length - 1) el.appendChild(document.createTextNode(' '));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
setupWordSplit();
|
||||||
|
setupReveal();
|
||||||
|
setupCounters();
|
||||||
|
setupMagnetic();
|
||||||
|
setupHeroGlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
+177
-50
@@ -1,57 +1,54 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Romanian earth & sky palette */
|
/* Refined Romanian palette — slightly more sophisticated, less saturated */
|
||||||
--color-terra-50: #fdf6f1;
|
--color-terra-50: #fdf5ee;
|
||||||
--color-terra-100: #fae8da;
|
--color-terra-100: #f9e3d0;
|
||||||
--color-terra-200: #f4cfb1;
|
--color-terra-200: #f0c39b;
|
||||||
--color-terra-300: #ecae82;
|
--color-terra-300: #e69d6a;
|
||||||
--color-terra-400: #e08c5a;
|
--color-terra-400: #d97942;
|
||||||
--color-terra-500: #d4703f;
|
--color-terra-500: #c25c2c;
|
||||||
--color-terra-600: #b35831;
|
--color-terra-600: #a14622;
|
||||||
--color-terra-700: #8a4226;
|
--color-terra-700: #7d361c;
|
||||||
--color-terra-800: #5e2d1a;
|
--color-terra-800: #532516;
|
||||||
--color-terra-900: #3a1c10;
|
--color-terra-900: #2e160d;
|
||||||
--color-terra-950: #1f0e08;
|
--color-terra-950: #190b06;
|
||||||
|
|
||||||
--color-wood-50: #f9f5ef;
|
--color-wood-50: #f7f3ec;
|
||||||
--color-wood-100: #ede2cf;
|
--color-wood-100: #ebdec7;
|
||||||
--color-wood-200: #d8bf9e;
|
--color-wood-200: #d3b78f;
|
||||||
--color-wood-300: #bf9b6f;
|
--color-wood-300: #b58f5d;
|
||||||
--color-wood-400: #a17a4b;
|
--color-wood-400: #936b3c;
|
||||||
--color-wood-500: #7e5d36;
|
--color-wood-500: #6d4f2b;
|
||||||
--color-wood-600: #604527;
|
--color-wood-600: #503820;
|
||||||
--color-wood-700: #41301a;
|
--color-wood-700: #382617;
|
||||||
--color-wood-800: #261c0f;
|
--color-wood-800: #1f150c;
|
||||||
--color-wood-900: #110c06;
|
--color-wood-900: #0d0805;
|
||||||
|
|
||||||
--color-sky-mist-50: #f1f7fa;
|
--color-sky-mist-50: #eef5f9;
|
||||||
--color-sky-mist-100: #dbeaf2;
|
--color-sky-mist-100: #d6e6ee;
|
||||||
--color-sky-mist-200: #b6d4e3;
|
--color-sky-mist-200: #afcedb;
|
||||||
--color-sky-mist-300: #84b6cd;
|
--color-sky-mist-300: #7eb1c2;
|
||||||
--color-sky-mist-400: #5394b3;
|
--color-sky-mist-400: #5293a8;
|
||||||
--color-sky-mist-500: #347697;
|
--color-sky-mist-500: #347697;
|
||||||
--color-sky-mist-600: #285d79;
|
--color-sky-mist-600: #285d79;
|
||||||
--color-sky-mist-700: #1f475c;
|
--color-sky-mist-700: #1f475c;
|
||||||
--color-sky-mist-800: #142e3c;
|
--color-sky-mist-800: #142e3c;
|
||||||
--color-sky-mist-900: #0a1820;
|
--color-sky-mist-900: #0a1820;
|
||||||
|
|
||||||
--color-bone-50: #faf8f3;
|
--color-bone-50: #fbf8f1;
|
||||||
--color-bone-100: #f1ecdf;
|
--color-bone-100: #f3ecdc;
|
||||||
--color-bone-200: #e2d8be;
|
--color-bone-200: #e3d6b6;
|
||||||
--color-bone-300: #cfbf94;
|
--color-bone-300: #cdb985;
|
||||||
--color-bone-400: #b9a06a;
|
--color-bone-400: #b89a5f;
|
||||||
|
|
||||||
--color-ink-900: #15110c;
|
--color-ink-900: #110d08;
|
||||||
--color-ink-800: #261f17;
|
--color-ink-800: #1f1810;
|
||||||
|
|
||||||
--font-display: "Fraunces", "Georgia", "Times New Roman", serif;
|
/* Modern typography pairing — Instrument Serif for display, Geist Sans for body */
|
||||||
--font-sans: "Inter", "system-ui", sans-serif;
|
--font-display: "Instrument Serif", "Cormorant Garamond", "Georgia", serif;
|
||||||
--font-mono: "JetBrains Mono", "Menlo", monospace;
|
--font-sans: "Geist", "Inter", "system-ui", sans-serif;
|
||||||
|
--font-mono: "Geist Mono", "JetBrains Mono", "Menlo", monospace;
|
||||||
--animate-float: float 6s ease-in-out infinite;
|
|
||||||
--animate-shimmer: shimmer 2.4s linear infinite;
|
|
||||||
--animate-fade-up: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
@@ -63,22 +60,40 @@
|
|||||||
100% { background-position: 200% 0; }
|
100% { background-position: 200% 0; }
|
||||||
}
|
}
|
||||||
@keyframes fade-up {
|
@keyframes fade-up {
|
||||||
from { opacity: 0; transform: translateY(24px); }
|
from { opacity: 0; transform: translateY(28px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
@keyframes mask-reveal {
|
||||||
|
from { clip-path: inset(0 0 100% 0); }
|
||||||
|
to { clip-path: inset(0 0 0 0); }
|
||||||
|
}
|
||||||
|
@keyframes blur-in {
|
||||||
|
from { opacity: 0; filter: blur(12px); transform: translateY(20px); }
|
||||||
|
to { opacity: 1; filter: blur(0); transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes draw-line {
|
||||||
|
from { stroke-dashoffset: 100%; }
|
||||||
|
to { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
@keyframes pulse-soft {
|
||||||
|
0%, 100% { opacity: 0.4; transform: scale(1); }
|
||||||
|
50% { opacity: 0.8; transform: scale(1.06); }
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
font-feature-settings: "ss01", "cv11";
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(180deg, #faf8f3 0%, #f1ecdf 100%);
|
background: var(--color-bone-50);
|
||||||
color: var(--color-ink-900);
|
color: var(--color-ink-900);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
font-variation-settings: "opsz" 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
@@ -88,14 +103,19 @@ body {
|
|||||||
|
|
||||||
.font-display {
|
.font-display {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-feature-settings: "ss01", "ss02";
|
letter-spacing: -0.018em;
|
||||||
letter-spacing: -0.02em;
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.font-display-italic {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-balance { text-wrap: balance; }
|
.title-balance, .text-balance { text-wrap: balance; }
|
||||||
.text-balance { text-wrap: balance; }
|
|
||||||
.text-pretty { text-wrap: pretty; }
|
.text-pretty { text-wrap: pretty; }
|
||||||
|
|
||||||
|
/* Subtle paper grain */
|
||||||
.grain {
|
.grain {
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
@@ -105,19 +125,19 @@ body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.6 0 0 0 0 0.4 0 0 0 0 0.2 0 0 0 0.18 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.6 0 0 0 0 0.4 0 0 0 0 0.2 0 0 0 0.18 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||||
opacity: 0.45;
|
opacity: 0.35;
|
||||||
mix-blend-mode: multiply;
|
mix-blend-mode: multiply;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shimmer-text {
|
.shimmer-text {
|
||||||
background: linear-gradient(90deg, var(--color-terra-700) 0%, var(--color-terra-400) 50%, var(--color-terra-700) 100%);
|
background: linear-gradient(90deg, var(--color-terra-700) 0%, var(--color-terra-300) 50%, var(--color-terra-700) 100%);
|
||||||
background-size: 200% auto;
|
background-size: 200% auto;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
animation: var(--animate-shimmer);
|
animation: shimmer 4s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-fade-edges {
|
.canvas-fade-edges {
|
||||||
@@ -125,6 +145,112 @@ body {
|
|||||||
mask-image: radial-gradient(ellipse at center, #000 60%, transparent 95%);
|
mask-image: radial-gradient(ellipse at center, #000 60%, transparent 95%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Word reveal animation — used for hero text */
|
||||||
|
.reveal-word {
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(0.6em) rotate(2deg);
|
||||||
|
transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
.reveal-word.in {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) rotate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reveal-on-scroll utility */
|
||||||
|
[data-reveal] {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(28px);
|
||||||
|
transition: opacity 0.9s cubic-bezier(0.22, 1, 0.36, 1), transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
[data-reveal].in {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
[data-reveal-blur] {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(10px);
|
||||||
|
transform: translateY(16px);
|
||||||
|
transition: opacity 1s ease, filter 1s ease, transform 1s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
[data-reveal-blur].in {
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Magnetic CTA hover */
|
||||||
|
.magnetic {
|
||||||
|
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Marquee */
|
||||||
|
.marquee {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
|
||||||
|
}
|
||||||
|
.marquee-track {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 3rem;
|
||||||
|
animation: marquee 38s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes marquee {
|
||||||
|
from { transform: translateX(0); }
|
||||||
|
to { transform: translateX(-50%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient orbs (used in hero/section backgrounds) */
|
||||||
|
.orb {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(80px);
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.55;
|
||||||
|
animation: float 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decorative section divider — architectural blueprint line */
|
||||||
|
.divider-blueprint {
|
||||||
|
height: 56px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(90deg, transparent calc(50% - 1px), rgba(0,0,0,0.18) calc(50% - 1px), rgba(0,0,0,0.18) 50%, transparent 50%),
|
||||||
|
repeating-linear-gradient(90deg, rgba(0,0,0,0.12) 0 8px, transparent 8px 16px);
|
||||||
|
background-size: 100% 100%, 100% 1px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center, center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow ring around buttons */
|
||||||
|
.glow-ring {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
.glow-ring::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: conic-gradient(from var(--angle, 0deg), var(--color-terra-300), var(--color-terra-500), var(--color-terra-700), var(--color-terra-300));
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
animation: spin 4s linear infinite;
|
||||||
|
}
|
||||||
|
.glow-ring:hover::before { opacity: 0.7; }
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(1turn); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --angle {
|
||||||
|
syntax: "<angle>";
|
||||||
|
initial-value: 0deg;
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html { scroll-behavior: auto; }
|
html { scroll-behavior: auto; }
|
||||||
*,
|
*,
|
||||||
@@ -134,4 +260,5 @@ body {
|
|||||||
animation-iteration-count: 1 !important;
|
animation-iteration-count: 1 !important;
|
||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms !important;
|
||||||
}
|
}
|
||||||
|
[data-reveal], [data-reveal-blur], .reveal-word { opacity: 1; transform: none; filter: none; }
|
||||||
}
|
}
|
||||||
|
|||||||
+407
-258
@@ -2,377 +2,481 @@ import * as THREE from 'three';
|
|||||||
|
|
||||||
export type HouseId = 'maramures' | 'cula' | 'sasesc' | 'interbelic' | 'comunist' | 'contemporan';
|
export type HouseId = 'maramures' | 'cula' | 'sasesc' | 'interbelic' | 'comunist' | 'contemporan';
|
||||||
|
|
||||||
const TAU = Math.PI * 2;
|
/* ----------------------------------------------------------------
|
||||||
|
* Toon gradient — single 1×3 LUT shared by all materials.
|
||||||
function makeRoofMaterial(color: string) {
|
* Gives that flat, designer-toy / clay-render look forgiving of
|
||||||
return new THREE.MeshStandardMaterial({ color, roughness: 0.85, metalness: 0.05, flatShading: true });
|
* rough geometry.
|
||||||
}
|
* ---------------------------------------------------------------- */
|
||||||
function makeWallMaterial(color: string) {
|
let _toonGrad: THREE.DataTexture | null = null;
|
||||||
return new THREE.MeshStandardMaterial({ color, roughness: 0.95, metalness: 0, flatShading: true });
|
function toonGradient(): THREE.DataTexture {
|
||||||
}
|
if (_toonGrad) return _toonGrad;
|
||||||
function makeWindowMaterial() {
|
// 3-step ramp packed as RGBA (RGBFormat was removed after three r136)
|
||||||
return new THREE.MeshStandardMaterial({ color: '#fdf6f1', emissive: '#f4cfb1', emissiveIntensity: 0.45, roughness: 0.4 });
|
const data = new Uint8Array([
|
||||||
|
90, 90, 90, 255,
|
||||||
|
180, 180, 180, 255,
|
||||||
|
255, 255, 255, 255,
|
||||||
|
]);
|
||||||
|
const tex = new THREE.DataTexture(data, 3, 1, THREE.RGBAFormat);
|
||||||
|
tex.minFilter = THREE.NearestFilter;
|
||||||
|
tex.magFilter = THREE.NearestFilter;
|
||||||
|
tex.generateMipmaps = false;
|
||||||
|
tex.needsUpdate = true;
|
||||||
|
_toonGrad = tex;
|
||||||
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------- Maramureș: tall steep-roofed wooden church-like house ----------- */
|
function toon(color: string): THREE.MeshToonMaterial {
|
||||||
|
return new THREE.MeshToonMaterial({ color, gradientMap: toonGradient() });
|
||||||
|
}
|
||||||
|
|
||||||
|
function emissive(color: string, intensity = 0.55): THREE.MeshBasicMaterial {
|
||||||
|
// Window glow — emissive without lighting cost
|
||||||
|
return new THREE.MeshBasicMaterial({ color });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper: an "extruded" shape from a 2D Shape, with bevel — used for arches */
|
||||||
|
function extruded(shape: THREE.Shape, depth: number): THREE.ExtrudeGeometry {
|
||||||
|
return new THREE.ExtrudeGeometry(shape, { depth, bevelEnabled: false, curveSegments: 12 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============== MARAMUREȘ — wooden church/house ============== */
|
||||||
function buildMaramures(): THREE.Group {
|
function buildMaramures(): THREE.Group {
|
||||||
const g = new THREE.Group();
|
const g = new THREE.Group();
|
||||||
const wood = makeWallMaterial('#5e3a1a');
|
const wood = toon('#7a4a23');
|
||||||
const darkWood = makeWallMaterial('#3a1c10');
|
const woodDark = toon('#3a1f0e');
|
||||||
const shingle = makeRoofMaterial('#2d1810');
|
const stone = toon('#c8b58a');
|
||||||
|
const shingle = toon('#2a160b');
|
||||||
|
|
||||||
// Stone foundation
|
// Stone foundation
|
||||||
const found = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.3, 1.6), makeWallMaterial('#b9a06a'));
|
const f = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.32, 1.8), stone);
|
||||||
found.position.y = 0.15;
|
f.position.y = 0.16;
|
||||||
g.add(found);
|
g.add(f);
|
||||||
|
|
||||||
// Log walls
|
// Log wall body (single mass with strip texture via boxes)
|
||||||
for (let i = 0; i < 8; i++) {
|
const body = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.6, 1.6), wood);
|
||||||
const log = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.15, 1.5), i % 2 ? wood : darkWood);
|
body.position.y = 1.12;
|
||||||
log.position.y = 0.35 + i * 0.16;
|
g.add(body);
|
||||||
g.add(log);
|
|
||||||
|
// Visible log stripes (subtle)
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const stripe = new THREE.Mesh(new THREE.BoxGeometry(2.42, 0.04, 1.62), woodDark);
|
||||||
|
stripe.position.y = 0.4 + i * 0.22;
|
||||||
|
g.add(stripe);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pridvor posts
|
// Pridvor (porch) — 4 thin posts in front + lintel
|
||||||
const postGeo = new THREE.CylinderGeometry(0.07, 0.07, 1.6, 8);
|
const post = new THREE.CylinderGeometry(0.07, 0.07, 1.55, 10);
|
||||||
for (const x of [-1.05, 1.05]) {
|
for (const x of [-1.1, -0.45, 0.45, 1.1]) {
|
||||||
const p = new THREE.Mesh(postGeo, darkWood);
|
const p = new THREE.Mesh(post, woodDark);
|
||||||
p.position.set(x, 1.05, 0.85);
|
p.position.set(x, 1.07, 0.9);
|
||||||
g.add(p);
|
g.add(p);
|
||||||
}
|
}
|
||||||
|
const lintel = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.08, 0.18), woodDark);
|
||||||
// Steep gable roof (very tall)
|
lintel.position.set(0, 1.85, 0.92);
|
||||||
const roofH = 3.2;
|
g.add(lintel);
|
||||||
const roofGeo = new THREE.ConeGeometry(1.85, roofH, 4, 1);
|
|
||||||
roofGeo.rotateY(Math.PI / 4);
|
|
||||||
const roof = new THREE.Mesh(roofGeo, shingle);
|
|
||||||
roof.position.y = 1.85 + roofH / 2;
|
|
||||||
roof.scale.set(1, 1, 0.62);
|
|
||||||
g.add(roof);
|
|
||||||
|
|
||||||
// Tiny tower on top
|
|
||||||
const tower = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.6, 8), shingle);
|
|
||||||
tower.position.y = 1.85 + roofH + 0.3;
|
|
||||||
g.add(tower);
|
|
||||||
|
|
||||||
// Door
|
// Door
|
||||||
const door = new THREE.Mesh(new THREE.BoxGeometry(0.55, 1.0, 0.05), darkWood);
|
const door = new THREE.Mesh(new THREE.BoxGeometry(0.55, 1.0, 0.05), woodDark);
|
||||||
door.position.set(0, 0.8, 0.78);
|
door.position.set(0, 0.82, 0.81);
|
||||||
g.add(door);
|
g.add(door);
|
||||||
|
|
||||||
// Carved sun on door lintel
|
// Steep gable roof — 4-sided pyramid stretched in Z
|
||||||
const sun = new THREE.Mesh(new THREE.RingGeometry(0.12, 0.18, 12), darkWood);
|
const roofH = 3.0;
|
||||||
sun.position.set(0, 1.5, 0.79);
|
const roof = new THREE.Mesh(new THREE.ConeGeometry(1.85, roofH, 4, 1), shingle);
|
||||||
g.add(sun);
|
roof.rotation.y = Math.PI / 4;
|
||||||
|
roof.scale.set(1, 1, 0.55);
|
||||||
|
roof.position.y = 1.92 + roofH / 2;
|
||||||
|
g.add(roof);
|
||||||
|
|
||||||
|
// Tower spire
|
||||||
|
const spire = new THREE.Mesh(new THREE.ConeGeometry(0.16, 0.85, 8), shingle);
|
||||||
|
spire.position.y = 1.92 + roofH + 0.42;
|
||||||
|
g.add(spire);
|
||||||
|
|
||||||
|
// Cross
|
||||||
|
const crossV = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.28, 0.04), woodDark);
|
||||||
|
crossV.position.y = 1.92 + roofH + 0.95;
|
||||||
|
g.add(crossV);
|
||||||
|
const crossH = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.04, 0.04), woodDark);
|
||||||
|
crossH.position.y = 1.92 + roofH + 0.95;
|
||||||
|
g.add(crossH);
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------- Cula olteană: tall stone tower house ----------- */
|
/* ============== CULA OLTEANĂ — stone tower-house ============== */
|
||||||
function buildCula(): THREE.Group {
|
function buildCula(): THREE.Group {
|
||||||
const g = new THREE.Group();
|
const g = new THREE.Group();
|
||||||
const stone = makeWallMaterial('#e2d8be');
|
const stone = toon('#e7d6b3');
|
||||||
const stoneDark = makeWallMaterial('#cfbf94');
|
const stoneDark = toon('#c8b380');
|
||||||
const roof = makeRoofMaterial('#7e5d36');
|
const woodDark = toon('#4a2818');
|
||||||
const woodDark = makeWallMaterial('#4a2818');
|
const roof = toon('#5a3220');
|
||||||
|
|
||||||
// Base stone block
|
// Lower (defensive) volume — narrow, fortified
|
||||||
const base = new THREE.Mesh(new THREE.BoxGeometry(2, 1.6, 1.8), stoneDark);
|
const base = new THREE.Mesh(new THREE.BoxGeometry(2.2, 1.8, 2.0), stoneDark);
|
||||||
base.position.y = 0.8;
|
base.position.y = 0.9;
|
||||||
g.add(base);
|
g.add(base);
|
||||||
|
|
||||||
// Upper floor (slightly inset)
|
// Upper volume slightly wider — corbeled out
|
||||||
const upper = new THREE.Mesh(new THREE.BoxGeometry(2.2, 1.2, 2), stone);
|
const upper = new THREE.Mesh(new THREE.BoxGeometry(2.5, 1.5, 2.2), stone);
|
||||||
upper.position.y = 2.2;
|
upper.position.y = 2.55;
|
||||||
g.add(upper);
|
g.add(upper);
|
||||||
|
|
||||||
// Top floor with loggia
|
// Loggia — actual arches via ExtrudeGeometry
|
||||||
const top = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.95, 2), stone);
|
const archShape = new THREE.Shape();
|
||||||
top.position.y = 3.3;
|
archShape.moveTo(-0.32, 0);
|
||||||
g.add(top);
|
archShape.lineTo(-0.32, 0.45);
|
||||||
|
archShape.absarc(0, 0.45, 0.32, Math.PI, 0, true);
|
||||||
|
archShape.lineTo(0.32, 0);
|
||||||
|
archShape.lineTo(-0.32, 0);
|
||||||
|
|
||||||
// Loggia arches (front)
|
for (let i = -1; i <= 1; i++) {
|
||||||
for (const x of [-0.7, 0, 0.7]) {
|
const arch = new THREE.Mesh(extruded(archShape, 0.18), new THREE.MeshToonMaterial({ color: '#1a0d05', gradientMap: toonGradient() }));
|
||||||
const arch = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.65, 0.3), woodDark);
|
arch.position.set(i * 0.78, 2.15, 1.04);
|
||||||
arch.position.set(x, 3.2, 0.95);
|
|
||||||
g.add(arch);
|
g.add(arch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loggia capitals + base trim
|
||||||
|
const trim = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.08, 2.22), stoneDark);
|
||||||
|
trim.position.y = 1.85;
|
||||||
|
g.add(trim);
|
||||||
|
|
||||||
// Hipped roof
|
// Hipped roof
|
||||||
const r = new THREE.Mesh(new THREE.ConeGeometry(1.7, 1, 4, 1), roof);
|
const r = new THREE.Mesh(new THREE.ConeGeometry(1.9, 0.95, 4, 1), roof);
|
||||||
r.rotateY(Math.PI / 4);
|
r.rotation.y = Math.PI / 4;
|
||||||
r.scale.set(1, 1, 0.92);
|
r.scale.set(1, 1, 0.94);
|
||||||
r.position.y = 4.3;
|
r.position.y = 3.78;
|
||||||
g.add(r);
|
g.add(r);
|
||||||
|
|
||||||
// Narrow defense windows on lower
|
// Narrow defense slits on lower wall
|
||||||
for (const y of [0.7, 1.3]) {
|
for (const y of [0.65, 1.25]) {
|
||||||
for (const x of [-0.6, 0, 0.6]) {
|
for (const x of [-0.65, 0, 0.65]) {
|
||||||
const w = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.25, 0.05), woodDark);
|
const slit = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.32, 0.04), woodDark);
|
||||||
w.position.set(x, y, 0.92);
|
slit.position.set(x, y, 1.01);
|
||||||
g.add(w);
|
g.add(slit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Door at upper level (defensive entry)
|
// Door (defensive, at upper level)
|
||||||
const door = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.85, 0.05), woodDark);
|
const door = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.85, 0.05), woodDark);
|
||||||
door.position.set(0, 2.05, 1.02);
|
door.position.set(0, 2.25, 1.13);
|
||||||
g.add(door);
|
g.add(door);
|
||||||
|
|
||||||
|
// Iron knocker dot
|
||||||
|
const knock = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 6), woodDark);
|
||||||
|
knock.position.set(0, 2.4, 1.16);
|
||||||
|
g.add(knock);
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------- Casa săsească: gabled long house perpendicular to street ----------- */
|
/* ============== SAȘEASCĂ — gabled long-house, perpendicular to street ============== */
|
||||||
function buildSasesc(): THREE.Group {
|
function buildSasesc(): THREE.Group {
|
||||||
const g = new THREE.Group();
|
const g = new THREE.Group();
|
||||||
const wall = makeWallMaterial('#f1ecdf');
|
const wall = toon('#f3ecdc');
|
||||||
const trim = makeWallMaterial('#d4703f');
|
const trim = toon('#c25c2c');
|
||||||
const roof = makeRoofMaterial('#8a4226');
|
const woodDark = toon('#4a2818');
|
||||||
const wood = makeWallMaterial('#5e2d1a');
|
const roof = toon('#7d361c');
|
||||||
const window = makeWindowMaterial();
|
const win = emissive('#fae8da');
|
||||||
|
|
||||||
// Long body
|
const W = 2; // width (across street)
|
||||||
const body = new THREE.Mesh(new THREE.BoxGeometry(2, 1.7, 4), wall);
|
const D = 4; // depth (perpendicular to street)
|
||||||
body.position.y = 0.85;
|
const H = 1.6; // wall height
|
||||||
|
const rise = 1.3; // gable rise
|
||||||
|
|
||||||
|
// Body
|
||||||
|
const body = new THREE.Mesh(new THREE.BoxGeometry(W, H, D), wall);
|
||||||
|
body.position.y = H / 2;
|
||||||
g.add(body);
|
g.add(body);
|
||||||
|
|
||||||
// Gable triangle (front)
|
// Gable triangles (front + back) — built as a shape to ensure clean form
|
||||||
const gable = new THREE.Shape();
|
const gShape = new THREE.Shape();
|
||||||
gable.moveTo(-1, 0);
|
gShape.moveTo(-W / 2, 0);
|
||||||
gable.lineTo(1, 0);
|
gShape.lineTo(W / 2, 0);
|
||||||
gable.lineTo(0, 1.3);
|
gShape.lineTo(0, rise);
|
||||||
gable.closePath();
|
gShape.closePath();
|
||||||
const gableGeo = new THREE.ExtrudeGeometry(gable, { depth: 0.05, bevelEnabled: false });
|
const gFront = new THREE.Mesh(extruded(gShape, 0.08), wall);
|
||||||
const gFront = new THREE.Mesh(gableGeo, wall);
|
gFront.position.set(0, H, D / 2 - 0.04);
|
||||||
gFront.position.set(0, 1.7, 2.0);
|
|
||||||
g.add(gFront);
|
g.add(gFront);
|
||||||
const gBack = gFront.clone();
|
const gBack = gFront.clone();
|
||||||
gBack.position.z = -2.05;
|
gBack.position.z = -D / 2 - 0.04;
|
||||||
g.add(gBack);
|
g.add(gBack);
|
||||||
|
|
||||||
// Pitched roof — two planes
|
// Roof — two extruded planes with proper proportions
|
||||||
const roofPlaneGeo = new THREE.PlaneGeometry(2.35, 4.2);
|
const slope = Math.sqrt((W / 2) ** 2 + rise ** 2);
|
||||||
const angle = Math.atan(1.3 / 1);
|
const roofShape = new THREE.Shape();
|
||||||
const left = new THREE.Mesh(roofPlaneGeo, roof);
|
roofShape.moveTo(0, 0);
|
||||||
left.rotation.x = -Math.PI / 2;
|
roofShape.lineTo(D + 0.4, 0);
|
||||||
left.rotation.y = -angle;
|
roofShape.lineTo(D + 0.4, 0.06);
|
||||||
left.position.set(-0.55, 2.32, 0);
|
roofShape.lineTo(0, 0.06);
|
||||||
g.add(left);
|
roofShape.closePath();
|
||||||
const right = left.clone();
|
const roofGeo = new THREE.ExtrudeGeometry(roofShape, { depth: slope + 0.1, bevelEnabled: false });
|
||||||
right.rotation.y = angle;
|
const angle = Math.atan(rise / (W / 2));
|
||||||
right.position.x = 0.55;
|
const roofL = new THREE.Mesh(roofGeo, roof);
|
||||||
g.add(right);
|
roofL.rotation.z = -Math.PI / 2;
|
||||||
|
roofL.rotation.x = Math.PI / 2 + (Math.PI / 2 - angle);
|
||||||
|
roofL.position.set(-W / 2, H, -D / 2 - 0.2);
|
||||||
|
g.add(roofL);
|
||||||
|
|
||||||
// Door on gable
|
const roofR = new THREE.Mesh(roofGeo, roof);
|
||||||
const door = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.06), wood);
|
roofR.rotation.z = -Math.PI / 2;
|
||||||
door.position.set(0, 0.5, 2.04);
|
roofR.rotation.x = Math.PI / 2 - (Math.PI / 2 - angle);
|
||||||
|
roofR.position.set(W / 2, H, -D / 2 - 0.2);
|
||||||
|
g.add(roofR);
|
||||||
|
|
||||||
|
// Front door + trim
|
||||||
|
const doorFrame = new THREE.Mesh(new THREE.BoxGeometry(0.62, 1.12, 0.08), trim);
|
||||||
|
doorFrame.position.set(0, 0.56, D / 2 + 0.02);
|
||||||
|
g.add(doorFrame);
|
||||||
|
const door = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.05), woodDark);
|
||||||
|
door.position.set(0, 0.5, D / 2 + 0.07);
|
||||||
g.add(door);
|
g.add(door);
|
||||||
|
|
||||||
// Windows symmetric on gable
|
// Symmetric windows on gable
|
||||||
for (const [x, y] of [[-0.5, 1.1], [0.5, 1.1], [0, 2.4]] as [number, number][]) {
|
for (const [x, y, w, h] of [[-0.55, 1.1, 0.42, 0.5], [0.55, 1.1, 0.42, 0.5], [0, 2.1, 0.36, 0.36]] as [number, number, number, number][]) {
|
||||||
const w = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.5, 0.06), window);
|
const frame = new THREE.Mesh(new THREE.BoxGeometry(w + 0.08, h + 0.08, 0.06), trim);
|
||||||
w.position.set(x, y, 2.04);
|
frame.position.set(x, y, D / 2 + 0.04);
|
||||||
g.add(w);
|
g.add(frame);
|
||||||
const tr = new THREE.Mesh(new THREE.BoxGeometry(0.46, 0.06, 0.07), trim);
|
const ww = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.05), win);
|
||||||
tr.position.set(x, y - 0.3, 2.05);
|
ww.position.set(x, y, D / 2 + 0.07);
|
||||||
g.add(tr);
|
g.add(ww);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Side windows
|
// Side windows along the long facades
|
||||||
for (let i = -1; i <= 1; i++) {
|
for (let i = -1; i <= 1; i++) {
|
||||||
const w = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.5, 0.4), window);
|
const ws = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.5, 0.4), win);
|
||||||
w.position.set(1.02, 1.1, i * 1.1);
|
ws.position.set(W / 2 + 0.03, 1.0, i * 1.1);
|
||||||
g.add(w);
|
g.add(ws);
|
||||||
const wL = w.clone();
|
const wsL = ws.clone();
|
||||||
wL.position.x = -1.02;
|
wsL.position.x = -(W / 2 + 0.03);
|
||||||
g.add(wL);
|
g.add(wsL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chimney
|
// Chimney
|
||||||
const chim = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.7, 0.3), trim);
|
const chim = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.85, 0.32), trim);
|
||||||
chim.position.set(0, 2.6, -1.2);
|
chim.position.set(0, H + rise * 0.6, -D / 4);
|
||||||
g.add(chim);
|
g.add(chim);
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------- Interbelic: cubist modernist villa with curved balcony ----------- */
|
/* ============== INTERBELIC — modernist villa with curved balcony ============== */
|
||||||
function buildInterbelic(): THREE.Group {
|
function buildInterbelic(): THREE.Group {
|
||||||
const g = new THREE.Group();
|
const g = new THREE.Group();
|
||||||
const wall = makeWallMaterial('#faf8f3');
|
const wall = toon('#f8f1e1');
|
||||||
const trim = makeWallMaterial('#cfbf94');
|
const accent = toon('#347697');
|
||||||
const accent = makeWallMaterial('#347697');
|
const trim = toon('#d3b78f');
|
||||||
const window = makeWindowMaterial();
|
const dark = toon('#15110c');
|
||||||
const dark = makeWallMaterial('#15110c');
|
const win = emissive('#cce4f0');
|
||||||
|
|
||||||
// Ground floor
|
// Ground floor
|
||||||
const f1 = new THREE.Mesh(new THREE.BoxGeometry(3, 1.4, 2.4), wall);
|
const f1 = new THREE.Mesh(new THREE.BoxGeometry(3.0, 1.4, 2.4), wall);
|
||||||
f1.position.y = 0.7;
|
f1.position.y = 0.7;
|
||||||
g.add(f1);
|
g.add(f1);
|
||||||
|
|
||||||
// First floor
|
// Cantilevered first floor — wider, projecting forward
|
||||||
const f2 = new THREE.Mesh(new THREE.BoxGeometry(3.4, 1.3, 2.4), wall);
|
const f2 = new THREE.Mesh(new THREE.BoxGeometry(3.4, 1.3, 2.6), wall);
|
||||||
f2.position.y = 2.05;
|
f2.position.set(0, 2.05, 0.1);
|
||||||
g.add(f2);
|
g.add(f2);
|
||||||
|
|
||||||
// Curved balcony (cylinder section)
|
// Curved balcony (proper half-cylinder, capped properly)
|
||||||
|
const balconyR = 0.85;
|
||||||
const balcony = new THREE.Mesh(
|
const balcony = new THREE.Mesh(
|
||||||
new THREE.CylinderGeometry(0.9, 0.9, 1.3, 24, 1, true, -Math.PI / 2, Math.PI),
|
new THREE.CylinderGeometry(balconyR, balconyR, 1.3, 28, 1, true, -Math.PI / 2, Math.PI),
|
||||||
wall
|
wall
|
||||||
);
|
);
|
||||||
balcony.position.set(0, 2.05, 1.4);
|
balcony.position.set(0, 2.05, 1.4 + balconyR * 0.55);
|
||||||
balcony.scale.set(1, 1, 0.55);
|
balcony.scale.set(1, 1, 0.55);
|
||||||
g.add(balcony);
|
g.add(balcony);
|
||||||
|
|
||||||
// Balcony rail
|
// Top of balcony (curved cap)
|
||||||
const rail = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.04, 8, 24, Math.PI), dark);
|
const balCap = new THREE.Mesh(
|
||||||
rail.rotation.x = Math.PI / 2;
|
new THREE.CylinderGeometry(balconyR + 0.05, balconyR + 0.05, 0.06, 28, 1, false, -Math.PI / 2, Math.PI),
|
||||||
rail.position.set(0, 2.7, 1.4);
|
trim
|
||||||
rail.scale.set(1, 0.55, 1);
|
);
|
||||||
g.add(rail);
|
balCap.position.set(0, 2.7, 1.4 + balconyR * 0.55);
|
||||||
|
balCap.scale.set(1, 1, 0.55);
|
||||||
|
g.add(balCap);
|
||||||
|
|
||||||
// Flat roof + parapet
|
// Flat roof + thin parapet
|
||||||
const roofTop = new THREE.Mesh(new THREE.BoxGeometry(3.5, 0.1, 2.5), trim);
|
const roofTop = new THREE.Mesh(new THREE.BoxGeometry(3.5, 0.1, 2.7), trim);
|
||||||
roofTop.position.y = 2.75;
|
roofTop.position.set(0, 2.75, 0.1);
|
||||||
g.add(roofTop);
|
g.add(roofTop);
|
||||||
|
|
||||||
// Hublou (porthole) accent
|
// Hublou (porthole)
|
||||||
const hublou = new THREE.Mesh(new THREE.RingGeometry(0.16, 0.22, 24), accent);
|
const hublou = new THREE.Mesh(new THREE.RingGeometry(0.16, 0.22, 24), accent);
|
||||||
hublou.position.set(-1.3, 1.9, 1.21);
|
hublou.position.set(-1.3, 1.95, 1.31);
|
||||||
g.add(hublou);
|
g.add(hublou);
|
||||||
|
const hubGlass = new THREE.Mesh(new THREE.CircleGeometry(0.16, 24), win);
|
||||||
|
hubGlass.position.set(-1.3, 1.95, 1.305);
|
||||||
|
g.add(hubGlass);
|
||||||
|
|
||||||
// Horizontal window bands
|
// Horizontal window band — ground floor
|
||||||
for (let i = 0; i < 4; i++) {
|
const longW1 = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.55, 0.04), win);
|
||||||
const w = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.55, 0.04), window);
|
longW1.position.set(-0.15, 0.85, 1.21);
|
||||||
w.position.set(-1.0 + i * 0.65, 0.85, 1.21);
|
g.add(longW1);
|
||||||
g.add(w);
|
const w1f = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.65, 0.05), trim);
|
||||||
}
|
w1f.position.set(-0.15, 0.85, 1.20);
|
||||||
// Upper band
|
g.add(w1f);
|
||||||
const longW = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.45, 0.04), window);
|
|
||||||
longW.position.set(0, 2.15, -1.21);
|
|
||||||
g.add(longW);
|
|
||||||
|
|
||||||
// Door
|
// Long window band — back of upper
|
||||||
|
const longW2 = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.5, 0.04), win);
|
||||||
|
longW2.position.set(0, 2.15, -1.21);
|
||||||
|
g.add(longW2);
|
||||||
|
|
||||||
|
// Door (offset right)
|
||||||
const door = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.1, 0.05), accent);
|
const door = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.1, 0.05), accent);
|
||||||
door.position.set(1.1, 0.55, 1.22);
|
door.position.set(1.15, 0.55, 1.22);
|
||||||
g.add(door);
|
g.add(door);
|
||||||
|
const handle = new THREE.Mesh(new THREE.SphereGeometry(0.025, 8, 6), trim);
|
||||||
|
handle.position.set(1.32, 0.7, 1.25);
|
||||||
|
g.add(handle);
|
||||||
|
|
||||||
|
// Vertical accent stripe (signature deco detail)
|
||||||
|
const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.06, 1.3, 0.04), accent);
|
||||||
|
stripe.position.set(0.85, 1.4, 1.205);
|
||||||
|
g.add(stripe);
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------- Comunist: panel block ----------- */
|
/* ============== COMUNIST — panel block ============== */
|
||||||
function buildComunist(): THREE.Group {
|
function buildComunist(): THREE.Group {
|
||||||
const g = new THREE.Group();
|
const g = new THREE.Group();
|
||||||
const wall = makeWallMaterial('#e2d8be');
|
const wall = toon('#e3d6b6');
|
||||||
const panelLine = makeWallMaterial('#a17a4b');
|
const wallAccent = toon('#cdb985');
|
||||||
const balcony = makeWallMaterial('#b35831');
|
const panelLine = toon('#8a6f44');
|
||||||
const window = makeWindowMaterial();
|
const balcony = toon('#a14622');
|
||||||
|
const win = emissive('#f3ecdc');
|
||||||
|
|
||||||
const floors = 9;
|
const floors = 9;
|
||||||
const cols = 5;
|
const cols = 5;
|
||||||
const W = 4;
|
const W = 4;
|
||||||
const D = 1.3;
|
const D = 1.4;
|
||||||
const H = floors * 0.55;
|
const FH = 0.55;
|
||||||
|
const H = floors * FH;
|
||||||
|
|
||||||
// Main mass
|
// Main mass
|
||||||
const block = new THREE.Mesh(new THREE.BoxGeometry(W, H, D), wall);
|
const block = new THREE.Mesh(new THREE.BoxGeometry(W, H, D), wall);
|
||||||
block.position.y = H / 2;
|
block.position.y = H / 2;
|
||||||
g.add(block);
|
g.add(block);
|
||||||
|
|
||||||
|
// Vertical risers (stairwell mass) — slightly recessed darker bands
|
||||||
|
for (const x of [-W / 2 + 0.5, W / 2 - 0.5]) {
|
||||||
|
const riser = new THREE.Mesh(new THREE.BoxGeometry(0.45, H, 0.05), wallAccent);
|
||||||
|
riser.position.set(x, H / 2, D / 2 + 0.025);
|
||||||
|
g.add(riser);
|
||||||
|
const riser2 = riser.clone();
|
||||||
|
riser2.position.z = -D / 2 - 0.025;
|
||||||
|
g.add(riser2);
|
||||||
|
}
|
||||||
|
|
||||||
// Window grid + balconies
|
// Window grid + balconies
|
||||||
|
const winGeo = new THREE.BoxGeometry(W / cols - 0.22, FH * 0.55, 0.04);
|
||||||
|
const balGeo = new THREE.BoxGeometry(W / cols - 0.08, 0.1, 0.28);
|
||||||
|
const balRail = new THREE.BoxGeometry(W / cols - 0.08, 0.18, 0.03);
|
||||||
for (let f = 0; f < floors; f++) {
|
for (let f = 0; f < floors; f++) {
|
||||||
for (let c = 0; c < cols; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const x = -W / 2 + (c + 0.5) * (W / cols);
|
const x = -W / 2 + (c + 0.5) * (W / cols);
|
||||||
const y = 0.18 + f * 0.55;
|
const y = 0.18 + f * FH;
|
||||||
const win = new THREE.Mesh(new THREE.BoxGeometry(W / cols - 0.18, 0.32, 0.04), window);
|
const wnd = new THREE.Mesh(winGeo, win);
|
||||||
win.position.set(x, y, D / 2 + 0.01);
|
wnd.position.set(x, y, D / 2 + 0.03);
|
||||||
g.add(win);
|
g.add(wnd);
|
||||||
const winB = win.clone();
|
const wndB = wnd.clone();
|
||||||
winB.position.z = -D / 2 - 0.01;
|
wndB.position.z = -D / 2 - 0.03;
|
||||||
g.add(winB);
|
g.add(wndB);
|
||||||
// Every other column has tiny balcony
|
// Balcony every other column from floor 1 up
|
||||||
if (c % 2 === 1 && f > 0) {
|
if (c % 2 === 1 && f > 0) {
|
||||||
const bal = new THREE.Mesh(new THREE.BoxGeometry(W / cols - 0.05, 0.12, 0.25), balcony);
|
const bal = new THREE.Mesh(balGeo, balcony);
|
||||||
bal.position.set(x, y - 0.22, D / 2 + 0.13);
|
bal.position.set(x, y - 0.18, D / 2 + 0.16);
|
||||||
g.add(bal);
|
g.add(bal);
|
||||||
|
const rail = new THREE.Mesh(balRail, panelLine);
|
||||||
|
rail.position.set(x, y - 0.06, D / 2 + 0.30);
|
||||||
|
g.add(rail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Horizontal panel line
|
// Horizontal panel joint line
|
||||||
const line = new THREE.Mesh(new THREE.BoxGeometry(W + 0.04, 0.04, D + 0.04), panelLine);
|
if (f > 0) {
|
||||||
line.position.y = (f + 1) * 0.55 - 0.275;
|
const line = new THREE.Mesh(new THREE.BoxGeometry(W + 0.04, 0.025, D + 0.04), panelLine);
|
||||||
|
line.position.y = f * FH;
|
||||||
g.add(line);
|
g.add(line);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Flat roof + small water tank
|
// Roof tank + antenna
|
||||||
const tank = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.4, 0.6), panelLine);
|
const tank = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 0.7), panelLine);
|
||||||
tank.position.set(W / 4, H + 0.2, 0);
|
tank.position.set(W / 4, H + 0.2, 0);
|
||||||
g.add(tank);
|
g.add(tank);
|
||||||
|
const antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.9, 8), panelLine);
|
||||||
|
antenna.position.set(-W / 4, H + 0.45, 0);
|
||||||
|
g.add(antenna);
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------- Contemporary: low-pitch wood + glass ----------- */
|
/* ============== CONTEMPORAN — wood + glass ============== */
|
||||||
function buildContemporan(): THREE.Group {
|
function buildContemporan(): THREE.Group {
|
||||||
const g = new THREE.Group();
|
const g = new THREE.Group();
|
||||||
const wall = makeWallMaterial('#f1ecdf');
|
const wall = toon('#f3ecdc');
|
||||||
const wood = makeWallMaterial('#7e5d36');
|
const wood = toon('#6d4f2b');
|
||||||
const woodLight = makeWallMaterial('#bf9b6f');
|
const woodLight = toon('#b58f5d');
|
||||||
// Cheap glass — avoid MeshPhysicalMaterial (transmission is very expensive on weak GPUs)
|
const concrete = toon('#a8a29a');
|
||||||
const glass = new THREE.MeshStandardMaterial({
|
const roof = toon('#2e160d');
|
||||||
color: '#9ec5d9',
|
// Cheap "glass" — emissive blue, no transmission
|
||||||
transparent: true,
|
const glass = new THREE.MeshBasicMaterial({ color: '#9ec5d9', transparent: true, opacity: 0.6 });
|
||||||
opacity: 0.55,
|
|
||||||
roughness: 0.15,
|
|
||||||
metalness: 0.4,
|
|
||||||
});
|
|
||||||
const roof = makeRoofMaterial('#3a1c10');
|
|
||||||
|
|
||||||
// Concrete base / plinth
|
// Concrete plinth
|
||||||
const plinth = new THREE.Mesh(new THREE.BoxGeometry(4, 0.25, 2.4), makeWallMaterial('#cfbf94'));
|
const plinth = new THREE.Mesh(new THREE.BoxGeometry(4.0, 0.25, 2.4), concrete);
|
||||||
plinth.position.y = 0.125;
|
plinth.position.y = 0.125;
|
||||||
g.add(plinth);
|
g.add(plinth);
|
||||||
|
|
||||||
// Volume A (white plaster, larger)
|
// Volume A — main, white, with low-pitch roof
|
||||||
const volA = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.6, 2.2), wall);
|
const volA = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.6, 2.2), wall);
|
||||||
volA.position.set(-0.6, 1.05, 0);
|
volA.position.set(-0.6, 1.05, 0);
|
||||||
g.add(volA);
|
g.add(volA);
|
||||||
|
|
||||||
// Volume B (wood-clad, smaller, offset)
|
// Volume B — wood-clad, smaller, offset slightly forward
|
||||||
const volB = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.4, 2.2), woodLight);
|
const volB = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.4, 2.2), woodLight);
|
||||||
volB.position.set(1.3, 0.95, 0);
|
volB.position.set(1.3, 0.95, 0);
|
||||||
g.add(volB);
|
g.add(volB);
|
||||||
|
|
||||||
// Wood slat pattern on volume B (front face)
|
// Wood slat pattern on volume B (front face, sized exactly to volume)
|
||||||
for (let i = -7; i <= 7; i++) {
|
const slatCount = 14;
|
||||||
const slat = new THREE.Mesh(new THREE.BoxGeometry(0.05, 1.35, 0.04), wood);
|
const slatW = 1.7 / slatCount;
|
||||||
slat.position.set(1.3 + i * 0.12, 0.95, 1.13);
|
for (let i = 0; i < slatCount; i++) {
|
||||||
|
const slat = new THREE.Mesh(new THREE.BoxGeometry(slatW * 0.6, 1.32, 0.03), wood);
|
||||||
|
slat.position.set(1.3 - 0.85 + (i + 0.5) * slatW, 0.95, 1.115);
|
||||||
g.add(slat);
|
g.add(slat);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Big glass wall (south facing on volume A)
|
// Big glass wall on volume A (south facing, exactly fits)
|
||||||
const glazing = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.4, 0.04), glass);
|
const glazing = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.4, 0.04), glass);
|
||||||
glazing.position.set(-0.6, 1.05, 1.12);
|
glazing.position.set(-0.6, 1.05, 1.105);
|
||||||
g.add(glazing);
|
g.add(glazing);
|
||||||
|
|
||||||
// Glass mullions
|
// Mullion grid
|
||||||
for (let i = -2; i <= 2; i++) {
|
for (let i = -2; i <= 2; i++) {
|
||||||
const m = new THREE.Mesh(new THREE.BoxGeometry(0.04, 1.4, 0.06), wood);
|
const m = new THREE.Mesh(new THREE.BoxGeometry(0.04, 1.4, 0.05), wood);
|
||||||
m.position.set(-0.6 + i * 0.6, 1.05, 1.13);
|
m.position.set(-0.6 + i * 0.6, 1.05, 1.13);
|
||||||
g.add(m);
|
g.add(m);
|
||||||
}
|
}
|
||||||
// Top + bottom mullion
|
const topM = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.05, 0.05), wood);
|
||||||
const topM = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.05, 0.06), wood);
|
|
||||||
topM.position.set(-0.6, 1.75, 1.13);
|
topM.position.set(-0.6, 1.75, 1.13);
|
||||||
g.add(topM);
|
g.add(topM);
|
||||||
const botM = topM.clone();
|
const botM = topM.clone();
|
||||||
botM.position.y = 0.35;
|
botM.position.y = 0.35;
|
||||||
g.add(botM);
|
g.add(botM);
|
||||||
|
|
||||||
// Low pitch roof on A (single slope)
|
// Low pitch roof on A — thin shape rising slightly
|
||||||
const roofGeo = new THREE.BoxGeometry(2.7, 0.08, 2.3);
|
const roofShape = new THREE.Shape();
|
||||||
const roofA = new THREE.Mesh(roofGeo, roof);
|
roofShape.moveTo(-1.35, 0);
|
||||||
roofA.position.set(-0.6, 1.92, 0);
|
roofShape.lineTo(1.35, 0.18);
|
||||||
roofA.rotation.x = -0.18;
|
roofShape.lineTo(1.35, 0.24);
|
||||||
|
roofShape.lineTo(-1.35, 0.06);
|
||||||
|
roofShape.closePath();
|
||||||
|
const roofA = new THREE.Mesh(extruded(roofShape, 2.3), roof);
|
||||||
|
roofA.rotation.y = Math.PI / 2;
|
||||||
|
roofA.position.set(-0.6, 1.85, -1.15);
|
||||||
g.add(roofA);
|
g.add(roofA);
|
||||||
|
|
||||||
// Flat roof on B with overhang
|
// Flat roof on B with overhang
|
||||||
@@ -380,11 +484,16 @@ function buildContemporan(): THREE.Group {
|
|||||||
roofB.position.set(1.3, 1.7, 0);
|
roofB.position.set(1.3, 1.7, 0);
|
||||||
g.add(roofB);
|
g.add(roofB);
|
||||||
|
|
||||||
// Solar panel rectangle on roof A
|
// Solar panel array on volume A roof
|
||||||
const solar = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.04, 0.9), new THREE.MeshStandardMaterial({ color: '#15110c', metalness: 0.4, roughness: 0.3 }));
|
for (let i = 0; i < 3; i++) {
|
||||||
solar.position.set(-0.6, 2.0, -0.4);
|
const panel = new THREE.Mesh(
|
||||||
solar.rotation.x = -0.18;
|
new THREE.BoxGeometry(0.45, 0.04, 0.7),
|
||||||
g.add(solar);
|
new THREE.MeshToonMaterial({ color: '#0d0805', gradientMap: toonGradient() })
|
||||||
|
);
|
||||||
|
panel.position.set(-1.05 + i * 0.5, 1.95 + i * 0.025, -0.3);
|
||||||
|
panel.rotation.x = -0.07;
|
||||||
|
g.add(panel);
|
||||||
|
}
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
@@ -407,79 +516,112 @@ export type SceneCtx = {
|
|||||||
scene: THREE.Scene;
|
scene: THREE.Scene;
|
||||||
camera: THREE.PerspectiveCamera;
|
camera: THREE.PerspectiveCamera;
|
||||||
house: THREE.Group;
|
house: THREE.Group;
|
||||||
destroy: () => void;
|
|
||||||
setSize: (w: number, h: number) => void;
|
setSize: (w: number, h: number) => void;
|
||||||
setActive: (active: boolean) => void;
|
setActive: (active: boolean) => void;
|
||||||
|
destroy: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createHouseScene(canvas: HTMLCanvasElement, id: HouseId, opts?: { background?: string; autoRotate?: boolean }): SceneCtx {
|
/* Per-house framing — camera positions tuned to each silhouette */
|
||||||
// Cap DPR aggressively — 1.5 is invisible visually, big perf win
|
const FRAMING: Record<HouseId, { pos: [number, number, number]; target: [number, number, number] }> = {
|
||||||
|
maramures: { pos: [6.5, 4.5, 7.5], target: [0, 2.2, 0] },
|
||||||
|
cula: { pos: [6.5, 4.0, 7.5], target: [0, 2.2, 0] },
|
||||||
|
sasesc: { pos: [7.5, 4.0, 7.0], target: [0, 1.4, 0] },
|
||||||
|
interbelic:{ pos: [6.8, 3.6, 7.0], target: [0, 1.5, 0] },
|
||||||
|
comunist: { pos: [8.0, 4.5, 7.5], target: [0, 2.5, 0] },
|
||||||
|
contemporan:{ pos: [7.0, 3.5, 6.5], target: [0, 1.2, 0] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createHouseScene(canvas: HTMLCanvasElement, id: HouseId, opts?: { autoRotate?: boolean }): SceneCtx {
|
||||||
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
||||||
const renderer = new THREE.WebGLRenderer({
|
const renderer = new THREE.WebGLRenderer({
|
||||||
canvas,
|
canvas,
|
||||||
antialias: dpr <= 1.25, // skip MSAA when DPR already > 1, FXAA-equivalent quality
|
antialias: dpr <= 1.25,
|
||||||
alpha: true,
|
alpha: true,
|
||||||
powerPreference: 'high-performance',
|
powerPreference: 'high-performance',
|
||||||
});
|
});
|
||||||
renderer.setPixelRatio(dpr);
|
renderer.setPixelRatio(dpr);
|
||||||
renderer.setClearColor(0x000000, 0);
|
renderer.setClearColor(0x000000, 0);
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
||||||
renderer.toneMappingExposure = 1.05;
|
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
if (opts?.background) scene.background = new THREE.Color(opts.background);
|
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 100);
|
const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 100);
|
||||||
camera.position.set(6, 4.2, 7);
|
const fr = FRAMING[id];
|
||||||
camera.lookAt(0, 1.6, 0);
|
camera.position.set(...fr.pos);
|
||||||
|
camera.lookAt(...fr.target);
|
||||||
|
|
||||||
// Lighting — minimal: hemi + 1 directional. Cuts uniform updates per frame.
|
// Lighting tuned for toon material — strong directional + soft hemi
|
||||||
const hemi = new THREE.HemisphereLight(0xfdf6f1, 0x3a1c10, 0.85);
|
const hemi = new THREE.HemisphereLight(0xfff5e8, 0x2e160d, 0.7);
|
||||||
scene.add(hemi);
|
scene.add(hemi);
|
||||||
const key = new THREE.DirectionalLight(0xffeacc, 1.2);
|
const key = new THREE.DirectionalLight(0xfff0d8, 1.6);
|
||||||
key.position.set(5, 8, 5);
|
key.position.set(6, 9, 5);
|
||||||
scene.add(key);
|
scene.add(key);
|
||||||
|
|
||||||
// Ground disc
|
// Ground disc — soft warm tone
|
||||||
const ground = new THREE.Mesh(
|
const ground = new THREE.Mesh(
|
||||||
new THREE.CircleGeometry(8, 48),
|
new THREE.CircleGeometry(8, 48),
|
||||||
new THREE.MeshStandardMaterial({ color: '#ede2cf', roughness: 1, metalness: 0 })
|
toon('#ebdec7')
|
||||||
);
|
);
|
||||||
ground.rotation.x = -Math.PI / 2;
|
ground.rotation.x = -Math.PI / 2;
|
||||||
ground.position.y = 0;
|
|
||||||
scene.add(ground);
|
scene.add(ground);
|
||||||
|
|
||||||
const house = builders[id]();
|
// Soft shadow puddle under house (cheap, no shadow map)
|
||||||
scene.add(house);
|
|
||||||
|
|
||||||
// Soft shadow circle under house
|
|
||||||
const shadow = new THREE.Mesh(
|
const shadow = new THREE.Mesh(
|
||||||
new THREE.CircleGeometry(2.2, 32),
|
new THREE.CircleGeometry(2.4, 32),
|
||||||
new THREE.MeshBasicMaterial({ color: 0x261f17, transparent: true, opacity: 0.18 })
|
new THREE.MeshBasicMaterial({ color: 0x1f150c, transparent: true, opacity: 0.18 })
|
||||||
);
|
);
|
||||||
shadow.rotation.x = -Math.PI / 2;
|
shadow.rotation.x = -Math.PI / 2;
|
||||||
shadow.position.y = 0.005;
|
shadow.position.y = 0.005;
|
||||||
scene.add(shadow);
|
scene.add(shadow);
|
||||||
|
|
||||||
|
const house = builders[id]();
|
||||||
|
scene.add(house);
|
||||||
|
|
||||||
|
// 2D→3D intro animation: start collapsed (scaleY tiny), grow on first activation
|
||||||
|
let introT = 0;
|
||||||
|
const INTRO_DUR = 1.1;
|
||||||
|
let introDone = false;
|
||||||
|
house.scale.y = 0.02;
|
||||||
|
house.position.y = 0;
|
||||||
|
|
||||||
let raf = 0;
|
let raf = 0;
|
||||||
let active = false;
|
let active = false;
|
||||||
let needsRender = true; // one initial render when first activated
|
let needsRender = true;
|
||||||
let rotationTime = 0; // accumulated time only while active
|
let rotationTime = 0;
|
||||||
let lastFrame = 0;
|
let lastFrame = 0;
|
||||||
let scrollAdd = 0;
|
let scrollAdd = 0;
|
||||||
|
let targetSpin = 0; // accumulator for hover speed-up
|
||||||
|
let spinBoost = 0;
|
||||||
|
|
||||||
|
function easeOut(x: number) { return 1 - Math.pow(1 - x, 4); }
|
||||||
|
|
||||||
function tick(now: number) {
|
function tick(now: number) {
|
||||||
if (!active) {
|
if (!active) { raf = 0; return; }
|
||||||
raf = 0;
|
const dt = lastFrame ? Math.min((now - lastFrame) / 1000, 0.08) : 0;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dt = lastFrame ? Math.min((now - lastFrame) / 1000, 0.1) : 0;
|
|
||||||
lastFrame = now;
|
lastFrame = now;
|
||||||
if (opts?.autoRotate !== false) {
|
|
||||||
rotationTime += dt;
|
// Intro reveal
|
||||||
house.rotation.y = rotationTime * 0.25 + scrollAdd;
|
if (!introDone) {
|
||||||
|
introT = Math.min(introT + dt, INTRO_DUR);
|
||||||
|
const p = easeOut(introT / INTRO_DUR);
|
||||||
|
house.scale.set(1, p, 1);
|
||||||
|
house.rotation.y = (1 - p) * Math.PI * 0.35 + scrollAdd;
|
||||||
|
needsRender = true;
|
||||||
|
if (introT >= INTRO_DUR) {
|
||||||
|
introDone = true;
|
||||||
|
house.scale.set(1, 1, 1);
|
||||||
|
}
|
||||||
|
} else if (opts?.autoRotate !== false) {
|
||||||
|
// Smooth spin with optional boost from hover
|
||||||
|
spinBoost += (targetSpin - spinBoost) * Math.min(dt * 4, 1);
|
||||||
|
const speed = 0.18 + spinBoost;
|
||||||
|
rotationTime += dt * speed;
|
||||||
|
house.rotation.y = rotationTime + scrollAdd;
|
||||||
|
// Subtle breathing
|
||||||
|
const breath = Math.sin(now * 0.0008) * 0.01;
|
||||||
|
house.position.y = breath;
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsRender) {
|
if (needsRender) {
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
needsRender = false;
|
needsRender = false;
|
||||||
@@ -501,9 +643,10 @@ export function createHouseScene(canvas: HTMLCanvasElement, id: HouseId, opts?:
|
|||||||
renderer.setSize(w, h, false);
|
renderer.setSize(w, h, false);
|
||||||
camera.aspect = w / h;
|
camera.aspect = w / h;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
needsRender = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pointer interaction — drag to rotate
|
// Pointer interaction — drag to rotate; hover speeds up
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
let lastX = 0;
|
let lastX = 0;
|
||||||
const onDown = (e: PointerEvent) => {
|
const onDown = (e: PointerEvent) => {
|
||||||
@@ -515,17 +658,21 @@ export function createHouseScene(canvas: HTMLCanvasElement, id: HouseId, opts?:
|
|||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
const dx = e.clientX - lastX;
|
const dx = e.clientX - lastX;
|
||||||
lastX = e.clientX;
|
lastX = e.clientX;
|
||||||
scrollAdd += dx * 0.01;
|
scrollAdd += dx * 0.012;
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
};
|
};
|
||||||
const onUp = (e: PointerEvent) => {
|
const onUp = (e: PointerEvent) => {
|
||||||
dragging = false;
|
dragging = false;
|
||||||
try { canvas.releasePointerCapture(e.pointerId); } catch {}
|
try { canvas.releasePointerCapture(e.pointerId); } catch {}
|
||||||
};
|
};
|
||||||
|
const onEnter = () => { targetSpin = 0.6; };
|
||||||
|
const onLeave = () => { targetSpin = 0; dragging = false; };
|
||||||
canvas.addEventListener('pointerdown', onDown);
|
canvas.addEventListener('pointerdown', onDown);
|
||||||
canvas.addEventListener('pointermove', onMove);
|
canvas.addEventListener('pointermove', onMove);
|
||||||
canvas.addEventListener('pointerup', onUp);
|
canvas.addEventListener('pointerup', onUp);
|
||||||
canvas.addEventListener('pointercancel', onUp);
|
canvas.addEventListener('pointercancel', onUp);
|
||||||
|
canvas.addEventListener('pointerenter', onEnter);
|
||||||
|
canvas.addEventListener('pointerleave', onLeave);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderer,
|
renderer,
|
||||||
@@ -541,6 +688,8 @@ export function createHouseScene(canvas: HTMLCanvasElement, id: HouseId, opts?:
|
|||||||
canvas.removeEventListener('pointermove', onMove);
|
canvas.removeEventListener('pointermove', onMove);
|
||||||
canvas.removeEventListener('pointerup', onUp);
|
canvas.removeEventListener('pointerup', onUp);
|
||||||
canvas.removeEventListener('pointercancel', onUp);
|
canvas.removeEventListener('pointercancel', onUp);
|
||||||
|
canvas.removeEventListener('pointerenter', onEnter);
|
||||||
|
canvas.removeEventListener('pointerleave', onLeave);
|
||||||
renderer.dispose();
|
renderer.dispose();
|
||||||
scene.traverse((o) => {
|
scene.traverse((o) => {
|
||||||
const m = o as THREE.Mesh;
|
const m = o as THREE.Mesh;
|
||||||
|
|||||||
Reference in New Issue
Block a user