一、why?

在运用 Figma 的绘图共功能时候,Fimga 供给的几种默许的制作形状

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

在 remix 环境中 运用制作 canvas 以上图像, 详细看下面的面的内容。

二、Remix 中的 css 计划

为简单起变,css 运用的计划是 css module 计划。不必装置任何其他的内容,直接运用。

三、Box 组件

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 内容如下:

.box {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 100px;
}
.draw {
  margin-top: 20px;
  display: flex;
  border: 1px solid seagreen;
}

3.1)一个回来按钮组件

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 款式:

.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;
}

四、主页导航页

因为有列表:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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