Hello everyone,
I’ve created a 3D book using Three.js, where the pages are attached to the spine. However, when turning a page, the spine also rotates, causing an alignment issue. As the page turns, the left-side pages shift downward toward the ground, while the right-side pages move upward, disrupting their alignment with the cover pages.
I want the spine’s rotation—up until the halfway point of the page turn—to influence only the page’s curvature rather than its movement. How can I achieve this?
My code:
const Page = ({
number,
front,
back,
page,
opened,
bookClosed,
totalPages,
...props
}) => {
const [clickedPage, setPage] = useAtom(pageAtom)
const isCover = number === 0 || number === pages.length - 1
const [picture, picture2] = useTexture([
`/textures/${front}.jpg`,
`/textures/${back}.jpg`,
])
picture.colorSpace = SRGBColorSpace
picture2.colorSpace = SRGBColorSpace
const group = useRef()
const turnedAt = useRef(0)
const lastOpened = useRef(opened)
const skinnedMeshRef = useRef()
const [highlighted, setHighlighted] = useState(false)
const pageTurnCount = useRef(8) // Start threshold at 8
useCursor(highlighted)
// Materials for regular pages
const pageMaterials = useMemo(() => {
return [
new MeshStandardMaterial({ color: "white" }),
new MeshStandardMaterial({ color: "white" }),
new MeshStandardMaterial({ color: "white" }),
new MeshStandardMaterial({ color: "white" }),
]
}, [])
// Create the skinned mesh
const manualSkinnedMesh = useMemo(() => {
const bones = []
for (let i = 0; i <= PAGE_SEGMENTS; i++) {
const bone = new Bone()
bone.position.x = i === 0 ? 0 : SEGMENT_WIDTH
if (i > 0) bones[i - 1].add(bone)
bones.push(bone)
}
const skeleton = new Skeleton(bones)
const materials = [
...pageMaterials,
new MeshStandardMaterial({
color: whiteColor,
map: picture,
roughness: isCover ? 0.2 : 0.1,
emissive: emissiveColor,
emissiveIntensity: 0,
}),
new MeshStandardMaterial({
color: whiteColor,
map: picture2,
roughness: isCover ? 0.2 : 0.1,
emissive: emissiveColor,
emissiveIntensity: 0,
}),
]
const mesh = new SkinnedMesh(pageGeometry, materials)
mesh.castShadow = true
mesh.receiveShadow = true
mesh.frustumCulled = false
mesh.add(skeleton.bones[0])
mesh.bind(skeleton)
return mesh
}, [isCover, pageMaterials, picture, picture2])
// Calculate spine width for positioning
const SPINE_WIDTH = PAGE_DEPTH * (pages.length - 2)
// Page-turn logic
useFrame((_, delta) => {
if (!skinnedMeshRef.current || !skinnedMeshRef.current.skeleton || !group.current)
return
// Track open/close timing changes
if (lastOpened.current !== opened) {
turnedAt.current = +new Date()
}
lastOpened.current = opened
let turningTime = Math.min(400, new Date() - turnedAt.current) / 400
turningTime = Math.sin(turningTime * -Math.PI)
// Dynamic rotation calculation
const middleIndex = (totalPages - 1) / 2
let targetRotation = opened ? -Math.PI / 2 : -Math.PI / 2
// Adjust rotation based on page position
if (!bookClosed) {
if (clickedPage > number) {
targetRotation += degToRad(number * 0.8)
} else {
targetRotation += degToRad(number * 0.2)
}
}
const normalizedProgress = Math.max(0, Math.min(1, clickedPage / (totalPages - 1)));
if (bookClosed) {
// Flatten all pages
easing.dampAngle(group.current.rotation, "y", -Math.PI / 2, easingFactor, delta)
skinnedMeshRef.current.skeleton.bones.forEach((b) => {
easing.dampAngle(b.rotation, "y", 0, easingFactor, delta)
easing.dampAngle(b.rotation, "x", 0, easingFactor, delta)
})
} else {
if (clickedPage > number) {
const bones = skinnedMeshRef.current.skeleton.bones
const dynamicMultiplier =
0.159 + ((0.082 - 0.06) * (500 - pages.length)) / (500 - 20)
for (let i = 0; i < bones.length; i++) {
const insideCurveIntensity = i < 8 ? (1 - Math.sin(i * dynamicMultiplier)) : 0
const outsideCurveIntensity = i >= 8
? (1 - Math.cos(((i - 8) / (PAGE_SEGMENTS - 8)) * Math.PI * 0.4))
: 0
const turningIntensity =
Math.sin(i * Math.PI * (1 / bones.length)) * turningTime
let rotationAngle =
(insideCurveStrength * insideCurveIntensity * targetRotation -
outsideCurveStrength * outsideCurveIntensity * targetRotation +
turningCurveStrength * turningIntensity * targetRotation) * 3
let foldRotationAngle = degToRad(Math.sign(targetRotation) * 2)
if (bookClosed) {
rotationAngle = 0
foldRotationAngle = 0
}
easing.dampAngle(bones[i].rotation, "y", rotationAngle, easingFactor, delta)
// Folding effect
const foldIntensity =
i > 8
? Math.sin((i * Math.PI) / bones.length - 0.5) * turningTime
: 0
easing.dampAngle(
bones[i].rotation,
"x",
foldRotationAngle * foldIntensity,
easingFactorFold,
delta
)
}
} else {
if (clickedPage === 1) {
easing.dampAngle(group.current.rotation, "y", -Math.PI / 2, easingFactor, delta)
skinnedMeshRef.current.skeleton.bones.forEach((b) => {
easing.dampAngle(b.rotation, "y", 0, easingFactor, delta)
easing.dampAngle(b.rotation, "x", 0, easingFactor, delta)
})
}
else {
easing.dampAngle(group.current.rotation, "y", targetRotation, easingFactor, delta)
const bones = skinnedMeshRef.current.skeleton.bones
const dynamicMultiplier =0.159 + ((0.082 - 0.06) * (50 - pages.length)) / (50 - 20)
let count = 0;
for (let i = 0; i < bones.length; i++) {
const target = i === 0 ? group.current : bones[i]
const insideCurveIntensity = i < pageTurnCount.current ? (- Math.sin(i * normalizedProgress)) : 0;
const outsideCurveIntensity = -Math.PI*normalizedProgress;
const turningIntensity =
Math.sin(i * Math.PI * (1 / bones.length)) * turningTime
let rotationAngle =
(insideCurveStrength * insideCurveIntensity * targetRotation -
outsideCurveStrength * outsideCurveIntensity * targetRotation +
turningCurveStrength * turningIntensity * targetRotation)
let foldRotationAngle = degToRad(Math.sign(targetRotation) * 2)
if (bookClosed) {
if (i === 0) {
rotationAngle = targetRotation
foldRotationAngle = 0
} else {
rotationAngle = 0
foldRotationAngle = 0
}
}
easing.dampAngle(
bones[i].rotation,
"y",
rotationAngle,
easingFactor,
delta
)
const foldIntensity =
i > 8
? Math.sin(i * Math.PI * (1 / bones.length) - 0.5) * turningTime
: 0
easing.dampAngle(
target.rotation,
"x",
foldRotationAngle * foldIntensity,
easingFactorFold,
delta
)
}
}
}
}
})
return (
<group
{...props}
ref={group}
onPointerEnter={(e) => {
e.stopPropagation()
setHighlighted(true)
}}
onPointerLeave={(e) => {
e.stopPropagation()
setHighlighted(false)
}}
onClick={(e) => {
e.stopPropagation()
setPage(opened ? number : number + 1)
pageTurnCount.current = Math.min(pageTurnCount.current + 1, PAGE_SEGMENTS);
setHighlighted(false)
}}
>
<primitive
ref={skinnedMeshRef}
object={manualSkinnedMesh}
position={[0, 0, SPINE_WIDTH / 2 - number * PAGE_DEPTH]}
/>
</group>
)
}
I have attach the image while i turned till 5th page.[book’s look when page is on 5]
What i want:
Can someone help me to create the formula for insideCurveIntensity and outsideCurveIntensity in dynamic way so while turning page, curve changes in that way so it look like releastic book