467 lines
16 KiB
C#
467 lines
16 KiB
C#
//The implementation is based on this article:http://rbarraza.com/html5-canvas-pageflip/
|
|
//As the rbarraza.com website is not live anymore you can get an archived version from web archive
|
|
//or check an archived version that I uploaded on my website: https://dandarawy.com/html5-canvas-pageflip/
|
|
|
|
using UnityEngine;
|
|
using System.Collections;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.Events;
|
|
public enum FlipMode
|
|
{
|
|
RightToLeft,
|
|
LeftToRight
|
|
}
|
|
|
|
public class Book : MonoBehaviour
|
|
{
|
|
public Canvas canvas;
|
|
[SerializeField]
|
|
RectTransform BookPanel;
|
|
public Sprite background;
|
|
|
|
public Sprite bgLeft;
|
|
public Sprite bgRight;
|
|
|
|
public Sprite[] bookPages;
|
|
public bool interactable = true;
|
|
public bool enableShadowEffect = true;
|
|
//represent the index of the sprite shown in the right page
|
|
public int currentPage = 0;
|
|
public int TotalPageCount
|
|
{
|
|
get { return bookPages.Length; }
|
|
}
|
|
public Vector3 EndBottomLeft
|
|
{
|
|
get { return ebl; }
|
|
}
|
|
public Vector3 EndBottomRight
|
|
{
|
|
get { return ebr; }
|
|
}
|
|
public float Height
|
|
{
|
|
get
|
|
{
|
|
return BookPanel.rect.height;
|
|
}
|
|
}
|
|
public Image ClippingPlane;
|
|
public Image NextPageClip;
|
|
public Image Shadow;
|
|
public Image ShadowLTR;
|
|
public Image Left;
|
|
public Image LeftNext;
|
|
public Image Right;
|
|
public Image RightNext;
|
|
public UnityEvent OnFlip;
|
|
float radius1, radius2;
|
|
//Spine Bottom
|
|
Vector3 sb;
|
|
//Spine Top
|
|
Vector3 st;
|
|
//corner of the page
|
|
Vector3 c;
|
|
//Edge Bottom Right
|
|
Vector3 ebr;
|
|
//Edge Bottom Left
|
|
Vector3 ebl;
|
|
//follow point
|
|
Vector3 f;
|
|
bool pageDragging = false;
|
|
//current flip mode
|
|
FlipMode mode;
|
|
|
|
void Start()
|
|
{
|
|
if (!canvas) canvas = GetComponentInParent<Canvas>();
|
|
if (!canvas) Debug.LogError("Book should be a child to canvas");
|
|
|
|
Left.gameObject.SetActive(false);
|
|
Right.gameObject.SetActive(false);
|
|
UpdateSprites();
|
|
CalcCurlCriticalPoints();
|
|
|
|
float pageWidth = BookPanel.rect.width / 2.0f;
|
|
float pageHeight = BookPanel.rect.height;
|
|
NextPageClip.rectTransform.sizeDelta = new Vector2(pageWidth, pageHeight + pageHeight * 2);
|
|
|
|
|
|
ClippingPlane.rectTransform.sizeDelta = new Vector2(pageWidth * 2 + pageHeight, pageHeight + pageHeight * 2);
|
|
|
|
//hypotenous (diagonal) page length
|
|
float hyp = Mathf.Sqrt(pageWidth * pageWidth + pageHeight * pageHeight);
|
|
float shadowPageHeight = pageWidth / 2 + hyp;
|
|
|
|
Shadow.rectTransform.sizeDelta = new Vector2(pageWidth, shadowPageHeight);
|
|
Shadow.rectTransform.pivot = new Vector2(1, (pageWidth / 2) / shadowPageHeight);
|
|
|
|
ShadowLTR.rectTransform.sizeDelta = new Vector2(pageWidth, shadowPageHeight);
|
|
ShadowLTR.rectTransform.pivot = new Vector2(0, (pageWidth / 2) / shadowPageHeight);
|
|
|
|
}
|
|
|
|
private void CalcCurlCriticalPoints()
|
|
{
|
|
sb = new Vector3(0, -BookPanel.rect.height / 2);
|
|
ebr = new Vector3(BookPanel.rect.width / 2, -BookPanel.rect.height / 2);
|
|
ebl = new Vector3(-BookPanel.rect.width / 2, -BookPanel.rect.height / 2);
|
|
st = new Vector3(0, BookPanel.rect.height / 2);
|
|
radius1 = Vector2.Distance(sb, ebr);
|
|
float pageWidth = BookPanel.rect.width / 2.0f;
|
|
float pageHeight = BookPanel.rect.height;
|
|
radius2 = Mathf.Sqrt(pageWidth * pageWidth + pageHeight * pageHeight);
|
|
}
|
|
|
|
public Vector3 transformPoint(Vector3 mouseScreenPos)
|
|
{
|
|
if (canvas.renderMode == RenderMode.ScreenSpaceCamera)
|
|
{
|
|
Vector3 mouseWorldPos = canvas.worldCamera.ScreenToWorldPoint(new Vector3(mouseScreenPos.x, mouseScreenPos.y, canvas.planeDistance));
|
|
Vector2 localPos = BookPanel.InverseTransformPoint(mouseWorldPos);
|
|
|
|
return localPos;
|
|
}
|
|
else if (canvas.renderMode == RenderMode.WorldSpace)
|
|
{
|
|
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
|
Vector3 globalEBR = transform.TransformPoint(ebr);
|
|
Vector3 globalEBL = transform.TransformPoint(ebl);
|
|
Vector3 globalSt = transform.TransformPoint(st);
|
|
Plane p = new Plane(globalEBR, globalEBL, globalSt);
|
|
float distance;
|
|
p.Raycast(ray, out distance);
|
|
Vector2 localPos = BookPanel.InverseTransformPoint(ray.GetPoint(distance));
|
|
return localPos;
|
|
}
|
|
else
|
|
{
|
|
//Screen Space Overlay
|
|
Vector2 localPos = BookPanel.InverseTransformPoint(mouseScreenPos);
|
|
return localPos;
|
|
}
|
|
}
|
|
void Update()
|
|
{
|
|
if (pageDragging && interactable)
|
|
{
|
|
UpdateBook();
|
|
}
|
|
}
|
|
public void UpdateBook()
|
|
{
|
|
f = Vector3.Lerp(f, transformPoint(Input.mousePosition), Time.deltaTime * 10);
|
|
if (mode == FlipMode.RightToLeft)
|
|
UpdateBookRTLToPoint(f);
|
|
else
|
|
UpdateBookLTRToPoint(f);
|
|
}
|
|
public void UpdateBookLTRToPoint(Vector3 followLocation)
|
|
{
|
|
mode = FlipMode.LeftToRight;
|
|
f = followLocation;
|
|
ShadowLTR.transform.SetParent(ClippingPlane.transform, true);
|
|
ShadowLTR.transform.localPosition = new Vector3(0, 0, 0);
|
|
ShadowLTR.transform.localEulerAngles = new Vector3(0, 0, 0);
|
|
Left.transform.SetParent(ClippingPlane.transform, true);
|
|
|
|
Right.transform.SetParent(BookPanel.transform, true);
|
|
Right.transform.localEulerAngles = Vector3.zero;
|
|
LeftNext.transform.SetParent(BookPanel.transform, true);
|
|
|
|
c = Calc_C_Position(followLocation);
|
|
Vector3 t1;
|
|
float clipAngle = CalcClipAngle(c, ebl, out t1);
|
|
//0 < T0_T1_Angle < 180
|
|
clipAngle = (clipAngle + 180) % 180;
|
|
|
|
ClippingPlane.transform.localEulerAngles = new Vector3(0, 0, clipAngle - 90);
|
|
ClippingPlane.transform.position = BookPanel.TransformPoint(t1);
|
|
|
|
//page position and angle
|
|
Left.transform.position = BookPanel.TransformPoint(c);
|
|
float C_T1_dy = t1.y - c.y;
|
|
float C_T1_dx = t1.x - c.x;
|
|
float C_T1_Angle = Mathf.Atan2(C_T1_dy, C_T1_dx) * Mathf.Rad2Deg;
|
|
Left.transform.localEulerAngles = new Vector3(0, 0, C_T1_Angle - 90 - clipAngle);
|
|
|
|
NextPageClip.transform.localEulerAngles = new Vector3(0, 0, clipAngle - 90);
|
|
NextPageClip.transform.position = BookPanel.TransformPoint(t1);
|
|
LeftNext.transform.SetParent(NextPageClip.transform, true);
|
|
Right.transform.SetParent(ClippingPlane.transform, true);
|
|
Right.transform.SetAsFirstSibling();
|
|
|
|
ShadowLTR.rectTransform.SetParent(Left.rectTransform, true);
|
|
}
|
|
public void UpdateBookRTLToPoint(Vector3 followLocation)
|
|
{
|
|
mode = FlipMode.RightToLeft;
|
|
f = followLocation;
|
|
Shadow.transform.SetParent(ClippingPlane.transform, true);
|
|
Shadow.transform.localPosition = Vector3.zero;
|
|
Shadow.transform.localEulerAngles = Vector3.zero;
|
|
Right.transform.SetParent(ClippingPlane.transform, true);
|
|
|
|
Left.transform.SetParent(BookPanel.transform, true);
|
|
Left.transform.localEulerAngles = Vector3.zero;
|
|
RightNext.transform.SetParent(BookPanel.transform, true);
|
|
c = Calc_C_Position(followLocation);
|
|
Vector3 t1;
|
|
float clipAngle = CalcClipAngle(c, ebr, out t1);
|
|
if (clipAngle > -90) clipAngle += 180;
|
|
|
|
ClippingPlane.rectTransform.pivot = new Vector2(1, 0.35f);
|
|
ClippingPlane.transform.localEulerAngles = new Vector3(0, 0, clipAngle + 90);
|
|
ClippingPlane.transform.position = BookPanel.TransformPoint(t1);
|
|
|
|
//page position and angle
|
|
Right.transform.position = BookPanel.TransformPoint(c);
|
|
float C_T1_dy = t1.y - c.y;
|
|
float C_T1_dx = t1.x - c.x;
|
|
float C_T1_Angle = Mathf.Atan2(C_T1_dy, C_T1_dx) * Mathf.Rad2Deg;
|
|
Right.transform.localEulerAngles = new Vector3(0, 0, C_T1_Angle - (clipAngle + 90));
|
|
|
|
NextPageClip.transform.localEulerAngles = new Vector3(0, 0, clipAngle + 90);
|
|
NextPageClip.transform.position = BookPanel.TransformPoint(t1);
|
|
RightNext.transform.SetParent(NextPageClip.transform, true);
|
|
Left.transform.SetParent(ClippingPlane.transform, true);
|
|
Left.transform.SetAsFirstSibling();
|
|
|
|
Shadow.rectTransform.SetParent(Right.rectTransform, true);
|
|
}
|
|
private float CalcClipAngle(Vector3 c, Vector3 bookCorner, out Vector3 t1)
|
|
{
|
|
Vector3 t0 = (c + bookCorner) / 2;
|
|
float T0_CORNER_dy = bookCorner.y - t0.y;
|
|
float T0_CORNER_dx = bookCorner.x - t0.x;
|
|
float T0_CORNER_Angle = Mathf.Atan2(T0_CORNER_dy, T0_CORNER_dx);
|
|
float T0_T1_Angle = 90 - T0_CORNER_Angle;
|
|
|
|
float T1_X = t0.x - T0_CORNER_dy * Mathf.Tan(T0_CORNER_Angle);
|
|
T1_X = normalizeT1X(T1_X, bookCorner, sb);
|
|
t1 = new Vector3(T1_X, sb.y, 0);
|
|
|
|
//clipping plane angle=T0_T1_Angle
|
|
float T0_T1_dy = t1.y - t0.y;
|
|
float T0_T1_dx = t1.x - t0.x;
|
|
T0_T1_Angle = Mathf.Atan2(T0_T1_dy, T0_T1_dx) * Mathf.Rad2Deg;
|
|
return T0_T1_Angle;
|
|
}
|
|
private float normalizeT1X(float t1, Vector3 corner, Vector3 sb)
|
|
{
|
|
if (t1 > sb.x && sb.x > corner.x)
|
|
return sb.x;
|
|
if (t1 < sb.x && sb.x < corner.x)
|
|
return sb.x;
|
|
return t1;
|
|
}
|
|
private Vector3 Calc_C_Position(Vector3 followLocation)
|
|
{
|
|
Vector3 c;
|
|
f = followLocation;
|
|
float F_SB_dy = f.y - sb.y;
|
|
float F_SB_dx = f.x - sb.x;
|
|
float F_SB_Angle = Mathf.Atan2(F_SB_dy, F_SB_dx);
|
|
Vector3 r1 = new Vector3(radius1 * Mathf.Cos(F_SB_Angle), radius1 * Mathf.Sin(F_SB_Angle), 0) + sb;
|
|
|
|
float F_SB_distance = Vector2.Distance(f, sb);
|
|
if (F_SB_distance < radius1)
|
|
c = f;
|
|
else
|
|
c = r1;
|
|
float F_ST_dy = c.y - st.y;
|
|
float F_ST_dx = c.x - st.x;
|
|
float F_ST_Angle = Mathf.Atan2(F_ST_dy, F_ST_dx);
|
|
Vector3 r2 = new Vector3(radius2 * Mathf.Cos(F_ST_Angle),
|
|
radius2 * Mathf.Sin(F_ST_Angle), 0) + st;
|
|
float C_ST_distance = Vector2.Distance(c, st);
|
|
if (C_ST_distance > radius2)
|
|
c = r2;
|
|
return c;
|
|
}
|
|
public void DragRightPageToPoint(Vector3 point)
|
|
{
|
|
if (currentPage >= bookPages.Length) return;
|
|
pageDragging = true;
|
|
mode = FlipMode.RightToLeft;
|
|
f = point;
|
|
|
|
|
|
NextPageClip.rectTransform.pivot = new Vector2(0, 0.12f);
|
|
ClippingPlane.rectTransform.pivot = new Vector2(1, 0.35f);
|
|
|
|
Left.gameObject.SetActive(true);
|
|
Left.rectTransform.pivot = new Vector2(0, 0);
|
|
Left.transform.position = RightNext.transform.position;
|
|
Left.transform.eulerAngles = new Vector3(0, 0, 0);
|
|
Left.sprite = (currentPage < bookPages.Length) ? bookPages[currentPage] : bgLeft;
|
|
Left.transform.SetAsFirstSibling();
|
|
|
|
Right.gameObject.SetActive(true);
|
|
Right.transform.position = RightNext.transform.position;
|
|
Right.transform.eulerAngles = new Vector3(0, 0, 0);
|
|
Right.sprite = (currentPage < bookPages.Length - 1) ? bookPages[currentPage + 1] : bgRight;
|
|
|
|
RightNext.sprite = (currentPage < bookPages.Length - 2) ? bookPages[currentPage + 2] : bgRight;
|
|
|
|
LeftNext.transform.SetAsFirstSibling();
|
|
if (enableShadowEffect) Shadow.gameObject.SetActive(true);
|
|
UpdateBookRTLToPoint(f);
|
|
}
|
|
public void DragLeftPageToPoint(Vector3 point)
|
|
{
|
|
if (currentPage <= 0) return;
|
|
pageDragging = true;
|
|
mode = FlipMode.LeftToRight;
|
|
f = point;
|
|
|
|
NextPageClip.rectTransform.pivot = new Vector2(1, 0.12f);
|
|
ClippingPlane.rectTransform.pivot = new Vector2(0, 0.35f);
|
|
|
|
Right.gameObject.SetActive(true);
|
|
Right.transform.position = LeftNext.transform.position;
|
|
if (currentPage - 1 < bookPages.Length)
|
|
{
|
|
Right.sprite = bookPages[currentPage - 1];
|
|
}
|
|
|
|
Right.transform.eulerAngles = new Vector3(0, 0, 0);
|
|
Right.transform.SetAsFirstSibling();
|
|
|
|
Left.gameObject.SetActive(true);
|
|
Left.rectTransform.pivot = new Vector2(1, 0);
|
|
Left.transform.position = LeftNext.transform.position;
|
|
Left.transform.eulerAngles = new Vector3(0, 0, 0);
|
|
Left.sprite = (currentPage >= 2) ? bookPages[currentPage - 2] : bgLeft;
|
|
|
|
LeftNext.sprite = (currentPage >= 3) ? bookPages[currentPage - 3] : bgLeft;
|
|
|
|
RightNext.transform.SetAsFirstSibling();
|
|
if (enableShadowEffect) ShadowLTR.gameObject.SetActive(true);
|
|
UpdateBookLTRToPoint(f);
|
|
}
|
|
public void OnMouseDragRightPage()
|
|
{
|
|
if (interactable)
|
|
{
|
|
ZX8Controller.Instance.LoadNext2PageImage(currentPage + 2);
|
|
|
|
DragRightPageToPoint(transformPoint(Input.mousePosition));
|
|
}
|
|
}
|
|
public void OnMouseDragLeftPage()
|
|
{
|
|
if (interactable)
|
|
{
|
|
DragLeftPageToPoint(transformPoint(Input.mousePosition));
|
|
}
|
|
|
|
}
|
|
public void OnMouseRelease()
|
|
{
|
|
if (interactable)
|
|
ReleasePage();
|
|
}
|
|
public void ReleasePage()
|
|
{
|
|
if (pageDragging)
|
|
{
|
|
pageDragging = false;
|
|
float distanceToLeft = Vector2.Distance(c, ebl);
|
|
float distanceToRight = Vector2.Distance(c, ebr);
|
|
if (distanceToRight < distanceToLeft && mode == FlipMode.RightToLeft)
|
|
TweenBack();
|
|
else if (distanceToRight > distanceToLeft && mode == FlipMode.LeftToRight)
|
|
TweenBack();
|
|
else
|
|
TweenForward();
|
|
}
|
|
}
|
|
Coroutine currentCoroutine;
|
|
void UpdateSprites()
|
|
{
|
|
LeftNext.sprite = (currentPage > 0 && currentPage <= bookPages.Length) ? bookPages[currentPage - 1] : bgLeft;
|
|
RightNext.sprite = (currentPage >= 0 && currentPage < bookPages.Length) ? bookPages[currentPage] : bgRight;
|
|
}
|
|
public void TweenForward()
|
|
{
|
|
if (mode == FlipMode.RightToLeft)
|
|
{
|
|
currentCoroutine = StartCoroutine(TweenTo(ebl, 0.15f, () => { Flip(); }));
|
|
}
|
|
else
|
|
{
|
|
currentCoroutine = StartCoroutine(TweenTo(ebr, 0.15f, () => { Flip(); }));
|
|
}
|
|
}
|
|
void Flip()
|
|
{
|
|
if (mode == FlipMode.RightToLeft)
|
|
currentPage += 2;
|
|
else
|
|
currentPage -= 2;
|
|
LeftNext.transform.SetParent(BookPanel.transform, true);
|
|
Left.transform.SetParent(BookPanel.transform, true);
|
|
LeftNext.transform.SetParent(BookPanel.transform, true);
|
|
Left.gameObject.SetActive(false);
|
|
Right.gameObject.SetActive(false);
|
|
Right.transform.SetParent(BookPanel.transform, true);
|
|
RightNext.transform.SetParent(BookPanel.transform, true);
|
|
UpdateSprites();
|
|
Shadow.gameObject.SetActive(false);
|
|
ShadowLTR.gameObject.SetActive(false);
|
|
if (OnFlip != null)
|
|
OnFlip.Invoke();
|
|
}
|
|
public void TweenBack()
|
|
{
|
|
if (mode == FlipMode.RightToLeft)
|
|
{
|
|
currentCoroutine = StartCoroutine(TweenTo(ebr, 0.15f,
|
|
() =>
|
|
{
|
|
UpdateSprites();
|
|
RightNext.transform.SetParent(BookPanel.transform);
|
|
Right.transform.SetParent(BookPanel.transform);
|
|
|
|
Left.gameObject.SetActive(false);
|
|
Right.gameObject.SetActive(false);
|
|
pageDragging = false;
|
|
}
|
|
));
|
|
}
|
|
else
|
|
{
|
|
currentCoroutine = StartCoroutine(TweenTo(ebl, 0.15f,
|
|
() =>
|
|
{
|
|
UpdateSprites();
|
|
|
|
LeftNext.transform.SetParent(BookPanel.transform);
|
|
Left.transform.SetParent(BookPanel.transform);
|
|
|
|
Left.gameObject.SetActive(false);
|
|
Right.gameObject.SetActive(false);
|
|
pageDragging = false;
|
|
}
|
|
));
|
|
}
|
|
}
|
|
public IEnumerator TweenTo(Vector3 to, float duration, System.Action onFinish)
|
|
{
|
|
int steps = (int)(duration / 0.025f);
|
|
Vector3 displacement = (to - f) / steps;
|
|
for (int i = 0; i < steps - 1; i++)
|
|
{
|
|
if (mode == FlipMode.RightToLeft)
|
|
UpdateBookRTLToPoint(f + displacement);
|
|
else
|
|
UpdateBookLTRToPoint(f + displacement);
|
|
|
|
yield return new WaitForSeconds(0.025f);
|
|
}
|
|
if (onFinish != null)
|
|
onFinish();
|
|
}
|
|
}
|