RoughJS-手绘风格的图形库

520 阅读4分钟

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)

image.png

直线和椭圆

当然,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

image.png

填充

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' // 设置填充的类型
    });

image.png

填充样式

RoughJS 提供了多种填充样式,包括:hachure(default)、solidzigzagcross-hatchdotssunburstdashed或者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' 
    })

image.png

草图风格

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

image.png

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' });

image.png

如何在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>

image.png