在 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 来获取数据。

相关推荐
萧行之2 分钟前
Observable Plot 源码深度解析——4 核心抽象逐项学习
前端·学习·数据可视化
java porter6 分钟前
我开源了一个 Spring AI Agent 项目
后端
今日无bug19 分钟前
为什么大模型回答总是一个字一个字蹦出来?聊聊 SSE 流式输出与 BFF 层的那些事
前端·llm
CIO_Alliance23 分钟前
AI微调系列(1)| 全量微调、LoRA、QLoRA 三种方案怎么选
前端·人工智能·神经网络·机器学习·embedding·企业ai转型
hanbo17C224 分钟前
企业官网能不能看出公司实力?选建站公司前,先看这6条硬标准
运维·服务器·前端
1360967572330 分钟前
AgentScope 2.0 学习笔记:RAG 进阶——Top-K 调优与幻觉测试(让 Agent 敢说「不知道」)
后端
zzzll111134 分钟前
LLM 学习第 24 课:Agent Harness
前端·人工智能·学习
Bmob后端云34 分钟前
Bmob后端云实战|Python实现SSE流式输出,给备忘录AI加上打字机效果
前端·github
yume_sibai37 分钟前
07-Rust 异步编程完全指南(async/await + Tokio + Future + 并发原语 + 异步流)
开发语言·后端·rust
nunumaymax39 分钟前
【第九章-React Router 6】
前端·react.js