在 Remix 中绘制 Canvas 的各种常用图形

一、why?

在使用 Figma 的绘图共功能时候,Fimga 提供的几种默认的绘制形状

  • 线条 (line)
  • 矩形
  • 原型
  • 三角形
  • 五角星
  • 菱形(Excalidraw)
  • 箭头
  • 图片
  • 视频
  • ...

在 remix 环境中 使用绘制 canvas 以上图像, 具体看下面的面的内容。

二、Remix 中的 css 方案

为简单起变,css 使用的方案是 css module 方案。不用安装任何其他的内容,直接使用。

三、Box 组件

ts 复制代码
import styles from './index.module.css';

type IBoxProps = {
  title: string;
  children: React.ReactNode
}

export default function Box(props: IBoxProps) {
  return <div className={styles.box}>
    <div>{props.title}</div>
    <div className={styles.draw}>
      {props.children}
    </div>
  </div>
}

Box 组件给一个 title 渲染一个标题,Canvas 的内容通过 Children 进行渲染。对应的 css 内容如下:

css 复制代码
.box {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 100px;
}

.draw {
  margin-top: 20px;
  display: flex;
  border: 1px solid seagreen;
}

3.1)一个返回按钮组件

ts 复制代码
import { useNavigate } from "@remix-run/react";
import styles from "./index.module.css";

export default function Back() {
  const navigate = useNavigate();
  return (
    <div
      className={styles.back}
      onClick={() => {
        navigate(-1);
      }}
    >
      返回
    </div>
  );
}

使用 css 样式:

css 复制代码
.back {
  position: fixed;
  display: flex;
  justify-content: center;
  align-items: center;
  bottom: 10px;
  right: 10px;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: 1px solid saddlebrown;
  cursor: pointer;
}

.back:hover {
  background-color: saddlebrown;
  color: aliceblue;
}

四、主页导航页

因为有列表:

ts 复制代码
import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";
import styles from '~/styles/modules/index.module.css'

export const meta: MetaFunction = () => {
  return [
    { title: "New Remix App" },
    { name: "description", content: "Welcome to Remix!" },
  ];
};

export default function Index() {

  const data = [
    {id: 0, name: 'line' },
    {id: 1, name: 'arrow' },
    {id: 2, name: 'circle' },
    {id: 3, name: 'diamond' },
    {id: 4, name: 'image' },
    {id: 6, name: 'rectangle' },
    {id: 7, name: 'square' },
    {id: 8, name: 'star' },
    {id: 9, name: 'triangle' },
    {id: 10, name: 'video' },
  ]
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <h1 style={{ textAlign: 'center' }}>Canvas With Remix and RxJS</h1>
      <div className={styles.listWrap}>
        {data.map((item) => {
          return <div key={item.id} className={styles.item}>
            <Link to={`/shape/${item.name}`}>{item.name}</Link>
          </div>
        })}
      </div>
    </div>
  );
}

具体由一个 canvas 形状需要绘画。

五、line

ts 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";

import Box from "~/components/Box";

const LineComponentImpl = ({ x1, y1, x2, y2 }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    const drawLine = () => {
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.strokeStyle = "blue";
      ctx.stroke();
    };

    drawLine();
  }, [x1, y1, x2, y2]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function LineComponent() {
  return (
    <Box title="line">
      <LineComponentImpl x1={50} y1={100} x2={150} y2={100} />
      <LineComponentImpl x1={50} y1={150} x2={150} y2={150} />
      <Back />
    </Box>
  );
}

export default LineComponent;

六、arrow

ts 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";
import Box from "~/components/Box";

const ArrowComponentImpl = ({ x, y, width, height }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    const drawArrow = () => {
      const arrowHeadSize = 10;

      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(x + width, y);
      ctx.lineTo(x + width, y + height / 2 - arrowHeadSize / 2);
      ctx.lineTo(x + width + arrowHeadSize, y + height / 2);
      ctx.lineTo(x + width, y + height / 2 + arrowHeadSize / 2);
      ctx.lineTo(x + width, y + height);
      ctx.lineTo(x, y + height);
      ctx.closePath();
      ctx.fillStyle = "blue";
      ctx.fill();
    };

    drawArrow();
  }, [x, y, width, height]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function ArrowComponent() {
  return (
    <Box title="arrow">
      <ArrowComponentImpl x={10} y={100} width={100} height={5} />
      <Back />
    </Box>
  );
}

export default ArrowComponent;

七、circle

tsx 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";

import Box from "~/components/Box";

const CircleComponentImpl = ({ x, y, radius }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fillStyle = "green";
    ctx.fill();
    ctx.closePath();
  }, [x, y, radius]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function CircleComponent() {
  return (
    <Box title="circle">
      <CircleComponentImpl x={100} y={100} radius={50} />
      <CircleComponentImpl x={100} y={100} radius={50} />
      <Back />
    </Box>
  );
}

export default CircleComponent;

八、diamond

ts 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";
import Box from "~/components/Box";

const DiamondComponentImpl = ({ x, y, size }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    const drawDiamond = () => {
      ctx.beginPath();
      ctx.moveTo(x, y - size / 2); // 上
      ctx.lineTo(x + size / 2, y); // 右
      ctx.lineTo(x, y + size / 2); // 下
      ctx.lineTo(x - size / 2, y); // 左
      ctx.closePath();
      ctx.fillStyle = "orange";
      ctx.fill();
    };

    drawDiamond();
  }, [x, y, size]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function DiamondComponent() {
  return (
    <Box title="diamond">
      <DiamondComponentImpl x={100} y={100} size={50} />
      <DiamondComponentImpl x={100} y={100} size={50} />
      <Back />
    </Box>
  );
}

export default DiamondComponent;

九、image

ts 复制代码
import { useRef, useEffect } from "react";
import Box from "~/components/Box";
import { Toaster } from 'sonner'
import Back from "~/components/Back";

const ImageComponentImpl = ({ imageUrl }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    const loadImage = () => {
      const image = new Image();
      import('sonner').then((s) => {
        s.toast.info('image loading')
      })
      image.onload = () => {
        ctx.drawImage(image, 0, 0);
      };
      image.src = imageUrl;
    };

    loadImage();
  }, [imageUrl]);

  return <canvas ref={canvasRef} width={500} height={500} />;
};

function ImageComponent() {
  return (
    <Box title="image">
      <Toaster />
      <ImageComponentImpl imageUrl="https://picsum.photos/500/500" />
      <Back />
    </Box>
  );
}

export default ImageComponent;

十、rectangle

ts 复制代码
import { useEffect, useRef } from "react";
import Back from "~/components/Back";
import Box from "~/components/Box";

const RectangleComponentImpl = ({ x, y, width, height }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    ctx.fillStyle = "blue";
    ctx.fillRect(x, y, width, height);
  }, [x, y, width, height]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function RectangleComponent() {
  return (
    <Box title="rectangle">
      <RectangleComponentImpl x={10} y={10} width={150} height={100} />
      <Back />
    </Box>
  );
}

export default RectangleComponent;

十一、square

ts 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";
import Box from "~/components/Box";

const SquareComponentImpl = ({ x, y, size }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    ctx.fillStyle = "red";
    ctx.fillRect(x, y, size, size);
  }, [x, y, size]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function SquareComponent() {
  return (
    <Box title="rectangle">
      <SquareComponentImpl x={0} y={0} size={50} />
      <Back />
    </Box>
  );
}

export default SquareComponent;

十二、star

ts 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";
import Box from "~/components/Box";

const StarComponentImpl = ({ x, y, radius, spikes }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    const drawStar = () => {
      let rotation = (Math.PI / 2) * 3;
      let xPoint = x;
      let yPoint = y;
      let step = Math.PI / spikes;

      ctx.beginPath();
      ctx.moveTo(x, y - radius);

      for (let i = 0; i < spikes; i++) {
        xPoint = x + Math.cos(rotation) * radius;
        yPoint = y + Math.sin(rotation) * radius;
        ctx.lineTo(xPoint, yPoint);
        rotation += step;

        xPoint = x + Math.cos(rotation) * (radius * 0.5);
        yPoint = y + Math.sin(rotation) * (radius * 0.5);
        ctx.lineTo(xPoint, yPoint);
        rotation += step;
      }

      ctx.lineTo(x, y - radius);
      ctx.closePath();
      ctx.fillStyle = "purple";
      ctx.fill();
    };

    drawStar();
  }, [x, y, radius, spikes]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function StarComponent() {
  return (
    <Box title="star">
      <StarComponentImpl x={100} y={100} radius={50} spikes={5} />
      <StarComponentImpl x={100} y={100} radius={50} spikes={5} />
      <Back />
    </Box>
  );
}

export default StarComponent;

十三、triangle

ts 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";
import Box from "~/components/Box";

const TriangleComponentImpl = ({ x1, y1, x2, y2, x3, y3 }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    const drawTriangle = () => {
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.lineTo(x3, y3);
      ctx.closePath();
      ctx.fillStyle = "orange";
      ctx.fill();
    };

    drawTriangle();
  }, [x1, y1, x2, y2, x3, y3]);

  return <canvas ref={canvasRef} width={200} height={200} />;
};

function TriangleComponent() {
  return (
    <Box title="triangle">
      <TriangleComponentImpl x1={0} y1={0} x2={100} y2={100} x3={200} y3={0} />
      <Back />
    </Box>
  );
}

export default TriangleComponent;

十四、video

ts 复制代码
import { useRef, useEffect } from "react";
import Back from "~/components/Back";
import Box from "~/components/Box";

const VideoComponentImpl = ({ videoUrl }: any) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  const handePlay = () => {
    const video = document.querySelector(".video")! as HTMLVideoElement;
    video?.play();
  };

  const handlePause = () => {
    const video = document.querySelector(".video")! as HTMLVideoElement;
    video?.pause();
  };

  useEffect(() => {
    const canvas = canvasRef.current!;
    const ctx = canvas.getContext("2d")!;
    const video = document.createElement("video");

    video.className = "video";
    video.style.width = "320px";
    video.style.height = "176px";
    video.style.visibility = "hidden";

    const loadVideo = () => {
      video.src = videoUrl;
      video.autoplay = false;

      video.onloadedmetadata = () => {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        drawVideo();
      };
    };

    loadVideo();

    const drawVideo = () => {
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      requestAnimationFrame(drawVideo);
    };

    if (!document.querySelector(".video")) {
      document.body.appendChild(video);
    }

    return () => {
      video.pause();
      document.body.removeChild(video);
    };
  }, [videoUrl]);

  return (
    <div style={{ display: "flex", flexDirection: "column" }}>
      <canvas ref={canvasRef} />
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          margin: "10px 0px",
        }}
      >
        <button onClick={handePlay} style={{ margin: "0px 10px 0px 0px" }}>
          play
        </button>
        <button onClick={handlePause}>pause</button>
      </div>
    </div>
  );
};

function VideoComponent() {
  return (
    <Box title="video">
      <VideoComponentImpl videoUrl="https://www.w3schools.com/html/mov_bbb.mp4" />
      <Back />
    </Box>
  );
}

export default VideoComponent;

十五、当然也可以访问仓库

🚀canvas-with-remix 仓库

🚀canvas-with-remix 部署地址

十六、小结

本文的主要的来源主要是 Fimga 的绘制基本图形和 Excalidraw 的基本图形。 在本文中实现了 canvas 的多种基本图像绘制,使用的是基本的 api。配合 Remix 快速实现页面。其中比较特殊的是视频。视频的绘制其实也是用的 drawImage API 来获取数据。

相关推荐
To_OC4 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic5 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy5 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志5 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble6 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯7 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
戮漠summer8 小时前
Missing Semester 计算机教育中缺失的一课 Lecture 01 Shell
开发语言·后端·scala
名字还没想好☜8 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司8 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g9 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js