一、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;
十五、当然也可以访问仓库
十六、小结
本文的主要的来源主要是 Fimga
的绘制基本图形和 Excalidraw
的基本图形。 在本文中实现了 canvas 的多种基本图像绘制,使用的是基本的 api。配合 Remix 快速实现页面。其中比较特殊的是视频。视频的绘制其实也是用的 drawImage API 来获取数据。