Add annotated glasses 3D showcase
This commit is contained in:
@@ -6,6 +6,7 @@ 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.
|
||||
|
||||
241
components/GlassesModelSection.tsx
Normal file
241
components/GlassesModelSection.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
"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<HTMLCanvasElement | null>(null);
|
||||
const wrapRef = useRef<HTMLDivElement | null>(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.2, 7.2);
|
||||
|
||||
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();
|
||||
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);
|
||||
renderer.setPixelRatio(pixelRatio);
|
||||
renderer.setSize(width, height, false);
|
||||
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 (
|
||||
<section id="model" className="relative px-4 py-20 sm:px-6 lg:py-28">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="relative z-10 max-w-3xl">
|
||||
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-optical/75">{t.model.eyebrow}</p>
|
||||
<h2 className="text-4xl font-semibold tracking-[-0.055em] text-ink sm:text-6xl">{t.model.title}</h2>
|
||||
<p className="mt-5 max-w-2xl text-base leading-8 text-ink/62 sm:text-lg">{t.model.body}</p>
|
||||
</div>
|
||||
|
||||
<div ref={wrapRef} className="relative left-1/2 mt-8 min-h-[32rem] w-screen -translate-x-1/2 overflow-hidden lg:min-h-[38rem]" data-testid="glasses-3d-wrap">
|
||||
<canvas ref={canvasRef} className="absolute inset-0 h-full w-full touch-pan-y" aria-label={t.model.canvasLabel} data-testid="glasses-3d-canvas" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_45%,rgba(255,255,255,0.58),transparent_34%),radial-gradient(circle_at_50%_72%,rgba(49,95,143,0.15),transparent_28%)]" />
|
||||
|
||||
<Annotation className="left-4 top-8 sm:left-10 lg:left-16 lg:top-20" align="left" label={t.model.annotations[0].label} body={t.model.annotations[0].body} />
|
||||
<Annotation className="right-4 top-36 sm:right-10 lg:right-20 lg:top-24" align="right" label={t.model.annotations[1].label} body={t.model.annotations[1].body} />
|
||||
<Annotation className="bottom-8 left-6 sm:left-16 lg:bottom-16 lg:left-[42%]" align="left" label={t.model.annotations[2].label} body={t.model.annotations[2].body} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Annotation({ label, body, align, className }: { label: string; body: string; align: "left" | "right"; className: string }) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`pointer-events-none absolute z-10 max-w-[14rem] ${className} ${align === "right" ? "text-right" : "text-left"}`}
|
||||
initial={{ opacity: 0, y: 14 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.6 }}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
<div className={`mb-3 flex items-center gap-3 ${align === "right" ? "justify-end" : ""}`}>
|
||||
{align === "right" ? <span className="h-px w-16 bg-ink/18" /> : null}
|
||||
<span className="grid size-3 place-items-center rounded-full bg-optical shadow-[0_0_0_7px_rgba(49,95,143,0.12)]" />
|
||||
{align === "left" ? <span className="h-px w-16 bg-ink/18" /> : null}
|
||||
</div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-optical/80">{label}</p>
|
||||
<p className="mt-2 text-sm leading-6 text-ink/60">{body}</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function createGlassesModel() {
|
||||
const group = new THREE.Group();
|
||||
group.scale.setScalar(1.08);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -38,6 +39,7 @@ export default function SiteShell() {
|
||||
<Navbar locale={locale} onLocaleChange={changeLocale} t={t} whatsappUrl={whatsappUrl} />
|
||||
<main>
|
||||
<HeroSection t={t} whatsappUrl={whatsappUrl} />
|
||||
<GlassesModelSection t={t} />
|
||||
<AboutSection t={t} />
|
||||
<ServicesSection t={t} />
|
||||
<CollectionsSection t={t} />
|
||||
|
||||
@@ -29,6 +29,17 @@ export const ar = {
|
||||
body: "منذ حوالي 2004، يرافق نيو أوبتيك زبناء تمارة بطريقة بسيطة: نصيحة واضحة، نظارات أصلية، أثمنة منصفة وخدمة متابعة بعد الشراء.",
|
||||
cards: ["سمعة محلية قوية", "نظارات أصلية فقط", "مواكبة من اختيار الإطار حتى الضمان"]
|
||||
},
|
||||
model: {
|
||||
eyebrow: "معاينة ثلاثية الأبعاد",
|
||||
title: "شوف التفاصيل قبل الاختيار.",
|
||||
body: "معاينة تفاعلية كتوضح المهم: شكل الإطار، الراحة، العدسات والتعديل النهائي.",
|
||||
canvasLabel: "نموذج ثلاثي الأبعاد لنظارات مع ملاحظات",
|
||||
annotations: [
|
||||
{ label: "العدسات", body: "نصيحة في العدسات المناسبة للاستعمال اليومي." },
|
||||
{ label: "الإطار", body: "إطارات أصلية بجودة وتشطيب واضح." },
|
||||
{ label: "التعديل", body: "تعديل في المحل لراحة أفضل كل يوم." }
|
||||
]
|
||||
},
|
||||
services: {
|
||||
eyebrow: "الخدمات",
|
||||
title: "كل ما تحتاجه لنظاراتك في مكان واحد.",
|
||||
|
||||
@@ -29,6 +29,17 @@ 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.",
|
||||
|
||||
@@ -29,6 +29,17 @@ 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.",
|
||||
|
||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -12,12 +12,14 @@
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react-dom": "^19.2.0",
|
||||
"three": "^0.184.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",
|
||||
@@ -279,6 +281,13 @@
|
||||
"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",
|
||||
@@ -1259,6 +1268,13 @@
|
||||
"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",
|
||||
@@ -1321,6 +1337,35 @@
|
||||
"@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",
|
||||
@@ -3411,6 +3456,13 @@
|
||||
"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",
|
||||
@@ -4589,6 +4641,13 @@
|
||||
"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",
|
||||
@@ -5084,7 +5143,6 @@
|
||||
"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",
|
||||
@@ -6120,6 +6178,12 @@
|
||||
"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",
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react-dom": "^19.2.0",
|
||||
"three": "^0.184.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",
|
||||
|
||||
Reference in New Issue
Block a user