烘焙转换为 SVG 路径元素命令
tl;dr 摘要:给我资源或帮助修复以下代码,以通过任意矩阵转换 SVG
元素的路径命令。
详细信息:
我正在编写一个库来将任意 SVG 形状转换为
元素。当层次结构中没有 transform="..."
元素时,我可以使用它,但现在我想将对象的本地转换烘焙到 路径数据命令本身。
在处理简单的 moveto/lineto 命令时,这主要是有效的(代码如下)。但是,我不确定转换贝塞尔曲线手柄或 arcTo 参数的适当方法。
例如,我能够将此圆角矩形转换为
:
<rect x="10" y="30" rx="10" ry="20" width="80" height="70" />
--> <path d="M20,30 L80,30 A10,20,0,0,1,90,50 L90,80 A10,20,0,0,1,80,100
L20,100 A10,20,0,0,1,10,80 L10,50 A10,20,0,0,1,20,30" />
并且在没有任何圆角的情况下进行转换时,我得到了有效的结果:
<rect x="10" y="30" width="80" height="70"
transform="translate(-200,0) scale(1.5) rotate(50)" />
--> <path d="M10,30 L90,30 L90,100 L10,100 L10,30" />
但是,仅转换 < 的 x/y 坐标a href="http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands">椭圆弧 命令会产生有趣的结果:
虚线是实际转换的矩形,绿色填充是我的路径。
以下是我到目前为止的代码(稍微精简)。我还有一个测试页面,我正在其中测试各种形状。请帮助我确定如何在给定任意变换矩阵的情况下正确变换椭圆弧
和各种其他贝塞尔命令。
function flattenToPaths(el,transform,svg){
if (!svg) svg=el; while(svg && svg.tagName!='svg') svg=svg.parentNode;
var doc = el.ownerDocument;
var svgNS = svg.getAttribute('xmlns');
// Identity transform if nothing passed in
if (!transform) transform= svg.createSVGMatrix();
// Calculate local transform matrix for the object
var localMatrix = svg.createSVGMatrix();
for (var xs=el.transform.baseVal,i=xs.numberOfItems-1;i>=0;--i){
localMatrix = xs.getItem(i).matrix.multiply(localMatrix);
}
// Transform the local transform by whatever was recursively passed in
transform = transform.multiply(localMatrix);
var path = doc.createElementNS(svgNS,'path');
switch(el.tagName){
case 'rect':
path.setAttribute('stroke',el.getAttribute('stroke'));
var x = el.getAttribute('x')*1, y = el.getAttribute('y')*1,
w = el.getAttribute('width')*1, h = el.getAttribute('height')*1,
rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1;
if (rx && !el.hasAttribute('ry')) ry=rx;
else if (ry && !el.hasAttribute('rx')) rx=ry;
if (rx>w/2) rx=w/2;
if (ry>h/2) ry=h/2;
path.setAttribute('d',
'M'+(x+rx)+','+y+
'L'+(x+w-rx)+','+y+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w)+','+(y+ry)) : '') +
'L'+(x+w)+','+(y+h-ry)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w-rx)+','+(y+h)) : '')+
'L'+(x+rx)+','+(y+h)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+x+','+(y+h-ry)) : '')+
'L'+x+','+(y+ry)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+rx)+','+y) : '')
);
break;
case 'circle':
var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
r = el.getAttribute('r')*1, r0 = r/2+','+r/2;
path.setAttribute('d','M'+cx+','+(cy-r)+' A'+r0+',0,0,0,'+cx+','+(cy+r)+' '+r0+',0,0,0,'+cx+','+(cy-r) );
break;
case 'ellipse':
var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1;
path.setAttribute('d','M'+cx+','+(cy-ry)+' A'+rx+','+ry+',0,0,0,'+cx+','+(cy+ry)+' '+rx+','+ry+',0,0,0,'+cx+','+(cy-ry) );
break;
case 'line':
var x1=el.getAttribute('x1')*1, y1=el.getAttribute('y1')*1,
x2=el.getAttribute('x2')*1, y2=el.getAttribute('y2')*1;
path.setAttribute('d','M'+x1+','+y1+'L'+x2+','+y2);
break;
case 'polyline':
case 'polygon':
for (var i=0,l=[],pts=el.points,len=pts.numberOfItems;i<len;++i){
var p = pts.getItem(i);
l[i] = p.x+','+p.y;
}
path.setAttribute('d',"M"+l.shift()+"L"+l.join(' ') + (el.tagName=='polygon') ? 'z' : '');
break;
case 'path':
path = el.cloneNode(false);
break;
}
// Convert local space by the transform matrix
var x,y;
var pt = svg.createSVGPoint();
var setXY = function(x,y,xN,yN){
pt.x = x; pt.y = y;
pt = pt.matrixTransform(transform);
if (xN) seg[xN] = pt.x;
if (yN) seg[yN] = pt.y;
};
// Extract rotation and scale from the transform
var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI;
var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c);
var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d);
// FIXME: Must translate any Horizontal or Vertical lineto commands into absolute moveto
for (var segs=path.pathSegList,c=segs.numberOfItems,i=0;i<c;++i){
var seg = segs.getItem(i);
// Odd-numbered path segments are all relative
// http://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg
var isRelative = (seg.pathSegType%2==1);
var hasX = seg.x != null;
var hasY = seg.y != null;
if (hasX) x = isRelative ? x+seg.x : seg.x;
if (hasY) y = isRelative ? y+seg.y : seg.y;
if (hasX || hasY) setXY( x, y, hasX && 'x', hasY && 'y' );
if (seg.x1 != null) setXY( seg.x1, seg.y1, 'x1', 'y1' );
if (seg.x2 != null) setXY( seg.x2, seg.y2, 'x2', 'y2' );
if (seg.angle != null){
seg.angle += rotation;
seg.r1 *= sx; // FIXME; only works for uniform scale
seg.r2 *= sy; // FIXME; only works for uniform scale
}
}
return path;
}
tl;dr summary: Give me the resources or help fix the below code to transform path commands for SVG <path>
elements by an arbitrary matrix.
details:
I'm writing a library to convert any arbitrary SVG shape into a <path>
element. I have it working when there are no transform="..."
elements in the hierarchy, but now I want to bake the local transform of the object into the path data commands themselves.
This is mostly working (code below) when dealing with the simple moveto/lineto commands. However, I'm not sure of the appropriate way to transform the bezier handles or arcTo parameters.
For example, I am able to convert this rounded rectangle to a <path>
:
<rect x="10" y="30" rx="10" ry="20" width="80" height="70" />
--> <path d="M20,30 L80,30 A10,20,0,0,1,90,50 L90,80 A10,20,0,0,1,80,100
L20,100 A10,20,0,0,1,10,80 L10,50 A10,20,0,0,1,20,30" />
And I get a valid result when transforming without any round corners:
<rect x="10" y="30" width="80" height="70"
transform="translate(-200,0) scale(1.5) rotate(50)" />
--> <path d="M10,30 L90,30 L90,100 L10,100 L10,30" />
However, transforming only the x/y coords of the elliptical arc commands yields amusing results:
The dotted line is the actual transformed rect, the green fill is my path.
Following is the code I have so far (slightly pared-down). I also have a test page where I'm testing various shapes. Please help me determine how to properly transform the elliptical arc
and various other bezier commands given an arbitrary transformation matrix.
function flattenToPaths(el,transform,svg){
if (!svg) svg=el; while(svg && svg.tagName!='svg') svg=svg.parentNode;
var doc = el.ownerDocument;
var svgNS = svg.getAttribute('xmlns');
// Identity transform if nothing passed in
if (!transform) transform= svg.createSVGMatrix();
// Calculate local transform matrix for the object
var localMatrix = svg.createSVGMatrix();
for (var xs=el.transform.baseVal,i=xs.numberOfItems-1;i>=0;--i){
localMatrix = xs.getItem(i).matrix.multiply(localMatrix);
}
// Transform the local transform by whatever was recursively passed in
transform = transform.multiply(localMatrix);
var path = doc.createElementNS(svgNS,'path');
switch(el.tagName){
case 'rect':
path.setAttribute('stroke',el.getAttribute('stroke'));
var x = el.getAttribute('x')*1, y = el.getAttribute('y')*1,
w = el.getAttribute('width')*1, h = el.getAttribute('height')*1,
rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1;
if (rx && !el.hasAttribute('ry')) ry=rx;
else if (ry && !el.hasAttribute('rx')) rx=ry;
if (rx>w/2) rx=w/2;
if (ry>h/2) ry=h/2;
path.setAttribute('d',
'M'+(x+rx)+','+y+
'L'+(x+w-rx)+','+y+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w)+','+(y+ry)) : '') +
'L'+(x+w)+','+(y+h-ry)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w-rx)+','+(y+h)) : '')+
'L'+(x+rx)+','+(y+h)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+x+','+(y+h-ry)) : '')+
'L'+x+','+(y+ry)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+rx)+','+y) : '')
);
break;
case 'circle':
var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
r = el.getAttribute('r')*1, r0 = r/2+','+r/2;
path.setAttribute('d','M'+cx+','+(cy-r)+' A'+r0+',0,0,0,'+cx+','+(cy+r)+' '+r0+',0,0,0,'+cx+','+(cy-r) );
break;
case 'ellipse':
var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1;
path.setAttribute('d','M'+cx+','+(cy-ry)+' A'+rx+','+ry+',0,0,0,'+cx+','+(cy+ry)+' '+rx+','+ry+',0,0,0,'+cx+','+(cy-ry) );
break;
case 'line':
var x1=el.getAttribute('x1')*1, y1=el.getAttribute('y1')*1,
x2=el.getAttribute('x2')*1, y2=el.getAttribute('y2')*1;
path.setAttribute('d','M'+x1+','+y1+'L'+x2+','+y2);
break;
case 'polyline':
case 'polygon':
for (var i=0,l=[],pts=el.points,len=pts.numberOfItems;i<len;++i){
var p = pts.getItem(i);
l[i] = p.x+','+p.y;
}
path.setAttribute('d',"M"+l.shift()+"L"+l.join(' ') + (el.tagName=='polygon') ? 'z' : '');
break;
case 'path':
path = el.cloneNode(false);
break;
}
// Convert local space by the transform matrix
var x,y;
var pt = svg.createSVGPoint();
var setXY = function(x,y,xN,yN){
pt.x = x; pt.y = y;
pt = pt.matrixTransform(transform);
if (xN) seg[xN] = pt.x;
if (yN) seg[yN] = pt.y;
};
// Extract rotation and scale from the transform
var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI;
var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c);
var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d);
// FIXME: Must translate any Horizontal or Vertical lineto commands into absolute moveto
for (var segs=path.pathSegList,c=segs.numberOfItems,i=0;i<c;++i){
var seg = segs.getItem(i);
// Odd-numbered path segments are all relative
// http://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg
var isRelative = (seg.pathSegType%2==1);
var hasX = seg.x != null;
var hasY = seg.y != null;
if (hasX) x = isRelative ? x+seg.x : seg.x;
if (hasY) y = isRelative ? y+seg.y : seg.y;
if (hasX || hasY) setXY( x, y, hasX && 'x', hasY && 'y' );
if (seg.x1 != null) setXY( seg.x1, seg.y1, 'x1', 'y1' );
if (seg.x2 != null) setXY( seg.x2, seg.y2, 'x2', 'y2' );
if (seg.angle != null){
seg.angle += rotation;
seg.r1 *= sx; // FIXME; only works for uniform scale
seg.r2 *= sy; // FIXME; only works for uniform scale
}
}
return path;
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我制作了一个通用的 SVG 拼合器 flatten.js,它支持所有形状和路径命令:
https://gist.github.com/timo22345/9413158
基本用法:
flatten( document.getElementById('svg'))
;它的作用:展平元素(将元素转换为路径并展平变换)。
如果参数元素(其 id 高于“svg”)有子元素,或者它的后代有子元素,
这些子元素也被扁平化。
可以展平的内容:整个 SVG 文档、单个形状(路径、圆形、椭圆形等)和组。嵌套组会自动处理。
属性又如何呢?所有属性均被复制。仅删除路径元素中无效的参数(例如 r、rx、ry、cx、cy),但不再需要它们。变换属性也被删除,因为变换被扁平化为路径命令。
如果您想使用非仿射方法修改路径坐标(例如透视扭曲),
您可以使用以下方法将所有线段转换为三次曲线:
flatten(document.getElementById('svg'), true);
还有参数“toAbsolute”(将坐标转换为绝对坐标)和“dec”,
小数点分隔符后的位数。
极端路径和形状测试器:https://jsfiddle.net/fjm9423q/embedded/result/
基本用法示例: http://jsfiddle.net/nrjvmqur/embedded/result/
缺点:文本元素不起作用。这可能是我的下一个目标。
I have made a general SVG flattener flatten.js, that supports all shapes and path commands:
https://gist.github.com/timo22345/9413158
Basic usage:
flatten(document.getElementById('svg'))
;What it does: Flattens elements (converts elements to paths and flattens transformations).
If the argument element (whose id is above 'svg') has children, or it's descendants has children,
these children elements are flattened also.
What can be flattened: entire SVG document, individual shapes (path, circle, ellipse etc.) and groups. Nested groups are handled automatically.
How about attributes? All attributes are copied. Only arguments that are not valid in path element, are dropped (eg. r, rx, ry, cx, cy), but they are not needed anymore. Also transform attribute is dropped, because transformations are flattened to path commands.
If you want to modify path coordinates using non-affine methods (eg. perspective distort),
you can convert all segments to cubic curves using:
flatten(document.getElementById('svg'), true);
There are also arguments 'toAbsolute' (convert coordinates to absolute) and 'dec',
number of digits after decimal separator.
Extreme path and shape tester: https://jsfiddle.net/fjm9423q/embedded/result/
Basic usage example: http://jsfiddle.net/nrjvmqur/embedded/result/
CONS: text element is not working. It could be my next goal.
如果每个对象(圆圈等)首先转换为路径,那么考虑变换就相当容易了。我制作了一个测试台( http://jsbin.com/oqojan/73 ),您可以在其中测试功能。测试床创建随机路径命令并对路径应用随机变换,然后展平变换。当然,实际上路径命令和变换不是随机的,但对于测试准确性来说这是可以的。
有一个函数 flatten_transformations(),它完成了主要任务:
代码使用 Raphael.pathToRelative()、Raphael._pathToAbsolute() 和 Raphael.path2curve()。 Raphael.path2curve() 是错误修复版本。
如果使用参数 normalize_path=true 调用 flatten_transformations(),则所有命令都将转换为 Cubics,并且一切正常。可以通过删除
if (letter == "A") { ... }
以及删除 H、V 和 Z 的处理来简化代码。简化版本可以类似于 这个。但是因为有人可能只想烘焙转换而不是制作所有分段 ->三次标准化,我添加了这种可能性。因此,如果您想使用 normalize_path=false 来展平变换,这意味着椭圆弧参数也必须展平,并且不可能通过简单地将矩阵应用于坐标来处理它们。两个半径(rx ry)、x轴旋转、大弧标志和扫掠标志必须单独处理。所以下面的函数可以展平弧的变换。矩阵参数是一个关系矩阵,它已在 flatten_transformations() 中使用。
旧示例:
我制作了示例,其中包含包含段
MQAAQ M
的路径,其中应用了变换。该路径位于 g 内部,也应用了反式。并确保这个 g 位于另一个应用了不同变换的 g 内。代码可以:A)首先规范化所有路径段(感谢 Raphaël 的 path2curve,我对此进行了错误修复 ,在此修复之后,所有可能的路径段组合最终起作用: http://jsbin.com/oqojan/42 原始 Raphaël 2.1.0 存在错误行为,如您在此处看到的,如果没有点击路径的话几次即可生成新曲线。)
B) 然后使用本机函数
getTransformToElement()
、createSVGPoint()
和matrixTransform()
展平变换。唯一缺少的是将圆形、矩形和多边形转换为路径命令的方法,但据我所知,您有一个很好的代码。
If every object (circles etc) are converted first to paths, then taking transforms into account is rather easy. I made a testbed ( http://jsbin.com/oqojan/73 ) where you can test the functionality. The testbed creates random path commands and applies random transforms to paths and then flattens transforms. Of course in reality the path commands and transforms are not random, but for testing accuracy it is fine.
There is a function flatten_transformations(), which makes the main task:
The code uses Raphael.pathToRelative(), Raphael._pathToAbsolute() and Raphael.path2curve(). The Raphael.path2curve() is bugfixed version.
If flatten_transformations() is called using argument normalize_path=true, then all commands are converted to Cubics and everything is fine. And the code can be simplified by removing
if (letter == "A") { ... }
and also removing handling of H, V and Z. The simplified version can be something like this.But because someone may want to only bake transformations and not to make All Segs -> Cubics normalization, I added there a possibility to this. So, if you want to flatten transformations with normalize_path=false, this means that Elliptical Arc parameters have to be flattened also and it's not possible to handle them by simply applying matrix to coordinates. Two radiis (rx ry), x-axis-rotation, large-arc-flag and sweep-flag have to handle separately. So the following function can flatten transformations of Arcs. The matrix parameter is a relation matrix which comes from is used already in flatten_transformations().
OLD EXAMPLE:
I made an example that has a path with segments
M Q A A Q M
, which has transformations applied. The path is inside g that also has trans applied. And to make very sure this g is inside another g which has different transformations applied. And the code can:A) First normalize those all path segments (thanks to Raphaël's path2curve, to which I made a bug fix, and after this fix all possible path segment combinations worked finally: http://jsbin.com/oqojan/42. The original Raphaël 2.1.0 has buggy behavior as you can see here, if not click paths few times to generate new curves.)
B) Then flatten transformations using native functions
getTransformToElement()
,createSVGPoint()
andmatrixTransform()
.The only one that lacks is the way to convert Circles, Rectangles and Polygons to path commands, but as far as I know, you have an excellent code for it.
这是我作为“答案”所做的任何进展的更新日志,以帮助通知其他人;如果我自己能以某种方式解决问题,我就会接受这个。
更新 1:我已经得到了 绝对 arcto 命令完美运行,除非比例不均匀。以下是补充内容:
感谢这个答案,让问题变得更简单比我使用的提取方法,以及用于提取非均匀尺度的数学。
This is an updated log of any forward progress I am making as an 'answer', to help inform others; if I somehow solve the problem on my own, I'll just accept this.
Update 1: I've got the absolute arcto command working perfectly except in cases of non-uniform scale. Here were the additions:
Thanks to this answer for a simpler extraction method than I was using, and for the math for extracting non-uniform scale.
只要将所有坐标转换为绝对坐标,所有贝塞尔曲线都可以正常工作;他们的手柄没有什么神奇之处。至于椭圆弧命令,唯一的通用解决方案(处理非均匀缩放,正如您所指出的,在一般情况下,弧命令无法表示)是首先将它们转换为贝塞尔近似值。
https://github.com/johan /svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113 (在同一文件中使用
absolutizePath
,您的 将 SVG 路径转换为绝对命令 hack)可以实现前者,但还不能实现后者。如何用贝塞尔曲线最好地近似几何弧?链接转换弧的数学到贝塞尔曲线(每个
0 <α <= π/2
弧段一个贝塞尔曲线段); 这篇论文在页面末尾显示了方程(它更漂亮pdf 版本在第 3.4.1 节末尾)。As long as you translate all coordinates to absolute coordinates, all béziers will work just fine; there is nothing magical about the their handles. As for the elliptical arc commands, the only general solution (handling non-uniform scaling, as you point out, which the arc command can not represent, in the general case) is to first convert them to their bézier approximations.
https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113 (uses
absolutizePath
in the same file, a straight port of your Convert SVG Path to Absolute Commands hack) does the former, but not yet the latter.How to best approximate a geometrical arc with a Bezier curve? links the math for converting arcs to béziers (one bézier segment per
0 < α <= π/2
arc segment); this paper shows the equations at the end of the page (its prettier pdf rendition has it at the end of section 3.4.1).受到 Timo Kähkönen 的回答和他的展平的启发。 Node.js 要点
我使用 Jarek Foksa 的 getpathData() polyfill 编写了一个类似的帮助程序脚本获取所需的数据。
其工作原理
。通过调用element.getpathData({normalize:true})
我们还可以获取
或
等基元的路径数据.此外,所有命令都仅使用三次贝塞尔曲线和线条转换为绝对命令。 Arctos 转换为三次贝塞尔曲线。
这实际上只是(不幸的是)已弃用的
getTransformToElement()
方法的替代,该方法也可以像这样matrixTransform()
命令坐标,像这样
组元素的所有变换属性和样式属性。此步骤应该最后完成 - 否则我们将无法获得子元素的正确变换矩阵值。“反变换”
元素?显然,我们无法将文本元素转换为路径(除非我们使用 opentype.js 或 fontkit 等库)。但是我们可以合并对文本有影响的所有变换并应用独立的变换属性值(就像我们之前对几何元素所做的那样)。
我正在使用基于 AndreaBogazzi 的精彩回答:“查找矩阵变换的旋转和倾斜” 将当前矩阵拆分为单独的变换函数,例如
translate()
、scale ()
等。用于测试:Codepen 示例
Inspired by Timo Kähkönen's answer and his flatten.js gist
I've written a similar helper script using Jarek Foksa's getpathData() polyfill to get the required data.
How it works
<path>
. By callingelement.getpathData({normalize:true})
we get path data also for primitives like<rect>
or<circle>
.Besides, all commands are converted to absolute commands using only cubic béziers and linetos. Arctos are converted to cubic béziers.
This is actually a just replacement for the (unfortunately) deprecated
getTransformToElement()
method which could also be polyfilled like somatrixTransform()
like so<g>
group elements. This step should be done last – otherwise we won't get the correct transform matrix values for child elements."Detransform"
<text>
elements?Obviously, we can't convert text elements to paths (unless, we're using a library like opentype.js or fontkit). But we can consolidate all the transformations that have an effect on a text and apply a self-contained transform attribute value (as we did before for geometry elements).
I'm using
qrDecomposeMatrix()
helper function based on AndreaBogazzi's great answer: "Find the Rotation and Skew of a Matrix transformation" to split the current matrix to seperate transform functions liketranslate()
,scale()
etc.For testing: Codepen example