Created
September 2, 2020 14:16
-
-
Save ccprog/4692b04705f1d022077876b6aafe5922 to your computer and use it in GitHub Desktop.
add path direction reversal to pathfit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Parser = require('./pathParser.js'); | |
const Formatter = require('./formatter.js'); | |
function parse (str) { | |
const sequence = []; | |
const parser = new Parser(); | |
const splited = parser.commands(str); | |
if (splited.length && !splited[0].match(/m/i)) { | |
parser.throw_parse_error('expected moveto at start', splited.slice(0, 2).join(''), 0); | |
} | |
let first_point, last_point, last_segment; | |
const get_missing = (relative, raw) => { | |
try { | |
const last_control = last_segment.controls.slice(-1)[0]; | |
const f = relative ? 1 : 2; | |
return { | |
x: f * last_point.x - last_control.x, | |
y: f * last_point.y - last_control.y | |
}; | |
} catch (e) { | |
parser.throw_parse_error('expected bezier curve before symetrical point', raw); | |
} | |
}; | |
const to_absolute = point => { | |
point.x += last_point.x; | |
point.y += last_point.y; | |
}; | |
const write = (segment, point, relative) => { | |
if (relative) { | |
to_absolute(point); | |
if (segment.controls) { | |
segment.controls.forEach(to_absolute); | |
} | |
} | |
if (segment.geometry) { | |
last_segment = segment; | |
sequence.push(segment); | |
} | |
last_point = point; | |
sequence.push(point); | |
}; | |
const push_sequence = (collect, command, relative, init = {}) => { | |
parser.collect_arguments(...collect).forEach((single, i) => { | |
const point = { | |
form: init.form ? init.form(i) : 'default', | |
...(init.pair ? init.pair(single) : single.coordinate_pair) | |
}; | |
const segment = { | |
geometry: init.geometry ? init.geometry(i) : command, | |
controls: init.controls ? init.controls(single) : undefined, | |
arc: init.arc ? init.arc(single) : undefined, | |
}; | |
if (init.finally) init.finally(point, segment, i); | |
write(segment, point, relative); | |
}); | |
}; | |
while (splited.length > 1) { | |
const letter = splited.shift(); | |
const command = letter.toUpperCase(); | |
const relative = command !== letter && command !== 'Z'; | |
const raw = parser.get_raw(letter, splited.shift()); | |
raw.get_token('wsp'); | |
switch (command) { | |
case 'M': | |
push_sequence( | |
['coordinate_pair_sequence', raw, 1], | |
command, | |
relative, | |
{ | |
form: (i) => i ? 'default' : 'open', | |
geometry: (i) => i ? 'L' : false, | |
finally: (point, segment, i) => { if (!i) first_point = point; } | |
} | |
); | |
break; | |
case 'L': | |
push_sequence( | |
['coordinate_pair_sequence', raw, 1], | |
command, | |
relative | |
); | |
break; | |
case 'H': | |
push_sequence( | |
['coordinate', raw], | |
command, | |
relative, | |
{ | |
pair: (single) => ({ x: single.coordinate, y: relative ? 0 : last_point.y }) | |
} | |
); | |
break; | |
case 'V': | |
push_sequence( | |
['coordinate', raw], | |
command, | |
relative, | |
{ | |
pair: (single) => ({ x: relative ? 0 : last_point.x, y: single.coordinate, }) | |
} | |
); | |
break; | |
case 'A': | |
push_sequence( | |
['elliptical_arc', raw], | |
command, | |
relative, | |
{ | |
arc: (single) => ({...single}), | |
finally: (point, segment) => delete segment.arc.coordinate_pair | |
} | |
); | |
break; | |
case 'Q': | |
push_sequence( | |
['coordinate_pair_sequence', raw, 2], | |
command, | |
relative, | |
{ | |
controls: (single) => [single.control_1] | |
} | |
); | |
break; | |
case 'T': | |
push_sequence( | |
['coordinate_pair_sequence', raw, 1], | |
command, | |
relative, | |
{ | |
geometry: () => 'Q', | |
controls: () => [get_missing(relative, raw)], | |
finally: () => last_point.form = 'symetrical' | |
} | |
); | |
break; | |
case 'C': | |
push_sequence( | |
['coordinate_pair_sequence', raw, 3], | |
command, | |
relative, | |
{ | |
controls: (single) => [single.control_1, single.control_2] | |
} | |
); | |
break; | |
case 'S': | |
push_sequence( | |
['coordinate_pair_sequence', raw, 2], | |
command, | |
relative, | |
{ | |
geometry: () => 'C', | |
controls: (single) => [get_missing(relative, raw), single.control_1], | |
finally: () => last_point.form = 'symetrical' | |
} | |
); | |
break; | |
case 'Z': { | |
const point = { form: 'closed' }; | |
const segment = { geometry: 'L' }; | |
parser.test_end(raw); | |
({x: point.x, y: point.y} = first_point); | |
first_point.form = 'closed'; | |
if (last_point.x === point.x && last_point.y === point.y) { | |
last_point.form = 'closed'; | |
continue; | |
} | |
write(segment, point, relative); | |
break; } | |
} | |
} | |
if (last_point.form === 'default') { | |
last_point.form = 'open'; | |
} | |
return sequence; | |
} | |
function reverse (sequence) { | |
sequence.reverse().forEach(obj => { | |
if (obj.geometry === 'C') obj.controls.reverse(); | |
if (obj.geometry === 'A') obj.arc.sweep = !obj.arc.sweep; | |
}); | |
} | |
function format (seq) { | |
const sequence = [...seq]; | |
let parts = []; | |
const formatter = new Formatter(); | |
let last_point, last_letter; | |
while (sequence.length) { | |
const segment = sequence[0].geometry ? sequence.shift() : { geometry: 'M' }; | |
const point = sequence.shift(); | |
const command = [ | |
segment.geometry, | |
formatter.pair(point, 'x', 'y') | |
]; | |
switch (segment.geometry) { | |
case 'H': | |
command[1] = formatter.number(point.x); | |
break; | |
case 'V': | |
command[1] = formatter.number(point.y); | |
break; | |
case 'Q': | |
case 'C': { | |
const controls = [...segment.controls]; | |
if (last_point.form === 'symetrical') { | |
command[0] = segment.geometry === 'Q' ? 'T' : 'S'; | |
controls.splice(0, 1); | |
} | |
const arg_str = controls.map(ctr => formatter.pair(ctr, 'x', 'y')).join(Formatter.arg_wsp); | |
if (arg_str.length) command.splice(1, 0, arg_str); | |
break; } | |
case 'A': | |
command.splice(1, 0, [ | |
formatter.pair(segment.arc, 'rx', 'ry'), | |
formatter.number(segment.arc.rotation), | |
formatter.flag(segment.arc.large_arc), | |
formatter.flag(segment.arc.sweep) | |
].join(Formatter.arg_wsp)); | |
break; | |
} | |
if (command[0] === last_letter && command[0] !== 'M') { | |
command.splice(0, 1); | |
} else { | |
last_letter = command[0] === 'M' ? 'L' : command[0]; | |
} | |
last_point = point; | |
if (point.form === 'closed' && command[0] !== 'M') { | |
if (last_letter === 'L') { | |
command[0] = 'Z'; | |
command.splice(1); | |
} else { | |
command.push('Z'); | |
} | |
} | |
parts.push(command.join(Formatter.arg_wsp)); | |
} | |
return parts.join(Formatter.arg_wsp); | |
} | |
module.exports = {parse, reverse, format}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This file can be dropped into the
/src
folder of https://github.com/ccprog/pathfit. This is only a demonstration, it will probably never be mainlined into the module. Path reversal just has no merits in the context of clip paths.It exchanges the AST format to help with path reversal. Instead of command sequences, coordinate points and segments connecting them are stored alternating.
"M 10,50 Q 40,20 80,50 T 160,50"
is parsed intoPath reversal simply reverses the order of the AST array (with changes to the
A
andC
segment properties) and will then be serialized to"M 160,50 Q 120,80 80,50 T 10,50"
.