I'm working on a script to reverse the draw direction of SVG path commands, everything is working smoothly so far but not with S
path commands or T
.
In one of my implementations for path reverse functionality based only on cubic bezier C
curves, works flawlessly, however the output path string is considerably larger, sometimes double or triple the length.
Here's a simplified version of the reversePath.js
which, so far, implements some basic handling for S
and a test page:
// 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>
I started here from the the Raphael.js implementation on converting S and Q and T path commands to C
(cubicBezier), thinking and reverse engineering perhaps I could find a way to make it work.
So I need a little help figuring out a correct formula for these S
and T
path commands when reversing the shape. If someone can help me with S
I can figure out myself on T
.
Thanks for any reply.
Alright, the normalization did the trick. Also some new additions and more powerful value processing, but let's get to it, here's the updated function with new additions:
// 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>
You can check the latest SVGPathCommander version on npm or the demo page. It can also manage multiple T
path commands, noo problem.