Pages curve issue while spine is rotating

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