这是用户在 2024-10-31 12:54 为 https://bowencodes.com/post/react-rough-fiber 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

React Rough Fiber: A React renderer for rendering hand-drawn SVGs
React Rough Fiber:用于渲染手绘 SVG 的 React 渲染器

Open Source 2023 年 4 月 21 日开源
Home 

React Rough Fiber 反应粗纤维

A React renderer for rendering hand-drawn SVGs.
用于渲染手绘 SVG 的 React 渲染器。

Docs and Examples 文档和示例

Github  吉图布

Several weeks ago, I found an awesome project named perfect-freehand, which allows you to draw perfect pressure-sensitive freehand lines. The author also mentioned using this library in a Figma plugin to create freehand icons. This is really cool, and I'm inspired to create a library to render hand-drawn SVGs easily.
几周前,我发现了一个名为 Perfect-freehand 的很棒的项目,它可以让你绘制完美的压力敏感手绘线条。作者还提到在 Figma 插件中使用这个库来创建徒手图标。这真的很酷,我受到启发,创建了一个库来轻松渲染手绘 SVG。

There are already some libraries that can render hand-drawn SVGs, such as Rough.js. However, they might be difficult to integrate with existing SVG libraries. If you are currently using SVG icon or SVG chart libraries, you cannot use Rough.js directly.
已经有一些库可以渲染手绘 SVG,例如 Rough.js。然而,它们可能很难与现有的 SVG 库集成。如果您当前正在使用SVG图标或SVG图表库,则不能直接使用 Rough.js

It's a nice way to create hand-drawn SVGs in React:
这是在 React 中创建手绘 SVG 的好方法:

JSX
<RoughSVG>
  {/* ... any SVG */}
</RoughSVG>

Simple, right? This is what I want to do.
很简单,对吧?这就是我想做的。

Main Idea 大意

The main idea is to accept SVG props, like fill, stroke, d, cx, cy, etc., and then utilize Rough.js with these properties to generate SVGs.
主要思想是接受 SVG 属性,如 fillstrokedcxcy 等,然后利用 Rough.js 和这些属性来生成 SVG。

For example, we have a SVG like this:
例如,我们有一个像这样的 SVG:

JSX
<RoughSVG>
  <svg width="128" height="64" xmlns="http://www.w3.org/2000/svg">
    <circle cx="32" cy="32" r="24" fill="red" />
  </svg>
</RoughSVG>

We receive the props of the circle element { cx: 32, cy: 32, r: 24, fill: 'red' }
我们收到圆形元素的props { cx: 32, cy: 32, r: 24, fill: 'red' }

Then we can use Rough.js to generate a hand-drawn circle(pseudo code): rough.circle(32, 32, 24, { fill: 'red' })
然后我们可以使用 Rough.js 生成一个手绘圆圈(伪代码): rough.circle(32, 32, 24, { fill: 'red' })

Now, the question arises as to how to accept SVG properties and use them efficiently to render DOM elements in React?
现在,问题是如何接受 SVG 属性并有效地使用它们在 React 中渲染 DOM 元素?

Some Attempts 一些尝试

Traverse Children 穿越儿童

react-element-replace library provides React utility methods that transforms element subtrees, replacing elements following the provided rules.
react-element-replace 库提供了 React 实用方法来转换元素子树,按照提供的规则替换元素。

Here is a simple example of how to apply the color: #85A600 style attribute to all span elements:
以下是如何将 color: #85A600 样式属性应用于所有 span 元素的简单示例:

import { Replacer } from "react-element-replace"
export default function App() {
return (
<Replacer
matchElement="span"
replace={(item) => <span {...item.props} style={{ color: '#85A600' }} />}
>
<div>
<span>span</span>
<p>p</p>
</div>
</Replacer>
)
}

But this methods does not work with such as React.memo:
但此方法不适用于 React.memo

import { memo } from "react"
import { Replacer } from "react-element-replace"

const Memo = memo(() => (
<div>
<span>span</span>
<p>p</p>
</div>
))

export default function App() {
return (
<Replacer
matchElement="span"
replace={(item) => <span {...item.props} style={{ color: '#85A600' }} />}
>
<Memo />
</Replacer>
)
}

And as the author says in the README: "It does violate the explicit design of the framework. An important caveat is that replacing elements does sometimes interfere with React renderer operations, causing errors when there are changes of state below the replacer node".
正如作者在自述文件中所说:“它确实违反了框架的显式设计。一个重要的警告是,替换元素有时确实会干扰 React 渲染器操作,当替换节点下方的状态发生变化时会导致错误”。

So I think this library is not suitable for my purpose.
所以我认为这个库不适合我的目的。

Fake DOM 假DOM

React use container.ownerDocument.createElement to create DOM elements. So I tried to substitute container.ownerDocument.createElement with my own function for creating fake DOM elements.
React 使用 container.ownerDocument.createElement 创建 DOM 元素。所以我尝试用我自己的函数替换 container.ownerDocument.createElement 来创建假DOM元素。

I use proxy to create fake DOM elements, render specified element and properties by rewiring the createElement, appenChild, setAttribute methods
我使用代理创建假 DOM 元素,通过重新连接 createElementappenChildsetAttribute 方法来渲染指定的元素和属性

Here is a straightforward example that sets the fill attribute to #85A600 whenever the received fill attribute is red:
这是一个简单的示例,每当接收到的填充属性为红色时,将填充属性设置为 #85A600

import { useState, useRef, useEffect } from "react"
import { createPortal } from "react-dom"

const createProxy = (target, real) => {
return new Proxy(target, {
get(target, prop) {
const value = prop in target ? target[prop] : real[prop]

if (typeof value === "function") {
return prop in target ? value.bind(target) : value.bind(real)
}
return value
}
})
}

function createFakeDocument(realDocument) {
return createProxy(
{
createElementNS: (ns, type) => {
const realElement = realDocument.createElementNS(ns, type)
return createFakeElement(realElement)
}
},
realDocument
)
}

const fakeDocument = createFakeDocument(document)

function createFakeElement(realElement) {
return createProxy(
{
_element: realElement,
ownerDocument: fakeDocument,
setAttribute(name, value) {
if (name === "fill" && value === "red") {
realElement.setAttribute(name, "#85A600")
return
}
realElement.setAttribute(name, value)
},
appendChild(child) {
realElement.appendChild(child._element)
},
removeChild(child) {
realElement.removeChild(child._element)
}
},
realElement
)
}

const RoughSVG = ({ children }) => {
const ref = useRef()
const [fakeElement, setFakeElement] = useState(null)
useEffect(() => {

On this basis, we can use proxy to rewirte any function of a DOM element or document.
在此基础上,我们可以使用 proxy 来重写DOM元素或文档的任何功能。

This method works well, but it has a problem: it's difficult to merge multiple updates.
这种方法效果很好,但有一个问题:很难合并多个更新。

There will be four calls to the setAttribute function if a rect element receives changes in x, y, width, and height during a render. We have to call roughjs four times, because we don't know which update is the last one.
如果 rect 元素在渲染期间收到 xywidthheight 中的更改,则会对 setAttribute 函数进行四次调用。我们必须调用 roughjs 四次,因为我们不知道哪个更新是最后一个。

React Renderer 反应渲染器

Using react-reconciler to create a custom renderer for React:
使用 react-reconciler 为 React 创建自定义渲染器:

JS
import Reconciler from 'react-reconciler';
const hostConfig = {
	// ...
	createInstance(type, props) {
		// ...
	},
	commitUpdate(instance, updatePayload, type, prevProps, nextProps) {
		// ...
	},
};
const CustomRenderer = Reconciler(hostConfig);

The createInstance method is used to create a DOM element, and the commitUpdate method is used to update the DOM element.
createInstance 方法用于创建DOM元素, commitUpdate 方法用于更新DOM元素。

We can decide how to diff between prevProps and nextProps, and merge multiple props updates into one.
我们可以决定如何区分 prevProps 和 nextProps,并将多个 props 更新合并为一个。

In fact, this was the method I first tried. But I encountered several challenges while implementing it:
事实上,这是我第一次尝试的方法。但我在实施过程中遇到了一些挑战:

  • Can't share contexts between React renderers, see this issue
    无法在 React 渲染器之间共享上下文,请参阅此问题
  • It's so complex to implement a custom renderer. At the beginning, I attempted to copy the logic of react-dom. However, react-dom is a heavy libaray, and my aim is to develop a lightweight renderer.
    实现自定义渲染器非常复杂。一开始,我尝试复制 react-dom 的逻辑。然而, react-dom 是一个沉重的libaray,我的目标是开发一个轻量级的渲染器。
  • Rough.js render a SVG shape into two paths, one for fill and one for stroke. So we need to set the value of the fill attribute as the value of the stroke attribute for the fill path. But it's difficult to implement this when the fill attribute is inherited from the parent element:
    Rough.js 将 SVG 形状渲染为两条路径,一条用于填充,一条用于描边。所以我们需要将填充属性的值设置为填充路径的描边属性的值。但是当 fill 属性继承自父元素时,实现起来就很困难:
XML
<g fill="red" stroke="green">
  <!-- fill path. the stroke attribute should be red -->
  <path />
  <!-- stroke path. the stroke attribute should be green -->
  <path />
</g>

So, I set aside that implementation method for a while. But then, when I reconsidered, I realized that these problems were not unsolvable after all
因此,我暂时搁置了该实现方法。但后来我转念一想,我发现这些问题毕竟不是无法解决的

  • its-fine provides a ContextBridge that forward contexts between renderers. Both react-three-fiber and react-konva use it.
    its-fine 提供了一个 ContextBridge 在渲染器之间转发上下文。 react-three-fiberreact-konva 都使用它。
  • preact is a lightweight React implementation. It has a diffProps function for updating properties and events, which is implemented in 157 lines of code. preact has been proven by many applications. I created my custom renderer using this function as a basis.
    preact 是一个轻量级的 React 实现。它有一个用于更新属性和事件的 diffProps 函数,该函数用 157 行代码实现。 preact 已被许多应用证明。我使用此函数作为基础创建了自定义渲染器。
  • I tried three ways to solve the problem of the fill attribute being inherited from the parent element:
    我尝试了三种方法来解决从父元素继承fill属性的问题:
    1. Use a fill path to replace the stroke path
      使用填充路径代替描边路径
    2. HostContext 主机上下文
    3. SVG <defs>
    4. CSS variables CSS 变量

Use a fill path to mock the stroke path
使用填充路径来模拟描边路径

The method asked us to calculate the fill path d from the stroke path d and stroke-width. Look at the following SVG code:
该方法要求我们根据描边路径 dstroke-width 计算填充路径 d 。看下面的 SVG 代码:

HTML 超文本标记语言
<path d="M 4 12 L 32 12" stroke-width="2"></path>
<path d="M 4 24 L 32 24 L 32 26 L 4 26 Z" stroke="none"></path>

The first path is the outline stroke path, and the second path is the fill path used to create a mock stroke path. They are rendered in the same result.
第一条路径是轮廓笔划路径,第二条路径是用于创建模拟笔划路径的填充路径。它们呈现相同的结果。

This method is my first attempt, and it works well at first. But then I found that it has a problem: the fill path is not smooth, when the stroke-width is thin.
这个方法是我第一次尝试,一开始效果还不错。但后来我发现它有一个问题:当描边宽度很细时,填充路径不平滑。

I haven't solved this problem. I guess it's because of rasterization.
我还没有解决这个问题。我猜这是因为光栅化。

HostContext 主机上下文

react-reconciler provides a getChildHostContext(parentHostContext, type, rootContainer) function to create a host context for a child element. But there is no way to receive props from parent element in this function.
react-reconciler 提供 getChildHostContext(parentHostContext, type, rootContainer) 函数来为子元素创建宿主上下文。但在此函数中无法从父元素接收 props。

Algough someone has created an issue for this problem, it has not been resolved yet.
尽管有人为此问题创建了一个问题,但尚未解决。

SVG <defs>

We can use SVG <defs> to define a pattern for an element that has fill attribute, and then use fill="url(#id)" in the child element to reference it.
我们可以使用 SVG <defs> 为具有 fill 属性的元素定义一个模式,然后在子元素中使用 fill="url(#id)" 来引用它。

export default function App() {
return (
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<g stroke="black" fill="#85A600">
<defs>
<pattern
id="fill"
patternUnits="userSpaceOnUse"
width="10"
height="10"
>
<rect width={10} height={10} stroke="none" />
</pattern>
</defs>
<path
d="M 0 24 L 64 24"
fill="none"
stroke="url(#fill)"
strokeWidth={4}
/>
<path d="M 0 48 L 64 48" fill="none" strokeWidth={4} />
</g>
</svg>
);
}

Although this method works well, it has a potential issue of generating a lot of <defs> elements.
尽管此方法效果很好,但它存在生成大量 <defs> 元素的潜在问题。

CSS variables CSS 变量

We can declare a CSS variable for an element that has fill attribute, and then use this variable in the child element's stroke attribute to reference it.
我们可以为具有 fill 属性的元素声明一个 CSS 变量,然后在子元素的 border 属性中使用该变量来引用它。

export default function App() {
return (
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<g
stroke="black"
fill="#85A600"
style={{
"--fill-color": "#85A600"
}}
>
<path
d="M 0 24 L 64 24"
fill="none"
stroke="var(--fill-color)"
strokeWidth={4}
/>
<path d="M 0 48 L 64 48" fill="none" strokeWidth={4} />
</g>
</svg>
);
}

This method also have an issue: It only work with the inline fill attribute, does not work with the CSS fill attribute. I think this is okay because SVG libraries hardly use CSS fill attribute.
此方法也有一个问题:它仅适用于内联填充属性,不适用于 CSS 填充属性。我认为这没关系,因为 SVG 库几乎不使用 CSS 填充属性。

The Result 结果

Here's an example of how to use react-rough-fiber and recharts to render a hand-drawn BarChart with only three additional lines of code:
下面是如何使用 react-rough-fiberrecharts 渲染手绘 BarChart 的示例,仅需要额外三行代码:

import { RoughSVG } from 'react-rough-fiber';
import { BarChart, XAxis, YAxis, Tooltip, Legend, Bar } from 'recharts';
import { data } from './data'
import './style.css'

export default function App() {
return (
<RoughSVG>
<BarChart width={730} height={250} data={data} style={{fontFamily: "'Caveat'"}}>
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="pv" fill="#8884d8" stroke="#333" />
<Bar dataKey="uv" fill="#82ca9d" stroke="#333" />
</BarChart>
</RoughSVG>
)
}

Credits 制作人员

react-rough-fiber is powered or inspired by these open source projects:
react-rough-fiber 受到这些开源项目的支持或启发: