roughjs

github上面有个手绘的library: roughjs ,绘制效果大致如下:
image.png
个人比较喜欢这种风格,想用到自己的react项目之中。

react-rough

还有一个库叫react-rough,使用roughjs来绘制部分形状。

  1. <ReactRough
  2. height={500}
  3. width={700}
  4. >
  5. <Polygon
  6. fill="red"
  7. points={[
  8. [
  9. 490,
  10. 130
  11. ],
  12. [
  13. 590,
  14. 120
  15. ],
  16. [
  17. 550,
  18. 160
  19. ],
  20. [
  21. 490,
  22. 200
  23. ]
  24. ]}
  25. />
  26. </ReactRough>

实现上,外层的ReactRough是一个canvas画布,children为各种roughjs支持的图形。
问题还是使用canvas,不能使用浏览器原生能力,事件等都要自行模拟,工作量非常大,放弃。

rough-paint

rough-paint这个库思路比较奇特。使用的是CSS Paint API(详见附录一),然后桥接roughjs的内部render。结构大致如下:
image.png
效果还是很理想的,直接可以在div里面渲染想要的roughjs效果,如下图:
image.png
但是项目必须要有注册custom paint的一份代码

  1. CSS.paintWorklet.addModule('../dist/rough-painter.bundled.js');

这里的问题来了,addModule这个api只能支持资源类url,不支持es module(详见附录二)。所以在构建自己内部仓库会非常麻烦。忍痛放弃。

css-paint-polyfill

找了一圈,发现了谷歌实验室针对 css paint api 做了一套polyfill能力。实现原理基于 MutationObserver 监听元素更改来实时渲染。

步骤一:获取所有的样式,检查是否定义了paint(custom-painter),并存储下来

  1. let sheets = [].slice.call(document.styleSheets),
  2. context = {
  3. toProcess: [],
  4. toRemove: [],
  5. counters: {},
  6. isNew: false,
  7. sheetId: null
  8. };
  9. for (let i=0; i<sheets.length; i++) {
  10. let node = sheets[i].ownerNode;
  11. if (node.$$isPaint) continue;
  12. // Check that we can access the sheet.
  13. // The rules binding is required in order to prevent Terser from removing the block.
  14. // eslint-disable-next-line no-unused-vars
  15. let rules;
  16. try { rules = node.sheet.cssRules; }
  17. catch (e) { continue; }
  18. context.sheetId = node.$$paintid;
  19. context.isNew = context.sheetId == null;
  20. if (context.isNew) {
  21. context.sheetId = node.$$paintid = ++styleSheetCounter;
  22. // allow processing to defer parse
  23. if (processNewSheet(node)===false) {
  24. continue;
  25. }
  26. }
  27. walkStyles(node.sheet, paintRuleWalker, context);
  28. }

步骤二:监听MutationObserver变动,然后检查变动是否需要处理css paint api

  1. new MutationObserver(records => {
  2. if (lock===true) return;
  3. lock = true;
  4. for (let i = 0; i < records.length; i++) {
  5. let record = records[i], added;
  6. if (record.type === 'childList' && (added = record.addedNodes)) {
  7. for (let j = 0; j < added.length; j++) {
  8. if (added[j].nodeType === 1) {
  9. queueUpdate(added[j]);
  10. }
  11. }
  12. }
  13. else if (record.type==='attributes' && record.target.nodeType === 1) {
  14. if (record.target === a) {
  15. supportsStyleMutations = true;
  16. }
  17. else {
  18. walk(record.target, queueUpdate);
  19. }
  20. }
  21. }
  22. lock = false;
  23. }).observe(document.body, {
  24. childList: true,
  25. attributes: true,
  26. subtree: true
  27. });

步骤三:正则检查元素的css样式,命中则调用已注册的paint module来进行绘制

  1. while ((token = reg.exec(value))) {
  2. if (hasPaints === false) {
  3. paintId = ensurePaintId(element);
  4. }
  5. hasPaints = true;
  6. newValue += value.substring(0, token.index);
  7. let painterName = token[4] || token[5];
  8. let currentUri = token[3];
  9. let painter = getPainter(painterName);
  10. let contextOptions = painter && painter.Painter.contextOptions || {};
  11. let equivalentDpr = contextOptions.scaling === false ? 1 : dpr;
  12. let inst;
  13. if (painter) {
  14. if (painter.Painter.inputProperties) {
  15. observedProperties.push.apply(observedProperties, painter.Painter.inputProperties);
  16. }
  17. inst = getPainterInstance(painter);
  18. }
  19. if (contextOptions.nativePixels===true) {
  20. geom.width *= dpr;
  21. geom.height *= dpr;
  22. equivalentDpr = 1;
  23. }
  24. let actualWidth = equivalentDpr * geom.width,
  25. actualHeight = equivalentDpr * geom.height;
  26. let ctx = element.$$paintContext,
  27. cssContextId = `paint-${paintId}-${painterName}`;
  28. if (!ctx || !ctx.canvas || ctx.canvas.width!=actualWidth || ctx.canvas.height!=actualHeight) {
  29. if (USE_CSS_CANVAS_CONTEXT===true) {
  30. ctx = document.getCSSCanvasContext('2d', cssContextId, actualWidth, actualHeight);
  31. }
  32. else {
  33. let canvas = document.createElement('canvas');
  34. canvas.id = cssContextId;
  35. canvas.width = actualWidth;
  36. canvas.height = actualHeight;
  37. if (USE_CSS_ELEMENT===true) {
  38. canvas.style.display = 'none';
  39. root.appendChild(canvas);
  40. }
  41. ctx = canvas.getContext('2d');
  42. }
  43. element.$$paintContext = ctx;
  44. ctx.imageSmoothingEnabled = false;
  45. if (equivalentDpr!==1) ctx.scale(equivalentDpr, equivalentDpr);
  46. }
  47. else {
  48. ctx.clearRect(0, 0, actualWidth, actualHeight);
  49. }
  50. if (inst) {
  51. ctx.save();
  52. ctx.beginPath();
  53. inst.paint(ctx, geom, propertiesContainer);
  54. // Close any open path so clearRect() can dump everything
  55. ctx.closePath();
  56. // ctx.stroke(); // useful to verify that the polyfill painted rather than native paint().
  57. ctx.restore();
  58. // -webkit-canvas() is scaled based on DPI by default, we don't want to reset that.
  59. if (USE_CSS_CANVAS_CONTEXT===false && 'resetTransform' in ctx) {
  60. ctx.resetTransform();
  61. }
  62. }

步骤四:替换更新元素相应样式

整个过程还算比较顺利,至于这种方式的性能,官方宣称没什么问题

Performance is particularly good in Firefox and Safari, where this polyfill leverages -webkit-canvas() and -moz-element() for optimized rendering. For other browsers, framerate is governed by Canvas toDataURL() / toBlob() speed.

看到这里,聪明的你应该猜到接下来的内容了

css-paint-es

  1. css-paint-es = css-paint-polyfill + es module

我们通过更改css-paint-polyfill源码的入口,让它来适配 es module 。至此解决掉了 css paint api 不支持 es module 的问题。
详细的代码实现可参考: https://github.com/YouHan26/css-paint-polyfill

css-rough

我们再使用css-paint-es来改造rough-paint,便有了css-rough
image.png
详见: https://github.com/YouHan26/react-rough

附录一:CSS Paint API

介绍地址:https://developers.google.com/web/updates/2018/01/paintapi

CSS Paint API 也被成为”CSS Custom Paint”、“Houdini’s paint worklet”。简单来讲,它可以让开发者更自主的控制浏览器绘画底层来画出来各种图片。可以用在css 样式中的 background-imageborder-image 等之中。当然,同样的绘画能力直接用canvas也是可以的。CSS Paint API的主要优势有两个:

  1. 可以通过css来选择性的进行绘画
    canvas就比较受限,你不可能在保持原生浏览器tag(div、input) 的各种浏览器支持能力(浏览器事件、默认组件行为)同时,还能去自定义绘制一些自定义background image等。
  2. 性能
    由于CSS Paint API是浏览器提供的基础api,使用此API画出来的性能肯定是比较高效。同时因为css选择器的方式来使用,整个绘制是跟着浏览器重绘来自动更新,相较于canvas手动各种清除、重画显得特别高效。这里的高效不仅在于执行效率,更在于开发效率。

具体API相关的使用,看相应的网站就可以了,这里就不仔细介绍

  1. https://www.w3.org/TR/css-paint-api-1/
  2. https://css-houdini.rocks/
  3. https://developers.google.com/web/updates/2018/01/paintapi
  4. https://developer.mozilla.org/en-US/docs/Web/API/CSS_Painting_API
  5. https://www.zhangxinxu.com/wordpress/2018/11/css-paint-api-canvas/

附录二:V8 modules

es module

随着前端发展,模块化变成了一个命题。commonjs、AMD、UMD等不再详细介绍。值得注意的是Ecma262 2015规范中新增的模块化支持:es module。
但是在CSS Paint API中,添加worklet必须是一个url路径类型的文件。是不支持es module。

data-url

浏览器还支持一种url方式就是 URL.createObjectURL() 生成的data-url。这种方式就是比较粗暴,需要把源代码当作字符串,转成 Blob类型,然后来生成data-uri。
这种方式在遇到依赖npm包的情况下又会特别难搞。毕竟不可能把所有的依赖文件都丢到一个地方,反而又和现有的模块化背道而行。

参考链接:

  1. https://v8.dev/features/modules#worklets-workers
  2. https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
  3. https://stackoverflow.com/questions/10343913/how-to-create-a-web-worker-from-a-string