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 是个强大的函数式和响应式编程工具,在计算上提供诸多操作符完成响应的任务。

相关推荐
英俊潇洒美少年9 分钟前
react19和vue3的优缺点 对比
前端·javascript·vue.js·react.js
wuyikeer39 分钟前
Spring Framework 中文官方文档
java·后端·spring
Victor35644 分钟前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3561 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer1 小时前
Spring BOOT 启动参数
java·spring boot·后端
~无忧花开~2 小时前
React生命周期全解析
开发语言·前端·javascript·react.js·前端框架·react
子木HAPPY阳VIP2 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪
人间打气筒(Ada)2 小时前
如何基于 Go-kit 开发 Web 应用:从接口层到业务层再到数据层
开发语言·后端·golang
开心就好20252 小时前
使用Wireshark进行TCP数据包抓包分析:三次握手与四次挥手详解
后端·ios