Canvas 与 RxJS 制作动画-控制移动

一、简介

本系列是 RxJS 制作简单游戏教程,本文基于 React(Remix)/Canvas/RxJS 制作键盘控制box在水平和二维方向移动,移动方式有两种一种是键盘操作,一种是鼠标操作。

二、特点

  • rxjs 事件流与canvas结合
  • react(remix) 函数组件
  • 在订阅位置绘制内容
  • scan 操作符合并
  • canvas 绘制到清除到重新绘制流程

三、三个任务

  • 键盘控制一维水平移动
  • 键盘控制二维移动
  • 鼠标控制控制移动

四、键盘一维水平移动

目标:在画布中基于 React/RxJS 通过键盘的左右方向键控制 canvas 中的 box 盒子水平方向移动。

ts 复制代码
import { useRef, useEffect } from "react";
import { fromEvent, merge } from "rxjs";
import { scan, filter, map } from "rxjs/operators";

const step = 10
const Game = () => {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current! as HTMLCanvasElement;
    const context = canvas.getContext("2d");

    const leftKeyStream = fromEvent(document, "keydown").pipe(
      filter((event) => event.key === "ArrowLeft"),
      map(() => -step)
    );

    const rightKeyStream = fromEvent(document, "keydown").pipe(
      filter((event) => event.key === "ArrowRight"),
      map(() => step)
    );

    const moveStream = merge(leftKeyStream, rightKeyStream).pipe(
      scan((x, dx) => Math.max(0, Math.min(400 - 50, x + dx)), 0)
    );

    const subscription = moveStream.subscribe((x) => {
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.fillRect(x, 90, 50, 50);
    });

    return () => subscription.unsubscribe();
  }, []);

  return (
    <div>
      <h1>左右移动方块游戏 (RxJS)</h1>
      <canvas ref={canvasRef} width={400} height={200} style={{ border: "1px solid black" }} />
    </div>
  );
};

export default Game;

使用 RxJS 实现了三个流:

  • leftKeyStream
  • rightKeyStream
  • moveStream

moveStream 是将 leftKeyStream 和 rightKeyStream 合并而来。它的作用是通过 scan (与 js 的 reduce 功能基本一致) 操作计算出当前的水平的位置。moveStream 订阅之后执行清除画布,然后重新绘制内容。其中 step 是每次按下键盘移动步骤。在此组件汇总使用 RxJS 操作符有:

  • fromEvent 用于捕获事件
  • filter 过滤键盘事件
  • map 映射步骤(正负)
  • merge 合并左右移动留

此移动属于一维层面移动,相对简单,也 Canvas 动画的基础。

五、键盘二维移动

目标:在画布中基于 React/RxJS 通过键盘的左右方向键控制 canvas 中的 box 盒子水平方向和垂直方向进行移动。

ts 复制代码
import { useRef, useEffect } from "react";
import { fromEvent, merge } from "rxjs";
import { filter, map, scan } from "rxjs/operators";

const step = 10;

const Game = () => {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current! as HTMLCanvasElement;
    const context = canvas.getContext("2d");

    const leftKeyStream = fromEvent(document, "keydown").pipe(
      filter((event) => event.key === "ArrowLeft"),
      map(() => ({ x: -step, y: 0 }))
    );

    const rightKeyStream = fromEvent(document, "keydown").pipe(
      filter((event) => event.key === "ArrowRight"),
      map(() => ({ x: step, y: 0 }))
    );

    const upKeyStream = fromEvent(document, "keydown").pipe(
      filter((event) => event.key === "ArrowUp"),
      map(() => ({ x: 0, y: -step }))
    );

    const downKeyStream = fromEvent(document, "keydown").pipe(
      filter((event) => event.key === "ArrowDown"),
      map(() => ({ x: 0, y: step }))
    );

    const moveStream = merge(leftKeyStream, rightKeyStream, upKeyStream, downKeyStream).pipe(
      scan((position, delta) => {
        const x = Math.max(0, Math.min(400 - 50, position.x + delta.x));
        const y = Math.max(0, Math.min(200 - 50, position.y + delta.y));
        return { x, y };
      }, { x: 0, y: 0 })
    );

    const subscription = moveStream.subscribe((position) => {
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.fillRect(position.x, position.y, 50, 50);
    });

    return () => subscription.unsubscribe();
  }, []);

  return (
    <div>
      <h1>四方向移动方块游戏 (RxJS)</h1>
      <canvas ref={canvasRef} width={400} height={200} style={{ border: "1px solid black" }} />
    </div>
  );
};

export default Game;

在二维空间之后,需要更多的访问进行支持 相对水平方向增加了:

  • leftKeyStream { x: -step, y: 0 }
  • rightKeyStream { x: step, y: 0 }
  • upKeyStream { x: 0, y: -step }
  • downKeyStream { x: 0, y: step }

将以上四个流合并之后,通过 scan 计算新的位置,然后清除画布,重新绘制。

六、鼠标控制移动

ts 复制代码
import { useRef, useEffect } from "react";
import { fromEvent } from "rxjs";
import { map, scan } from "rxjs/operators";

const step = 10;

const Game = () => {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current! as HTMLCanvasElement;
    const context = canvas.getContext("2d");

    const canvasRect = canvas.getBoundingClientRect();

    const mouseMoveStream = fromEvent(canvas, "mousemove").pipe(
      map((event) => {
        return {
          x: event.clientX - canvasRect.left - 25, // 25 is half of the square width
          y: event.clientY - canvasRect.top - 25,   // 25 is half of the square height
        };
      })
    );

    const moveStream = mouseMoveStream.pipe(
      scan((position, target) => {
        const deltaX = target.x - position.x;
        const deltaY = target.y - position.y;
        const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // 三角计算水平距离

        if (distance <= step) {
          return target; // 距离小于步距的时候,直接返回 target
        }

        const ratio = step / distance;
        const x = position.x + deltaX * ratio;
        const y = position.y + deltaY * ratio;

        return { x, y };
      }, { x: 0, y: 0 })
    );

    const subscription = moveStream.subscribe((position) => {
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.fillRect(position.x, position.y, 50, 50);
    });

    return () => subscription.unsubscribe();
  }, []);

  return (
    <div>
      <canvas ref={canvasRef} width={400} height={200} style={{ border: "1px solid black" }} />
    </div>
  );
};

export default Game;

逻辑与键盘相似,使用 fromEvent 操作符获取鼠标移动事件 mousemove 的获取 canvas 画布在浏览器中的位置,然后计算出 target 的位置。在管道之内 pipe 操作符计算出新的元素的位置。

七、小结

本主要是练习 Canvas + RxJS 在 React 框架中,实现基本移动操作操作,分为键盘移动,鼠标移动,使用 RxJS 作为计算工具如何处理。RxJS 是个强大的函数式和响应式编程工具,在计算上提供诸多操作符完成响应的任务。

相关推荐
学习前端的小z4 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
XINGTECODE13 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶18 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺23 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
彭世瑜28 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40429 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish29 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five30 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序30 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54131 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript