Skip to content

Instantly share code, notes, and snippets.

@ccprog
Created Sep 2, 2020
Embed
What would you like to do?
add path direction reversal to pathfit
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};
@ccprog

This comment has been minimized.

Copy link
Owner Author

@ccprog ccprog commented Sep 2, 2020

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 into

[
    { form: "open", x: 10, y: 50 } // first and last point can be open or closed
    { geometry: "Q", controls: [{ x: 40, y: 20 }] } // segment geometry can be L, V, H, A, Q and C
    { form: "symetrical", x: 80, y: 50 } // non-final points can be default or symetrical
    { geometry: "Q", controls: [{ x: 120, y: 80 }] } // the control point is determined by the last control point of the last sequence
    { form: "open", x: 160, y: 50 } // first and last point must have same form
]

Path reversal simply reverses the order of the AST array (with changes to the A and C segment properties) and will then be serialized to "M 160,50 Q 120,80 80,50 T 10,50".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment