扭蛋机

Published on
/
/趣玩前端
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>扭蛋机</title>
    <style>
      * {
        box-sizing: border-box;
        user-select: none;
      }
      body {
        padding: 0;
        margin: 0;
        font-family: sans-serif;
        background-color: #ff3636;
        overscroll-behavior: contain;
      }
      p,
      h1,
      h2,
      h3,
      h4 {
        display: inline-block;
        margin-block-start: 0em;
        margin-block-end: 0em;
        margin-inline-start: 0px;
        margin-inline-end: 0px;
        padding-inline-start: 0px;
      }
      .wrapper {
        position: absolute;
        width: 100%;
        height: 100%;
        overflow: hidden;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .pix {
        background-size: calc(var(--w) * var(--m)) calc(var(--h) * var(--m)) !important;
        background-repeat: no-repeat !important;
        image-rendering: pixelated;
      }
      .toy-box {
        position: absolute;
        top: -84px;
        width: 320px;
        height: 64px;
      }
      .capsule-machine {
        position: relative;
        width: 320px;
        height: 500px;
        --m: 4;
        --w: 80px;
        --h: 125px;
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB9CAYAAAA1I+RFAAAAAXNSR0IArs4c6QAAAchJREFUeF7t08Ftg0AARcFsPS7QNbhA10O0SGuR2Ipw3tHDgcOKx2H4jK8X13a/bq/OP/1sXG7jt8GPA3DnJnKEfADCO4e3nlqIOyC89/COiAD/Z7dXc4XD+oLgRAQIsAnE2gIBRoGYWyDAKBBzCwQYBWJugQCjQMwtEGAUiLkFAowCMbdAgFEg5hYIMArE3AIBRoGYWyDAKBBzCwQYBWJugQCjQMwtEGAUiLkFAowCMbdAgFEg5hYIMArE3AIBRoGYWyDAKBBzCwQYBWJugQCjQMwtEGAUiLkFAowCMbdAgFEg5hYIMArE3AIBRoGYWyDAKBBzCwQYBWJugQCjQMwtEGAUiLkFAowCMbdAgFEg5hYIMArE3AIBRoGYWyDAKBBzCwQYBWJugQCjQMwtEGAUiLkFAowCMbdAgFEg5hYIMArE3AIBRoGYWyDAKBBzCwQYBWJugQCjQMwtEGAUiLkFAowCMbdAgFEg5hYIMArE3AIBRoGYWyDAKBBzCwQYBWJugQCjQMwtEGAUiLkFAowCMbdAgFEg5hYIMArEfMx+u1+3+J6PzMflNgCGT/8AtML3FSferPbbuvzK5yAX3hMgyL8Bj3DryW9l1KLLccC7FgAAAABJRU5ErkJggg==);
      }
      .capsule-machine.shake {
        animation: forwards shake 0.5s;
      }
      @keyframes shake {
        0%,
        40%,
        80% {
          margin-left: 5px;
        }
        20%,
        60%,
        100% {
          margin-left: -5px;
        }
      }
      .capsule-machine::after {
        position: absolute;
        content: '';
        width: calc(var(--w) * var(--m));
        height: calc(var(--h) * var(--m));
        background-size: calc(var(--w) * var(--m)) calc(var(--h) * var(--m)) !important;
        background-repeat: no-repeat !important;
        image-rendering: pixelated;
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB9CAYAAAA1I+RFAAAAAXNSR0IArs4c6QAAAg9JREFUeF7t3VFOg1AABVHZ/6JrIKFWJeXB8FVOP5FL0tOpGNPo9LXxeDwej63jdz82TdP01+DXgRVu68S7483Pf8vnCTh/EdxYJq9WCyC8MbjXs1YzgMftlsUTUH0nBVdEgADPC8TlEp8CzysCPG/3cyNR4HlFBZ63U2C0AwjwCoF4Dd8DAUaBOFcgwCgQ5woEGAXiXIEAo0CcKxBgFIhzBQKMAnGuQIBRIM4VCDAKxLkCAUaBOFcgwCgQ5woEGAXiXIEAo0CcKxBgFIhzBQKMAnGuQIBRIM4VCDAKxLkCAUaBOFcgwCgQ5woEGAXiXIEAo0CcKxBgFIhzBQKMAnGuQIBRIM4VCDAKxLkCAUaBOFcgwCgQ5woEGAXiXIFXAcbr3Hq+/Pm7WwvEJw8QYBSIcwUCjAJxrkCAUSDOFQgwCsS5AgFGgThXIMAoEOcKBBgF4lyBAKNAnCsQYBSIcwUCjAJxrkCAUSDOFQgwCsS5AgGOC+z9+98zn9K4RYF7cH9fgiOQHw94FG/FHEX8aMCzeEcQPxaw4o0iAty5B+29lT8S8Kr6RioEOPBT0LsKAQL8L+AtPFDFu1MAAowCca5AgFEgzhUIMArEuQIBRoE4VyDAKBDnCoyA8/wqxFv+PvAC/+FLTPOZe8rDV7vZiXPlAMOL/gRU4XHFNb6lwPXhrTwG+XqD+gUI8j3g1p39G7BRXt/gk08UAAAAAElFTkSuQmCC);
        pointer-events: none;
        z-index: 1;
        transition: 0.5s;
      }
      .capsule-machine.see-through::after,
      .capsule-machine.see-through .circle {
        opacity: 0.3;
      }
      .wrapper.lock {
        pointer-events: none;
      }
      .lock-cover {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        transition: 1s;
        background-color: transparent;
        pointer-events: none;
      }
      .wrapper.lock .lock-cover {
        background-color: #fab2cc;
        opacity: 0.8;
        z-index: 10;
      }
      .capsule {
        position: absolute;
        left: calc((var(--w) * var(--m)) / var(--offset));
        top: calc((var(--h) * var(--m)) / var(--offset));
        width: calc(var(--w) * var(--m));
        height: calc(var(--h) * var(--m));
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .lid,
      .base {
        position: absolute;
        --h-m: var(--m) / 2;
        width: calc(var(--w) * var(--m));
        height: calc(var(--h) * var(--h-m));
        background-size: calc(var(--w) * var(--m)) calc(var(--h) * var(--h-m)) !important;
        background-repeat: no-repeat !important;
        image-rendering: pixelated;
      }
      .lid {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAAAXNSR0IArs4c6QAAADlJREFUKFNjZEAD/////48uhsxnZGRkROHDODCN6ApwWQBTBzYNpJmQRmwGgfQwkqMZ2dWjBvz/DwCs+Tv1GZ2dFQAAAABJRU5ErkJggg==);
        top: 0;
        --start: 0;
        --end: -100px;
      }
      .base {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAAAXNSR0IArs4c6QAAADBJREFUKFNj/O/Y/p+BAsA4agADIyj8yA0Hxv2VjGADyDEEpBmkD24AsQbBNMLUAwDsrRnng7/G/QAAAABJRU5ErkJggg==);
        bottom: 0;
        --start: 0;
        --end: 100px;
      }
      .base.white {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAAAXNSR0IArs4c6QAAAC1JREFUKFNj/P///38GCgDjqAEMjKDwIzccGEEAFgGkGgLTCzeAWIOQLQXpAQC80x/1FKzGmwAAAABJRU5ErkJggg==);
      }
      .base.pink {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAAAXNSR0IArs4c6QAAADBJREFUKFNj/O/Y/p+BAsA4agADIyj8yA0Hxv2VjGADyDEEpBmkD24AsQbBNMLUAwDsrRnng7/G/QAAAABJRU5ErkJggg==);
      }
      .base.red {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAAAXNSR0IArs4c6QAAADBJREFUKFNj/G9m9p+BAsA4agADIyj8yA0HxlOnGMEGkGMISDNIH9wAYg2CaYSpBwAEFBgbcBUePAAAAABJRU5ErkJggg==);
      }
      .base.blue {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAAAXNSR0IArs4c6QAAADBJREFUKFNj1Nn36D8DBYBx1AAGRlD4kRsOV5zkGMEGkGMISDNIH9wAYg2CaYSpBwAPYhoAi8QRLQAAAABJRU5ErkJggg==);
      }
      .capsule-wrapper.open .lid,
      .capsule-wrapper.open .base {
        animation: open-capsule forwards 1s;
      }
      @keyframes open-capsule {
        0% {
          transform: translateY(var(--start));
          opacity: 1;
        }
        100% {
          transform: translateY(var(--end));
          opacity: 0;
        }
      }
      .capsule-wrapper {
        position: absolute;
        --m: 4;
        --w: 16px;
        --h: 16px;
        --offset: -2;
        width: 0;
        height: 0;
        transition: 0.05s;
        cursor: pointer;
      }
      .capsule-wrapper.enlarge {
        --m: 8;
        z-index: 11;
        transition: 0.8s;
        pointer-events: none;
      }
      .capsule-wrapper.enlarge .toy {
        --m: 4;
      }
      .capsule-wrapper.enlarge.open .toy {
        --m: 6;
      }
      .capsule-wrapper.enlarge.open .toy.collected {
        --m: 2;
      }
      .line-start,
      .line-end {
        position: absolute;
        height: 0;
        width: 0;
      }
      .line {
        position: absolute;
        height: 1px;
        width: 0;
        border-top: solid #ff3636 4px;
      }
      .circle {
        position: absolute;
        bottom: 30px;
        left: 26px;
        width: 160px;
        height: 160px;
        display: flex;
        align-items: center;
        z-index: 5;
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAAXNSR0IArs4c6QAAAQJJREFUWEft2e0NAiEMBmAZRCfQIXRwHUIn0EG8YFJSsUD5CLSx98vkIPfkhdij53Yd1/H6fHOmPy4HxxlHjameiFH385713NPtFcbVYtlAgHFRKTlgudAicBQsBnOhWaDH9SZW2gMemkszCZyBA3wOSQJn4krIH+AKXA75BVyJSyH1ACWkR6UYEhQNlISLU/wkaMBSOSHuw5+3k5geXmYDNqxumOKX2RK0BHsS6J1re/A/ErRa3LjOoRarSFAaEh9DdbxRwzaR8OoVH+L1nOokpEi1QPS1PlYkWd08molsbr9hpP89uk84pIGJq9SoTisXBs8utoDjUiq2iU7V/BmfITaUSgYmL25tRwAAAABJRU5ErkJggg==);
        --w: 40px;
        --h: 40px;
        --m: 4;
        transition: 0.5s;
      }
      .circle::after {
        position: absolute;
        top: -10px;
        right: -22px;
        content: '';
        --w: 15px;
        --h: 15px;
        --m: 4;
        width: calc(var(--w) * var(--m));
        height: calc(var(--h) * var(--m));
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAAXNSR0IArs4c6QAAAGVJREFUOE9j1N376D8DErjsLMeIzMfHZkTXDFNMjCEotmAzCJ8hWJ1IrFfw+g/ZEGwuIBg4+AwgqBkUgDAD0G0nSTPIIGQDiNKMy3aSNSPbTrRmbLbTXjOuJEy0zdhSHdGasfkZAFdWOttEm2PiAAAAAElFTkSuQmCC);
        background-size: calc(var(--w) * var(--m)) calc(var(--h) * var(--m)) !important;
        background-repeat: no-repeat !important;
        image-rendering: pixelated;
      }
      .handle {
        position: relative;
        width: 160px;
        height: 50px;
        display: flex;
        justify-content: space-between;
        cursor: pointer;
      }
      .handle::after {
        position: absolute;
        top: -7px;
        content: '';
        --w: 40px;
        --h: 16px;
        --m: 4;
        width: calc(var(--w) * var(--m));
        height: calc(var(--h) * var(--m));
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAQCAYAAABk1z2tAAAAAXNSR0IArs4c6QAAANRJREFUSEvVlusNwjAMhMkgMAEdAgaHIWACGITKlU4K1plzQ4ho/+V1/nx2m5Zdh+d4ebyYzP18KN/KNwl4oNtpTzmm6/NtvgU4BZgFUm61AFPAXkA9gBfAUUAtwMXgoh5Sgmq9LqnFUGOvZ/spIBNCgCgQkrSzPmHM+bVoHqAhoG2oxbwQG9uZOgloINinxFhSOB+WWAECqE4GzisHo4TTJWYOeid8yVmJVc/59VWA6gUYsb704N9/ZpgTo76LmZtlm1ed6q+swxmHVKyUg0rkl79bMwwK5tMkyZ5mAAAAAElFTkSuQmCC);
        background-size: calc(var(--w) * var(--m)) calc(var(--h) * var(--m)) !important;
        background-repeat: no-repeat !important;
        image-rendering: pixelated;
      }
      .cover.white {
        position: absolute;
        bottom: 116px;
        width: 100%;
        height: calc(220px - 116px);
        background-color: white;
        z-index: 1;
        transition: 0.5s;
      }
      button {
        border: 0;
        background-color: white;
        padding: 4px 8px;
        font-family: 'Press Start 2P', sans-serif;
        font-size: 10px;
        margin-right: 10px;
      }
      .button-wrapper {
        position: absolute;
        left: 20px;
        bottom: 20px;
        z-index: 10;
      }
      .toy {
        position: absolute;
        --w: 16px;
        --h: 16px;
        --m: 2;
        width: calc(var(--w) * var(--m));
        height: calc(var(--h) * var(--m));
        z-index: -1;
        transition: 0.5s;
      }
      .bunny {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIJJREFUOE+tU1sOwCAIk/sfmkVmDc/oMvyktdQiNOLhVSIHpfVAYn55RAIB56K+CWhWEb8LKBcD3RNntYMkm+xpRsDY1F3ROctBh9gnAPtrEiYDn4MZ47ZQBYAPAmU15wnJ/Yn592tH4OBufwYtDg4RmG9+2oWtdbsLEuafbTy5D/gDlg9nEQXylfgAAAAASUVORK5CYII=);
      }
      .duck-yellow {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAG9JREFUOE9jZKAQMFKonwGXAf+xGIxVLTbB//9fBGHoZ5RYBxLDUI8ugFUzzDRshuA0AKQY3SU4Dfj//z/Czy+DwRZSbAC22CHJC/iiF9kgkgIR2VCYIcPQAJA38SYmWBTDUiUpeQElDOGpk9LcCAA2WjgR81D3SgAAAABJRU5ErkJggg==);
      }
      .duck-pink {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAG5JREFUOE9jZKAQMFKonwGXAf+xGIxVLTbB//8d2zH0M+6vBIlhqEcXwKoZZho2Q3AaAFKM7hKcBvx/EYTwc6Qp2EKKDcAWOyR5AV/0IhtEUiAiGwozZBgaAPIm3sQEi2JYqiQlL6CEITx1UpobAdngNhF5e7c/AAAAAElFTkSuQmCC);
      }
      .star {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAJRJREFUOE+1U0EOwCAIk1/4Lh/qu/yFi0QIIp1ZtnlZ4mxpC1B6eeiA7/M/fHdH0HsrjKdc+RMV+41Aq0tVpAIpeEwgYSXxjcKdSlTYUBBW80QDGN2JhYUEPbYEkonNYFNysMFYH+LSezsHaCYWgt6KhmkGKPKuuM8sbCG6HWBlMESRbnoMB8wSUa6kbTxtneuIbukFwYNUEDYqJhQAAAAASUVORK5CYII=);
      }
      .water-melon {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIdJREFUOE/Fk0sOwCAIRGHXQ3kaD9bT9FDubCAZgx+qjU3qTmEeSAamzcML+kxEbp4XEBEd16n8FCLqdPntQ4Zo1JmAJJ5CLDoLUDGSAPDugABQKr8ASA2uAKtiM5ePASDbQT7MoP/CNuDXGYhB1H2rXbQ+UN8AMtsPz4lWp90MznQXZsW7+A1tU4YRf0fGXgAAAABJRU5ErkJggg==);
      }
      .panda {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIVJREFUOE+1U1sOwCAIk/sf2gWirFSMLNv8UmzLwyrt5RLgd9hrHM96xTHj7gRa75EvglBLlQswkTsEoUVAs3LZ6YSEVQbK+PNu1wJg1hY+EcCaD9X8MIOHngoVlKafJHB3mEDxFf2l1Exsr6MVxmCdtwgU51AWmMDsY90fgrJO8K66EL8ABN49ETdlUwYAAAAASUVORK5CYII=);
      }
      .penguin {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAH5JREFUOE+1k9ENgDAIRGEL53JQ53ILTKvFeoAhQfvZ5q7HvZapuLioJzSQpKHqZoOseNzRtcZARIjZnwzOfjIY+WRfH3XwsmE9foLPDBI0TIKm6SRaWd6ayo0xRmJldxOKKbzFxxTmJSYTuCOc41/4EBvshwZaJLzScL/8Gw8v8zIR/QQO5QAAAABJRU5ErkJggg==);
      }
      .dino {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIpJREFUOE+lk90RABEMhKMRFahC4apQgUbcMGLiL8vcveVGPpvNMvTzMz/7SQLyBWy5kH9kFxLsj96+AaK3JMGlJhpU92KQz01PgJsRyvWzisVEBNIA1UQXkroNBKjNmgoIQCMIH+pGdkm8zcQegDyYN6EqaPN2T7iWYTo9Jt5Ej3rL+THK8B2cDnzs+TgRPHoJSgAAAABJRU5ErkJggg==);
      }
      .roboto-san {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAKZJREFUOE+1k+0RgCAIhnWQmqCGqMFriJqgBqFDhdBAu+7yp8DDy5d39oPC5DVX9TM5AkBkeB/cXgHKrJY+hkkqDMtRqeg27XPPih6AbercuJ4qiGz/Aih1ysJKZHlVBW+aYAEw9vMUOLA1iaK0uCGYmZYGJ4Dd1p600XIxgAJQQQ1gKaD45jLJBlr7HcpJErkS8ZfdhHYgEpD1SDsqFSAa+OhReZUXbdFUEQlelEMAAAAASUVORK5CYII=);
      }
      .roboto-sama {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAKNJREFUOE+1U20RgCAMZS2sIxFMowkkjRG0ji3mMWHiGILeyR85x/vYG4ApLxQl0I6qP8NBxH2gLXQLfVoIpGrJH5OlrIj9/NDRVYJtYkc5wToaY51OFGo/EwTtoMJO0vaeHTSkUCLw0E9TiCAfaHUSiTrh4hRYmXr1aWvLOiOyAVKNubGDOgHj5PV808J5zRWntSBvmNJj8iSZO02w5iALWZIcuqhDD326nv0AAAAASUVORK5CYII=);
      }
      .turtle {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAKlJREFUOE+1k8ENgCAMRcvNERjGKRjBwRzBKRiGEbzVtAECpUWjkQtJ7X/wf8XBx+UMPRr1oX8oLHG3xMw8163TSAAucacmoL1dpUY7AFTdP4Bycj6tu83dDaj5dQbsPwelDkF8Z/ttBjVATEEFOH+wndbGEKIlLkSCzKYAmMI0A+eP6X9AdJxZELa7DFhcPEqI5l+GWAHaJLQJaACG5MCGgLW69RofP/ILf2RUEUrHVC0AAAAASUVORK5CYII=);
      }
      .cover {
        position: absolute;
        z-index: 4;
        cursor: auto;
      }
      .cover.a {
        top: 0;
        left: 0;
        width: 320px;
        height: 280px;
      }
      .cover.b {
        top: 280px;
        left: 0;
        width: 212px;
        height: 220px;
      }
      .cover.c {
        top: 280px;
        left: 212px;
        width: 108px;
        height: 108px;
      }
      .cover.d {
        bottom: 0;
        left: 212px;
        width: 80px;
        height: 24px;
      }
      .cover.e {
        bottom: 0;
        left: 292px;
        height: 112px;
        width: 28px;
      }
      .sign {
        position: absolute;
        color: white;
        bottom: 10px;
        right: 10px;
        font-size: 10px;
      }
      a {
        color: white;
        text-decoration: none;
      }
      a:hover {
        text-decoration: underline;
      }
    </style>
  </head>
  <body>
    <body>
      <div class="wrapper">
        <div class="capsule-machine pix">
          <div class="toy-box"></div>
          <div class="lock-cover"></div>
          <div class="cover a"></div>
          <div class="cover b"></div>
          <div class="cover c"></div>
          <div class="cover d"></div>
          <div class="cover e"></div>
          <div class="circle pix"><div class="handle"></div></div>
        </div>
        <div class="button-wrapper">
          <button class="shake">摇一摇</button
          ><button class="see-inside">偷看</button>
        </div>
      </div>
    </body>
    <script>
      const settings = {
        capsuleNo: 20,
        isTurningHandle: false,
        isHandleLocked: false,
        handlePrevDeg: 0,
        handleDeg: 0,
        handleRotate: 0,
        flapRotate: 0,
        collectedNo: 0,
      };
      const elements = {
        wrapper: document.querySelector('.wrapper'),
        capsuleMachine: document.querySelector('.capsule-machine'),
        shakeButton: document.querySelector('.shake'),
        seeInsideButton: document.querySelector('.see-inside'),
        circle: document.querySelector('.circle'),
        handle: document.querySelector('.handle'),
        toyBox: document.querySelector('.toy-box'),
      };
      const vector = {
        x: 0,
        y: 0,
        create: function (x, y) {
          const obj = Object.create(this);
          obj.x = x;
          obj.y = y;
          return obj;
        },
        setXy: function ({ x, y }) {
          this.x = x;
          this.y = y;
        },
        setAngle: function (angle) {
          const length = this.magnitude();
          this.x = Math.cos(angle) * length;
          this.y = Math.sin(angle) * length;
        },
        setLength: function (length) {
          const angle = Math.atan2(this.y, this.x);
          this.x = Math.cos(angle) * length;
          this.y = Math.sin(angle) * length;
        },
        magnitude: function () {
          return Math.sqrt(this.x * this.x + this.y * this.y);
        },
        multiply: function (n) {
          return this.create(this.x * n, this.y * n);
        },
        addTo: function (v2) {
          this.x += v2.x;
          this.y += v2.y;
        },
        multiplyBy: function (n) {
          this.x *= n;
          this.y *= n;
        },
      };
      const rotatePoint = ({ angle, axis, point }) => {
        const a = degToRad(angle);
        const aX = point.x - axis.x;
        const aY = point.y - axis.y;
        return {
          x: aX * Math.cos(a) - aY * Math.sin(a) + axis.x,
          y: aX * Math.sin(a) + aY * Math.cos(a) + axis.y,
        };
      };
      const px = (num) => `${num}px`;
      const randomN = (max) => Math.ceil(Math.random() * max);
      const degToRad = (deg) => deg / (180 / Math.PI);
      const radToDeg = (rad) => Math.round(rad * (180 / Math.PI));
      const angleTo = ({ a, b }) => Math.atan2(b.y - a.y, b.x - a.x);
      const distanceBetween = (a, b) =>
        Math.round(Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)));
      const getPage = (e, type) =>
        e.type[0] === 'm' ? e[`page${type}`] : e.touches[0][`page${type}`];
      const calcCollectedX = () => (settings.collectedNo % 10) * 32;
      const calcCollectedY = () => Math.floor(settings.collectedNo / 10) * 32;
      const nearest360 = (n) =>
        n === 0 ? 0 : n - 1 + Math.abs(((n - 1) % 360) - 360);
      const setStyles = ({ el, x, y, w, deg }) => {
        if (w) el.style.width = w;
        el.style.transform = `translate(${x ? px(x) : 0},${
          y ? px(y) : 0
        })rotate(${deg || 0}deg)`;
      };
      const lineData = [
        {
          start: { x: 0, y: 280 },
          end: { x: 160, y: 360 },
          point: 'end',
          axis: 'start',
          id: 'flap_1',
        },
        {
          start: { x: 160, y: 360 },
          end: { x: 320, y: 280 },
          point: 'start',
          axis: 'end',
          id: 'flap_2',
        },
        {
          start: { x: 70, y: 340 },
          end: { x: 230, y: 490 },
          point: 'start',
          axis: 'end',
          id: 'ramp',
        },
      ];
      const getRandomToy = () => {
        return [
          'bunny',
          'duck-yellow',
          'duck-pink',
          'star',
          'water-melon',
          'panda',
          'dino',
          'roboto-san',
          'roboto-sama',
          'penguin',
          'turtle',
        ][randomN(11) - 1];
      };
      new Array(settings.capsuleNo).fill('').forEach(() => {
        const capsule = Object.assign(document.createElement('div'), {
          className: 'capsule-wrapper pix',
          innerHTML: `<div class="capsule"><div class="lid"></div><div class="${getRandomToy()} toy pix"></div><div class="base ${
            ['red', 'pink', 'white', 'blue'][randomN(4) - 1]
          }"></div></div>`,
        });
        elements.capsuleMachine.appendChild(capsule);
      });
      lineData.forEach(() => {
        [
          Object.assign(document.createElement('div'), {
            className: 'line-start',
            innerHTML: '<div class="line"></div>',
          }),
          Object.assign(document.createElement('div'), {
            className: 'line-end',
          }),
        ].forEach((ele) => {
          elements.capsuleMachine.appendChild(ele);
        });
      });
      const lineStarts = document.querySelectorAll('.line-start');
      const lines = document.querySelectorAll('.line');
      const lineEnds = document.querySelectorAll('.line-end');
      const toys = document.querySelectorAll('.toy');
      const { width: capsuleMachineWidth, height: capsuleMachineHeight } =
        elements.capsuleMachine.getBoundingClientRect();
      const handleAxis = () => {
        const { left: handleX, top: handleY } =
          elements.circle.getBoundingClientRect();
        const { top, left } = elements.capsuleMachine.getBoundingClientRect();
        return { x: handleX - left + 80, y: handleY - top + 80 };
      };
      const updateLines = () => {
        lineData.forEach((l, i) => {
          l.length = distanceBetween(l.start, l.end);
          setStyles({
            el: lineStarts[i],
            x: l.start.x,
            y: l.start.y,
            deg: radToDeg(angleTo({ a: l.start, b: l.end })),
          });
          setStyles({ el: lines[i], w: px(l.length) });
          setStyles({ el: lineEnds[i], x: l.end.x, y: l.end.y });
        });
      };
      const capsuleData = Array.from(
        document.querySelectorAll('.capsule-wrapper')
      ).map((c, i) => {
        const data = {
          ...vector,
          el: c,
          id: i,
          deg: 0,
          radius: 36,
          bounce: -0.3,
          friction: 0.99,
          toy: toys[i],
        };
        data.velocity = data.create(0, 1);
        data.velocity.setLength(10);
        data.velocity.setAngle(degToRad(90));
        data.setXy({
          x: randomN(capsuleMachineWidth - 32),
          y: randomN(capsuleMachineHeight - 250),
        });
        data.acceleration = data.create(0, 4);
        data.accelerate = function (acceleration) {
          this.velocity.addTo(acceleration);
        };
        return data;
      });
      const getNewPosBasedOnTarget = ({
        start,
        target,
        distance: d,
        fullDistance,
      }) => {
        const { x: aX, y: aY } = start;
        const { x: bX, y: bY } = target;
        const remainingD = fullDistance - d;
        return {
          x: Math.round((remainingD * aX + d * bX) / fullDistance),
          y: Math.round((remainingD * aY + d * bY) / fullDistance),
        };
      };
      const shake = () => {
        capsuleData.forEach((c) => {
          c.velocity.setAngle(degToRad(randomN(270)));
          c.velocity.setXy({ x: 10, y: 10 });
          c.accelerate(c.acceleration);
        });
        elements.capsuleMachine.classList.add('shake');
        setTimeout(
          () => elements.capsuleMachine.classList.remove('shake'),
          500
        );
      };
      const rotateLines = (angles) => {
        angles.forEach((angle, i) => {
          const { axis, point } = lineData[i];
          lineData[i][point] = rotatePoint({
            angle,
            axis: lineData[i][axis],
            point: lineData[i][point],
          });
        });
      };
      const openFlap = () => {
        if (settings.flapRotate > -20) {
          settings.flapRotate -= 2;
          rotateLines([2, -2, -4]);
          updateLines();
          setTimeout(openFlap, 30);
        } else {
          setTimeout(closeFlap, 800);
        }
      };
      const closeFlap = () => {
        if (settings.flapRotate < 0) {
          settings.flapRotate += 1;
          if (settings.flapRotate === 0) {
            [
              { x: 160, y: 360 },
              { x: 160, y: 360 },
              { x: 70, y: 340 },
            ].forEach((item, i) => {
              lineData[i][lineData[i].point].x = item.x;
              lineData[i][lineData[i].point].y = item.y;
            });
            settings.isHandleLocked = false;
          } else {
            rotateLines([-1, 1, 2]);
          }
          updateLines();
          setTimeout(closeFlap, 30);
        }
      };
      const release = () => {
        settings.flapRotate = 0;
        settings.isHandleLocked = true;
        setTimeout(openFlap, 30);
      };
      capsuleData.forEach((c) => {
        c.el.addEventListener('click', () => {
          const { width: bodyWidth, height: bodyHeight } =
            elements.wrapper.getBoundingClientRect();
          const { top, left } = elements.capsuleMachine.getBoundingClientRect();
          const { left: toyBoxLeft, top: toyBoxTop } =
            elements.toyBox.getBoundingClientRect();
          elements.wrapper.classList.add('lock');
          c.el.classList.add('enlarge');
          c.selected = true;
          setStyles({
            el: c.el,
            x: bodyWidth / 2 - left,
            y: bodyHeight / 2 - top,
            deg: nearest360(c.deg),
          });
          setStyles({ el: c.toy, deg: 0 });
          setTimeout(() => c.el.classList.add('open'), 700);
          setTimeout(() => {
            elements.wrapper.classList.remove('lock');
            c.toy.classList.add('collected');
            setStyles({
              el: c.el,
              x: toyBoxLeft - left + 16 + calcCollectedX(),
              y: toyBoxTop - top + 16 + calcCollectedY(),
            });
            settings.collectedNo++;
          }, 1800);
        });
        setStyles(c);
      });
      const spaceOutCapsules = (c) => {
        capsuleData.forEach((c2) => {
          if (c.id === c2.id || c2.selected) return;
          const distanceBetweenCapsules = distanceBetween(c, c2);
          if (distanceBetweenCapsules < c.radius * 2) {
            c.velocity.multiplyBy(-0.6);
            const overlap = distanceBetweenCapsules - c.radius * 2;
            c.setXy(
              getNewPosBasedOnTarget({
                start: c,
                target: c2,
                distance: overlap / 2,
                fullDistance: distanceBetweenCapsules,
              })
            );
          }
        });
      };
      const hitCheckLines = (c) => {
        lineData.forEach((l) => {
          const d1 = distanceBetween(c, l.start);
          const d2 = distanceBetween(c, l.end);
          if (
            d1 + d2 >= l.length - c.radius &&
            d1 + d2 <= l.length + c.radius
          ) {
            const dot =
              ((c.x - l.start.x) * (l.end.x - l.start.x) +
                (c.y - l.start.y) * (l.end.y - l.start.y)) /
              Math.pow(l.length, 2);
            const closestXy = {
              x: l.start.x + dot * (l.end.x - l.start.x),
              y: l.start.y + dot * (l.end.y - l.start.y),
            };
            const fullDistance = distanceBetween(c, closestXy);
            if (fullDistance < c.radius) {
              c.velocity.multiplyBy(-0.6);
              const overlap = fullDistance - c.radius;
              c.setXy(
                getNewPosBasedOnTarget({
                  start: c,
                  target: closestXy,
                  distance: overlap / 2,
                  fullDistance,
                })
              );
            }
          }
        });
      };
      const hitCheckCapsuleMachineWalls = (c) => {
        const buffer = 5;
        if (c.x + c.radius + buffer > capsuleMachineWidth) {
          c.x = capsuleMachineWidth - (c.radius + buffer);
          c.velocity.x = c.velocity.x * c.bounce;
        }
        if (c.x - (c.radius + buffer) < 0) {
          c.x = c.radius;
          c.velocity.x = c.velocity.x * c.bounce;
        }
        if (c.y + c.radius + buffer > capsuleMachineHeight) {
          c.y = capsuleMachineHeight - c.radius - buffer;
          c.velocity.y = c.velocity.y * c.bounce;
        }
        if (c.y - c.radius < 0) {
          c.y = c.radius;
          c.velocity.y = c.velocity.y * c.bounce;
        }
      };
      const animateCapsules = () => {
        capsuleData.forEach((c, i) => {
          if (c.selected) return;
          c.prevX = c.x;
          c.prevY = c.y;
          c.accelerate(c.acceleration);
          c.velocity.multiplyBy(c.friction);
          c.addTo(c.velocity);
          spaceOutCapsules(c);
          hitCheckLines(c);
          hitCheckCapsuleMachineWalls(c);
          if (Math.abs(c.prevX - c.x) < 2 && Math.abs(c.prevY - c.y) < 2) {
            c.velocity.setXy({ x: 0, y: 0 });
            c.setXy({ x: c.prevX, y: c.prevY });
          } else {
            if (Math.abs(c.prevX - c.x)) {
              setStyles({ el: c.toy, deg: c.deg + (c.x - c.prevX) * 2 });
              c.deg += (c.x - c.prevX) * 2;
            }
          }
          setStyles(capsuleData[i]);
        });
      };
      const grabHandle = (e) => {
        if (settings.isHandleLocked) return;
        const { top, left } = elements.capsuleMachine.getBoundingClientRect();
        settings.isTurningHandle = true;
        settings.handleDeg = radToDeg(
          angleTo({
            a: { x: getPage(e, 'X') - left, y: getPage(e, 'Y') - top },
            b: handleAxis(),
          })
        );
        settings.handleRotate = 0;
      };
      const releaseHandle = () => {
        settings.isTurningHandle = false;
        setStyles({ el: elements.handle, deg: 0 });
      };
      const rotateHandle = (e) => {
        if (!settings.isTurningHandle || settings.isHandleLocked) return;
        const { top, left } = elements.capsuleMachine.getBoundingClientRect();
        settings.prevHandleDeg = settings.handleDeg;
        const deg = radToDeg(
          angleTo({
            a: { x: getPage(e, 'X') - left, y: getPage(e, 'Y') - top },
            b: handleAxis(),
          })
        );
        settings.handleDeg = deg;
        const diff = settings.handleDeg - settings.prevHandleDeg;
        if (diff >= 1) {
          setStyles({ el: elements.handle, deg: settings.handleRotate });
        }
        if (diff > 0 && diff < 50) settings.handleRotate += diff;
        if (settings.handleRotate > 350) {
          setStyles({ el: elements.handle, deg: 10 });
          release();
          settings.isTurningHandle = false;
        }
      };
      ['mousedown', 'touchstart'].forEach((action) => {
        elements.handle.addEventListener(action, grabHandle);
      });
      ['mouseup', 'mouseleave', 'touchend'].forEach((action) => {
        elements.circle.addEventListener(action, releaseHandle);
      });
      ['mousemove', 'touchmove'].forEach((action) => {
        window.addEventListener(action, rotateHandle);
      });
      elements.shakeButton.addEventListener('click', shake);
      elements.seeInsideButton.addEventListener('click', () => {
        elements.capsuleMachine.classList.toggle('see-through');
        elements.seeInsideButton.innerHTML =
          elements.capsuleMachine.classList.contains('see-through')
            ? '隐藏'
            : '偷看';
      });
      updateLines();
      setInterval(animateCapsules, 30);
    </script>
  </body>
</html>