前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用物理引擎matterjs实现键盘特效动画

使用物理引擎matterjs实现键盘特效动画

作者头像
赤蓝紫
发布2023-10-23 15:57:41
3740
发布2023-10-23 15:57:41
举报
文章被收录于专栏:clz

使用物理引擎matterjs实现键盘特效动画

前言

偶然间看到一个网站,觉得这个动画很炫酷。就收藏了一下,在稍微学习了一下matterjs后,打算跟着源码学习,弄懂并且自己实现一个。

准备

先安装matter-jswebpack,并且稍微配置一下webpack

代码语言:javascript
复制
const path = require("path");

module.exports = {
  entry: path.join(__dirname, "src", "index.js"),
  mode: "development",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname),
  },
  devServer: {
    hot: true,
    static: {
      directory: path.join(__dirname),
    },
  }
}

Matterjs初体验

根据Matterjs官网的demo,可以知道Matterjs的使用主要分为几个步骤:

  1. 创建引擎
  2. 创建渲染器
  3. 创建物体
  4. 将物体添加到引擎的world
  5. 执行渲染器
  6. 创建执行器runner,并运行引擎(如果没有这一步,则没有办法触发物理动画)

index.html

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html, body {
      padding: 0;
      margin: 0;
      overflow: hidden;
    }
  </style>
</head>
<body>
  <div class="content"></div>
  <script src="./bundle.js"></script>
</body>
</html>

代码语言:javascript
复制
import Matter from 'matter-js';

// module aliases
const Engine = Matter.Engine;
const Render = Matter.Render;
const Runner = Matter.Runner;
const Bodies = Matter.Bodies;
const Composite = Matter.Composite;

const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

// 1. 创建引擎
const engine = Engine.create();

// 2. 创建渲染器
const render = Render.create({
  element: document.querySelector('.content'),
  engine: engine,
  options: {
    width: WIDTH,
    height: HEIGHT,
    background: '#222'
  }
});

// 3. 创建物体
const boxA = Bodies.rectangle(600, 200, 200, 200);
const boxB = Bodies.rectangle(800, 300, 80, 80);
const ground = Bodies.rectangle(700, 400, 800, 40, { isStatic: true });

// 4. 将物体添加到引擎的`world`中
Composite.add(engine.world, [boxA, boxB, ground]);

// 5. 执行渲染器
Render.run(render);

// 6. 创建执行器runner,并运行引擎
const runner = Runner.create();
Runner.run(runner, engine);

添加键盘事件,每次点击生成一个物体

代码语言:javascript
复制
document.body.addEventListener('keydown', (e) => {
  const ball = Bodies.circle(400, 200, 50, {
    restitution: 0.9,  // 设置弹力
    friction: 0.001    // 设置摩檫力,默认为0.5
  });

  Composite.add(engine.world, ball);
})

添加力

代码语言:javascript
复制
document.body.addEventListener('keydown', (e) => {
  const ball = Bodies.circle(400, 200, 50, {
    restitution: 0.9,
    friction: 0.001    // 设置摩檫力,默认为0.5
  });

  const vector = {
    x: (Date.now() % 10) * 0.004  - 0.02,
    y: (-1 * HEIGHT / 3200)
  };

  Matter.Body.applyForce(ball, ball.position, vector);

  Composite.add(engine.world, ball);
})

上面的vector就是力。y轴方向上,力会根据窗口高度变化的一个,而x轴方向上,力会随着时间的变化而发生变化,力的区间为[-0.02, 0.02],将它分成10部分,根据时间戳来获取x轴方向上的力。(Date.now() % 10) * 0.004 - 0.02

下面的演示用的里的区间是[-0.2, 0.2],(Date.now() % 10) * 0.04  - 0.2(为了看的效果明显点)

添加倾斜边界

添加倾斜边界,让弹出来的球能够慢慢消失。

代码语言:javascript
复制
const OFFSET = 1;  // 使用的是Matterjs 0.10.0。如果是最新的,可能要设置成10
const boundaries = generateBoundaries();
Composite.add(engine.world, boundaries);

function generateBoundaries() {
  return [
    Bodies.rectangle(WIDTH / 4, HEIGHT - 400, WIDTH / 2, OFFSET, {
      angle: -0.1,
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff'
      }
    }),

    Bodies.rectangle((WIDTH / 4) * 3, HEIGHT - 400, WIDTH / 2, OFFSET, {
      angle: 0.1,
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff'
      }
    }),
  ]
}

添加兜底边界

当我们添加倾斜边界时,球会越变越多,包括消失的球也没有进行处理。所以可以添加一个兜底边界,后面再添加碰撞事件,清除接触到兜底边界的小球。

下面的例子会先将倾斜边界变短,查看实际效果

代码语言:javascript
复制
function generateBoundaries() {
  return [
    Bodies.rectangle(WIDTH / 4, HEIGHT - 400, WIDTH / 4, OFFSET, {
      angle: -0.1,
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff'
      }
    }),

    Bodies.rectangle((WIDTH / 4) * 3, HEIGHT - 400, WIDTH / 4, OFFSET, {
      angle: 0.1,
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff'
      }
    }),

    Bodies.rectangle(WIDTH / 2, HEIGHT - 200, WIDTH, OFFSET, {
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff'
      }
    })
  ]
}

添加一个platform变量用来存储兜底边界,便于实现碰撞检测。

代码语言:javascript
复制
const OFFSET = 1;  // 使用的是Matterjs 0.10.0。如果是最新的,可能要设置成10
const boundaries = generateBoundaries();
Composite.add(engine.world, boundaries);

let platform = boundaries[2];
Matter.Events.on(engine, 'collisionStart', (e) => {
  e.pairs.forEach(function (pair) {  // e.pairs表示发生碰撞的两个物体
    const bodyA = pair.bodyA;
    const bodyB = pair.bodyB;

    if (bodyA === platform) {   // bodyA是兜底边界,则将bodyB从engine的世界中移除。
      Composite.remove(engine.world, [bodyB]);
    }

    if (bodyB === platform) {
      Composite.remove(engine.world, [bodyA]);
    }
  })
})

位置设定

上面的实现会导致不论点击什么键,小球出现的位置都是固定的,所以进行一个位置设定的操作,来实现点击键盘后,位置和键盘上的位置差不多一样。

首先,需要构建一个二维数组,元素的值则是每一行键盘对应的顺序(功能键为null

代码语言:javascript
复制
const KEYS = [
// 字母键和符号键
['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', null],
[null, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'],
[null, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '\'', null],
[null, null, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', null, null],

// 数字键
[null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-/', 'num-*', 'num--'],
[null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-7', 'num-8', 'num-9', 'num-+'],
[null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-4', 'num-5', 'num-6', null],
[null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-1', 'num-2', 'num-3', null],
[null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-0', null, 'num-.', null]
]; 

遍历每一个键,设定好每个键的位置。

代码语言:javascript
复制
const positions = {};

// 生成键盘字母对应的位置
generatePositions();

function generatePositions() {
KEYS.forEach((row) => {
  row.forEach((letter, i) => {
    if (!letter) {
      return; // 功能键,忽略
    }

    positions[letter] = ((i / row.length) + (0.5 / row.length)) * WIDTH;
  })
})
}

((i / row.length) + (0.5 / row.length)) * WIDTHi是点在行中的索引,row.length是该行中点的总数,(i / row.length) * WIDTH的值就是该点应该在的位置,但是这样子得到的将不会是对应单元格的中心位置,而是左侧边缘的位置,所以还应该加半个单元格的宽度,即(0.5 / row.length) * WIDTH

使用vkey库,将数字键变为<num-0>的形式,用来与一般的区分,并且会将所有的英文变成大写。

代码语言:javascript
复制
document.body.addEventListener('keydown', (e) => {
let key = vkey[e.keyCode];

if (key == null) {
  return;
}

key = key.replace(/</g, '').replace(/>/g, '');  // 将`<num-0>`形式变为`num-0`

if (key in positions) {
  addLetter(key, positions[key], HEIGHT - 50);
}
})

function addLetter(key, x, y) {
const ball = Bodies.circle(x, y, 30, {
  restitution: 0.9
});

const vector = {
  x: (Date.now() % 10) * 0.004 - 0.02,
  y: (-1 * HEIGHT / 3600)
};

Matter.Body.applyForce(ball, ball.position, vector);

Composite.add(engine.world, [ball]);
}

小球换成图片

通过render.sprite.texture来设置图片。

代码语言:javascript
复制
const ball = Bodies.circle(x, y, 30, {
  restitution: 0.9,
  friction: 0.001    // 设置摩檫力,默认为0.5
  render: {
    sprite: {
      texture: getImagePath(key)
    }
  }
}); 

function getImagePath(key) {
  if (key.indexOf('num-') === 0) {
    key = key.substring(4);
  }

  if (key === '*') key = 'star';
  if (key === '+') key = 'plus';
  if (key === '.') key = 'dot';
  if (key === '/') key = 'slash';
  if (key === '\\') key = 'backslash';

  return './img/' + key + '.png';
}

\color{red}{还需要修改一下创建渲染器时的设置}

代码语言:javascript
复制
const render = Render.create({
  element: document.querySelector('.content'),
  engine: engine,
  options: {
    width: WIDTH,
    height: HEIGHT,
    background: '#222',
    wireframes: false,  // 关闭线框
  }
})

变化边界长度,并隐藏

最下面的边界长度为WIDTH * 4,这样子为了避免内存泄漏,把一些球给移除。

代码语言:javascript
复制
function generateBoundaries() {
  return [
    Bodies.rectangle(WIDTH / 4, HEIGHT + 50, WIDTH / 2, OFFSET, {
      angle: -0.1,
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff',
        visible: false
      }
    }),

    Bodies.rectangle((WIDTH / 4) * 3, HEIGHT + 50, WIDTH / 2, OFFSET, {
      angle: 0.1,
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff',
        visible: false
      }
    }),

    Bodies.rectangle(WIDTH / 2, HEIGHT + 400, WIDTH * 4, OFFSET, {
      isStatic: true,
      friction: 0.001    // 设置摩檫力,默认为0.5
      render: {
        fillStyle: '#fff',
        visible: false
      }
    })
  ]
}

抽离初始化步骤,并在窗口大小变化时重新初始化

如果在串口大小变化时,不重新初始化,就会导致一些字符并不会在窗口中看得见。

代码语言:javascript
复制
let WIDTH;
let HEIGHT;

let engine = null;
let render = null;

let boundaries = null;
let platform = null;

const KEYS = [
  // 字母键、符号键
  ['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', null],
  [null, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'],
  [null, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '\'', null],
  [null, null, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', null, null],

  // 数字键
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-/', 'num-*', 'num--'],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-7', 'num-8', 'num-9', 'num-+'],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-4', 'num-5', 'num-6', null],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-1', 'num-2', 'num-3', null],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-0', null, 'num-.', null]
];
const positions = {};

function init() {
  WIDTH = window.innerWidth;
  HEIGHT = window.innerHeight;

  if (!engine) {
    engine = Engine.create();
  }

  // 生成键盘字母对应的位置
  generatePositions();

  // 如果渲染器不存在,则创建
  if (!render) {
    render = Render.create({
      element: document.querySelector('.content'),
      engine: engine,
      options: {
        width: WIDTH,
        height: HEIGHT,
        background: '#222',
        wireframes: false
      }
    });
  } else {
    // 如果渲染器已存在,则获取渲染器的画布对象,并重新设置宽度和高度
    const canvas = render.canvas;

    // 设置画布的宽度和高度
    canvas.width = WIDTH;
    canvas.height = HEIGHT;
  }

  render.options.width = WIDTH;
  render.options.height = HEIGHT;

  // 如果有边界,那就移除
  if (boundaries) {
    Composite.remove(engine.world, boundaries)
  }

  boundaries = generateBoundaries();
  Composite.add(engine.world, boundaries);

  platform = boundaries[2];
}

init();
window.addEventListener('resize', init);

添加音效

代码语言:javascript
复制
<body>
  <div class="content"></div>
  <audio id="type" src="./type.mp3"></audio>
  <script src="./bundle.js"></script>
</body>

代码语言:javascript
复制
function playSound(name) {
  const sound = document.getElementById(name);
  sound.play();
}

function addLetter(key, x, y) {
  playSound('type'); 
  ...
}

预加载图片

当实际部署上线后,会发现,一开始点击键盘时,是没有反应的。因为当我们点击时,才去请求对应图片。所以可以做一个预加载的操作,优化一下。

预加载就可以通过新建Image,并设置src值来实现

代码语言:javascript
复制
function preloadImg (url) {
  const img = new window.Image()
  img.src = url
}

当遍历设置字符位置时,就能够进行一个预加载操作。

代码语言:javascript
复制
function generatePositions() {
  KEYS.forEach((row) => {
    row.forEach((letter, i) => {
      if (!letter) {
        return; // 功能键,忽略
      }

      positions[letter] = ((i / row.length) + (0.5 / row.length)) * WIDTH;

       // 预加载图片
       preloadImg(getImagePath(letter));
    })
  })
}

添加下雨模式

通过使用lastKeys来记录点击过的字符,当最后点击的字符为rain时(不区分大小写),开启一个下雨模式。而下雨模式也很简单,只需要重新设置力vector,和球的坐标即可。

代码语言:javascript
复制
function secretWords(key) {
  lastKeys = lastKeys.slice(-3) + key;

  if (lastKeys === 'RAIN') {
    rainMode = !rainMode;

    if (rainMode) {
      playSound('rain');
    }
  }
}

代码语言:javascript
复制
function addLetter(key, x, y) {
  playSound('type');

  const ball = Bodies.circle(x, y, 30, {
    restitution: 0.9,
    friction: 0.001,
    render: {
      sprite: {
        texture: getImagePath(key)
      }
    }
  });

  let vector = {
    x: (Date.now() % 10) * 0.004 - 0.02,
    y: (-1 * HEIGHT / 3600)
  }; 

  // 新增。下雨模式重新设置坐标,以及力vector
  if (rainMode) {
    vector = {
      x: 0,
      y: 0
    };

    Matter.Body.setPosition(ball, {
      x: ball.position.x,
      y: -30
    })
  }

  secretWords(key);
  // 新增。

  Matter.Body.applyForce(ball, ball.position, vector);

  Composite.add(engine.world, [ball]);
}

完整代码

代码语言:javascript
复制
import Matter from 'matter-js';
import vkey from 'vkey';

// module aliases
const Engine = Matter.Engine;
const Render = Matter.Render;
const Runner = Matter.Runner;
const Bodies = Matter.Bodies;
const Composite = Matter.Composite;

const OFFSET = 1;  // 使用的是Matterjs 0.10.0。如果是最新的,可能要设置成10

let WIDTH;
let HEIGHT;

let engine = null;
let render = null;

let boundaries = null;
let platform = null;

const KEYS = [
  // 字母键、符号键
  ['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', null],
  [null, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'],
  [null, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '\'', null],
  [null, null, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', null, null],

  // 数字键
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-/', 'num-*', 'num--'],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-7', 'num-8', 'num-9', 'num-+'],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-4', 'num-5', 'num-6', null],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-1', 'num-2', 'num-3', null],
  [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 'num-0', null, 'num-.', null]
];
const positions = {};

function init() {
  WIDTH = window.innerWidth;
  HEIGHT = window.innerHeight;

  if (!engine) {
    engine = Engine.create();
  }

  // 生成键盘字母对应的位置
  generatePositions();

  // 如果渲染器不存在,则创建
  if (!render) {
    render = Render.create({
      element: document.querySelector('.content'),
      engine: engine,
      options: {
        width: WIDTH,
        height: HEIGHT,
        background: '#222',
        wireframes: false
      }
    });
  } else {
    // 如果渲染器已存在,则获取渲染器的画布对象,并重新设置宽度和高度
    const canvas = render.canvas;

    // 设置画布的宽度和高度
    canvas.width = WIDTH;
    canvas.height = HEIGHT;
  }

  render.options.width = WIDTH;
  render.options.height = HEIGHT;

  // 如果有边界,那就移除
  if (boundaries) {
    Composite.remove(engine.world, boundaries)
  }

  boundaries = generateBoundaries();
  Composite.add(engine.world, boundaries);

  platform = boundaries[2];
}

init();
window.addEventListener('resize', init);

Composite.add(engine.world, boundaries);

// 5. 执行渲染器
Render.run(render);

// 6. 创建执行器runner,并运行引擎
const runner = Runner.create();
Runner.run(runner, engine);


Matter.Events.on(engine, 'collisionStart', (e) => {
  e.pairs.forEach(function (pair) {  // e.pairs表示发生碰撞的两个物体
    const bodyA = pair.bodyA;
    const bodyB = pair.bodyB;

    if (bodyA === platform) {   // bodyA是兜底边界,则将bodyB从engine的世界中移除。
      Composite.remove(engine.world, [bodyB]);
    }

    if (bodyB === platform) {
      Composite.remove(engine.world, [bodyA]);
    }
  })
});

document.body.addEventListener('keydown', (e) => {
  let key = vkey[e.keyCode];

  if (key == null) {
    return;
  }

  key = key.replace(/</g, '').replace(/>/g, '');

  if (key in positions) {
    addLetter(key, positions[key], HEIGHT - 50);
  }
})

function generatePositions() {
  KEYS.forEach((row) => {
    row.forEach((letter, i) => {
      if (!letter) {
        return; // 功能键,忽略
      }

      positions[letter] = ((i / row.length) + (0.5 / row.length)) * WIDTH;
    
       // 预加载图片
       preloadImg(getImagePath(letter));
    })
  })
}

let rainMode = false;
function addLetter(key, x, y) {
  playSound('type');

  const ball = Bodies.circle(x, y, 30, {
    restitution: 0.9,
    friction: 0.001,
    render: {
      sprite: {
        texture: getImagePath(key)
      }
    }
  });

  let vector = {
    x: (Date.now() % 10) * 0.004 - 0.02,
    y: (-1 * HEIGHT / 3600)
  }; 

  // 新增。下雨模式重新设置坐标,以及力vector
  if (rainMode) {
    vector = {
      x: 0,
      y: 0
    };

    Matter.Body.setPosition(ball, {
      x: ball.position.x,
      y: -30
    })
  }

  secretWords(key);
  // 新增。

  Matter.Body.applyForce(ball, ball.position, vector);

  Composite.add(engine.world, [ball]);
}

function getImagePath(key) {
  if (key.indexOf('num-') === 0) {
    key = key.substring(4);
  }

  if (key === '*') key = 'star';
  if (key === '+') key = 'plus';
  if (key === '.') key = 'dot';
  if (key === '/') key = 'slash';
  if (key === '\\') key = 'backslash';

  return './img/' + key + '.png';
}

function generateBoundaries() {
  return [
    Bodies.rectangle(WIDTH / 4, HEIGHT + 50, WIDTH / 2, OFFSET, {
      angle: -0.1,
      isStatic: true,
      render: {
        fillStyle: '#fff',
        visible: false
      }
    }),

    Bodies.rectangle((WIDTH / 4) * 3, HEIGHT + 50, WIDTH / 2, OFFSET, {
      angle: 0.1,
      isStatic: true,
      render: {
        fillStyle: '#fff',
        visible: false
      }
    }),

    Bodies.rectangle(WIDTH / 2, HEIGHT + 400, WIDTH * 4, OFFSET, {
      isStatic: true,
      render: {
        fillStyle: '#fff',
        visible: false
      }
    })
  ]
}

function playSound(name) {
  const sound = document.getElementById(name);
  sound.play();
}

function preloadImg (url) {
  const img = new window.Image()
  img.src = url
}

let lastKeys = '';
function secretWords(key) {
  lastKeys = lastKeys.slice(-3) + key;

  if (lastKeys === 'RAIN') {
    rainMode = !rainMode;

    if (rainMode) {
      playSound('rain');
    }
  }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-09-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用物理引擎matterjs实现键盘特效动画
    • 前言
      • 准备
        • Matterjs初体验
          • 添加键盘事件,每次点击生成一个物体
            • 添加力
              • 添加倾斜边界
                • 添加兜底边界
                  • 位置设定
                    • 小球换成图片
                      • 变化边界长度,并隐藏
                        • 抽离初始化步骤,并在窗口大小变化时重新初始化
                          • 添加音效
                            • 预加载图片
                              • 添加下雨模式
                                • 完整代码
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档