js+svg实现翻书翻页效果代码

js+svg实现翻书翻页效果代码

<!DOCTYPE html>
<html lang="en" >

<head>

  <meta charset="UTF-8">

  
  
<style>
body {
  display: grid;
  min-height: 100vh;
  place-content: center;
  margin: 0;
  padding: 0;
  overflow: hidden;
  background: linear-gradient(135deg, #fff 0%, #ddd 55%);
}
a.how-to {
  position: fixed;
  top: 0;
  left: 0;
  padding: 5vmin;
  color: #222;
  text-shadow: 0 0 1px #000a;
}
.wrap {
  width: 80vmin;
  height: 60vmin;
  position: relative;
  filter: drop-shadow(0 0 10px #0003);
}
.point {
  position: absolute;
  width: 1em;
  height: 1em;
  background: red;
  pointer-events: none;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  z-index: 10;
}
.point.r,
.point.t,
.point.q,
.point.s {
  opacity: 0.1;
}
.point.p {
  pointer-events: unset;
  cursor: move;
  transform: translate(-50%, -50%) scale(1.25);
  text-indent: -100%;
  overflow: hidden;
  border-radius: 50%;
}
.page {
  width: 50%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}
.page.front {
  z-index: 1;
  width: 100%;
  background: linear-gradient(#222, #444, #333);
  box-sizing: border-box;
  color: #eee;
}
.page.front > .content {
  display: grid;
  place-content: center;
  height: 100%;
}
.page.back {
  z-index: 2;
}
.page.back > .content {
  width: 100%;
  height: 100%;
}
.page.back > .content img {
  width: 100%;
  height: 100%;
  display: block;
  background: linear-gradient(#222, #444, #333);
}
.page.bottom {
  z-index: 0;
  -webkit-user-select: auto;
     -moz-user-select: auto;
      -ms-user-select: auto;
          user-select: auto;
  overflow: auto;
  -webkit-clip-path: url(#bar);
          clip-path: url(#bar);
  position: relative;
  overflow: hidden;
  display: grid;
  place-content: center;
  color: white;
  filter: drop-shadow(0 0 10px red);
  transition: all 0.3s;
}
.page.bottom a {
  text-decoration: underline;
  color: #eee;
}
.page.bottom::after {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  transform: scale(1.3);
  background: #333;
  -webkit-clip-path: url(#bar);
          clip-path: url(#bar);
}
.page.bottom:before {
  content: "";
  position: absolute;
  left: -50%;
  top: -50%;
  z-index: -1;
  display: block;
  width: 200%;
  height: 200%;
  background: repeating-conic-gradient(lightblue, transparent, lightsalmon, transparent, lightblue 90deg);
  -webkit-animation: 10s f infinite linear;
          animation: 10s f infinite linear;
}
@-webkit-keyframes f {
  to {
    transform: rotate(1turn);
  }
}
@keyframes f {
  to {
    transform: rotate(1turn);
  }
}
.page.bottom:hover {
  filter: drop-shadow(0 0 10px purple);
  background: transparent;
}
.canvas {
  width: 100%;
  height: 100%;
  background: #0003;
  display: none;
}
</style>



</head>

<body  >
  
<div class="wrap">
  <div class="point p">P</div>
  <div class="point q">Q</div>
  <div class="point r">R</div>
  <div class="point s">S</div>
  <div class="point t">T</div>
  <div class="page bottom">
    <div class="content">牛</div>
  </div>
  <div class="page front"> 
    <div class="content">牛年大吉</div>
  </div>
  <div class="page back">
    <div class="content"><img src="//repo.bfw.wiki/bfwrepo/image/60330330a3a9e.png"/></div>
  </div>
  <canvas class="canvas"></canvas>
</div>
<svg width="0" height="0">
  <defs>
    <clipPath id="bar" clipPathUnits="objectBoundingBox">
      <path d="M0 0C.0507.1646.0973.3338.0999.5071.0901.6669.0465.8221.0065.9764 -0.0122 1.0248 .0695.9693 .0987.9702.1940.9438.2906.9210.3888.9081.4249.9398.4618.9707.5 1 .5379.9723.5687.9252.6091.9079.7420.9257.8723.9595 1 1 .9492.8353.9026.6661.9000.4928.9098.3330.9534.1778.9934.0235 1.0122-0.0248.9304.0306.9012.0297.8059.0561.7093.0789.6111.0918.5750.0601.5381.0292.5 0 .4620.0276.4312.0747.3908.0920.2579.0742.1276.0404 0 0ZM.4999.0500C.5206.0701.5829.1074.5199.0996.4851.1007.4257.1057.4794.0698.4862.0632.4930.0565.4999.0500ZM.15.15C.2046.1613.2598.1705.3151.1779.2905.2271.2437.2101.2001.2.2110.2507.2239.3024.1769.3384.1699.2753.1610.2125.15.15ZM.8501.15C.8416.2074.8339.2649.8271.3226.7885.2891.7869.2468.8.2.7599.2145.7237.2158.7007.1757.7507.1686.8006.1601.8501.15ZM.6418.1830C.6564.2009.6983.2302.6493.2255.6241.2340.5690.1728.6240.1850.6300.1843.6359.1837.6418.1830ZM.3698.1843C.4025.1846.4262.1871.3923.2132.3767.2430.3102.2258.3570.1987.3612.1939.3655.1891.3698.1843ZM.7789.3632C.8131.4209.8175.4908.8074.5557.8007.5787.7707.6578.7730.5967.7629.5187.7697.4409.7789.3632ZM.2240.3737C.2354.4581.2334.5432.2220.6275.2141.6380.1947.5764.1911.5546.1852.4980.1840.4371.2163.3874L.2201.3806ZM.8230.6615C.8300.7246.8389.7874.85.85.7953.8386.7401.8294.6848.8220.7094.7728.7562.7898.7998.8.7898.7461.7743.7011.8230.6615ZM.1728.6773C.2114.7108.2130.7531.2.8.2400.7854.2762.7841.2992.8242.2492.8313.1993.8398.1498.85.1583.7925.1660.7350.1728.6773ZM.6213.7710C.6536.7711.6753.7762.6429.8012.6322.8266.5604.8166.6031.7918.6092.7849.6152.7780.6213.7710ZM.3679.7723C.3848.7926.4258.8200.3759.8149.3514.8257.2983.7630.3506.7744.3563.7737.3621.7729.3679.7723ZM.5.9C.5243.9004.5674.8961.5288.9219.5090.9786.4535.9137.4525.9018.4683.9007.4841.9000.5.9Z"></path>
    </clipPath>
  </defs>
</svg>

  
      <script >
//
//   A-------B-------E
//   |       |       |
//   |       |       |
//   |       |       |
//   C-------D-------F

const elWrap = document.querySelector('.wrap');
const elP = elWrap.querySelector('.p');
const elQ = elWrap.querySelector('.q');
const elR = elWrap.querySelector('.r');
const elS = elWrap.querySelector('.s');
const elT = elWrap.querySelector('.t');
const elBack = elWrap.querySelector('.back');
const elFront = elWrap.querySelector('.front');

const P = class P {
  constructor(x, y, el) {
    this.x = x;
    this.y = y;
    this.el = el;
  }
  clone() {
    return new P(this.x, this.y);
  }
  copy(p) {
    this.x = p.x;
    this.y = p.y;
    return this;
  }
  scale(sx, sy) {
    this.x *= sx;
    this.y *= sy;
    return this;
  }
  distTo(p) {
    return Math.hypot(p.y - this.y, p.x - this.x);
  }
  update() {
    this.el.style.left = `${this.x}px`;
    this.el.style.top = `${this.y}px`;
  }
  show() {
    this.el.style.display = 'unset';
  }
  hide() {
    this.el.style.display = 'none';
  }
  static MidPoint(a, b) {
    return new P((a.x + b.x) / 2, (a.y + b.y) / 2);
  }
  static Slope(a, b) {
    return (a.y - b.y) / (a.x - b.x);
  }};


const $w = 0.5;
const $h = 1.0;
const $A = new P(0.0, 0.0);
const $B = new P($A.x + $w, $A.y);
const $C = new P($A.x, $A.y + $h);
const $D = new P($A.x + $w, $A.y + $h);
const $E = new P($A.x + 2 * $w, $A.y);
const $F = new P($A.x + 2 * $w, $A.y + $h);

// ---- Pointer coords
const pointer = (() => {
  const { width, height } = elWrap.getBoundingClientRect();
  return new P(width * 0.5, height * 0.7);
})();
let isDragging = false;
elP.addEventListener('pointerdown', () => isDragging = true);
window.addEventListener('pointerup', () => isDragging = false);
window.addEventListener('pointermove', e => {
  const { x, y } = elWrap.getBoundingClientRect();
  pointer.x = e.clientX - x;
  pointer.y = e.clientY - y;
  if (isDragging) {
    fold();
  }
});

const pP = new P(0, 0, elP);
const pQ = new P(0, 0, elQ);
const pR = new P(0, 0, elR);
const pS = new P(0, 0, elS);
const pT = new P(0, 0, elT);
const pA = new P(0, 0, null);
const pB = new P(0, 0, null);
const pC = new P(0, 0, null);
const pD = new P(0, 0, null);
const pE = new P(0, 0, null);
const pF = new P(0, 0, null);

function fold() {
  const wrapRect = elWrap.getBoundingClientRect();
  pA.copy($A).scale(wrapRect.width, wrapRect.height);
  pB.copy($B).scale(wrapRect.width, wrapRect.height);
  pC.copy($C).scale(wrapRect.width, wrapRect.height);
  pD.copy($D).scale(wrapRect.width, wrapRect.height);
  pE.copy($E).scale(wrapRect.width, wrapRect.height);
  pF.copy($F).scale(wrapRect.width, wrapRect.height);

  // --- constraint pP
  {
    if (pointer.y < pD.y) {
      const d = pD.distTo(pointer);
      const D = $w * wrapRect.width;
      if (d > D) {
        const ang = Math.atan2(pointer.y - pD.y, pointer.x - pD.x);
        pP.x = D * Math.cos(ang) + pD.x;
        pP.y = D * Math.sin(ang) + pD.y;
      } else {
        pP.copy(pointer);
      }
    } else {
      const d = pB.distTo(pointer);
      const D = pB.clone().distTo(pC);
      if (d > D) {
        const low = Math.atan2($h * wrapRect.height, $w * wrapRect.width) * 1.0001;
        const high = (Math.PI - low) * 0.9999;
        const ang = Math.max(low, Math.min(high, Math.atan2(pointer.y - pB.y, pointer.x - pB.x)));
        pP.x = D * Math.cos(ang) + pB.x;
        pP.y = D * Math.sin(ang) + pB.y;
      } else {
        pP.copy(pointer);
      }
    }

    pP.update();
  }

  const mid = P.MidPoint(pC, pP);
  const slope = -1 / P.Slope(pC, pP);
  const c = mid.y - slope * mid.x;

  // y = slope * x + c 
  // x = (y - c) / slope

  pQ.y = pC.y;
  pQ.x = (pQ.y - c) / slope;
  pQ.update();

  pR.x = pC.x;
  pR.y = slope * pR.x + c;
  pR.update();

  pR.hide();
  pS.hide();
  pT.hide();

  let mode = '';

  if (pR.y > pA.y && pR.y < pC.y) {// PQR mode (P=back bot right corner)
    mode = 'PRQ';
    pR.show();
  } else {
    mode = 'PQST';

    // find S
    pS.y = pA.y;
    pS.x = (pS.y - c) / slope;
    pS.update();
    pS.show();

    // find T
    // # line ST has same slope of PQ
    const slope_PQ = P.Slope(pP, pQ);
    // y = slope * x + k
    // k = pS.y - slope * pS.x
    const k = pS.y - slope_PQ * pS.x;
    // y = slope * x + (k) ... (!1)
    // # line PR
    // (pP.y-pR.y)/(pP.x-pR.x)=(y-pR.y)/(x-pR.x)
    const m = (pP.y - pR.y) / (pP.x - pR.x);
    // y = m * (x - pR.x) + pR.y ... (!2)
    // # !2 into !1
    // m * (x - pR.x) + pR.y = slope * x + k
    // m * x - slope * x = k + m * pR.x - pR.y
    // x = (k + m * pR.x - pR.y) / (m - slope)
    pT.x = (k + m * pR.x - pR.y) / (m - slope_PQ);
    pT.y = slope_PQ * pT.x + k;
    pT.update();
    pT.show();
  }

  //// set page position
  elBack.style.left = `${pP.x}px`;
  elBack.style.top = `${pP.y}px`;
  const rot = Math.atan2(pP.y - pQ.y, pP.x - pQ.x);
  elBack.style.transformOrigin = 'top left';
  elBack.style.transform = `rotate(${rot}rad) translate(-100%, -100%)`;

  //// set clippath
  if (mode == 'PRQ') {
    const yR = wrapRect.height - pP.distTo(pR);
    const xQ = wrapRect.width / 2 - pP.distTo(pQ);
    elBack.style.clipPath = `polygon(100% 100%, 100% ${yR}px, ${xQ}px 100%)`;
    {
      const xQ = pC.distTo(pQ);
      elFront.style.clipPath = `polygon(100% 100%, 100% 0, 0 0, 0 ${yR}px, ${xQ}px 100%)`;
    }
  } else {
    const xQ = wrapRect.width / 2 - pP.distTo(pQ);
    const xS = wrapRect.width / 2 - pS.distTo(pT);
    elBack.style.clipPath = `polygon(100% 100%, 100% 0, ${xS}px 0, ${xQ}px 100%)`;
    {
      const xS = pA.distTo(pS);
      const xQ = pC.distTo(pQ);
      elFront.style.clipPath = `polygon(100% 100%, 100% 0, ${xS}px 0, ${xQ}px 100%)`;
    }
  }
}

let timer;
window.addEventListener('resize', () => {
  clearTimeout(timer);
  timer = setTimeout(() => {
    pointer.x = 0;
    pointer.y = 0;
    fold();
  }, 20);
});
fold();


// // DEBUG
// const can = document.querySelector('.canvas');
// let ctx;
// window.addEventListener('resize', () => {
//   can.width = can.getBoundingClientRect().width;
//   can.height = can.getBoundingClientRect().height;
//   ctx = can.getContext('2d');
// });
// window.dispatchEvent(new Event('resize'));
// setInterval(() => {
//   const drawLine = (p, q) => {
//     ctx.beginPath();
//     ctx.moveTo(p.x, p.y);
//     ctx.lineTo(q.x, q.y);
//     ctx.stroke();
//   }
//   ctx.clearRect(0, 0, can.width, can.height);

//   drawLine(pR, pQ);
//   drawLine(pP, pQ);
//   drawLine(pP, pR);

//   drawLine(pQ, pS);
//   drawLine(pS, pT);
//   drawLine(pT, pP);
//   drawLine(pP, pQ);
// });

    </script>

  

</body>

</html>
 

网友评论0