const PI = Math.PI; /** * 90度角弧度值 */ const A90 = PI / 2; let pageNumber = 0; let pageCount = -1; let moving = false; export const point2D = (x, y) => { return { x: x, y: y }; } export const setPageNumber = (number) => { pageNumber = number; } export const setPageCount = (count) => { pageCount = count; } let staticParam = { mode: 'half', page: { width: 0, height: 0 }, startPoint: point2D(0, 0), pointVerticalPosition: "c", moveHorizontalPosition: 'r', pageCorner: null } const reset = () => { staticParam = { mode: staticParam.mode, page: staticParam.page, startPoint: point2D(0, 0), pointVerticalPosition: "c", moveHorizontalPosition: 'r', pageCorner: null } moving = false; ending = false; return { wrapper: {}, content: {}, bwrapper: {}, bcontent: {}, gradient: {}, bgradient: {} }; } /** * 触摸开始事件 * @param page 包含页面的长和宽:{ width: w, height: h } * @param startPoint 起始触摸点:{ x: x, y: y } */ export const touchStartEvent = (page, startPoint) => { staticParam.page = page; staticParam.startPoint = startPoint; setVerticalPosition(page); } /** * 触摸移动事件 * @param point 移动时触摸点:{ x: x, y: y } * @returns Translate样式 */ export const touchMoveEvent = (point) => { getCornerPosition(point); let style = {}; /* * 满足以下条件,样式进行变化 * * 从右翻页(向下一页)并且存在下一页 * pageCount == -1 表示 不限制向下翻页,即:永远存在下一页 * pageNumber < pageCount - 1 表示 当前页不是最后一页 * 或者 * 从左翻页(向上一页)并且存在上一页 * pageNumber > 0 表示 当前页不是第一页 * moving 表示 正在进行翻页动作(非第一次触摸移动) */ if ( (staticParam.moveHorizontalPosition == 'r' && (pageCount == -1 || pageNumber < pageCount - 1)) || ((staticParam.moveHorizontalPosition == 'l') && (pageNumber > 0 || moving)) ) { style = getTranslateStyle(point); if (staticParam.moveHorizontalPosition == 'l' && !moving) { // 向上一页翻并且第一次触摸移动,翻动页面应展示上一页的数据 pageNumber--; } moving = true; } return { style: style, pageNumber: pageNumber }; } let ending = false; /** * 触摸结束事件 * @param point 结束时触摸点:{ x: x, y: y } * @returns Translate样式 */ export const touchEndEvent = (point, cb, stop, clickCenter) => { if(ending) { return; } ending = true; let o; let endPosition; let turn = false; let isClick = false; // 取消从左翻页 let isCancel4l = false; if (staticParam.pageCorner) { endPosition = getEndPointPosition(point); } else if (staticParam.mode == "half") { // staticParam.pageCorner不存在,说明触摸点未移动,即:点击 isClick = true; // 点击水平位置 staticParam.moveHorizontalPosition = getPointHorizontalPosition(point); if (staticParam.moveHorizontalPosition != "c") { // 点击右边向左翻页,反之 endPosition = staticParam.moveHorizontalPosition == "r" ? "l" : "r"; staticParam.pageCorner = "r"; staticParam.pointVerticalPosition = "c"; } else { // 点击中间 reset(); clickCenter && clickCenter(); return; } } // 翻页方向(从哪个方向翻页) let pointHorizontalPosition = staticParam.moveHorizontalPosition; // 如果是展示半本书模式,且从左翻页,设定为从右翻页 if (staticParam.mode == "half" && staticParam.moveHorizontalPosition == "l") { pointHorizontalPosition = "r"; } if (endPosition == pointHorizontalPosition) { // 触摸结束点位置与翻页方向相同 // 获取页面角落的坐标 o = getPageCornersCoordinates(staticParam.pageCorner, staticParam.page); if (staticParam.moveHorizontalPosition == "l") { if (!moving) { // 向上翻页并且是首页 if (pageNumber == 0) { staticParam.pageCorner = null; if (stop) stop({ ...animationOverStyle(), isFirst: true }); return; } pageNumber--; } } } else { // 触摸结束点位置与翻页方向不同 // 获取结束点(翻开书页的书角顶点最终的位置) o = getEndingPoint(staticParam.pageCorner, staticParam.page); if (staticParam.moveHorizontalPosition == "r") { if(pageCount != -1 && pageNumber >= pageCount - 1) { staticParam.pageCorner = null; if (stop) stop({ ...animationOverStyle(), isLast: true }); return; } turn = true; } else { // 取消翻页(从左翻页) isCancel4l = true; } } if (o) { const to = [o.x, o.y]; createAnimation({ from: [point.x, point.y], to: to, duration: 500, frame: (value) => { if (cb) cb({ style: getTranslateStyle(point2D(value[0], value[1])), pageNumber: pageNumber }); }, complete: () => { if ((staticParam.moveHorizontalPosition == "r" && turn) || isCancel4l) { pageNumber++; } if (stop) stop(animationOverStyle()); } }); } } const animationOverStyle = () => { return { style: reset(), pageNumber: pageNumber } } /** * 获取页面对角线长度 * @param page 包含页面的长和宽:{ width: w, height: h } */ export const getPageDiagonalLength = (page) => { return Math.sqrt(Math.pow(page.width, 2) + Math.pow(page.height, 2)); } /** * 获取Translate样式 * @param point 移动时触摸点:{ x: x, y: y } */ const getTranslateStyle = (point) => { let computeResult, a, tr, position, origin; /** * 结束点(翻开书页的书角顶点最终的位置) */ const endingPoint = getEndingPoint(staticParam.pageCorner, staticParam.page); switch (staticParam.pageCorner) { case 'l': return; case 'r': point.x = Math.max(Math.min(point.x, staticParam.page.width - 1), endingPoint.x); computeResult = compute(point); a = -radians2degrees(computeResult.alpha); tr = point2D(-computeResult.tr.x, computeResult.tr.y); position = [0, 0, 0, 1]; origin = [0, 0]; break; case 'tl': // point.x = Math.max(point.x, 1); return; case 'tr': point.x = Math.max(Math.min(point.x, staticParam.page.width - 1), endingPoint.x); computeResult = compute(point); a = -radians2degrees(computeResult.alpha); tr = point2D(-computeResult.tr.x, computeResult.tr.y); position = [0, 0, 0, 1]; origin = [0, 0]; break; case 'bl': // point.x = Math.max(point.x, 1); return; case 'br': point.x = Math.max(Math.min(point.x, staticParam.page.width - 1), endingPoint.x); computeResult = compute(point); a = radians2degrees(computeResult.alpha); tr = point2D(-computeResult.tr.x, -computeResult.tr.y); position = [0, 1, 1, 0] origin = [0, 100]; break; } return transformStyle(a, tr, computeResult, position, origin); } /** * 设置触摸在页面的上中下的位置,'t'、'c'、'b',分别代表上部、中部、下部。 * @param page 包含页面的长和宽:{ width: w, height: h } * @param startPoint 起始触摸点:{ x: x, y: y } */ const setVerticalPosition = (page) => { if (page.height * 0.3 > staticParam.startPoint.y) { staticParam.pointVerticalPosition = 't'; } else if (page.height * 0.7 > staticParam.startPoint.y) { staticParam.pointVerticalPosition = 'c'; } else if (page.height > staticParam.startPoint.y) { staticParam.pointVerticalPosition = 'b'; } } /** * 设置翻开页面页角的位置,'tl'、'tr'、'bl'、'br'、'l' 和 'r',分别代表左上角、右上角、左下角、右下角、左边和右边。 * @param point 移动触摸点:{ x: x, y: y } */ const setCornerPosition = (point) => { if (staticParam.startPoint.x >= point.x) { staticParam.moveHorizontalPosition = 'r'; } else { staticParam.moveHorizontalPosition = 'l'; } if (staticParam.moveHorizontalPosition == 'l' && staticParam.mode == 'half') { staticParam.pageCorner = "r"; staticParam.pointVerticalPosition = "c"; } else if (staticParam.pointVerticalPosition == 'c') { staticParam.pageCorner = staticParam.moveHorizontalPosition; } else { staticParam.pageCorner = staticParam.pointVerticalPosition + staticParam.moveHorizontalPosition; } } /** * 获取翻开页面页角的位置,'tl'、'tr'、'bl'、'br'、'l' 和 'r',分别代表左上角、右上角、左下角、右下角、左边和右边。 * @param point 移动触摸点:{ x: x, y: y } */ const getCornerPosition = (point) => { if (!staticParam.pageCorner) { setCornerPosition(point); } return staticParam.pageCorner; } /** * 获取结束触摸点在页面的位置,'l'、'r',分别代表左边和右边。 * @param point 结束触摸点:{ x: x, y: y } */ const getEndPointPosition = (point) => { let middle; if (staticParam.mode == 'half') { if (staticParam.moveHorizontalPosition == 'l') { middle = staticParam.page.width * 0.2 } else { middle = staticParam.page.width * 0.8 } } if (middle > point.x) { return 'l'; } else { return 'r'; } } /** * 获取触摸点在页面的位置,'l'、'c'、'r',分别代表左边,中间和右边。 * @param point 触摸点:{ x: x, y: y } */ const getPointHorizontalPosition = (point) => { if (staticParam.page.width * 0.3 > point.x) { return 'l'; } if (staticParam.page.width * 0.7 > point.x) { return 'c'; } else { return 'r'; } } const compute = (point) => { const page = staticParam.page; const top = staticParam.pointVerticalPosition == 't'; const center = staticParam.pointVerticalPosition == "c" const left = staticParam.mode != 'half' && staticParam.moveHorizontalPosition == 'l'; const pageCorner = getCornerPosition(point); // 获取页面角落的坐标 const o = getPageCornersCoordinates(pageCorner, page); const pageDiagonalLength = getPageDiagonalLength(page); // 触摸/鼠标点相对于页面角落的坐标 const rel = point2D(0, 0); rel.x = (o.x) ? o.x - point.x : point.x; rel.y = (o.y) ? o.y - point.y : point.y; // 触摸/鼠标点与页面角落的中间点坐标 const middle = point2D(0, 0); middle.x = (left) ? page.width - rel.x / 2 : point.x + rel.x / 2; middle.y = rel.y / 2; /** * 计算触摸/鼠标点与y轴的夹角弧度值alpha(α)。 * 其中, * A90是一个常量,表示90度的角度弧度值, * Math.atan2函数返回由两个参数(y和x)确定的点与x轴的夹角弧度值,范围为[-π, π]。 */ const alpha = center ? A90 : A90 - Math.atan2(rel.y, rel.x); /** ?? * 计算弧度值gamma(γ),gamma的计算方式为alpha减去middle点与x轴的夹角弧度值(反正切值, (0,0)为原点) */ const gamma = alpha - Math.atan2(middle.y, middle.x); // console.log("gamma:", gamma) /** ?? * 计算动态的距离,其中点middle的坐标为(x, y),gamma为一个角度值。具体实现过程如下: * 首先,根据middle点的坐标,计算出该点到原点(0,0)的距离,即sqrt(x^2 + y^2)。 * 然后,根据gamma的正弦值,计算出一个缩放因子,即sin(gamma)。 * 最后,将距离与缩放因子相乘,并取最大值为0,得到最终的距离值。 * 作用是根据给定的角度和一个点的坐标,计算出一个距离值,该距离值表示了该点到原点的最远距离。 */ const distance = Math.max(0, Math.sin(gamma) * Math.sqrt(Math.pow(middle.x, 2) + Math.pow(middle.y, 2))); // console.log("distance:", distance) /** ?? * 根据middle点到原点的距离和alpha角度计算并返回一个二维点。 */ const tr = point2D(distance * Math.sin(alpha), distance * Math.cos(alpha)); // console.log("tr:", tr) if (alpha > A90) { tr.x = tr.x + Math.abs(tr.y * rel.y / rel.x); tr.y = 0; if (Math.round(tr.x * Math.tan(PI - alpha)) < page.height) { point.y = Math.sqrt(Math.pow(page.height, 2) + 2 * middle.x * rel.x); if (top) point.y = page.height - point.y; return compute(point); } } const mv = point2D(0, 0); if (alpha > A90) { const beta = PI - alpha; const dd = pageDiagonalLength - page.height / Math.sin(beta); mv.x = Math.round(dd * Math.cos(beta)); mv.y = Math.round(dd * Math.sin(beta)); if (left) mv.x = -mv.x; if (top) mv.y = -mv.y; } /** * 中缝(订口边)与上边(从上翻)或下边(从下翻)(书顶或书底)未翻开的长度 */ const px = Math.round(tr.y / Math.tan(alpha) + tr.x); /** * 上边(从上翻)或下边(从下翻)(书顶或书底)翻开的长度 */ const side = page.width - px; /** * 翻开书页的书角顶点与上边(从上翻)或下边(从下翻)(书顶或书底)的距离 */ const sideX = side * Math.cos(alpha * 2); /** * 翻开书页的顶点与翻口的距离 */ const sideY = side * Math.sin(alpha * 2); /** * 翻开书页的书角顶点与中缝(订口边)和上边(从下翻)或下边(从上翻)(书顶或书底)的相对距离 */ const df = point2D( Math.round((left ? side - sideX : px + sideX)), Math.round(top ? sideY : center ? 0 : page.height - sideY) ); /** * 翻开的斜边长度 */ const gradientSize = side * Math.sin(alpha); /** * 结束点(翻开书页的书角顶点最终的位置) */ const endingPoint = getEndingPoint(pageCorner, page); const far = Math.sqrt(Math.pow(endingPoint.x - point.x, 2) + (center ? 0 : Math.pow(endingPoint.y - point.y, 2))) / page.width; /** * 翻开书角的阴影透明度 */ const shadowVal = Math.sin(A90 * ((far > 1) ? 2 - far : far)); /** * 翻开书角的斜边阴影透明度 */ const gradientOpacity = Math.min(far, 1); /** * 翻开书角的斜边阴影颜色初始透明度 */ const gradientStartVal = gradientSize > 100 ? (gradientSize - 100) / gradientSize : 0; /** * */ const gradientEndPointA = point2D( gradientSize * Math.sin(alpha) / page.width * 100, gradientSize * Math.cos(alpha) / page.height * 100); if (center) gradientEndPointA.y = 100 - gradientEndPointA.y; /** * */ const gradientEndPointB = point2D( gradientSize * 1.2 * Math.sin(alpha) / page.width * 100, gradientSize * 1.2 * Math.cos(alpha) / page.height * 100); if (!left) gradientEndPointB.x = 100 - gradientEndPointB.x; if (!top) gradientEndPointB.y = 100 - gradientEndPointB.y; tr.x = Math.round(tr.x); tr.y = Math.round(tr.y); return { alpha: alpha, df: df, tr: tr, mv: mv, shadowVal: shadowVal, gradientOpacity: gradientOpacity, gradientStartVal: gradientStartVal, gradientEndPointA: gradientEndPointA, gradientEndPointB: gradientEndPointB } } /** * 转换样式 * @param page 包含页面的长和宽:{ width: w, height: h } * @param a 角度 * @param tr * @param computeResult 计算结果 * @param position 定位,数字数组:[left, top, right, bootom],取值(0,1)0:0,1:auto * @param origin 变换原点,数字数组:[x, y] */ const transformStyle = (a, tr, computeResult, position, origin) => { const top = staticParam.pointVerticalPosition == 't'; const center = staticParam.pointVerticalPosition == "c" const left = staticParam.mode != 'half' && staticParam.moveHorizontalPosition == 'l'; const page = staticParam.page; const { df, mv, shadowVal, gradientOpacity, gradientStartVal, gradientEndPointA, gradientEndPointB } = computeResult; // 获取页面对角线长度 const pageDiagonalLength = getPageDiagonalLength(page); // 定位选项 const positionOpt = ['0', 'auto']; const mvW = (page.width - pageDiagonalLength) * origin[0] / 100; const mvH = (page.height - pageDiagonalLength) * origin[1] / 100; const positionStyle = { left: positionOpt[position[0]], top: positionOpt[position[1]], right: positionOpt[position[2]], bottom: positionOpt[position[3]] }; const aliasingFk = (a != 90 && a != -90) ? 1 : 0; const transformOrigin = origin[0] + '% ' + origin[1] + '%'; const style = {}; // 页面正面外框样式 style.wrapper = { transform: translate(-tr.x + mvW - aliasingFk, -tr.y + mvH) + rotate(-a), transformOrigin: transformOrigin, }; // 页面正面内容框样式 style.content = { ...positionStyle, transform: rotate(a) + translate(tr.x + aliasingFk, tr.y), transformOrigin: transformOrigin, }; // 页面背面外框样式 style.bwrapper = { transform: translate(-tr.x + mv.x + mvW, -tr.y + mv.y + mvH) + rotate(-a), transformOrigin: transformOrigin, }; // 页面背面内容框样式 style.bcontent = { ...positionStyle, transform: rotate(a) + translate(tr.x + df.x - mv.x - page.width * origin[0] / 100, tr.y + df.y - mv.y - page.height * origin[1] / 100) + rotate((180 / a - 2) * a), transformOrigin: transformOrigin, boxShadow: '0 0 20px rgba(0,0,0,' + Math.max(0.3, 0.5 * shadowVal) + ')' }; if (origin[0]) gradientEndPointA.x = 100 - gradientEndPointA.x; if (origin[1]) gradientEndPointA.y = 100 - gradientEndPointA.y; // 翻开斜边样式 style.gradient = gradientStyle(page, point2D(left ? 0 : 100, top ? 0 : 100), point2D(gradientEndPointB.x, gradientEndPointB.y), [ [0.6, 'rgba(0,0,0,0)'], [0.8, 'rgba(0,0,0,' + (0.3 * gradientOpacity) + ')'], [1, 'rgba(0,0,0,0)'] ], 3); // 翻开背面斜边样式 style.bgradient = gradientStyle(page, point2D(left ? 100 : 0, top ? 0 : 100), point2D(gradientEndPointA.x, gradientEndPointA.y), [ [gradientStartVal, 'rgba(0,0,0,0)'], [((1 - gradientStartVal) * 0.8) + gradientStartVal, 'rgba(0,0,0,' + (0.2 * gradientOpacity) + ')' ], [1, 'rgba(255,255,255,' + (0.2 * gradientOpacity) + ')'] ], 3); return style; } const gradientStyle = (page, p0, p1, colors, numColors) => { let j; const cols = []; // for (j = 0; j < numColors; j++) { // cols.push('color-stop(' + colors[j][0] + ', ' + colors[j][1] + ')'); // } // return { // backgroundImage: '-webkit-gradient(linear, ' + // p0.x + '% ' + // p0.y + '%,' + // p1.x + '% ' + // p1.y + '%, ' + // cols.join(',') + ' )' // } p0 = { x: p0.x / 100 * page.width, y: p0.y / 100 * page.height }; p1 = { x: p1.x / 100 * page.width, y: p1.y / 100 * page.height }; const dx = p1.x - p0.x; const dy = p1.y - p0.y; const angle = Math.atan2(dy, dx); const angle2 = angle - Math.PI / 2; const diagonal = Math.abs(page.width * Math.sin(angle2)) + Math.abs(page.height * Math.cos(angle2)); const gradientDiagonal = Math.sqrt(Math.pow(dy, 2) + Math.pow(dx, 2)); const corner = point2D((p1.x < p0.x) ? page.width : 0, (p1.y <= p0.y) ? page.height : 0); const slope = Math.tan(angle); const inverse = slope == 0 ? 0 : -1 / slope; const x = inverse - slope == 0 ? 0 : (inverse * corner.x - corner.y - slope * p0.x + p0.y) / (inverse - slope); const c = { x: x, y: inverse * x - inverse * corner.x + corner.y }; const segA = (Math.sqrt(Math.pow(c.x - p0.x, 2) + Math.pow(c.y - p0.y, 2))); for (j = 0; j < numColors; j++) { cols.push(' ' + colors[j][1] + ' ' + ((segA + gradientDiagonal * colors[j][0]) * 100 / diagonal) + '%'); } return { backgroundImage: 'linear-gradient(' + (Math.PI / 2 + angle) + 'rad,' + cols.join(',') + ')' } } /** * 将角度从弧度转换为度 * @param radians */ const radians2degrees = (radians) => { return radians / PI * 180; } /** * 返回CSS旋转值 */ const rotate = (degrees) => { return ' rotate(' + degrees + 'deg) '; } /** * 返回CSS变换值 */ const translate = (x, y) => { return ' translate(' + x + 'px, ' + y + 'px) '; } /** * 获取页面角落坐标 * @param corner 角落位置,'tl'、'tr'、'bl'、'br'、'l' 和 'r',分别代表左上角、右上角、左下角、右下角、左边和右边。 * @param page 包含页面的长和宽:{ width: w, height: h } * @param opts 偏移量 */ const getPageCornersCoordinates = (corner, page, opts) => { opts = opts || 0; switch (corner) { case 'tl': // 左上角 return point2D(opts, opts); case 'tr': // 右上角 return point2D(page.width - opts, opts); case 'bl': // 左下角 return point2D(opts, page.height - opts); case 'br': // 右下角 return point2D(page.width - opts, page.height - opts); case 'l': // 左边 return point2D(opts, 0); case 'r': // 右边 return point2D(page.width - opts, 0); } } /** * 获取结束点(翻开书页的书角顶点最终的位置) * @param corner 角落位置,'tl'、'tr'、'bl'、'br'、'l' 和 'r',分别代表左上角、右上角、左下角、右下角、左边和右边。 * @param page 包含页面的长和宽:{ width: w, height: h } */ const getEndingPoint = (corner, page) => { switch (corner) { case 'tl': return point2D(page.width * 2, 0); case 'tr': return point2D(-page.width, 0); case 'bl': return point2D(page.width * 2, page.height); case 'br': return point2D(-page.width, page.height); case 'l': if (staticParam.mode == 'half') { return point2D(page.width, 0); } else { return point2D(page.width * 2, 0); } case 'r': return point2D(-page.width, 0); } } const requestAnimation = (callback) => { let windowRequestAnimationFrame; if (window) { windowRequestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame; } if (windowRequestAnimationFrame) { windowRequestAnimationFrame(callback); } else { setTimeout(callback, 1000 / 60); } } /** * 创建动画 */ const createAnimation = (param) => { if (param) { // 动画开始时间 const startTime = new Date().getTime(); param.duration = param.duration || 0; param.count = 0; animationFun(startTime, param); } } const animationFun = (startTime, param) => { // 动画已执行时间 let timeDiff = Math.min(param.duration, (new Date()).getTime() - startTime); // 变化值 const transformValues = []; if (param.from && param.to) { if (!param.from.length) param.from = [param.from]; if (!param.to.length) param.to = [param.to]; // 最小变换参数长度 const valueLength = Math.min(param.from.length, param.to.length); // 计算当前执行的参数值 for (let i = 0; i < valueLength; i++) { transformValues.push(easing(timeDiff, param.from[i], param.to[i], param.duration)); } } if (param.frame) param.frame((transformValues.length == 1) ? transformValues[0] : transformValues); // 判断动画是否结束 if (timeDiff == param.duration) { if (param.complete) param.complete(); } else { requestAnimation(() => { animationFun(startTime, param); }); } } const easing = (timeDiff, from, to, duration) => { const t = timeDiff / duration - 1; return (to - from) * Math.sqrt(1 - Math.pow(t, 2)) + from; }