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

相关推荐
爬山算法8 分钟前
Hibernate(47)Hibernate的会话范围(Scope)如何控制?
java·后端·hibernate
光影少年34 分钟前
前端如何调用gpu渲染,提升gpu渲染
前端·aigc·web·ai编程
Surplusx1 小时前
运用VS Code前端开发工具完成网页头部导航栏
前端·html
小宇的天下1 小时前
Calibre 3Dstack --每日一个命令day13【enclosure】(3-13)
服务器·前端·数据库
源码宝1 小时前
云HIS二次开发实施路径指南
后端·源码·二次开发·saas·云his·医院信息系统
一只小bit2 小时前
Qt 文件:QFile 文件读写与管理教程
前端·c++·qt·gui
午安~婉2 小时前
整理知识点
前端·javascript·vue
军军君013 小时前
Three.js基础功能学习十二:常量与核心
前端·javascript·学习·3d·threejs·three·三维
m0_748254663 小时前
CSS AI 编程
前端·css·人工智能
27669582923 小时前
dy bd-ticket-guard-client-data bd-ticket-guard-ree-public-key 逆向
前端·javascript·python·abogus·bd-ticket·mstoken·ticket-guard