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

相关推荐
用户490558160812512 分钟前
lvs会话同步
后端
用户490558160812512 分钟前
linux内核网络协议栈报文的处理过程
后端
Bottle41412 分钟前
深入探究 React Fiber(译文)
前端
夜宵饽饽13 分钟前
上下文工程实践 - 工具管理(上篇)
javascript·后端
ERP老兵_冷溪虎山15 分钟前
Python/JS/Go/Java同步学习(第十三篇)四语言“字符串转码解码“对照表: 财务“小南“纸式转码术处理凭证乱码崩溃(附源码/截图/参数表/避坑指南)
java·后端·python
汤姆Tom18 分钟前
JavaScript Proxy 对象详解与应用
前端·javascript
努力的小郑19 分钟前
MySQL索引(四):深入剖析索引失效的原因与优化方案
后端·mysql·性能优化
智商偏低23 分钟前
ASP.NET Core 中的简单授权
后端·asp.net
xiaopengbc24 分钟前
在 React 中如何使用 useMemo 和 useCallback 优化性能?
前端·javascript·react.js
GISer_Jing26 分钟前
React 18 过渡更新:并发渲染的艺术
前端·javascript·react.js