diff --git a/CHANGELOG.md b/CHANGELOG.md index 2632c1a..0856f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ All notable changes to the New Optic website will be documented in this file. ### Changed -- Added a `3D` branch experiment with an interactive annotated glasses model section. - Removed the desktop liquid glass WebGL layer from navbar and CTA controls so text remains readable and glass surfaces do not show dark rendering artifacts while scrolling. - Replaced the mobile hamburger dropdown with a fast, solid menu animation without bounce or stretch effects. - Kept the language switcher visible in the mobile top navigation bar. diff --git a/components/GlassesModelSection.tsx b/components/GlassesModelSection.tsx deleted file mode 100644 index 4dafeb2..0000000 --- a/components/GlassesModelSection.tsx +++ /dev/null @@ -1,243 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import { useEffect, useRef } from "react"; -import * as THREE from "three"; -import type { Messages } from "@/messages"; - -export default function GlassesModelSection({ t }: { t: Messages }) { - const canvasRef = useRef(null); - const wrapRef = useRef(null); - const pointerRef = useRef({ x: 0, y: 0 }); - - useEffect(() => { - const canvas = canvasRef.current; - const wrap = wrapRef.current; - if (!canvas || !wrap) return; - - let frame = 0; - let disposed = false; - const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100); - camera.position.set(0, 0.15, 7.9); - - const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" }); - renderer.setClearColor(0x000000, 0); - renderer.outputColorSpace = THREE.SRGBColorSpace; - renderer.toneMapping = THREE.ACESFilmicToneMapping; - renderer.toneMappingExposure = 1.08; - - const group = createGlassesModel(); - group.position.y = -0.34; - scene.add(group); - scene.add(new THREE.HemisphereLight(0xffffff, 0xaeb9c6, 1.8)); - - const key = new THREE.DirectionalLight(0xffffff, 3.8); - key.position.set(3.4, 4.6, 5.2); - scene.add(key); - - const rim = new THREE.DirectionalLight(0x9ec7ff, 1.7); - rim.position.set(-4, 1.8, -2); - scene.add(rim); - - const resize = () => { - const { width, height } = wrap.getBoundingClientRect(); - const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.7); - const scale = width < 640 ? 0.5 : width < 1024 ? 0.68 : 0.9; - renderer.setPixelRatio(pixelRatio); - renderer.setSize(width, height, false); - group.scale.setScalar(scale); - group.position.y = width < 640 ? -0.44 : -0.34; - camera.aspect = width / Math.max(height, 1); - camera.updateProjectionMatrix(); - }; - - const observer = new ResizeObserver(resize); - observer.observe(wrap); - resize(); - - const handlePointerMove = (event: PointerEvent) => { - const bounds = wrap.getBoundingClientRect(); - pointerRef.current.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2; - pointerRef.current.y = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2; - }; - - const handlePointerLeave = () => { - pointerRef.current.x = 0; - pointerRef.current.y = 0; - }; - - wrap.addEventListener("pointermove", handlePointerMove); - wrap.addEventListener("pointerleave", handlePointerLeave); - - const animate = () => { - if (disposed) return; - const targetY = pointerRef.current.x * 0.28; - const targetX = pointerRef.current.y * 0.16; - group.rotation.y += (targetY + Math.sin(performance.now() * 0.00045) * 0.22 - group.rotation.y) * 0.055; - group.rotation.x += (-0.08 - targetX - group.rotation.x) * 0.055; - group.rotation.z = Math.sin(performance.now() * 0.00062) * 0.025; - - renderer.render(scene, camera); - frame = window.requestAnimationFrame(animate); - }; - - animate(); - - return () => { - disposed = true; - window.cancelAnimationFrame(frame); - observer.disconnect(); - wrap.removeEventListener("pointermove", handlePointerMove); - wrap.removeEventListener("pointerleave", handlePointerLeave); - scene.traverse((object) => { - if (!(object instanceof THREE.Mesh)) return; - object.geometry.dispose(); - const material = object.material; - if (Array.isArray(material)) material.forEach((item) => item.dispose()); - else material.dispose(); - }); - renderer.dispose(); - }; - }, []); - - return ( -
-
- -
- -
-

{t.model.eyebrow}

-

{t.model.title}

-

{t.model.body}

-
- - - - -
-
- ); -} - -function Annotation({ label, body, align, className }: { label: string; body: string; align: "left" | "right"; className: string }) { - return ( - -
- {align === "right" ? : null} - - {align === "left" ? : null} -
-

{label}

-

{body}

-
- ); -} - -function createGlassesModel() { - const group = new THREE.Group(); - group.scale.setScalar(0.9); - - const frameMaterial = new THREE.MeshPhysicalMaterial({ - color: 0x161a20, - roughness: 0.28, - metalness: 0.55, - clearcoat: 0.85, - clearcoatRoughness: 0.18 - }); - const lensMaterial = new THREE.MeshPhysicalMaterial({ - color: 0xbfd7e3, - roughness: 0.12, - metalness: 0, - transmission: 0.48, - transparent: true, - opacity: 0.28, - thickness: 0.12, - side: THREE.DoubleSide - }); - const padMaterial = new THREE.MeshPhysicalMaterial({ - color: 0xf8fbff, - roughness: 0.18, - transparent: true, - opacity: 0.52 - }); - const accentMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.42 }); - - const ringGeometry = new THREE.TorusGeometry(1.04, 0.065, 18, 112); - const lensGeometry = new THREE.CircleGeometry(0.93, 72); - const hingeGeometry = new THREE.BoxGeometry(0.22, 0.22, 0.22); - const padGeometry = new THREE.SphereGeometry(0.16, 24, 16); - - [-1.18, 1.18].forEach((xPosition) => { - const ring = new THREE.Mesh(ringGeometry, frameMaterial); - ring.position.x = xPosition; - ring.scale.y = 0.68; - group.add(ring); - - const lens = new THREE.Mesh(lensGeometry, lensMaterial); - lens.position.set(xPosition, 0, -0.03); - lens.scale.set(1, 0.66, 1); - group.add(lens); - - const highlight = new THREE.Mesh(new THREE.PlaneGeometry(0.58, 0.045), accentMaterial); - highlight.position.set(xPosition - 0.2, 0.34, 0.02); - highlight.rotation.z = -0.22; - group.add(highlight); - - const hinge = new THREE.Mesh(hingeGeometry, frameMaterial); - hinge.position.set(xPosition > 0 ? 2.18 : -2.18, 0.03, -0.02); - hinge.scale.set(0.55, 0.8, 0.62); - group.add(hinge); - }); - - const bridgeCurve = new THREE.CatmullRomCurve3([ - new THREE.Vector3(-0.3, 0.08, 0), - new THREE.Vector3(-0.12, 0.25, 0.04), - new THREE.Vector3(0, 0.3, 0.05), - new THREE.Vector3(0.12, 0.25, 0.04), - new THREE.Vector3(0.3, 0.08, 0) - ]); - group.add(new THREE.Mesh(new THREE.TubeGeometry(bridgeCurve, 38, 0.052, 14, false), frameMaterial)); - - [-0.28, 0.28].forEach((xPosition) => { - const pad = new THREE.Mesh(padGeometry, padMaterial); - pad.position.set(xPosition, -0.22, 0.14); - pad.scale.set(0.55, 0.9, 0.28); - pad.rotation.z = xPosition > 0 ? -0.22 : 0.22; - group.add(pad); - }); - - const leftTemple = new THREE.CatmullRomCurve3([ - new THREE.Vector3(-2.18, 0.03, -0.03), - new THREE.Vector3(-2.75, 0.03, -0.72), - new THREE.Vector3(-3.1, -0.04, -1.75), - new THREE.Vector3(-2.84, -0.28, -2.42) - ]); - const rightTemple = new THREE.CatmullRomCurve3([ - new THREE.Vector3(2.18, 0.03, -0.03), - new THREE.Vector3(2.75, 0.03, -0.72), - new THREE.Vector3(3.1, -0.04, -1.75), - new THREE.Vector3(2.84, -0.28, -2.42) - ]); - - group.add(new THREE.Mesh(new THREE.TubeGeometry(leftTemple, 54, 0.055, 14, false), frameMaterial)); - group.add(new THREE.Mesh(new THREE.TubeGeometry(rightTemple, 54, 0.055, 14, false), frameMaterial)); - - const shadow = new THREE.Mesh( - new THREE.CircleGeometry(2.6, 96), - new THREE.MeshBasicMaterial({ color: 0x315f8f, transparent: true, opacity: 0.08, depthWrite: false }) - ); - shadow.position.set(0, -1.06, -0.92); - shadow.scale.y = 0.18; - group.add(shadow); - - group.rotation.set(-0.08, -0.2, 0); - return group; -} diff --git a/components/SiteShell.tsx b/components/SiteShell.tsx index 589ef74..183c582 100644 --- a/components/SiteShell.tsx +++ b/components/SiteShell.tsx @@ -7,7 +7,6 @@ import AboutSection from "./AboutSection"; import ContactSection from "./ContactSection"; import CollectionsSection from "./CollectionsSection"; import Footer from "./Footer"; -import GlassesModelSection from "./GlassesModelSection"; import HeroSection from "./HeroSection"; import LiquidGlass from "./LiquidGlass"; import Navbar from "./Navbar"; @@ -39,7 +38,6 @@ export default function SiteShell() {
- diff --git a/messages/ar.ts b/messages/ar.ts index 57c5c51..34d129c 100644 --- a/messages/ar.ts +++ b/messages/ar.ts @@ -29,17 +29,6 @@ export const ar = { body: "منذ حوالي 2004، يرافق نيو أوبتيك زبناء تمارة بطريقة بسيطة: نصيحة واضحة، نظارات أصلية، أثمنة منصفة وخدمة متابعة بعد الشراء.", cards: ["سمعة محلية قوية", "نظارات أصلية فقط", "مواكبة من اختيار الإطار حتى الضمان"] }, - model: { - eyebrow: "معاينة ثلاثية الأبعاد", - title: "شوف التفاصيل قبل الاختيار.", - body: "معاينة تفاعلية كتوضح المهم: شكل الإطار، الراحة، العدسات والتعديل النهائي.", - canvasLabel: "نموذج ثلاثي الأبعاد لنظارات مع ملاحظات", - annotations: [ - { label: "العدسات", body: "نصيحة في العدسات المناسبة للاستعمال اليومي." }, - { label: "الإطار", body: "إطارات أصلية بجودة وتشطيب واضح." }, - { label: "التعديل", body: "تعديل في المحل لراحة أفضل كل يوم." } - ] - }, services: { eyebrow: "الخدمات", title: "كل ما تحتاجه لنظاراتك في مكان واحد.", diff --git a/messages/en.ts b/messages/en.ts index d74b04d..8c78388 100644 --- a/messages/en.ts +++ b/messages/en.ts @@ -29,17 +29,6 @@ export const en = { body: "Since around 2004, New Optic has supported customers in Temara with a simple promise: honest guidance, authentic frames, fair prices and reliable after-sales support.", cards: ["Strong local reputation", "Original frames only", "Guidance from frame to warranty"] }, - model: { - eyebrow: "3D inspection", - title: "See the details before choosing.", - body: "An interactive preview for the essentials: shape, comfort, lenses and the final fit.", - canvasLabel: "3D glasses model with annotations", - annotations: [ - { label: "Lenses", body: "Guidance on lenses suited to everyday use." }, - { label: "Frame", body: "Original frames with a clean, durable finish." }, - { label: "Fit", body: "In-store adjustment for daily comfort." } - ] - }, services: { eyebrow: "Services", title: "Everything essential for your eyewear, in one place.", diff --git a/messages/fr.ts b/messages/fr.ts index d274bcb..ba691e3 100644 --- a/messages/fr.ts +++ b/messages/fr.ts @@ -29,17 +29,6 @@ export const fr = { body: "Depuis 2004, New Optic accompagne les clients de Temara avec une approche claire: bien conseiller, proposer des montures authentiques, garder des prix justes et assurer le suivi apres l'achat.", cards: ["Reputation locale solide", "Montures originales uniquement", "Conseil de la monture a la garantie"] }, - model: { - eyebrow: "Inspection 3D", - title: "Regardez les details avant de choisir.", - body: "Un apercu interactif pour montrer ce qui compte vraiment: la forme, le confort, les verres et l'ajustement final.", - canvasLabel: "Modele 3D de lunettes avec annotations", - annotations: [ - { label: "Verres", body: "Conseil sur les verres adaptes a votre usage." }, - { label: "Monture", body: "Selection de montures originales et bien finies." }, - { label: "Ajustement", body: "Reglage en magasin pour le confort au quotidien." } - ] - }, services: { eyebrow: "Services", title: "Tout l'essentiel pour vos lunettes, au meme endroit.", diff --git a/package-lock.json b/package-lock.json index 43500dd..751ac8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,12 @@ "lucide-react": "^0.468.0", "next": "^16.0.0", "react": "^19.2.0", - "react-dom": "^19.2.0", - "three": "^0.184.0" + "react-dom": "^19.2.0" }, "devDependencies": { "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", - "@types/three": "^0.184.1", "autoprefixer": "^10.4.20", "eslint": "^9.17.0", "eslint-config-next": "^16.0.0", @@ -281,13 +279,6 @@ "node": ">=6.9.0" } }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1268,13 +1259,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "dev": true, - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1337,35 +1321,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "fflate": "~0.8.2", - "meshoptimizer": "~1.1.1" - } - }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", @@ -3456,13 +3411,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4641,13 +4589,6 @@ "node": ">= 8" } }, - "node_modules/meshoptimizer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", - "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "dev": true, - "license": "MIT" - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5143,6 +5084,7 @@ "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -6178,12 +6120,6 @@ "node": ">=0.8" } }, - "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", diff --git a/package.json b/package.json index fd24a69..e043fff 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,12 @@ "lucide-react": "^0.468.0", "next": "^16.0.0", "react": "^19.2.0", - "react-dom": "^19.2.0", - "three": "^0.184.0" + "react-dom": "^19.2.0" }, "devDependencies": { "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", - "@types/three": "^0.184.1", "autoprefixer": "^10.4.20", "eslint": "^9.17.0", "eslint-config-next": "^16.0.0",