Context
I’m trying to port a React.js page flipping animation tutorial to Next.js 15 with TypeScript and Tailwind CSS. The project involves a 3D book model with animated page flips.
Current Issues
- Rectangle Frame Issue
-
The 3D model appears to be constrained within a rectangular frame, limiting its movement and presentation.
-
Expected: Model should have free movement without visible boundaries
-
Actual: Model is stuck within a rectangular frame
- Bone Deformation Problem
-
When attempting to modify a bone in the model:
- Only the peripheral/outline of the model changes the mesh itself doesn’t deform as expected
-
Expected: Entire mesh should deform smoothly with bone movement
-
Actual: Only the outline changes, leaving the internal mesh unaffected
- Animation Styling Issue
- CSS styles for the animation’s heading wrapper aren’t being applied correctly.
Code Structure
Main Components
layout.tsx
: Root layout with font configurations
Book.tsx
: Main book component handling page rendering
Experience.tsx
: 3D scene setup with lighting and controls
Page.tsx
: Individual page component with bone and mesh setup
Landing.tsx
: Canvas setup and UI integration
UI.tsx & HeadingWrapper.tsx
: Animation and heading components
Here is the code :
layout.tsx
:
<code>const poppins = Poppins({
weight: ["100", "300", "400", "500", "600", "700", "900"],
variable: "--font-poppins",
const geistSans = Geist({
variable: "--font-geist-sans",
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
export const metadata: Metadata = {
export default function RootLayout({
}: Readonly<{ children: React.ReactNode }>) {
<html lang="en" suppressHydrationWarning>
className={`flex justify-center items-center ${geistSans.variable} ${geistMono.variable} ${poppins.variable} antialiased main-bg`}
<code>const poppins = Poppins({
weight: ["100", "300", "400", "500", "600", "700", "900"],
subsets: ["latin"],
variable: "--font-poppins",
});
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "GPU ZONE"
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`flex justify-center items-center ${geistSans.variable} ${geistMono.variable} ${poppins.variable} antialiased main-bg`}
>
{children}
</body>
</html>
);
}
</code>
const poppins = Poppins({
weight: ["100", "300", "400", "500", "600", "700", "900"],
subsets: ["latin"],
variable: "--font-poppins",
});
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "GPU ZONE"
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`flex justify-center items-center ${geistSans.variable} ${geistMono.variable} ${poppins.variable} antialiased main-bg`}
>
{children}
</body>
</html>
);
}
page.tsx
<code>const Page = () => {
<code>const Page = () => {
return (
<Landing/>
)
}
export default Page
</code>
const Page = () => {
return (
<Landing/>
)
}
export default Page
/threeD/Book.tsx:
<code>const Book = ({...props}) => {
[...pages].map((item, index) => (
position={[index * 0.15, 0, 0]}
<code>const Book = ({...props}) => {
return (
<group {...props}>
{
[...pages].map((item, index) => (
index===0?
<Page
position={[index * 0.15, 0, 0]}
key={index}
number={index}
{...item}
/>:null
))
}
</group>
)
}
export default Book
</code>
const Book = ({...props}) => {
return (
<group {...props}>
{
[...pages].map((item, index) => (
index===0?
<Page
position={[index * 0.15, 0, 0]}
key={index}
number={index}
{...item}
/>:null
))
}
</group>
)
}
export default Book
Experience.tsx
<code>const Experience = () => {
<Environment preset="studio"></Environment>
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
<mesh position-y={-1.5} rotation-x={-Math.PI / 2} receiveShadow>
<planeGeometry args={[100, 100]} />
<shadowMaterial transparent opacity={0.2} />
export default Experience
<code>const Experience = () => {
return (
<>
<Book/>
<OrbitControls />
<Environment preset="studio"></Environment>
<directionalLight
position={[2, 5, 2]}
intensity={2.5}
castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
shadow-bias={-0.0001}
/>
<mesh position-y={-1.5} rotation-x={-Math.PI / 2} receiveShadow>
<planeGeometry args={[100, 100]} />
<shadowMaterial transparent opacity={0.2} />
</mesh>
</>
);
};
export default Experience
</code>
const Experience = () => {
return (
<>
<Book/>
<OrbitControls />
<Environment preset="studio"></Environment>
<directionalLight
position={[2, 5, 2]}
intensity={2.5}
castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
shadow-bias={-0.0001}
/>
<mesh position-y={-1.5} rotation-x={-Math.PI / 2} receiveShadow>
<planeGeometry args={[100, 100]} />
<shadowMaterial transparent opacity={0.2} />
</mesh>
</>
);
};
export default Experience
threeD/Page.tsx
<code>const PAGE_WIDTH=1.28
const SEGMENT_WIDTH= PAGE_WIDTH/PAGE_SEGMENTS
const pageGeomtry= new THREE.BoxGeometry(
pageGeomtry.translate(PAGE_WIDTH/2,0,0)
const position= pageGeomtry.attributes.position
const vertex= new THREE.Vector3()
const whiteColor= new THREE.Color('white')
new THREE.MeshStandardMaterial(
new THREE.MeshStandardMaterial(
new THREE.MeshStandardMaterial(
new THREE.MeshStandardMaterial(
new THREE.MeshStandardMaterial(
new THREE.MeshStandardMaterial(
for (let i=0;i<position.count ; i++){
vertex.fromBufferAttribute(position,1)
const skinIndex= Math.max(0,Math.floor(x/SEGMENT_WIDTH))
let skinWeight= (x%SEGMENT_WIDTH)/SEGMENT_WIDTH
skinIndexes.push(skinIndex,skinIndex+1,0,0)
skinWeights.push(1-skinWeight,skinWeight,0,0)
pageGeomtry.setAttribute(
new THREE.Uint16BufferAttribute(skinIndexes,4)
pageGeomtry.setAttribute(
new THREE.Float32BufferAttribute(skinWeights,4)
const Page = ({ number, front, back, ...props }: { number: number; front: string; back: string } & React.ComponentProps<'group'>) => {
const pageRef = useRef<THREE.Group>(null)
const skinnedMeshRef = useRef(null as any);
const manualSkinnedMesh= useMemo(
for (let i=0;i<=PAGE_SEGMENTS;i++){
let bone= new THREE.Bone()
bone.position.x=SEGMENT_WIDTH
const skeleton = new THREE.Skeleton(bones)
const materials= pageMaterials;
const mesh= new THREE.SkinnedMesh(pageGeomtry,materials)
mesh.add(skeleton.bones[0])
useHelper(skinnedMeshRef, THREE.SkeletonHelper)
<group ref={pageRef} {...props} >
<primitive object={manualSkinnedMesh} ref={skinnedMeshRef} />
<code>const PAGE_WIDTH=1.28
const PAGE_HEIGHT=1.71
const PAGE_DEPTH= 0.003
const PAGE_SEGMENTS=5
const SEGMENT_WIDTH= PAGE_WIDTH/PAGE_SEGMENTS
const pageGeomtry= new THREE.BoxGeometry(
PAGE_WIDTH,
PAGE_HEIGHT,
PAGE_DEPTH,
PAGE_SEGMENTS,
2
)
pageGeomtry.translate(PAGE_WIDTH/2,0,0)
const position= pageGeomtry.attributes.position
const vertex= new THREE.Vector3()
const skinIndexes = []
const skinWeights = []
const whiteColor= new THREE.Color('white')
const pageMaterials=[
new THREE.MeshStandardMaterial(
{color:whiteColor}
),
new THREE.MeshStandardMaterial(
{
color:"#111"
}
),
new THREE.MeshStandardMaterial(
{
color:whiteColor
}
),
new THREE.MeshStandardMaterial(
{
color:whiteColor
}
),
new THREE.MeshStandardMaterial(
{
color:"pink"
}
),
new THREE.MeshStandardMaterial(
{
color:'blue'
}
)
]
for (let i=0;i<position.count ; i++){
vertex.fromBufferAttribute(position,1)
const x= vertex.x
const skinIndex= Math.max(0,Math.floor(x/SEGMENT_WIDTH))
let skinWeight= (x%SEGMENT_WIDTH)/SEGMENT_WIDTH
skinIndexes.push(skinIndex,skinIndex+1,0,0)
skinWeights.push(1-skinWeight,skinWeight,0,0)
}
pageGeomtry.setAttribute(
"skinIndex",
new THREE.Uint16BufferAttribute(skinIndexes,4)
)
pageGeomtry.setAttribute(
"skinWeight",
new THREE.Float32BufferAttribute(skinWeights,4)
)
const Page = ({ number, front, back, ...props }: { number: number; front: string; back: string } & React.ComponentProps<'group'>) => {
const pageRef = useRef<THREE.Group>(null)
const skinnedMeshRef = useRef(null as any);
const manualSkinnedMesh= useMemo(
()=>{
const bones=[]
for (let i=0;i<=PAGE_SEGMENTS;i++){
let bone= new THREE.Bone()
bones.push(bone)
if(i===0){
bone.position.x=0
}else{
bone.position.x=SEGMENT_WIDTH
}
if(i>0){
bones[i-1].add(bone)
}
}
const skeleton = new THREE.Skeleton(bones)
const materials= pageMaterials;
const mesh= new THREE.SkinnedMesh(pageGeomtry,materials)
mesh.castShadow=true
mesh.receiveShadow=true
mesh.frustumCulled=false
mesh.add(skeleton.bones[0])
mesh.bind(skeleton)
return mesh
},[]
)
useHelper(skinnedMeshRef, THREE.SkeletonHelper)
return (
<group ref={pageRef} {...props} >
<mesh>
<primitive object={manualSkinnedMesh} ref={skinnedMeshRef} />
</mesh>
</group>
)
}
export default Page
</code>
const PAGE_WIDTH=1.28
const PAGE_HEIGHT=1.71
const PAGE_DEPTH= 0.003
const PAGE_SEGMENTS=5
const SEGMENT_WIDTH= PAGE_WIDTH/PAGE_SEGMENTS
const pageGeomtry= new THREE.BoxGeometry(
PAGE_WIDTH,
PAGE_HEIGHT,
PAGE_DEPTH,
PAGE_SEGMENTS,
2
)
pageGeomtry.translate(PAGE_WIDTH/2,0,0)
const position= pageGeomtry.attributes.position
const vertex= new THREE.Vector3()
const skinIndexes = []
const skinWeights = []
const whiteColor= new THREE.Color('white')
const pageMaterials=[
new THREE.MeshStandardMaterial(
{color:whiteColor}
),
new THREE.MeshStandardMaterial(
{
color:"#111"
}
),
new THREE.MeshStandardMaterial(
{
color:whiteColor
}
),
new THREE.MeshStandardMaterial(
{
color:whiteColor
}
),
new THREE.MeshStandardMaterial(
{
color:"pink"
}
),
new THREE.MeshStandardMaterial(
{
color:'blue'
}
)
]
for (let i=0;i<position.count ; i++){
vertex.fromBufferAttribute(position,1)
const x= vertex.x
const skinIndex= Math.max(0,Math.floor(x/SEGMENT_WIDTH))
let skinWeight= (x%SEGMENT_WIDTH)/SEGMENT_WIDTH
skinIndexes.push(skinIndex,skinIndex+1,0,0)
skinWeights.push(1-skinWeight,skinWeight,0,0)
}
pageGeomtry.setAttribute(
"skinIndex",
new THREE.Uint16BufferAttribute(skinIndexes,4)
)
pageGeomtry.setAttribute(
"skinWeight",
new THREE.Float32BufferAttribute(skinWeights,4)
)
const Page = ({ number, front, back, ...props }: { number: number; front: string; back: string } & React.ComponentProps<'group'>) => {
const pageRef = useRef<THREE.Group>(null)
const skinnedMeshRef = useRef(null as any);
const manualSkinnedMesh= useMemo(
()=>{
const bones=[]
for (let i=0;i<=PAGE_SEGMENTS;i++){
let bone= new THREE.Bone()
bones.push(bone)
if(i===0){
bone.position.x=0
}else{
bone.position.x=SEGMENT_WIDTH
}
if(i>0){
bones[i-1].add(bone)
}
}
const skeleton = new THREE.Skeleton(bones)
const materials= pageMaterials;
const mesh= new THREE.SkinnedMesh(pageGeomtry,materials)
mesh.castShadow=true
mesh.receiveShadow=true
mesh.frustumCulled=false
mesh.add(skeleton.bones[0])
mesh.bind(skeleton)
return mesh
},[]
)
useHelper(skinnedMeshRef, THREE.SkeletonHelper)
return (
<group ref={pageRef} {...props} >
<mesh>
<primitive object={manualSkinnedMesh} ref={skinnedMeshRef} />
</mesh>
</group>
)
}
export default Page
Landing.tsx
<Canvas shadows camera={{ position: [-0.5, 1, 4], fov: 45 }}>
<Suspense fallback={null}>
<code>"use client"
const Landing = () => {
return (
<>
<UI></UI>
<Loader/>
<Canvas shadows camera={{ position: [-0.5, 1, 4], fov: 45 }}>
<group position-y={0}>
<Suspense fallback={null}>
<Experience />
</Suspense>
</group>
</Canvas>
</>
)
}
export default Landing
</code>
"use client"
const Landing = () => {
return (
<>
<UI></UI>
<Loader/>
<Canvas shadows camera={{ position: [-0.5, 1, 4], fov: 45 }}>
<group position-y={0}>
<Suspense fallback={null}>
<Experience />
</Suspense>
</group>
</Canvas>
</>
)
}
export default Landing
Now moving on to the Heading issue :
UI.tsx:
<code>export const pageAtom = atom(0);
export const UI = () => {
const [page, setPage] = useAtom(pageAtom);
<main className=" pointer-events-none select-none z-10 fixed inset-0 flex justify-between flex-col">
className="pointer-events-auto mt-10 ml-10"
<div className="w-full overflow-auto pointer-events-auto flex justify-center">
<div className="overflow-auto flex items-center gap-4 max-w-full p-10">
{[...pages].map((_, index) => (
className={`border-transparent hover:border-white transition-all duration-300 px-4 py-3 rounded-full text-lg uppercase shrink-0 border ${
? "bg-white/90 text-black"
: "bg-black/30 text-white"
onClick={() => setPage(index)}
{index === 0 ? "Cover" : `Page ${index}`}
className={`border-transparent hover:border-white transition-all duration-300 px-4 py-3 rounded-full text-lg uppercase shrink-0 border ${
? "bg-white/90 text-black"
: "bg-black/30 text-white"
onClick={() => setPage(pages.length)}
<div className="fixed inset-0 flex items-center -rotate-2 select-none ">
<div className="relative">
<HeadingWrapper styling="bg-white/0 animate-horizontal-scroll flex items-center gap-8 w-max px-8"></HeadingWrapper>
<HeadingWrapper styling="absolute top-0 left-0 bg-white/0 animate-horizontal-scroll-2 flex items-center gap-8 px-8 w-max"></HeadingWrapper>
<code>export const pageAtom = atom(0);
export const UI = () => {
const [page, setPage] = useAtom(pageAtom);
return (
<>
<main className=" pointer-events-none select-none z-10 fixed inset-0 flex justify-between flex-col">
<a
className="pointer-events-auto mt-10 ml-10"
href=""
>
</a>
<div className="w-full overflow-auto pointer-events-auto flex justify-center">
<div className="overflow-auto flex items-center gap-4 max-w-full p-10">
{[...pages].map((_, index) => (
<button
key={index}
className={`border-transparent hover:border-white transition-all duration-300 px-4 py-3 rounded-full text-lg uppercase shrink-0 border ${
index === page
? "bg-white/90 text-black"
: "bg-black/30 text-white"
}`}
onClick={() => setPage(index)}
>
{index === 0 ? "Cover" : `Page ${index}`}
</button>
))}
<button
className={`border-transparent hover:border-white transition-all duration-300 px-4 py-3 rounded-full text-lg uppercase shrink-0 border ${
page === pages.length
? "bg-white/90 text-black"
: "bg-black/30 text-white"
}`}
onClick={() => setPage(pages.length)}
>
Back Cover
</button>
</div>
</div>
</main>
<div className="fixed inset-0 flex items-center -rotate-2 select-none ">
<div className="relative">
<HeadingWrapper styling="bg-white/0 animate-horizontal-scroll flex items-center gap-8 w-max px-8"></HeadingWrapper>
<HeadingWrapper styling="absolute top-0 left-0 bg-white/0 animate-horizontal-scroll-2 flex items-center gap-8 px-8 w-max"></HeadingWrapper>
</div>
</div>
</>
);
};
</code>
export const pageAtom = atom(0);
export const UI = () => {
const [page, setPage] = useAtom(pageAtom);
return (
<>
<main className=" pointer-events-none select-none z-10 fixed inset-0 flex justify-between flex-col">
<a
className="pointer-events-auto mt-10 ml-10"
href=""
>
</a>
<div className="w-full overflow-auto pointer-events-auto flex justify-center">
<div className="overflow-auto flex items-center gap-4 max-w-full p-10">
{[...pages].map((_, index) => (
<button
key={index}
className={`border-transparent hover:border-white transition-all duration-300 px-4 py-3 rounded-full text-lg uppercase shrink-0 border ${
index === page
? "bg-white/90 text-black"
: "bg-black/30 text-white"
}`}
onClick={() => setPage(index)}
>
{index === 0 ? "Cover" : `Page ${index}`}
</button>
))}
<button
className={`border-transparent hover:border-white transition-all duration-300 px-4 py-3 rounded-full text-lg uppercase shrink-0 border ${
page === pages.length
? "bg-white/90 text-black"
: "bg-black/30 text-white"
}`}
onClick={() => setPage(pages.length)}
>
Back Cover
</button>
</div>
</div>
</main>
<div className="fixed inset-0 flex items-center -rotate-2 select-none ">
<div className="relative">
<HeadingWrapper styling="bg-white/0 animate-horizontal-scroll flex items-center gap-8 w-max px-8"></HeadingWrapper>
<HeadingWrapper styling="absolute top-0 left-0 bg-white/0 animate-horizontal-scroll-2 flex items-center gap-8 px-8 w-max"></HeadingWrapper>
</div>
</div>
</>
);
};
HeadingWrapper.tsx
<code>type HeadingWrapperProps = {
const HeadingWrapper: React.FC<HeadingWrapperProps> = ({ styling }) => (
<div className={styling}>
{array_ts.map((item, index) =>
React.createElement(item.type, { key: index, className: item.styling }, item.text)
export default HeadingWrapper;
<code>type HeadingWrapperProps = {
styling: string;
};
const HeadingWrapper: React.FC<HeadingWrapperProps> = ({ styling }) => (
<div className={styling}>
{array_ts.map((item, index) =>
React.createElement(item.type, { key: index, className: item.styling }, item.text)
)}
</div>
);
export default HeadingWrapper;
</code>
type HeadingWrapperProps = {
styling: string;
};
const HeadingWrapper: React.FC<HeadingWrapperProps> = ({ styling }) => (
<div className={styling}>
{array_ts.map((item, index) =>
React.createElement(item.type, { key: index, className: item.styling }, item.text)
)}
</div>
);
export default HeadingWrapper;
HeadingWrapper.ts
<code>const headingsArray= [
{ type: "h1", styling: "shrink-0 text-white text-10xl font-black", text: "Welcome" },
{ type: "h2", styling: "shrink-0 text-white text-8xl italic font-light", text: "To" },
{ type: "h2", styling: "shrink-0 text-white text-12xl font-bold", text: "GPU" },
{ type: "h2", styling: "shrink-0 text-transparent text-12xl font-bold italic outline-text", text: "Zone" },
{ type: "h2", styling: "shrink-0 text-white text-9xl font-medium", text: "One Stop" },
{ type: "h2", styling: "shrink-0 text-white text-9xl font-extralight italic", text: "Solution" },
{ type: "h2", styling: "shrink-0 text-white text-13xl font-bold", text: "For All Your Queries" },
{ type: "h2", styling: "shrink-0 text-transparent text-13xl font-bold outline-text italic", text: "Regarding" },
{ type: "h1", styling: "shrink-0 text-transparent text-15xl font-black italic outline-text metallic-text", text: "GPU PROGRAMMING" }
export default headingsArray
<code>const headingsArray= [
{ type: "h1", styling: "shrink-0 text-white text-10xl font-black", text: "Welcome" },
{ type: "h2", styling: "shrink-0 text-white text-8xl italic font-light", text: "To" },
{ type: "h2", styling: "shrink-0 text-white text-12xl font-bold", text: "GPU" },
{ type: "h2", styling: "shrink-0 text-transparent text-12xl font-bold italic outline-text", text: "Zone" },
{ type: "h2", styling: "shrink-0 text-white text-9xl font-medium", text: "One Stop" },
{ type: "h2", styling: "shrink-0 text-white text-9xl font-extralight italic", text: "Solution" },
{ type: "h2", styling: "shrink-0 text-white text-13xl font-bold", text: "For All Your Queries" },
{ type: "h2", styling: "shrink-0 text-transparent text-13xl font-bold outline-text italic", text: "Regarding" },
{ type: "h1", styling: "shrink-0 text-transparent text-15xl font-black italic outline-text metallic-text", text: "GPU PROGRAMMING" }
];
export default headingsArray
</code>
const headingsArray= [
{ type: "h1", styling: "shrink-0 text-white text-10xl font-black", text: "Welcome" },
{ type: "h2", styling: "shrink-0 text-white text-8xl italic font-light", text: "To" },
{ type: "h2", styling: "shrink-0 text-white text-12xl font-bold", text: "GPU" },
{ type: "h2", styling: "shrink-0 text-transparent text-12xl font-bold italic outline-text", text: "Zone" },
{ type: "h2", styling: "shrink-0 text-white text-9xl font-medium", text: "One Stop" },
{ type: "h2", styling: "shrink-0 text-white text-9xl font-extralight italic", text: "Solution" },
{ type: "h2", styling: "shrink-0 text-white text-13xl font-bold", text: "For All Your Queries" },
{ type: "h2", styling: "shrink-0 text-transparent text-13xl font-bold outline-text italic", text: "Regarding" },
{ type: "h1", styling: "shrink-0 text-transparent text-15xl font-black italic outline-text metallic-text", text: "GPU PROGRAMMING" }
];
export default headingsArray
Summary Question
Could someone help identify why my Next.js 15 page-flipping animation is experiencing three simultaneous issues: the 3D model being confined to a rectangular frame, bone deformations only affecting the model’s outline instead of the entire mesh, and CSS animation styles not being properly applied to the heading wrapper? I’m particularly interested in understanding if these issues are related to the Three.js implementation in Next.js or if they stem from my TypeScript/Tailwind CSS integration.