SVG:反向三次和二次贝塞尔曲线
SVG: reverse cubic and quadratic bezier curves
我正在编写 SVG 路径命令 reverse the draw direction 的脚本,到目前为止一切正常,但 S
路径命令或 T
.
其中一个 my implementations 仅基于三次贝塞尔 C
曲线的路径反转功能完美运行,但输出路径字符串相当大,有时是长度的两倍或三倍。
这是 reversePath.js
的简化版本,到目前为止,它实现了对 S
和测试页的一些基本处理:
// the script I'm working on works with these arrays
var path = [['M',10,80],['C',40, 10, 65, 10, 95, 80],['S',150,150,180,80],['S',230,10,270,80]],
target = document.getElementById('target');
// our focus is RIGHT HERE
function reversePath(pathInput){
let isClosed = pathInput.slice(-1)[0][0] === 'Z',
params = {x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null},
pathCommand = '', pLen = 0,
reversedPath = [];
reversedPath = pathInput.map((seg,i,pathArray)=>{
pLen = pathArray.length
pathCommand = seg[0]
switch(pathCommand){
case 'M':
x = seg[1]
y = seg[2]
break
case 'Z':
x = pathArray[0][1]
y = pathArray[0][2]
break
default:
x = seg[seg.length - 2]
y = seg[seg.length - 1]
}
return {
c: pathCommand,
x: x,
y: y,
seg: seg
}
}).map((seg,i,pathArray)=>{
let segment = seg.seg,
prevSeg = i && pathArray[i-1],
nextSeg = pathArray[i+1] && pathArray[i+1],
result = []
pLen = pathArray.length
pathCommand = seg.c
params.x = i ? pathArray[i-1].x : pathArray[pLen-1].x
params.y = i ? pathArray[i-1].y : pathArray[pLen-1].y
switch(pathCommand){
case 'M':
result = isClosed ? ['Z'] : [pathCommand, params.x,params.y]
break
case 'C':
if ('S' === nextSeg.c) {
params.x2 = params.x1 + params.x2 / 2
params.y2 = params.y1 + params.y2 / 2
result = ['S', params.x2,params.y2, params.x,params.y]
} else {
params.x1 = segment[3]
params.y1 = segment[4]
params.x2 = segment[1]
params.y2 = segment[2]
result = [pathCommand, params.x1,params.y1, params.x2,params.y2, params.x,params.y];
}
break
case 'S':
params.x2 = params.x1 + params.x2 / 2
params.y2 = params.y1 + params.y2 / 2
if (nextSeg && 'S' === nextSeg.c) {
result = [pathCommand, params.x2,params.y2, params.x,params.y]
} else {
params.x1 = params.x1 + params.x2 / 2
params.y1 = params.y1 + params.y2 / 2
params.x2 = segment[1];
params.y2 = segment[2];
result = ['C', params.x1,params.y1, params.x2,params.y2, params.x,params.y];
}
break
case 'Z':
result = ['M',params.x,params.y]
break
default:
result = segment.slice(0,-2).concat([params.x,params.y])
}
return result
})
return isClosed ? reversedPath.reverse() : [reversedPath[0]].concat(reversedPath.slice(1).reverse())
}
function pathToString(pathArray) {
return pathArray.map(x=>x[0].concat(x.slice(1).join(' '))).join(' ')
}
function reverse(){
var reversed = pathToString(reversePath(path));
target.setAttribute('d',reversed)
target.closest('.col').innerHTML += '<br><p class="text-left">'+reversed+'</p>'
}
.row {width: 100%; display: flex; flex-direction: row}
.col {width: 50%; text-align: center}
.text-left {text-align: left}
<button onclick="reverse()">REVERSE</button>
<hr>
<div class="row">
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="example" d="M10 80 C40 10, 65 10, 95 80S 150 150, 180 80S 230 10 270 80" stroke="green" stroke-width="2" fill="transparent" />
</svg>
normal path
</div>
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="target" d="M0 0L0 0" stroke="orange" stroke-width="2" fill="transparent" />
</svg>
reversed path (click the button)
</div>
</div>
我从 Raphael.js 实现将 S、Q 和 T 路径命令转换为 C
(cubicBezier)开始,思考和逆向工程也许我可以找到一种方法让它工作.
所以我需要一些帮助来为这些 S
和 T
路径命令找出正确的公式来反转形状。如果有人可以帮助我 S
我可以在 T
.
上弄清楚自己
感谢您的回复。
好的,规范化成功了。还有一些新增功能和更强大的价值处理,但让我们开始吧,这是带有新增功能的更新功能:
// the script I'm working on works with these arrays
var pathCubic = [['M',10,80],['C',40,10,65,10,95,80],['S',150,150,180,80],['S',230,10,270,80]],
targetCubic = document.getElementById('target');
// the updated function
function reversePath(absolutePath){
var isClosed = absolutePath.slice(-1)[0][0] === 'Z',
reversedPath = normalizePath(absolutePath).map(function (segment,i){
return {
c: absolutePath[i][0],
x: segment[segment.length - 2],
y: segment[segment.length - 1],
seg: absolutePath[i],
normalized: segment
}
}).map(function (seg,i,pathArray){
var segment = seg.seg,
data = seg.normalized,
prevSeg = i && pathArray[i-1],
nextSeg = pathArray[i+1] && pathArray[i+1],
pathCommand = seg.c,
pLen = pathArray.length,
x = i ? pathArray[i-1].x : pathArray[pLen-1].x,
y = i ? pathArray[i-1].y : pathArray[pLen-1].y,
result = [];
switch(pathCommand){
case 'M':
result = isClosed ? ['Z'] : [pathCommand, x,y];
break
case 'C':
if (nextSeg && nextSeg.c === 'S') {
result = ['S', segment[1],segment[2], x,y];
} else {
result = [pathCommand, segment[3],segment[4], segment[1],segment[2], x,y];
}
break
case 'S':
if ( prevSeg && 'CS'.indexOf(prevSeg.c)>-1 && (!nextSeg || nextSeg && nextSeg.c !== 'S')) {
result = ['C', data[3],data[4], data[1],data[2], x,y];
} else {
result = [pathCommand, data[1],data[2], x,y];
}
break
case 'Z':
result = ['M',x,y];
break
}
return result
});
return isClosed ? reversedPath.reverse() : [reversedPath[0]].concat(reversedPath.slice(1).reverse())
}
// new additions
function shorthandToCubic(x1,y1,x2,y2,prevCommand){
return 'CS'.indexOf(prevCommand)>-1 ? { x1: x1 * 2 - x2, y1: y1 * 2 - y2}
: { x1 : x1, y1 : y1 }
}
function normalizeSegment(segment, params, prevCommand) {
var nqxy, nxy;
switch (segment[0]) {
case "S":
nxy = shorthandToCubic(params.x1,params.y1, params.x2,params.y2, prevCommand);
params.x1 = nxy.x1;
params.y1 = nxy.y1;
segment = ["C", nxy.x1, nxy.y1].concat(segment.slice(1));
break
}
return segment
}
function normalizePath(pathArray) {
var params = {x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null},
allPathCommands = [], pathCommand = '', prevCommand = '', ii = pathArray.length,
segment, seglen;
for (var i = 0; i < ii; i++) {
pathArray[i] && (pathCommand = pathArray[i][0]);
allPathCommands[i] = pathCommand;
i && ( prevCommand = allPathCommands[i - 1]);
pathArray[i] = normalizeSegment(pathArray[i], params, prevCommand);
segment = pathArray[i];
seglen = segment.length;
params.x1 = +segment[seglen - 2];
params.y1 = +segment[seglen - 1];
params.x2 = +(segment[seglen - 4]) || params.x1;
params.y2 = +(segment[seglen - 3]) || params.y1;
}
return pathArray
}
function pathToString(pathArray) {
return pathArray.map(x=>x[0].concat(x.slice(1).join(' '))).join(' ')
}
function reverse(){
var reversedCubic = pathToString(reversePath(pathCubic));
targetCubic.setAttribute('d',reversedCubic)
targetCubic.closest('.col').innerHTML += '<br><p class="text-left">'+reversedCubic+'</p>'
}
.row {width: 100%; display: flex; flex-direction: row}
.col {width: 50%; text-align: center}
.text-left {text-align: left}
<button onclick="reverse()">REVERSE</button> Now works with multiple `S` shorthands
<hr>
<div class="row">
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="example" d="M10 80 C40 10, 65 10, 95 80S 150 150, 180 80S 230 10 270 80" stroke="green" stroke-width="2" fill="transparent" />
</svg>
normal path
</div>
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="target" d="M0 0L0 0" stroke="orange" stroke-width="2" fill="transparent" />
</svg>
reversed CUBIC BEZIER path
</div>
</div>
您可以查看最新的SVGPathCommander version on npm or the demo page。它还可以管理多个 T
路径命令,没问题。
我正在编写 SVG 路径命令 reverse the draw direction 的脚本,到目前为止一切正常,但 S
路径命令或 T
.
其中一个 my implementations 仅基于三次贝塞尔 C
曲线的路径反转功能完美运行,但输出路径字符串相当大,有时是长度的两倍或三倍。
这是 reversePath.js
的简化版本,到目前为止,它实现了对 S
和测试页的一些基本处理:
// the script I'm working on works with these arrays
var path = [['M',10,80],['C',40, 10, 65, 10, 95, 80],['S',150,150,180,80],['S',230,10,270,80]],
target = document.getElementById('target');
// our focus is RIGHT HERE
function reversePath(pathInput){
let isClosed = pathInput.slice(-1)[0][0] === 'Z',
params = {x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null},
pathCommand = '', pLen = 0,
reversedPath = [];
reversedPath = pathInput.map((seg,i,pathArray)=>{
pLen = pathArray.length
pathCommand = seg[0]
switch(pathCommand){
case 'M':
x = seg[1]
y = seg[2]
break
case 'Z':
x = pathArray[0][1]
y = pathArray[0][2]
break
default:
x = seg[seg.length - 2]
y = seg[seg.length - 1]
}
return {
c: pathCommand,
x: x,
y: y,
seg: seg
}
}).map((seg,i,pathArray)=>{
let segment = seg.seg,
prevSeg = i && pathArray[i-1],
nextSeg = pathArray[i+1] && pathArray[i+1],
result = []
pLen = pathArray.length
pathCommand = seg.c
params.x = i ? pathArray[i-1].x : pathArray[pLen-1].x
params.y = i ? pathArray[i-1].y : pathArray[pLen-1].y
switch(pathCommand){
case 'M':
result = isClosed ? ['Z'] : [pathCommand, params.x,params.y]
break
case 'C':
if ('S' === nextSeg.c) {
params.x2 = params.x1 + params.x2 / 2
params.y2 = params.y1 + params.y2 / 2
result = ['S', params.x2,params.y2, params.x,params.y]
} else {
params.x1 = segment[3]
params.y1 = segment[4]
params.x2 = segment[1]
params.y2 = segment[2]
result = [pathCommand, params.x1,params.y1, params.x2,params.y2, params.x,params.y];
}
break
case 'S':
params.x2 = params.x1 + params.x2 / 2
params.y2 = params.y1 + params.y2 / 2
if (nextSeg && 'S' === nextSeg.c) {
result = [pathCommand, params.x2,params.y2, params.x,params.y]
} else {
params.x1 = params.x1 + params.x2 / 2
params.y1 = params.y1 + params.y2 / 2
params.x2 = segment[1];
params.y2 = segment[2];
result = ['C', params.x1,params.y1, params.x2,params.y2, params.x,params.y];
}
break
case 'Z':
result = ['M',params.x,params.y]
break
default:
result = segment.slice(0,-2).concat([params.x,params.y])
}
return result
})
return isClosed ? reversedPath.reverse() : [reversedPath[0]].concat(reversedPath.slice(1).reverse())
}
function pathToString(pathArray) {
return pathArray.map(x=>x[0].concat(x.slice(1).join(' '))).join(' ')
}
function reverse(){
var reversed = pathToString(reversePath(path));
target.setAttribute('d',reversed)
target.closest('.col').innerHTML += '<br><p class="text-left">'+reversed+'</p>'
}
.row {width: 100%; display: flex; flex-direction: row}
.col {width: 50%; text-align: center}
.text-left {text-align: left}
<button onclick="reverse()">REVERSE</button>
<hr>
<div class="row">
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="example" d="M10 80 C40 10, 65 10, 95 80S 150 150, 180 80S 230 10 270 80" stroke="green" stroke-width="2" fill="transparent" />
</svg>
normal path
</div>
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="target" d="M0 0L0 0" stroke="orange" stroke-width="2" fill="transparent" />
</svg>
reversed path (click the button)
</div>
</div>
我从 Raphael.js 实现将 S、Q 和 T 路径命令转换为 C
(cubicBezier)开始,思考和逆向工程也许我可以找到一种方法让它工作.
所以我需要一些帮助来为这些 S
和 T
路径命令找出正确的公式来反转形状。如果有人可以帮助我 S
我可以在 T
.
感谢您的回复。
好的,规范化成功了。还有一些新增功能和更强大的价值处理,但让我们开始吧,这是带有新增功能的更新功能:
// the script I'm working on works with these arrays
var pathCubic = [['M',10,80],['C',40,10,65,10,95,80],['S',150,150,180,80],['S',230,10,270,80]],
targetCubic = document.getElementById('target');
// the updated function
function reversePath(absolutePath){
var isClosed = absolutePath.slice(-1)[0][0] === 'Z',
reversedPath = normalizePath(absolutePath).map(function (segment,i){
return {
c: absolutePath[i][0],
x: segment[segment.length - 2],
y: segment[segment.length - 1],
seg: absolutePath[i],
normalized: segment
}
}).map(function (seg,i,pathArray){
var segment = seg.seg,
data = seg.normalized,
prevSeg = i && pathArray[i-1],
nextSeg = pathArray[i+1] && pathArray[i+1],
pathCommand = seg.c,
pLen = pathArray.length,
x = i ? pathArray[i-1].x : pathArray[pLen-1].x,
y = i ? pathArray[i-1].y : pathArray[pLen-1].y,
result = [];
switch(pathCommand){
case 'M':
result = isClosed ? ['Z'] : [pathCommand, x,y];
break
case 'C':
if (nextSeg && nextSeg.c === 'S') {
result = ['S', segment[1],segment[2], x,y];
} else {
result = [pathCommand, segment[3],segment[4], segment[1],segment[2], x,y];
}
break
case 'S':
if ( prevSeg && 'CS'.indexOf(prevSeg.c)>-1 && (!nextSeg || nextSeg && nextSeg.c !== 'S')) {
result = ['C', data[3],data[4], data[1],data[2], x,y];
} else {
result = [pathCommand, data[1],data[2], x,y];
}
break
case 'Z':
result = ['M',x,y];
break
}
return result
});
return isClosed ? reversedPath.reverse() : [reversedPath[0]].concat(reversedPath.slice(1).reverse())
}
// new additions
function shorthandToCubic(x1,y1,x2,y2,prevCommand){
return 'CS'.indexOf(prevCommand)>-1 ? { x1: x1 * 2 - x2, y1: y1 * 2 - y2}
: { x1 : x1, y1 : y1 }
}
function normalizeSegment(segment, params, prevCommand) {
var nqxy, nxy;
switch (segment[0]) {
case "S":
nxy = shorthandToCubic(params.x1,params.y1, params.x2,params.y2, prevCommand);
params.x1 = nxy.x1;
params.y1 = nxy.y1;
segment = ["C", nxy.x1, nxy.y1].concat(segment.slice(1));
break
}
return segment
}
function normalizePath(pathArray) {
var params = {x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null},
allPathCommands = [], pathCommand = '', prevCommand = '', ii = pathArray.length,
segment, seglen;
for (var i = 0; i < ii; i++) {
pathArray[i] && (pathCommand = pathArray[i][0]);
allPathCommands[i] = pathCommand;
i && ( prevCommand = allPathCommands[i - 1]);
pathArray[i] = normalizeSegment(pathArray[i], params, prevCommand);
segment = pathArray[i];
seglen = segment.length;
params.x1 = +segment[seglen - 2];
params.y1 = +segment[seglen - 1];
params.x2 = +(segment[seglen - 4]) || params.x1;
params.y2 = +(segment[seglen - 3]) || params.y1;
}
return pathArray
}
function pathToString(pathArray) {
return pathArray.map(x=>x[0].concat(x.slice(1).join(' '))).join(' ')
}
function reverse(){
var reversedCubic = pathToString(reversePath(pathCubic));
targetCubic.setAttribute('d',reversedCubic)
targetCubic.closest('.col').innerHTML += '<br><p class="text-left">'+reversedCubic+'</p>'
}
.row {width: 100%; display: flex; flex-direction: row}
.col {width: 50%; text-align: center}
.text-left {text-align: left}
<button onclick="reverse()">REVERSE</button> Now works with multiple `S` shorthands
<hr>
<div class="row">
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="example" d="M10 80 C40 10, 65 10, 95 80S 150 150, 180 80S 230 10 270 80" stroke="green" stroke-width="2" fill="transparent" />
</svg>
normal path
</div>
<div class="col">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 160">
<path id="target" d="M0 0L0 0" stroke="orange" stroke-width="2" fill="transparent" />
</svg>
reversed CUBIC BEZIER path
</div>
</div>
您可以查看最新的SVGPathCommander version on npm or the demo page。它还可以管理多个 T
路径命令,没问题。