@magflip/flipview
Version:
One of the core view plugins of MagFlip, displaying the book in a flipping format.
1,238 lines (1,218 loc) • 112 kB
JavaScript
class FlipActionLine {
constructor(leftX = 0, rightX = 0, y = 0) {
this.leftX = leftX;
this.rightX = rightX;
this.y = y;
this._leftP = { x: leftX, y: y };
this._rightP = { x: rightX, y: y };
this._centerP = { x: (leftX + rightX) / 2, y: y };
}
get leftP() { return this._leftP; }
get rightP() { return this._rightP; }
get centerP() { return this._centerP; }
}
class FlipData {
constructor(flipData) {
this.alpa = 0;
this.a = 0;
this.b = 0;
this.c = 0;
this.d = 0;
this.alpa = flipData.alpa;
this.a = flipData.a;
this.b = flipData.b;
this.c = flipData.c;
this.d = flipData.d;
this.page2 = flipData.page2;
this.mask = flipData.mask;
this.shadow = flipData.shadow;
}
printPage1MaskShape(zoomLevel) {
const pg = this.mask.page1;
return `${pg.p1.x / zoomLevel},${pg.p1.y / zoomLevel} ${pg.p2.x / zoomLevel},${pg.p2.y / zoomLevel} ${pg.p3.x / zoomLevel},${pg.p3.y / zoomLevel} ${pg.p4.x / zoomLevel},${pg.p4.y / zoomLevel}`;
}
printPage2MaskShape(zoomLevel) {
const pg = this.mask.page2;
return `${pg.p1.x / zoomLevel},${pg.p1.y / zoomLevel} ${pg.p2.x / zoomLevel},${pg.p2.y / zoomLevel} ${pg.p3.x / zoomLevel},${pg.p3.y / zoomLevel} ${pg.p4.x / zoomLevel},${pg.p4.y / zoomLevel}`;
}
printShadow6(bookSize) {
this.mask.page1;
return `0,0 ${bookSize.width},0 ${bookSize.width},${bookSize.height} 0,${bookSize.height}`;
}
}
class MZEvent {
constructor() {
this.listeners = {};
}
// 이벤트 리스너 등록
addEventListener(event, listener) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(listener);
}
// 이벤트 리스너 제거
removeEventListener(event, listener) {
if (!this.listeners[event])
return;
this.listeners[event] = this.listeners[event].filter(l => l !== listener);
}
// 이벤트 발생
emitEvent(event, ...args) {
if (!this.listeners[event])
return;
this.listeners[event].forEach(listener => listener(...args));
}
}
var PageType;
(function (PageType) {
PageType["Page"] = "Page";
PageType["Cover"] = "Cover";
PageType["Empty"] = "Empty";
PageType["Blank"] = "Blank";
})(PageType || (PageType = {}));
var PageLabelType;
(function (PageLabelType) {
PageLabelType["Default"] = "Default";
PageLabelType["Empty"] = "Empty";
})(PageLabelType || (PageLabelType = {}));
var DefaultSize;
(function (DefaultSize) {
DefaultSize[DefaultSize["bookWidth"] = 600] = "bookWidth";
DefaultSize[DefaultSize["bookHeight"] = 900] = "bookHeight";
DefaultSize[DefaultSize["pageWidth"] = 600] = "pageWidth";
DefaultSize[DefaultSize["pageHeight"] = 900] = "pageHeight";
})(DefaultSize || (DefaultSize = {}));
var BookType;
(function (BookType) {
BookType["Book"] = "Book";
BookType["Magazine"] = "Magazine";
BookType["Newspaper"] = "Newspaper";
})(BookType || (BookType = {}));
var BookStatus;
(function (BookStatus) {
BookStatus["Open"] = "Open";
BookStatus["Close"] = "Close";
})(BookStatus || (BookStatus = {}));
var EventStatus;
(function (EventStatus) {
EventStatus[EventStatus["None"] = 0] = "None";
EventStatus[EventStatus["AutoFlip"] = 8] = "AutoFlip";
EventStatus[EventStatus["AutoFlipFromCorner"] = 12] = "AutoFlipFromCorner";
EventStatus[EventStatus["AutoFlipToCorner"] = 10] = "AutoFlipToCorner";
EventStatus[EventStatus["Flipping"] = 128] = "Flipping";
EventStatus[EventStatus["SnappingBack"] = 144] = "SnappingBack";
EventStatus[EventStatus["FlippingForward"] = 160] = "FlippingForward";
EventStatus[EventStatus["FlippingBackward"] = 192] = "FlippingBackward";
EventStatus[EventStatus["Dragging"] = 2048] = "Dragging";
})(EventStatus || (EventStatus = {}));
var Zone;
(function (Zone) {
Zone[Zone["LT"] = 66] = "LT";
Zone[Zone["LC"] = 34] = "LC";
Zone[Zone["LB"] = 18] = "LB";
Zone[Zone["RT"] = 65] = "RT";
Zone[Zone["RC"] = 33] = "RC";
Zone[Zone["RB"] = 17] = "RB";
Zone[Zone["Left"] = 2] = "Left";
Zone[Zone["Right"] = 1] = "Right";
Zone[Zone["Top"] = 64] = "Top";
Zone[Zone["Center"] = 32] = "Center";
Zone[Zone["Bottom"] = 16] = "Bottom";
})(Zone || (Zone = {}));
var AutoFlipType;
(function (AutoFlipType) {
AutoFlipType[AutoFlipType["FixedWidth"] = 0] = "FixedWidth";
AutoFlipType[AutoFlipType["MouseCursor"] = 1] = "MouseCursor";
})(AutoFlipType || (AutoFlipType = {}));
var ViewerType;
(function (ViewerType) {
ViewerType["Flipping"] = "flipping";
ViewerType["Scrolling"] = "scrolling";
})(ViewerType || (ViewerType = {}));
class Point {
constructor(point) {
this.x = (point === null || point === void 0 ? void 0 : point.x) || 0;
this.y = (point === null || point === void 0 ? void 0 : point.y) || 0;
}
toString() { return `${this.x},${this.y}`; }
}
class Line {
constructor(p1, p2) {
this.p1 = p1;
this.p2 = p2;
}
}
class Rect {
constructor(rect) {
this.left = (rect === null || rect === void 0 ? void 0 : rect.left) || 0;
this.right = (rect === null || rect === void 0 ? void 0 : rect.right) || 0;
this.top = (rect === null || rect === void 0 ? void 0 : rect.top) || 0;
this.bottom = (rect === null || rect === void 0 ? void 0 : rect.bottom) || 0;
this.width = (rect === null || rect === void 0 ? void 0 : rect.width) || 0;
this.height = (rect === null || rect === void 0 ? void 0 : rect.height) || 0;
this.center = new Point({ x: (this.left + this.right) / 2, y: (this.top + this.bottom) / 2 });
}
get leftTop() { return { x: this.left, y: this.top }; }
get leftCenter() { return { x: this.left, y: this.center.y }; }
get leftBottom() { return { x: this.left, y: this.bottom }; }
get rightTop() { return { x: this.right, y: this.top }; }
get rightCenter() { return { x: this.right, y: this.center.y }; }
get rightBottom() { return { x: this.right, y: this.bottom }; }
get centerTop() { return { x: this.center.x, y: this.top }; }
get centerCenter() { return this.center; }
get centerBottom() { return { x: this.center.x, y: this.bottom }; }
}
/**
* This is Magzog Math object that contains math util-methods.
*/
class MZMath {
/**
* Finds the symmetric point of a given target point with respect to a reference origin point.
*
* This function calculates the point that is symmetric to the target point,
* using the origin point as the reference. The symmetric point is determined by
* reflecting the target point across the origin.
*
* @param {Object} originPoint - The reference point for symmetry, containing x and y coordinates.
* @param {Object} targetPoint - The point for which the symmetric point is calculated, containing x and y coordinates.
* @returns {Object} - The symmetric point with x and y coordinates.
*/
static findSymmetricPoint(originPoint, targetPoint) {
const symmetricX = 2 * originPoint.x - targetPoint.x;
const symmetricY = 2 * originPoint.y - targetPoint.y;
return { x: symmetricX, y: symmetricY };
}
/**
* Get degree.
* 0 <= degree <= 180 or 0 < degree < -180
* @param startPoint
* @param destPoint
* @returns
*/
static getDegree(startPoint, destPoint) {
const radian = MZMath.getRadian(startPoint, destPoint);
const degree = radian * 180 / Math.PI;
return degree;
}
/**
* Get degree.
* 0 <= degree
* @param startPoint
* @param destPoint
* @returns
*/
static getDegreePositive(startPoint, destPoint) {
const radian = MZMath.getRadian(startPoint, destPoint);
let degree = (radian * 180 / Math.PI) % 360;
if (degree < 0) {
degree += 360;
}
return degree;
}
/**
* Get radian angle.
* 0 <= radian <= pi or 0 < radian < -pi
* @param startPoint
* @param destPoint
* @returns
*/
static getRadian(startPoint, destPoint) {
const dx = destPoint.x - startPoint.x;
const dy = destPoint.y - startPoint.y;
const radian = Math.atan2(dy, dx);
return radian;
}
/**
* Get radian angle.
* 0 <= radian
* @param startPoint
* @param destPoint
* @returns
*/
static getRadianPositive(startPoint, destPoint) {
const dx = destPoint.x - startPoint.x;
const dy = destPoint.y - startPoint.y;
let radian = Math.atan2(dy, dx) % (2 * Math.PI);
if (radian < 0) {
radian += 2 * Math.PI; // 음수일 경우 2π를 더해 0 ~ 2π로 변환
}
return radian;
}
/**
* Returns the global location and size of the input element.
* @param el
* @returns
*/
static getOffset(el) {
var top = 0, left = 0, width = 0, height = 0;
let bound = el.getBoundingClientRect();
height = bound.height;
width = bound.width;
do {
bound = el.getBoundingClientRect();
top += bound.top;
left += bound.left;
el = el.offsetParent;
if (el !== null) {
bound = el.getBoundingClientRect();
top -= bound.top - window.scrollY;
left -= bound.left - window.scrollX;
}
} while (el);
return {
top: top,
left: left,
width: width,
height: height,
bottom: top + height,
right: left + width,
};
}
/**
* Ignore the scroll position.
* @param el
* @returns
*/
static getOffset4Fixed(el) {
var top = 0, left = 0, width = 0, height = 0;
let bound = el.getBoundingClientRect();
height = bound.height;
width = bound.width;
do {
bound = el.getBoundingClientRect();
top += bound.top;
left += bound.left;
el = el.offsetParent;
if (el !== null) {
bound = el.getBoundingClientRect();
top -= bound.top;
left -= bound.left;
}
} while (el);
return new Rect({
top: top,
left: left,
width: width,
height: height,
bottom: top + height,
right: left + width,
});
}
/**
* Returns the length between two points.
* @param point1
* @param point2
* @returns
*/
static getLength(point1, point2) {
return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
}
/**
* Find a point on line AB that is a fixed distance from point A toward point B.
* @param a
* @param b
* @param distance
* @returns
*/
static findPointOnLine(a, b, distance) {
// AB Vector calculation.
const ab = { x: b.x - a.x, y: b.y - a.y };
// The length of vector AB
const abLength = Math.sqrt(ab.x * ab.x + ab.y * ab.y);
// Calculates the vector AB's unit.
const unitVector = { x: ab.x / abLength, y: ab.y / abLength };
// Find the point.
const point = {
x: a.x + unitVector.x * distance,
y: a.y + unitVector.y * distance
};
return point;
}
/**
* Calculates and returns the coordinates of point D, where the perpendicular from point C meets the line segment AB.
* @param line line
* @param c
* @returns
*/
static findPerpendicularFoot(line, c) {
const vectorX = line.p2.x - line.p1.x;
const vectorY = line.p2.y - line.p1.y;
const vector_squared = vectorX * vectorX + vectorY * vectorY;
if (vector_squared === 0) {
return line.p1;
}
// Vector p1,c
const vector_p1_c_x = c.x - line.p1.x;
const vector_p1_c_y = c.y - line.p1.y;
// Vector production.
const dotProduct = vector_p1_c_x * vectorX + vector_p1_c_y * vectorY;
const t = dotProduct / vector_squared;
// Point D
const dx = line.p1.x + vectorX * t;
const dy = line.p1.y + vectorY * t;
return { x: dx, y: dy };
}
}
function deepMerge(target, source) {
for (const key in source) {
if (source[key] instanceof Object && key in target) {
deepMerge(target[key], source[key]); // 재귀적으로 병합
}
else {
target[key] = source[key]; // 값 복사
}
}
return target;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var BookEvent;
(function (BookEvent) {
BookEvent["pageAdded"] = "pageAdded";
})(BookEvent || (BookEvent = {}));
class FlipDiagonal {
get length() { return this._length; }
get radian() { return this._radian; }
constructor(startP, endP) {
this._length = 0;
this._radian = 0;
this._length = MZMath.getLength(startP, endP);
this._radian = MZMath.getRadianPositive(startP, endP);
}
}
class FlipDiagonals {
/**
*
* @param rect The container that book is spread open.
* @param actionCenter The center of FlipActionLine.
*/
constructor(rect, actionCenter) {
const zeroP = new Point();
const centerLeftP = { x: (rect === null || rect === void 0 ? void 0 : rect.left) || 0, y: (actionCenter === null || actionCenter === void 0 ? void 0 : actionCenter.y) || 0 };
const centerRightP = { x: (rect === null || rect === void 0 ? void 0 : rect.right) || 0, y: (actionCenter === null || actionCenter === void 0 ? void 0 : actionCenter.y) || 0 };
const centerTop = (rect === null || rect === void 0 ? void 0 : rect.centerTop) || zeroP;
const centerBottom = (rect === null || rect === void 0 ? void 0 : rect.centerBottom) || zeroP;
//
const diagonalLeftInArea1 = new FlipDiagonal(centerBottom, centerLeftP);
const diagonalRightInArea1 = new FlipDiagonal(centerBottom, centerRightP);
const diagonalLeftInArea2 = new FlipDiagonal(centerTop, centerLeftP);
const diagonalRightInArea2 = new FlipDiagonal(centerTop, centerRightP);
diagonalRightInArea1.radian;
diagonalRightInArea2.radian || 2 * Math.PI;
// Upper side
this.area1 = {
length: diagonalLeftInArea1.length,
radian: {
low: diagonalLeftInArea1.radian,
high: diagonalRightInArea1.radian || 2 * Math.PI,
}
};
// Lower side
this.area2 = {
length: diagonalLeftInArea2.length,
radian: {
low: diagonalRightInArea2.radian,
high: diagonalLeftInArea2.radian,
}
};
// Right side
this.area3 = {
length: 0,
radian: {
low: diagonalRightInArea1.radian,
high: diagonalRightInArea2.radian || 2 * Math.PI,
}
};
// Left side
this.area4 = {
length: 0,
radian: {
low: diagonalLeftInArea2.radian,
high: diagonalLeftInArea1.radian,
}
};
}
}
class Gutter extends Rect {
constructor(gutter) {
super(gutter);
}
get topPoint() { return { x: this.left, y: this.top }; }
get bottomPoint() { return { x: this.left, y: this.bottom }; }
}
/**
* PageWindow class
*/
class PageWindow extends MZEvent {
constructor() {
super();
/**
* Window array size. The default value is 6.
*/
this.windowSize = 6;
this.windows = [];
for (let i = 0; i < this.windowSize; i++) {
this.windows[i] = ({ page: undefined });
}
}
/**
* Loads and adds a page to the window.
* @param index Window index
* @param page
*/
loadPageToWindow(index, page) {
this.windows[index].page = page;
}
/**
* Loads and adds pages to the window.
* @param pages The length of the pages should be 6.
*/
loadPagesToWindow(pages) {
for (let i = 0; i < this.windowSize; i++) {
this.windows[i].page = pages[i];
}
}
/**
* Clears all pages on the window.
*/
clearPageWindow() {
this.windows = [];
for (let i = 0; i < this.windowSize; i++) {
this.windows[i] = ({ page: undefined });
}
}
/**
* Window moves to the right.
* Before: ----[2][3][4][5][6][7]----------
* After: ----------[4][5][6][7][8][9]----
* @param page4
* @param page5
*/
moveRight(page4, page5) {
this.windows.shift();
this.windows.shift();
this.windows.push({ page: page4 });
this.windows.push({ page: page5 });
}
/**
* Window moves to the right.
* Before: ----------[4][5][6][7][8][9]----
* After: ----[2][3][4][5][6][7]----------
* @param page0
* @param page1
*/
moveLeft(page0, page1) {
this.windows.pop();
this.windows.pop();
this.windows.unshift({ page: page1 });
this.windows.unshift({ page: page0 });
}
/**
* Get a page with window index.
* @param index Window index
* @returns
*/
getPageInWindow(index) { return this.windows[index].page; }
}
/**
* Flipping class
*/
class Flipping extends PageWindow {
/**
* Setter for eventStatus to update the current status of the event.
*/
set eventStatus(status) { this._eventStatus = status; }
;
/**
* Getter for eventStatus to retrieve the current event status.
*/
get eventStatus() { return this._eventStatus; }
constructor() {
super();
/**
* Current status of the event, initialized to 'None'.
*/
this._eventStatus = EventStatus.None;
/**
* The area representing the flipping region.
* The width is the same as the opened book width.
*/
this.flipGRect = new Rect();
/**
* The gutter of the book, representing the central area between the pages.
* The position
*/
this.gutter = new Gutter();
/**
* The current zone where the event is occurring, initialized to the right-bottom (RB) zone.
*/
this.eventZone = Zone.RB;
/**
* The previous zone where the event occurred, initialized to the right-top (RT) zone.
*/
this.oldEventZone = Zone.RT;
/**
* The center point of the active flipping process.
*/
this.activeCenterGP = new Point();
/**
* The point representing the active corner during the flip.
*/
this.activeCornerGP = new Point();
/**
* The point representing the left corner of book.
*/
// leftCornerGP:Point = new Point();
/**
* The point representing the right corner of book.
*/
// rightCornerGP:Point = new Point();
/**
* The opposite corner to the active corner during the flip.
*/
this.activeCornerOppositeGP = new Point();
/**
* Diagonal values used to calculate flipping geometry.
* The diagonals are depends on the dragging corner point.
*/
this.diagonals = new FlipDiagonals();
/**
* A line that represents the action of flipping a page.
* The width is the same as the opened book width.
*/
this.flipActionLine = new FlipActionLine();
/**
* The current width being used for automatic flipping when the AutoFlipType is FixedWidth.
*/
this.curAutoFlipWidth = { x: 0, y: 0 };
/**
* The width for automatic flipping.
*/
this.autoFlipDimension = {
top: { width: 0, height: 0 },
center: { width: 0, height: 0 },
bottom: { width: 0, height: 0 },
};
/**
* The width of the zone.
* TODO: Should be setting options.
*/
this.zoneDimension = {
top: { width: 0, height: 0 },
center: { width: 0, height: 0 },
bottom: { width: 0, height: 0 },
};
/**
* Returns and sets current mouse pointer global point.
*/
this.curMouseGP = new Point();
/**
* Settings related to the flipping behavior.
*/
this.setting = { autoFlip: { type: AutoFlipType.MouseCursor } };
}
/**
* Initializes the flipping process, setting the event zone and configuring the flip action line,
* diagonal properties, and active corner points.
* @param eventZone The zone where the event is happening (e.g., LT, RT).
* @param mouseGP The current position of the mouse pointer.
* @param containerRect The rectangle defining the boundaries of the container element.
*/
setInitFlipping(eventZone, mouseGP, containerRect, zoomLevel) {
this.eventZone = eventZone;
let flipActionLineGY = mouseGP.y;
// If dragging a corner, the flipActionLineGY will be the top or bottom of the container.
switch (eventZone) {
case Zone.LT:
case Zone.RT:
flipActionLineGY = this.gutter.top;
break;
case Zone.LB:
case Zone.RB:
flipActionLineGY = this.gutter.bottom;
break;
}
//
// Flip Rect
//
const left = containerRect.left == this.gutter.left ? containerRect.left - containerRect.width : containerRect.left;
const right = containerRect.right == this.gutter.right ? containerRect.right + containerRect.width : containerRect.right;
this.flipGRect = new Rect({
left: left,
right: right,
top: this.gutter.top,
bottom: this.gutter.bottom,
width: right - left,
height: this.gutter.height
});
//
// FlipActionLine, Diagonals, ActiveCenterGP
//
this.flipActionLine = new FlipActionLine(left, right, flipActionLineGY);
this.diagonals = new FlipDiagonals(this.flipGRect, this.flipActionLine.centerP);
this.activeCenterGP = this.flipActionLine.centerP;
// this.leftCornerGP = this.flipActionLine.leftP;
// this.rightCornerGP = this.flipActionLine.rightP;
//
// ActiveCornerGP
//
let originX = 0;
switch (eventZone) {
case Zone.LT:
case Zone.LC:
case Zone.LB:
originX = this.flipGRect.width / 2;
this.activeCornerGP = this.flipActionLine.leftP;
break;
case Zone.RT:
case Zone.RC:
case Zone.RB:
originX = 0;
this.activeCornerGP = this.flipActionLine.rightP;
break;
}
this.activeCornerOppositeGP = MZMath.findSymmetricPoint(this.activeCenterGP, this.activeCornerGP);
//
// Transform origin
//
const originY = this.flipActionLine.y - containerRect.top;
const doc = document;
const docEl = doc.documentElement;
docEl.style.setProperty('--page2-origin', `${originX / zoomLevel}px ${originY / zoomLevel}px`);
// Zone Dimension
const zoneLT = doc.getElementById('mzZoneLT');
const zoneLC = doc.getElementById('mzZoneLC');
const zoneLB = doc.getElementById('mzZoneLB');
const zoneRT = doc.getElementById('mzZoneRT');
const zoneRC = doc.getElementById('mzZoneRC');
const zoneRB = doc.getElementById('mzZoneRB');
this.zoneDimension = {
top: { width: (zoneLT === null || zoneLT === void 0 ? void 0 : zoneLT.clientWidth) || (zoneRT === null || zoneRT === void 0 ? void 0 : zoneRT.clientWidth) || 0, height: (zoneLT === null || zoneLT === void 0 ? void 0 : zoneLT.clientHeight) || (zoneRT === null || zoneRT === void 0 ? void 0 : zoneRT.clientHeight) || 0 },
center: { width: (zoneLC === null || zoneLC === void 0 ? void 0 : zoneLC.clientWidth) || (zoneRC === null || zoneRC === void 0 ? void 0 : zoneRC.clientWidth) || 0, height: (zoneLC === null || zoneLC === void 0 ? void 0 : zoneLC.clientHeight) || (zoneRC === null || zoneRC === void 0 ? void 0 : zoneRC.clientHeight) || 0 },
bottom: { width: (zoneLB === null || zoneLB === void 0 ? void 0 : zoneLB.clientWidth) || (zoneRB === null || zoneRB === void 0 ? void 0 : zoneRB.clientWidth) || 0, height: (zoneLB === null || zoneLB === void 0 ? void 0 : zoneLB.clientHeight) || (zoneRB === null || zoneRB === void 0 ? void 0 : zoneRB.clientHeight) || 0 },
};
// TODO: 각 존별 너비가 다를 경우 처리 필요
this.autoFlipDimension = {
top: { width: this.zoneDimension.top.width * 0.9, height: this.zoneDimension.top.height * 0.9 },
center: { width: this.zoneDimension.center.width * 0.9, height: this.zoneDimension.center.height * 0.9 },
bottom: { width: this.zoneDimension.bottom.width * 0.9, height: this.zoneDimension.bottom.height * 0.9 },
};
}
/**
* Returns the point of the corner that flipping page has to go back.
* @param mouseGP The current position of the mouse pointer.
* @returns The corner point closest to the mouse pointer.
*/
getTargetCorner(mouseGP) {
const activeLength = MZMath.getLength(mouseGP, this.activeCornerGP);
const inactiveLength = MZMath.getLength(mouseGP, this.activeCornerOppositeGP);
return inactiveLength < activeLength ? this.activeCornerOppositeGP : this.activeCornerGP;
}
/**
* Retrieves information needed to execute the flip, such as the target corner,
* whether the flip is snapping back, and whether the flip is forward.
* @param mouseGP The current position of the mouse pointer.
* @returns An object containing the target corner, snap-back status, and flip direction.
*/
getInfoToFlip(mouseGP) {
const targetCornerGP = this.getTargetCorner(mouseGP);
const isSnappingBack = targetCornerGP == this.activeCornerGP;
const isForward = targetCornerGP.x < this.gutter.left ? true : false;
return {
targetCornerGP: targetCornerGP,
isSnappingBack: isSnappingBack,
isFlippingForward: isForward,
};
}
/**
* Animates the page flip action, gradually moving from the start point to the target point,
* while applying easing to make the motion smooth. It supports auto-flip from or to the corner.
* @param isAutoFlippingFromCorner Whether the flip is an automatic flip from the corner.
* @param mouseGP The current position of the mouse pointer.
* @param pageWH The width and height of the page.
* @param onFlip A callback executed during the flip, receiving the mouse position and page size.
* @param onComplete A callback executed when the flip is complete.
*/
animateReadyToFlip(isAutoFlippingFromCorner, mouseGP, onFlip, onComplete) {
let currentValue = this.curAutoFlipWidth;
const isFlipTypeMouseCursor = (this.setting.autoFlip.type == AutoFlipType.MouseCursor); // && !(this.eventZone & Zone.Center);
const targetValue = { x: 0, y: 0 };
if (isFlipTypeMouseCursor && !isAutoFlippingFromCorner) {
mouseGP = this.activeCornerGP;
}
const startTime = performance.now();
// TODO: Should be setting options.
const duration = 300; // 2000ms
let startP = new Point();
let endP = new Point();
switch (this.eventZone) {
case Zone.LT:
if (isAutoFlippingFromCorner) {
targetValue.x = this.autoFlipDimension.top.width;
targetValue.y = this.autoFlipDimension.top.height;
}
startP = { x: this.activeCornerGP.x + currentValue.x, y: this.activeCornerGP.y + currentValue.y };
endP = isFlipTypeMouseCursor ? mouseGP : { x: this.activeCornerGP.x + targetValue.x, y: this.activeCornerGP.y + targetValue.y };
break;
case Zone.LC:
if (isAutoFlippingFromCorner) {
targetValue.x = this.autoFlipDimension.center.width;
}
startP = { x: this.activeCornerGP.x + currentValue.x, y: this.activeCornerGP.y };
endP = isFlipTypeMouseCursor ? mouseGP : { x: this.activeCornerGP.x + targetValue.x, y: this.activeCornerGP.y };
break;
case Zone.LB:
if (isAutoFlippingFromCorner) {
targetValue.x = this.autoFlipDimension.bottom.width;
targetValue.y = this.autoFlipDimension.bottom.height;
}
startP = { x: this.activeCornerGP.x + currentValue.x, y: this.activeCornerGP.y - currentValue.y };
endP = isFlipTypeMouseCursor ? mouseGP : { x: this.activeCornerGP.x + targetValue.x, y: this.activeCornerGP.y - targetValue.y };
break;
case Zone.RT:
if (isAutoFlippingFromCorner) {
targetValue.x = this.autoFlipDimension.top.width;
targetValue.y = this.autoFlipDimension.top.height;
}
startP = { x: this.activeCornerGP.x - currentValue.x, y: this.activeCornerGP.y + currentValue.y };
endP = isFlipTypeMouseCursor ? mouseGP : { x: this.activeCornerGP.x - targetValue.x, y: this.activeCornerGP.y + targetValue.y };
break;
case Zone.RC:
if (isAutoFlippingFromCorner) {
targetValue.x = this.autoFlipDimension.center.width;
}
startP = { x: this.activeCornerGP.x - currentValue.x, y: this.activeCornerGP.y };
endP = isFlipTypeMouseCursor ? mouseGP : { x: this.activeCornerGP.x - targetValue.x, y: this.activeCornerGP.y };
break;
case Zone.RB:
if (isAutoFlippingFromCorner) {
targetValue.x = this.autoFlipDimension.bottom.width;
targetValue.y = this.autoFlipDimension.bottom.height;
}
startP = { x: this.activeCornerGP.x - currentValue.x, y: this.activeCornerGP.y - currentValue.y };
endP = isFlipTypeMouseCursor ? mouseGP : { x: this.activeCornerGP.x - targetValue.x, y: this.activeCornerGP.y - targetValue.y };
break;
}
if (!isFlipTypeMouseCursor && (currentValue == targetValue)) {
return onComplete();
}
const animationFrame = (currentTime) => {
const eventStatus = this.eventStatus;
if (isFlipTypeMouseCursor) {
if (isAutoFlippingFromCorner) {
endP = this.adjustPointerToZone(this.eventZone, this.curMouseGP);
}
else {
startP = this.adjustPointerToZone(this.eventZone, this.curMouseGP);
}
}
if ((eventStatus != EventStatus.AutoFlipToCorner && eventStatus != EventStatus.AutoFlipFromCorner)
|| (isAutoFlippingFromCorner && eventStatus == EventStatus.AutoFlipToCorner)
|| (!isAutoFlippingFromCorner && eventStatus == EventStatus.AutoFlipFromCorner)) {
return onComplete();
}
const elapsed = (currentTime - startTime) / duration; // 0 ~ 1
const progress = Math.min(elapsed, 1); // Calculate progress (maximum 1)
const easingProgress = this.easeInOutQuad(progress);
const currentX = startP.x + (endP.x - startP.x) * easingProgress;
const currentY = startP.y + (endP.y - startP.y) * easingProgress;
if (!isFlipTypeMouseCursor) {
if (isAutoFlippingFromCorner) {
currentValue.x = progress * targetValue.x;
currentValue.y = progress * targetValue.y;
}
else {
currentValue.x = currentValue.x - progress * currentValue.x;
currentValue.y = currentValue.y - progress * currentValue.y;
}
this.curAutoFlipWidth = currentValue;
}
onFlip({ x: currentX, y: currentY });
if (progress < 1) {
requestAnimationFrame(animationFrame);
}
else {
onComplete();
}
};
requestAnimationFrame(animationFrame); // 애니메이션 시작
}
/**
* Adjusts the mouse pointer position to the edge of the specified zone.
* @param zone The zone to adjust the mouse pointer to.
* @param mouseGP The current position of the mouse pointer.
* @returns The adjusted mouse pointer position.
*/
adjustPointerToZone(zone, mouseGP) {
let x = mouseGP.x;
let y = mouseGP.y;
// Set the x position of the mouse pointer to the edge of the zone.
switch (zone) {
case Zone.LT:
if (mouseGP.x > this.flipGRect.left + this.zoneDimension.top.width) {
x = this.flipGRect.left + this.zoneDimension.top.width;
}
break;
case Zone.LC:
if (mouseGP.x > this.flipGRect.left + this.zoneDimension.center.width) {
x = this.flipGRect.left + this.zoneDimension.center.width;
}
break;
case Zone.LB:
if (mouseGP.x > this.flipGRect.left + this.zoneDimension.bottom.width) {
x = this.flipGRect.left + this.zoneDimension.bottom.width;
}
break;
case Zone.RT:
if (mouseGP.x < this.flipGRect.right - this.zoneDimension.top.width) {
x = this.flipGRect.right - this.zoneDimension.top.width;
}
break;
case Zone.RC:
if (mouseGP.x < this.flipGRect.right - this.zoneDimension.center.width) {
x = this.flipGRect.right - this.zoneDimension.center.width;
}
break;
case Zone.RB:
if (mouseGP.x < this.flipGRect.right - this.zoneDimension.bottom.width) {
x = this.flipGRect.right - this.zoneDimension.bottom.width;
}
break;
}
// Set the y position of the mouse pointer to the edge of the zone.
if (zone & Zone.Top) {
if (mouseGP.y < this.flipGRect.top) {
y = this.flipGRect.top;
}
else if (mouseGP.y > this.flipGRect.top + this.zoneDimension.top.height) {
y = this.flipGRect.top + this.zoneDimension.top.height;
}
}
else if (zone & Zone.Center) {
y = this.activeCornerGP.y;
}
else if (zone & Zone.Bottom) {
if (mouseGP.y > this.flipGRect.bottom) {
y = this.flipGRect.bottom;
}
else if (mouseGP.y < this.flipGRect.bottom - this.zoneDimension.bottom.height) {
y = this.flipGRect.bottom - this.zoneDimension.bottom.height;
}
}
return { x, y };
}
/**
* Starts the animation for flipping the page from the corner.
* @param mouseGP The current position of the mouse pointer.
* @param onFlip A callback executed during the flip, receiving the mouse position and page size.
* @param onComplete A callback executed when the flip is complete.
*/
animateFlipFromCorner(mouseGP, onFlip, onComplete) {
if (this.oldEventZone != this.eventZone) {
this.curAutoFlipWidth = { x: 0, y: 0 };
}
this.oldEventZone = this.eventZone;
this.animateReadyToFlip(true, mouseGP, onFlip, onComplete);
}
/**
* Starts the animation for flipping the page to the corner.
* @param mouseGP The current position of the mouse pointer.
* @param onFlip A callback executed during the flip, receiving the mouse position and page size.
* @param onComplete A callback executed when the flip is complete.
*/
animateFlipToCorner(mouseGP, onFlip, onComplete) {
this.oldEventZone = this.eventZone;
this.animateReadyToFlip(false, mouseGP, onFlip, onComplete);
}
/**
* Easing function for smooth transition during the flip animation.
* @param t A number between 0 and 1 representing the current progress of the animation.
* @returns A number representing the eased progress value.
*/
easeInOutQuad(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }
/**
* Animates the page flip, interpolating between the start and end points with easing.
* @param startP The starting point of the flip.
* @param endP The ending point of the flip.
* @param pageWH The width and height of the page.
* @param onFlip A callback executed during the flip, receiving the mouse position and page size.
* @param onComplete A callback executed when the flip is complete.
*/
animateFlip(startP, endP, onFlip, onComplete) {
const startTime = performance.now();
// TODO: Should be setting options.
const duration = 400; // 2000ms
const animationFrame = (currentTime) => {
if (this.eventStatus == EventStatus.Flipping) {
return;
}
const elapsed = (currentTime - startTime) / duration; // 0 ~ 1 사이 값
const progress = Math.min(elapsed, 1); // 진행률 계산 (최대 1)
const easingProgress = this.easeInOutQuad(progress); // easing 함수 적용
const currentX = startP.x + (endP.x - startP.x) * easingProgress;
const currentY = startP.y + (endP.y - startP.y) * easingProgress;
onFlip({ x: currentX, y: currentY });
if (progress < 1) {
requestAnimationFrame(animationFrame);
}
else {
onComplete();
}
};
requestAnimationFrame(animationFrame);
}
/**
* Updates and returns the mouse pointer position depends on the area that the mouse pointer position is located in.
* @param mouseGP The current position of the mouse pointer.
* @returns The updated mouse pointer position.
*/
updateMousePointOnArea(mouseGP) {
const radian4Area1 = MZMath.getRadianPositive(this.gutter.centerBottom, mouseGP);
const isArea1 = mouseGP.y < this.flipActionLine.y
&& MZMath.getLength(this.gutter.centerBottom, mouseGP) > this.diagonals.area1.length
&& this.diagonals.area1.radian.low <= radian4Area1 && radian4Area1 <= this.diagonals.area1.radian.high;
const radian4Area2 = MZMath.getRadianPositive(this.gutter.centerTop, mouseGP);
const isArea2 = mouseGP.y > this.flipActionLine.y
&& MZMath.getLength(this.gutter.centerTop, mouseGP) > this.diagonals.area2.length
&& this.diagonals.area2.radian.low <= radian4Area2 && radian4Area2 <= this.diagonals.area2.radian.high;
const radian4Area3 = MZMath.getRadianPositive(this.flipActionLine.rightP, mouseGP);
const radianLow4Area3 = this.diagonals.area3.radian.low;
const radianHigh4Area3 = this.diagonals.area3.radian.high;
const isArea3 = mouseGP.x > this.flipActionLine.rightX
&& MZMath.getLength(this.flipActionLine.rightP, mouseGP) > this.diagonals.area3.length
&& (radianLow4Area3 <= radian4Area3 && radian4Area3 <= radianHigh4Area3
// Ex) low = 5.xx, high = 0.4
|| (radianLow4Area3 > radianHigh4Area3
&& radianLow4Area3 <= radian4Area3 || radian4Area3 <= radianHigh4Area3));
const radian4Area4 = MZMath.getRadianPositive(this.flipActionLine.leftP, mouseGP);
const isArea4 = mouseGP.x < this.flipActionLine.leftX
&& MZMath.getLength(this.flipActionLine.leftP, mouseGP) > this.diagonals.area4.length
&& this.diagonals.area4.radian.low <= radian4Area4 && radian4Area4 <= this.diagonals.area4.radian.high;
if (isArea1) {
mouseGP = MZMath.findPointOnLine(this.gutter.centerBottom, mouseGP, this.diagonals.area1.length);
}
else if (isArea2) {
mouseGP = MZMath.findPointOnLine(this.gutter.centerTop, mouseGP, this.diagonals.area2.length);
}
else if (isArea3) {
mouseGP = this.flipActionLine.rightP;
}
else if (isArea4) {
mouseGP = this.flipActionLine.leftP;
}
return mouseGP;
}
/**
* Calculates and returns the data needed to flip the page, including the active corner,
* mask shape, shadow, and rotation angles.
* @param mouseGP The current position of the mouse pointer.
* @param pageWH The width and height of the page.
* @param isSpreadOpen Whether the page is spread open or not.
* @returns An object containing all the necessary data for rendering the flip.
*/
flip(mouseGP, pageWH, isSpreadOpen) {
const page2W = pageWH.width;
const page2H = pageWH.height;
let page2ActiveCorner;
let page3ActiveCorner;
let zeroX = 0;
let pivot = 1;
//
// Area
//
mouseGP = this.updateMousePointOnArea(mouseGP);
switch (this.eventZone) {
case Zone.LT:
case Zone.LC:
case Zone.LB:
page2ActiveCorner = { x: page2W, y: page2H };
page3ActiveCorner = { x: 0, y: page2H };
zeroX = 0;
pivot = -1;
break;
case Zone.RT:
case Zone.RC:
case Zone.RB:
page2ActiveCorner = { x: 0, y: page2H };
page3ActiveCorner = { x: page2W, y: page2H };
zeroX = isSpreadOpen ? page2W : 0;
pivot = 1;
break;
default: throw new Error("Not found an event zone.");
}
const halfPI = Math.PI / 2;
const diffH = this.gutter.bottom - this.flipActionLine.y;
const beta = MZMath.getRadianPositive(this.activeCornerGP, mouseGP);
const alpha = pivot * (halfPI * 3 - beta);
const page2Left = zeroX + (mouseGP.x - this.gutter.left);
const page2Top = mouseGP.y - this.flipActionLine.y;
const a = mouseGP.x - this.activeCornerGP.x; // a < 0
const b = mouseGP.y - this.activeCornerGP.y; // b < 0
const cosTheta = Math.cos(-halfPI + 2 * beta);
const tanAlpa = Math.tan(-halfPI - pivot * beta);
const d = b == 0 ? page2H : (-a / cosTheta) + diffH; // d > 0
const c = b == 0 ? -a * pivot / 2 : d / tanAlpa;
// Mask position on Page 2
const f = { x: page2ActiveCorner.x, y: page2ActiveCorner.y };
let g = { x: page2ActiveCorner.x + c * pivot, y: page2ActiveCorner.y };
let h = { x: 0, y: 0 };
const i = { x: page2ActiveCorner.x, y: page2ActiveCorner.y - d };
// Shadow
const closingDistance = MZMath.getLength(mouseGP, this.activeCornerOppositeGP);
//
// Update positions
//
if (b == 0) {
h = { x: g.x, y: i.y };
}
// Mask shape is triangle when top corner is one of the vertices.
else if (c < 0) {
h.x = page2ActiveCorner.x - pivot * c * (page2H - d) / d;
h.y = i.y = page2H - page2ActiveCorner.y;
f.y = page2ActiveCorner.y - d;
g = f;
}
// Mask shape is trapezoid and the top side is longer than the bottom side.
// It is happend when the corner is dragging under book.
else if (d < 0) {
h.x = page2ActiveCorner.x + pivot * c * (d - page2H) / d;
h.y = i.y = page2H - page2ActiveCorner.y;
}
// Mask shape is triangle when bottom corner is one of the vertices.
else if (d < page2H) {
h = i;
}
// Mask shape is trapezoid.
else if (d > page2H) {
h.x = page2ActiveCorner.x + pivot * c * (d - page2H) / d;
h.y = i.y = page2H - page2ActiveCorner.y;
}
// Mask position on Page 1
const j = { x: page3ActiveCorner.x, y: page3ActiveCorner.y };
const k = { x: page3ActiveCorner.x + page2ActiveCorner.x - g.x, y: g.y };
const l = { x: page3ActiveCorner.x + page2ActiveCorner.x - h.x, y: h.y };
const m = { x: page3ActiveCorner.x, y: i.y };
return new FlipData({
page2: {
top: page2Top,
left: page2Left,
rotate: (2 * beta) % (2 * Math.PI)
},
mask: {
page2: {
p1: f,
p2: g,
p3: h,
p4: i,
},
page1: {
p1: j,
p2: k,
p3: l,
p4: m,
}
},
alpa: alpha,
a: a,
b: b,
c: c,
d: d,
shadow: {
closingDistance: closingDistance,
rect: {
rotate: alpha,
origin: {
x: g.x,
y: 0
}
},
},
});
}
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* BookViewer class
* Gutter:
*
*/
class FlipView {
// readonly bookShelfManager: BookShelfManager;
/**
* This getter returns Rect data of the page container which is the child element of the book element.
*/
get pageContainerRect() {
var _a;
const el = (_a = this.book) === null || _a === void 0 ? void 0 : _a.pageContainerEl;
if (!el) {
throw new Error("Not found the page container.");
}
return el && MZMath.getOffset4Fixed(el);
}
/**
* Gets whether the book is ready to open.
*/
get isReadyToOpen() { return this.curOpenLeftPageIndex < 0; }
/**
* Gets whether the book is closed.
*/
get isClosed() { return this.curOpenLeftPageIndex >= this.bookLastPageIndex; }
/**
* Gets whether the page is flipping. (Includes auto-filp and draggring).
*/
get isFlipping() {
return (this.flipManager.eventStatus & EventStatus.AutoFlip
|| this.flipManager.eventStatus & EventStatus.Flipping
|| this.flipManager.eventStatus & EventStatus.Dragging);
}
;
/**
* Gets whether the left page is flipping.
*/
get isLeftPageFlipping() { return this.isFlipping && this.flipManager.eventZone & Zone.Left; }
;
/**
* Gets whether the right page is flipping.
*/
get isRightPageFlipping() { return this.isFlipping && this.flipManager.eventZone & Zone.Right; }
;
/**
* Gets whether the first page(index is 0) is Opening.
*/
get isFirstPageOpening() { var _a; return this.isRightPageFlipping && ((_a = this.getActivePage(1)) === null || _a === void 0 ? void 0 : _a.index) == 0; }
/**
* Gets whether the first page(index is 0) is closing.
*/
get isFirstPageClosing() { var _a; return this.isLeftPageFlipping && ((_a = this.getActivePage(2)) === null || _a === void 0 ? void 0 : _a.index) == 0; }
/**
* Gets whether the last page is Opening.
*/
get isLastPageOpening() { var _a; return this.isLeftPageFlipping && ((_a = this.getActivePage(1)) === null || _a === void 0