Escriba una versión simple de billar con JS nativo

00.gif

prefacio

Por un capricho, pensé en escribir un juego de billar en JS, después de algunos golpes y golpes, realicé una versión simplificada. El conocimiento utilizado es principalmente para llamar recursivamente a requestAnimationFrame, así como algunos cálculos de ángulos trigonométricos simples. requestAnimationFrame es un marco de animación JS, que es similar a setinterval, pero el efecto de la animación es mejor que el del temporizador.

1. Dibujar elementos del juego

  // CSS
    .table {
      position: relative;
      margin: 100px auto;
      width: 1080px;
      height: 596px;
      background: url(./台球桌.jpg) no-repeat;
      background-size: 100%;
    }

    .big {
      position: absolute;
      width: 1000px;
      height: 500px;
      left: 43px;
      top: 48px;
    }

    .box,
    .box2 {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5);
      position: absolute;
    }

    .box {
      background: radial-gradient(circle at 75% 30%, #fff 5px, #fffbfef1 8%, #aaaaaac4 60%, #faf6f9bd 100%);
    }

    .box2 {
      background: radial-gradient(circle at 75% 30%, #fff 5px, #ff21f4f1 8%, #d61d1dc4 60%, #ff219b 100%);
    }

    .big .box::before,
    .box2::before {
      content: '';
      position: absolute;
      width: 100%;
      height: 100%;
      transform: scale(0.25) translate(-70%, -70%);
      background: radial-gradient(#fff, transparent);
      border-radius: 50%;
    }

    .gan {
      display: flex;
      height: 20px;
      position: absolute;
      left: 25px;
      top: 15px;
      transform-origin: 0 50%;
      transform: rotate(50deg);
      cursor: pointer;
    }

    .gan2 {
      width: 25px;
      height: 20px;
    }

    .gan3 {
      width: 375px;
      height: 20px;
      background: url(./Snipaste_2022-07-18_19-52-54.jpg) no-repeat center;
      background-size: 100%;
    }
  
  //html
    <div class="table">
    <div class="big">
      <div class="box">
        <div class="gan">
          <div class="gan2"></div>
          <div class="gan3"></div>
        </div>
      </div>
      <div class="box2"></div>
    </div>
  </div>
  
  //JS
   // 设置球的位置
    //母球
    const box1 = document.querySelector('.box')
    box1.style.left = '300px'
    box1.style.top = '150px'
    //子球
    const box2 = document.querySelector('.box2')
    box2.style.left = '700px'
    box2.style.top = '300px'
    //球杆
    const gan = document.querySelector('.gan')
    const gan2 = document.querySelector('.gan2')
    const gan3 = document.querySelector('.gan3')

01.jpg

2. El palo gira con el ratón

Primero obtenga las coordenadas del mouse en la página y luego reste las coordenadas del centro de la esfera para obtener una coordenada relativa. Luego, tome el centro de la pelota como origen, calcule el ángulo del mouse en relación con el centro de la pelota y, finalmente, asigne este ángulo a la propiedad de transformación del palo, de modo que el efecto del palo que gira con el mouse pueda ser ser logrado.

    //声明鼠标相对坐标变量
    let x, y
    // 获取鼠标的坐标,来计算球杆的角度
    document.addEventListener('mousemove', function (e) {
      const position = box1.getBoundingClientRect()
      // 获取鼠标相对球心的坐标,因为盒子的position原点在左上角,所以要减去自身宽高的一半才是球心
      x = e.pageX - position.left - 25
      y = e.pageY - position.top - 25 - document.documentElement.scrollTop
      let z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); // 勾股定理计算斜边值
      let cos = y / z;// 余弦
      let radian = Math.acos(cos);//用反三角函数求弧度
      let angle = 180 / (Math.PI / radian);//将弧度转换成角度
      if (x > 0 && y > 0) {//鼠标在第四象限
        angle = 90 - angle
      }
      if (x == 0 && y > 0) {//鼠标在y轴负方向上
        angle = 90;
      }
      if (x == 0 && y < 0) {//鼠标在y轴正方向上
        angle = 270;
      }
      if (x > 0 && y == 0) {//鼠标在x轴正方向上
        angle = 0;
      }
      if (x < 0 && y > 0) {//鼠标在第三象限
        angle = 90 + angle
      }
      if (x < 0 && y == 0) {//鼠标在x轴负方向
        angle = 180;
      }
      if (x < 0 && y < 0) {//鼠标在第二象限
        angle = 90 + angle
      }
      if (x > 0 && y < 0) {//鼠标在第一象限
        angle = 450 - angle
      }
      // 把计算出来的角度取模后赋值给球杆旋转角度
      gan.style.transform = `rotate(${angle % 360}deg)`
    })

02.gif

2. La animación de golpeo del club.

02.jpg

El club en realidad se compone de 3 cajas. La caja grande más externa controla la rotación del club. Hay dos cajas gan2 y gan3 en la caja grande. La caja gan3 se usa para poner la imagen del club. La caja gan2 es invisible, es la encargada de apuntalar el palo. Entonces, la animación del club es muy simple, siempre que aumente y disminuya el ancho del cuadro gan2, se puede realizar la extensión y contracción del club.

Para lograr la animación es usar recursividad de cola para llamar repetidamente a la función requestAnimationFrame

    // // 球杆点击事件
    document.querySelector('.gan3').addEventListener('click', function () {
      moveGan(gan2, 0)
    })
    // 球杆打击动画
    function moveGan(item, num) {
      // i来控制函数的结束条件
      let i = num
      requestAnimationFrame(() => {
        //获取元素的坐标值,要把字符串里的数字提取出来
        let moveX = parseFloat(item.style.width) || 25
        moveX += 15
        // 每一次调用这个函数,就让元素的宽+15px
        item.style.width = moveX + 'px'
        i++
        if (i >= 10) {
          // i>10时,就让球杆再缩回去
          return returnGan(item, 0)
        }
        // 使用尾递归来重复调用
        return moveGan(item, i)
      })
    }
    function returnGan(item, num) {
      let i = num
      requestAnimationFrame(() => {
        let moveX = parseFloat(item.style.width) || 0
        moveX -= 15
        // 每一次调用这个函数,就让元素的宽-15px
        item.style.width = moveX + 'px'
        i++
        if (i >= 10) {
          return tick() //tick是击球的函数
        }
        return returnGan(item, i)
      })
    }

03.gif

3. Movimiento de la bola blanca después de que el palo golpea la bola

La animación de golpe de la bola blanca también llama a la función requestAnimationFrame repetidamente a través de la recursión de la cola, pero cuando se trata de rebotes en la pared y golpes en el otoño, los parámetros de la función de movimiento de la bola blanca serán un poco más complicados.

03.jpg

La velocidad y la distancia del movimiento de la tiradora están controladas por la variable i. Cada vez que se llama a esta función, i disminuirá. Los dos parámetros x e y recibirán un valor entre -1 y 1, que actúa como un coeficiente de dirección y pasa la dirección de impacto del palo a través de los parámetros. Después de golpear el límite, tome el coeficiente correspondiente negativo y luego use el nuevo coeficiente para realizar la función de movimiento, que puede tener el efecto de rebote.

// 击打母球的函数
    function tick() {
      // 通过绝对值判断打击角度,x和y就是鼠标相对球心的坐标
      if (Math.abs(x) > Math.abs(y)) {
        // 通过判断x,y是否大于0,判断打击方向
        if (x > 0 && y > 0 || x > 0 && y < 0) {
          raf(box1, -1, -1 / (x / y), 1000)
        } else {
          raf(box1, 1, 1 / (x / y), 1000)
        }
      } else {
        if (y > 0 && x > 0 || y > 0 && x < 0) {
          raf(box1, -1 / (y / x), -1, 1000)
        } else {
          raf(box1, 1 / (y / x), 1, 1000)
        }
      }
    }
    
   //..... 母球移动的函数里面还要加代码,所以这里就先不贴出来了。 
   
   // 判断是否进洞的函数
    function test(x, y) {
      if (x < 10 && y < 10 || x > 940 && y < 10 || x > 940 && y > 440 || x < 10 && y > 440
        || x > 475 && x < 525 && y < 5 || x > 475 && x < 525 && y > 445) {
        return true
      }
    }

04.gif

4. La bola blanca golpea la bola infantil y se mueve

Este es el paso más problemático, la trayectoria de ambas bolas cambiará después del impacto. Teniendo en cuenta solo el impacto más común, la dirección del movimiento de la bola secundaria debe ser la dirección de la línea recta entre el punto de impacto y el centro de la bola secundaria, que es más fácil de calcular. La dirección de la bola blanca después del impacto debe ser la tangente del punto de impacto. La función trigonométrica casi se olvida. No sé cómo calcular esto, así que usé un algoritmo simple para rebotar directamente como golpear una pared. .

04.jpg

05.jpgAgregue este juicio de impacto a la función del movimiento de la bola blanca, y luego agregue una función de movimiento de la bola secundaria, y todo el código está terminado.

    //母球移动
    // 获取坐标,要把字符串里的数字提取出来
    let fx = parseFloat(box1.style.left)
    let fy = parseFloat(box1.style.top)
    let gx = parseFloat(box2.style.left)
    let gy = parseFloat(box2.style.top)
    // 声明用判断撞球角度的变量
    let n
    // 控制子球移动函数的调用
    let p = true
    function raf(item, x, y, num) {
      //击球后隐藏球杆
      gan3.style.display = 'none'
      // item是目标元素,x和y对应移动方向的系数,i用来控制移动速度
      let i = num
      requestAnimationFrame(() => {
        fx += x * 5 * i / 500
        fy += y * 5 * i / 500
        item.style.left = fx + 'px'
        item.style.top = fy + 'px'
        i -= 2
        // 边界判断,球桌宽1000高500,球宽高50,所以边界就是0-950
        if (fx > 950) { // 右边界,让x系数反过来
          fx = 950
          return raf(item, -x, y, i)
        } else if (fy > 450) { // 下边界,让y系数反过来
          fy = 450
          return raf(item, x, -y, i)
        } else if (fx < 0) { // 左边界,让x系数反过来
          fx = 0
          return raf(item, -x, y, i)
        } else if (fy < 0) { // 上边界,让y系数反过来
          fy = 0
          return raf(item, x, -y, i)
        }
        // i<=50就停止移动,然后显示球杆
        if (i <= 50) return gan3.style.display = 'block'
        // 判断球是否进洞
        if (test(fx, fy)) {
          return item.style.display = 'none'
        }
        //两个球撞击时的判断
        if (fx < gx + 50 && fx > gx - 50 && fy < gy + 50 && fy > gy - 50) {
          // 子球前进的角度,就是撞击时,两个圆心连线的夹角
          n = Math.abs(gx - fx) >= Math.abs(gy - fy) ? Math.abs(gx - fx) : Math.abs(gy - fy)
          // n用来控制调用函数时x,y的大小,不能大于1,否则移动速度会异常
          if (p) raf2(box2, (gx - fx) / n, (gy - fy) / n, i)
          // 只有第一次碰撞时,会调用一次子球移动的函数,避免一次击球产生多次撞击时,这个函数被多次调用
          p = false
          return raf(item, -x, y, i)
        }
        return raf(item, x, y, i)
      })
    }
     //子球移动
    function raf2(item, x, y, num) {
      let i = num
      requestAnimationFrame(() => {
        //获取元素的坐标值,要把字符串里的数字提取出来
        gx += x * 5 * i / 700
        gy += y * 5 * i / 700
        item.style.left = gx + 'px'
        item.style.top = gy + 'px'
        i -= 2
        if (gx > 950) {
          gx = 950
          return raf2(item, -x, y, i)
        } else if (gy > 450) {
          gy = 450
          return raf2(item, x, -y, i)
        } else if (gx < 0) {
          gx = 0
          return raf2(item, -x, y, i)
        } else if (gy < 0) {
          gy = 0
          return raf2(item, x, -y, i)
        }
        //两个球触碰判断
        if (fx < gx + 50 && fx > gx - 50 && fy < gy + 50 && fy > gy - 50) {
          return raf2(box2, (gx - fx) / n, (gy - fy) / n, i)
        }
        if (i <= 50) return p = true // 移动函数执行完后,重置p这个变量
        // 判断球是否进洞
        if (test(gx, gy)) {
          return item.style.display = 'none'
        }
        return raf2(item, x, y, i)
      })
    }

05.gif

Resumir

06.jpg

La implementación de este pequeño juego no es perfecta, porque se usa demasiada recursividad, muchos detalles son difíciles de controlar y la trayectoria de la pelota es difícil de calcular. Aunque la pelota es redonda, su caja es cuadrada, por lo que el impacto a veces puede parecer extraño. La función de movimiento también es defectuosa. No se puede reutilizar. Si desea agregar varias bolas, la función debe cambiarse.

Esta versión fallida del billar está principalmente escrita y jugada, y probó la implementación de la animación JS. Si no te gusta, no la rocíes. "Estoy participando en el "Concurso de desarrollo creativo" para obtener más información, consulte: ¡El Concurso de desarrollo creativo de Nuggets ya está aquí! "

Supongo que te gusta

Origin juejin.im/post/7122337541281284126
Recomendado
Clasificación