WebGL Tutorial
and more

Canvas Replication

撰写时间:2025-04-03

修订时间:2025-04-08

图像资源太少?我们可以通过编程的方式,从无到有地创建出漂亮的图像。

本文中,我们先临摹一个Unicode字符的图案,然后为其生成一个可下载SVG的链接。

临摹

目标:在drawCanvas函数中进行临摹,以正好压住背景的图像。

function drawCanvas() { let centerX = canvas.clientWidth / 2; ctx.lineWidth = 9; let pathData = ` M ${centerX} 18 l -97 165 h 194 Z M ${centerX} 82 l -36 68 h 72 Z `; let path = new Path2D(pathData); ctx.strokeStyle = 'hsla(150, 50%, 50%, 0.7)'; ctx.stroke(path); } drawCanvas();
let bgCanvas; let bgCtx; function initBG() { bgCanvas = document.querySelector('#bg-canvas'); bgCanvas.style.width = bgCanvas.clientWidth + 'px'; bgCanvas.style.height = bgCanvas.clientHeight + 'px'; bgCanvas.width = bgCanvas.clientWidth * devicePixelRatio; bgCanvas.height = bgCanvas.clientHeight * devicePixelRatio; bgCtx = bgCanvas.getContext('2d'); bgCtx.scale(devicePixelRatio, devicePixelRatio); bgCtx.clearRect(0, 0, bgCanvas.clientWidth, bgCanvas.clientHeight); } function drawBG(char, fontSize, color) { bgCtx.font = `${fontSize}px Helvetica`; bgCtx.textAlign = 'center'; bgCtx.textBaseline = 'middle'; bgCtx.fillStyle = color; bgCtx.fillText(char, bgCanvas.clientWidth / 2, bgCanvas.clientHeight / 2); } initBG(); drawBG('\u{27C1}', 250, '#888');
body { display: grid; } #canvas, #bg-canvas { grid-row: 1; grid-column: 1; } #canvas { z-index: 10; }

drawCanvas函数调用屏蔽掉,则可看出使用灰色来绘制的背景图像。恢复drawCanvas函数调用,可以看出,上下两个图层完全匹配。

我们通过编写代码的方式,将任意的一个背景图像进行了精准的临摹,临摹的结果,是我们拥有了一套能生成特定图像的Canvas 2D的代码。

有了这段代码,我们一是可以在其基础上进行再创作;二是可以精准居中;三是可生成data:URL;四是应用滤镜,改变图像的效果;五是将其转化为其他格式的图像。

下节,我们将其在线转换为SVG图像格式并提供下载链接。

Canvas转换为SVG格式

Canvas 2D是基于BMP(位图)的绘图方式,SVG是一种矢量的绘图方式。将位图转换为矢量图的过程称为矢量化vectorization)过程,即将像素图像转换为由路径、形状和颜色组成的矢量图形。

矢量化过程是一个较为复杂的过程,目前尚没有直接将位图向SVG矢量化的规范。我们可以借助于诸如 Adobe Illustrator, Vector Magic, Vectormator 等相关的应用软件,或者通过诸如Potrace.js, ImageTracer.js, Sharp.js, Canva2SVG, OpenCV.js等第三方的JavaScript类库来实现目标,但总体说来,要么效果不大理想,要么学习曲线陡峭,要么需付费使用。简而言之,总有不尽人意的地方。

矢量化的本质是将像素转换为数学路径。好消息是,Canvas 2DPath2D,而SVGpath标签,这为矢量化过程搭建了一个很好的桥梁。

以此为抓手,我们可以不使用任何第三方软件或类库,就可将Canvas 2D所绘制的图像,直接转换为SVG图像。

const CENTER_X = canvas.clientWidth / 2; const pathWrapper = { lineWidth: 9, data: ` M ${CENTER_X} 18 l -97 165 h 194 Z M ${CENTER_X} 82 l -36 68 h 72 Z `, strokeColor: 'hsla(150, 50%, 50%, 1)', fillColor: 'transparent' }; let svgContentStr; function drawCanvas() { ctx.lineWidth = pathWrapper.lineWidth; let path = new Path2D(pathWrapper.data); ctx.strokeStyle = pathWrapper.strokeColor; ctx.stroke(path); ctx.fillStyle = pathWrapper.fillColor; ctx.fill(path); } function createSVG() { const SVG_NAME_SPACE = 'http://www.w3.org/2000/svg'; let svg = document.createElementNS(SVG_NAME_SPACE, 'svg'); svg.setAttribute('xmlns', SVG_NAME_SPACE); svg.setAttribute('version', '1.1'); svg.setAttribute('width', '150'); svg.setAttribute('height', '150'); svg.setAttribute('viewBox', `0 0 ${canvas.clientWidth} ${canvas.clientHeight}`); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); let path = document.createElementNS(svg.namespaceURI, 'path'); path.setAttribute('stroke-width', pathWrapper.lineWidth); path.setAttribute('d', pathWrapper.data); path.setAttribute('stroke', pathWrapper.strokeColor); path.setAttribute('fill', pathWrapper.fillColor); svg.appendChild(path); document.querySelector('#output').appendChild(svg); svgContentStr = new XMLSerializer().serializeToString(svg); console.log(svgContentStr); } function createDownloadLink() { let file = new File([svgContentStr], "two-triangles.svg", {type: "image/svg"}); let url = URL.createObjectURL(file); const link = document.createElement('a'); link.href = url; link.download = file.name; link.textContent = `Click to download`; document.querySelector('#output').appendChild(link); } drawCanvas(); createSVG(); createDownloadLink();
body { display: grid; grid-template-columns: auto auto; gap: 1em; } canvas { width: 250px; height: 200px; } #output { display: flex; flex-direction: column; gap: 1em; padding: 0.5em; border: 0.1px solid gray; width: fit-content; & svg { border: 0.11px solid gray; margin: 0 auto; } & a { color: #50B7E0; text-decoration: none; text-align: center; &:hover { color: #FCEE59; } } }

我们将Canvas 2DSVG均需要使用的路径数据,抽象为一个名为pathWrapper的对象,同时供给drawCanvascreateSVG函数使用。由于数据源相同,则创建出来的两种格式的图像内容完全相同。而若需要创建更复杂的图形,可通过构建多个子路径的方式来完成。

SVGviewBox是一个可自动转换坐标系及可实现自动居中的属性,利用此特性,我们将canvas的宽高值用以设置该属性值,这样,无论源图像的尺寸是多少,我们可以随意定制所生成的SVG的尺寸,最终都会按比例进行缩放并在水平、垂直方向上同时予以居中。具体细节,参见SVG坐标系

创建好svg后,我们通过XMLSerializerserializeToString方法,得到了整个SVG内容的字符串,打开console面板即可看到它。利用该字符串,我们既可生成一个独立的SVG文件,也可将其作为data:URL的数据源来直接使用。而在例子中,通过调用URLcreateObjectURL方法,传入一个指定了文件名及MIME TypeFile对象,创建了一个URL,最终创建一个HTMLAnchorElement实例,生成了一个可点击下载的链接。

HTML转换为SVG

利用SVGforeignObject,我们可以将任意的HTML的内容经渲染后,转换为SVG图像。

function createSVG() { const SVG_NAME_SPACE = 'http://www.w3.org/2000/svg'; let svg = document.createElementNS(SVG_NAME_SPACE, 'svg'); svg.setAttribute('width', '180'); svg.setAttribute('height', '120'); svg.innerHTML = `

This is a HTML paragraph.

This is a HTML paragraph.

This is a HTML paragraph.

`; return svg; } function svgToImg(svg) { let svgStr = new XMLSerializer().serializeToString(svg); let img = document.querySelector('img'); img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr); } let svg = createSVG(); svgToImg(svg);
img { border: .1px solid orange; width: 300px; aspect-ratio: 2 / 1; object-fit: contain; object-position: 50% 50%; }

调节svgwidthheight值,这是像机取景范围;调节imgwidthaspect-ratio值,这是投影大小;而object-fitobject-position用于设置是否按比例缩放以及如何居中。

关于图像设计知识产权的问题

通过这种方式,我们可以很方便地建立起自己的一套图标库,大小、内容、格式均可随意定制。

需要指出的是,一定要养成知识产权保护的意识。对于他人辛苦创建出来的有创意的图像,我们可以参考借鉴,并可在其基础上进行具备独创风格的第二次外观设计创作,只要具备新颖性、创造性及实用性,即可生成我们自己的受知识产权法保护的作品。唯独不能照搬照抄。

而在本例中,我们临摹的是Unicode字符,Unicode字符设计属于标准化的编码系统,其目的是为了在全球范围内实现字符的统一标识,不具备独创性,属于公共领域的知识产权,任何人均可合理地免费使用。因此大家可以放心使用。

参考资源

  1. 中华人民共和国专利法