RoughJS 是一个小巧(压缩后不到 9KB)的图形库,它能让你轻松绘制出类似手绘草图的图形。无论是线条、曲线、弧线,还是多边形、圆形和椭圆形,RoughJS 都能帮你搞定。最重要的是,它既支持 Canvas,也支持 SVG。
安装
npm install --save roughjs
快速入门
import { RoughSVG } from "roughjs/bin/svg"
const svg = document.querySelector('svg') as SVGSVGElement
const rc = new RoughSVG(svg)
const node = rc.rectangle(10, 10, 200, 200); // x, y, width, height
svg.appendChild(node)
直线和椭圆
当然,RoughJS 不仅仅能画矩形,它还能轻松绘制直线、圆形和椭圆
rc.circle(80, 120, 50); // centerX, centerY, 直径
rc.ellipse(300, 100, 150, 80); // centerX, centerY, width, height
rc.line(80, 120, 300, 100); // x1, y1, x2, y2
填充
RoughJS 还支持多种填充效果,让你的图形更加丰富多彩
rc.circle(50, 50, 80, { fill: 'red' }); // 使用红色的影线
rc.rectangle(120, 15, 80, 80, { fill: 'red' });
rc.circle(50, 150, 80, {
fill: "rgb(10,150,10)",
fillWeight: 3 // 设置填充的权重
});
rc.rectangle(220, 15, 80, 80, {
fill: 'red',
hachureAngle: 60, // 设置影线的角度
hachureGap: 8
});
rc.rectangle(120, 105, 80, 80, {
fill: 'rgba(255,0,200,0.2)',
fillStyle: 'solid' // 设置填充的类型
});
填充样式
RoughJS 提供了多种填充样式,包括:hachure
(default)、solid
、zigzag
、cross-hatch
、dots
、sunburst
、dashed
或者zigzag-line
rc.rectangle(10, 10, 80, 80)
rc.rectangle(110, 10, 80, 80, { fill: '#6366f1' })
rc.rectangle(210, 10, 80, 80, {
fill: 'pink',
fillStyle: 'solid'
})
rc.rectangle(310, 10, 80, 80, {
fill: '#f43f5e',
fillStyle: 'zigzag',
strokeWidth: 2,
fillWeight: 1.8, stroke: '#f43f5e'
})
rc.rectangle(410, 10, 80, 80, {
fill: '#ef4444',
fillStyle: 'cross-hatch',
strokeWidth: 1.2
})
rc.rectangle(10, 110, 80, 80, {
fill: '#c084fc',
fillStyle: 'dots',
hachureGap: 6
})
rc.rectangle(110, 110, 80, 80, {
fill: '#f97316',
stroke: '#fb7185',
hachureAngle: 0,
strokeWidth: 3,
fillStyle: 'sunburst'
})
rc.rectangle(210, 110, 80, 80, {
fill: '#14b8a6',
fillWeight: 5,
hachureGap: 10,
hachureAngle: 90,
fillStyle: 'dashed'
})
rc.rectangle(310, 110, 80, 80, {
fill: '#3b82f6',
fillStyle: ' zigzag-line'
})
草图风格
RoughJS通过设置roughness
(粗糙度)和bowing
(弯曲度)等参数来实现不同的草图风格
rc.rectangle(15, 15, 80, 80, { roughness: 0.5, fill: 'red' });
rc.rectangle(120, 15, 80, 80, { roughness: 2.8, fill: 'blue' });
rc.rectangle(220, 15, 80, 80, { bowing: 6, stroke: 'green', strokeWidth: 3 });
SVG Path
RoughJS 还支持通过path
来绘制复杂的图形
rc.path('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z', { fill: 'green' });
rc.path('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z', { fill: 'purple' });
rc.path('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z', { fill: 'red' });
rc.path('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z', { fill: 'blue' });
如何在React中使用RoughJS
RoughJS 直接操作 DOM 创建图形,但是在 React 中需要使用useEffect
添加到组件中,这种方式使用起来不太方便。我们可以封装成React组件来简化使用:
// 参考:<https://github.com/rough-stuff/rough/blob/master/src/svg.ts>
import React, { forwardRef } from "react";
import { Drawable, Options } from "roughjs/bin/core";
import { RoughGenerator } from "roughjs/bin/generator";
const gen = new RoughGenerator()
type RoughDrawProps = React.SVGAttributes<SVGGElement> & { drawable: Drawable }
const RoughDraw = forwardRef<SVGGElement, RoughDrawProps>(({
drawable,
...props
}, ref) => {
const sets = drawable.sets || [];
const o = drawable.options || gen.defaultOptions;
const precision = drawable.options.fixedDecimalPlaceDigits;
const paths = sets.map((drawing, index) => {
const d = gen.opsToPath(drawing, precision)
switch (drawing.type) {
case 'path':
return (
<path
key={index}
d={d}
stroke={o.stroke}
strokeWidth={o.strokeWidth}
fill={'none'}
strokeDasharray={o.strokeLineDash
? o.strokeLineDash.join(' ').trim()
: undefined
}
strokeDashoffset={o.strokeLineDashOffset
? o.strokeLineDashOffset
: undefined
}
/>
)
case 'fillPath':
return (
<path
key={index}
d={d}
stroke={'none'}
strokeWidth={0}
fill={o.fill || ''}
fillRule={drawable.shape === 'curve' || drawable.shape === 'polygon'
? 'evenodd'
: undefined
}
/>
)
case 'fillSketch':
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
return (
<path
key={index}
d={d}
stroke={o.fill || ''}
strokeWidth={fweight}
fill={'none'}
strokeDasharray={o.strokeLineDash
? o.strokeLineDash.join(' ').trim()
: undefined
}
strokeDashoffset={o.strokeLineDashOffset
? o.strokeLineDashOffset
: undefined
}
/>
)
default:
return null;
}
}).filter(item => !!item)
return (
<g {...props} ref={ref}>
{paths}
</g>
)
})
type RoughPathProps = React.SVGAttributes<SVGGElement> & { d: string, options?: Options }
export const RoughPath = forwardRef<SVGGElement, RoughPathProps>(({
d,
options,
...props
}, ref) => {
const drawing = gen.path(d, options);
return (
<RoughDraw drawable={drawing} {...props} ref={ref} />
)
})
type RoughEllipseProps = React.SVGAttributes<SVGGElement> & {
x: number,
y: number,
width: number,
height: number,
options?: Options
}
export const RoughEllipse = forwardRef<SVGGElement, RoughEllipseProps>(({
x,
y,
width,
height,
options,
...props
}, ref) => {
const drawing = gen.ellipse(x, y, width, height, options);
return (
<RoughDraw drawable={drawing} {...props} ref={ref} />
)
})
// 省略其他。。。
测试
最后,我们来测试一下这个 React 组件
<svg width={500} height={500} id='svg'>
<RoughEllipse
x={250}
y={250}
width={400}
height={400}
options={{ fill:'orange', fillStyle: 'solid', seed: 1 }}
/>
<RoughEllipse
x={150}
y={150}
width={50}
height={50}
options={{ fill:'black', fillStyle:'solid', roughness: 1.2 }}
/>
<RoughEllipse
x={350}
y={150}
width={50}
height={50}
options={{ fill:'black', fillStyle:'solid', roughness: 1.2 }}
/>
<RoughPath
d='M150,350 Q250,375 350,350'
options={{ stroke:'black', strokeWidth: 3 }}
/>
</svg>