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 分钟前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
乔峰不是张无忌33029 分钟前
【HTML】动态闪烁圣诞树+雪花+音效
前端·javascript·html·圣诞树
鸿蒙自习室36 分钟前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
m0_7482507443 分钟前
高性能Web网关:OpenResty 基础讲解
前端·openresty
机器之心1 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
前端没钱1 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
NoneCoder1 小时前
CSS系列(29)-- Scroll Snap详解
前端·css
无言非影1 小时前
vtie项目中使用到了TailwindCSS,如何打包成一个单独的CSS文件(优化、压缩)
前端·css
.生产的驴2 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
我曾经是个程序员2 小时前
鸿蒙学习记录
开发语言·前端·javascript