diff options
Diffstat (limited to 'share/extensions/interp.py')
-rwxr-xr-x | share/extensions/interp.py | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/share/extensions/interp.py b/share/extensions/interp.py new file mode 100755 index 0000000..bac878c --- /dev/null +++ b/share/extensions/interp.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# coding=utf-8 +# +# Copyright (C) 2005 Aaron Spike, aaron@ekips.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import copy +from collections import namedtuple +from itertools import combinations + +import inkex +from inkex.styles import Style +from inkex.utils import pairwise +from inkex.paths import CubicSuperPath +from inkex.tween import interppoints +from inkex.bezier import csplength, cspbezsplitatlength, cspbezsplit, bezlenapprx + +class Interp(inkex.EffectExtension): + def add_arguments(self, pars): + pars.add_argument("-e", "--exponent", type=float, default=0.0,\ + help="values other than zero give non linear interpolation") + pars.add_argument("-s", "--steps", type=int, default=5,\ + help="number of interpolation steps") + pars.add_argument("-m", "--method", type=int, default=2,\ + help="method of interpolation") + pars.add_argument("-d", "--dup", type=inkex.Boolean, default=True,\ + help="duplicate endpaths") + pars.add_argument("--style", type=inkex.Boolean, default=True,\ + help="try interpolation of some style properties") + pars.add_argument("--zsort", type=inkex.Boolean, default=False,\ + help="use z-order instead of selection order") + + def effect(self): + exponent = self.options.exponent + if exponent >= 0: + exponent += 1.0 + else: + exponent = 1.0 / (1.0 - exponent) + steps = [1.0 / (self.options.steps + 1.0)] + for i in range(self.options.steps - 1): + steps.append(steps[0] + steps[-1]) + steps = [step ** exponent for step in steps] + + if self.options.zsort: + # work around selection order swapping with Live Preview + objects = self.svg.get_z_selected() + else: + # use selection order (default) + objects = self.svg.selected + + objects = [node for node in objects.values() if isinstance(node, inkex.PathElement)] + + # prevents modification of original objects + objects = copy.deepcopy(objects) + + for node in objects: + node.apply_transform() + + objectpairs = pairwise(objects, start=False) + + for (elem1, elem2) in objectpairs: + start = elem1.path.to_superpath() + end = elem2.path.to_superpath() + sst = copy.deepcopy(elem1.style) + est = copy.deepcopy(elem2.style) + basestyle = copy.deepcopy(sst) + + if 'stroke-width' in basestyle: + basestyle['stroke-width'] = sst.interpolate_prop(est, 0, 'stroke-width') + + # prepare for experimental style tweening + if self.options.style: + styledefaults = Style( + {'opacity': 1.0, + 'stroke-opacity': 1.0, + 'fill-opacity': 1.0, + 'stroke-width': 1.0, + 'stroke': None, + 'fill': None}) + for key in styledefaults: + sst.setdefault(key, styledefaults[key]) + est.setdefault(key, styledefaults[key]) + + isnotplain = lambda x: not (x == 'none' or x[:1] == '#') + isgradient = lambda x: x.startswith('url(#') + + if isgradient(sst['stroke']) and isgradient(est['stroke']): + strokestyle = 'gradient' + elif isnotplain(sst['stroke']) or isnotplain(est['stroke']) or (sst['stroke'] == 'none' and est['stroke'] == 'none'): + strokestyle = 'notplain' + else: + strokestyle = 'color' + + if isgradient(sst['fill']) and isgradient(est['fill']): + fillstyle = 'gradient' + elif isnotplain(sst['fill']) or isnotplain(est['fill']) or (sst['fill'] == 'none' and est['fill'] == 'none'): + fillstyle = 'notplain' + else: + fillstyle = 'color' + + if strokestyle is 'color': + if sst['stroke'] == 'none': + sst['stroke-width'] = '0.0' + sst['stroke-opacity'] = '0.0' + sst['stroke'] = est['stroke'] + elif est['stroke'] == 'none': + est['stroke-width'] = '0.0' + est['stroke-opacity'] = '0.0' + est['stroke'] = sst['stroke'] + + if fillstyle is 'color': + if sst['fill'] == 'none': + sst['fill-opacity'] = '0.0' + sst['fill'] = est['fill'] + elif est['fill'] == 'none': + est['fill-opacity'] = '0.0' + est['fill'] = sst['fill'] + + if self.options.method == 2: + # subdivide both paths into segments of relatively equal lengths + slengths, stotal = csplength(start) + elengths, etotal = csplength(end) + lengths = {} + t = 0 + for sp in slengths: + for l in sp: + t += l / stotal + lengths.setdefault(t, 0) + lengths[t] += 1 + t = 0 + for sp in elengths: + for l in sp: + t += l / etotal + lengths.setdefault(t, 0) + lengths[t] += -1 + sadd = [k for (k, v) in lengths.items() if v < 0] + sadd.sort() + eadd = [k for (k, v) in lengths.items() if v > 0] + eadd.sort() + + t = 0 + s = [[]] + for sp in slengths: + if not start[0]: + s.append(start.pop(0)) + s[-1].append(start[0].pop(0)) + for l in sp: + pt = t + t += l / stotal + if sadd and t > sadd[0]: + while sadd and sadd[0] < t: + nt = (sadd[0] - pt) / (t - pt) + bezes = cspbezsplitatlength(s[-1][-1][:], start[0][0][:], nt) + s[-1][-1:] = bezes[:2] + start[0][0] = bezes[2] + pt = sadd.pop(0) + s[-1].append(start[0].pop(0)) + t = 0 + e = [[]] + for sp in elengths: + if not end[0]: + e.append(end.pop(0)) + e[-1].append(end[0].pop(0)) + for l in sp: + pt = t + t += l / etotal + if eadd and t > eadd[0]: + while eadd and eadd[0] < t: + nt = (eadd[0] - pt) / (t - pt) + bezes = cspbezsplitatlength(e[-1][-1][:], end[0][0][:], nt) + e[-1][-1:] = bezes[:2] + end[0][0] = bezes[2] + pt = eadd.pop(0) + e[-1].append(end[0].pop(0)) + start = s[:] + end = e[:] + else: + # which path has fewer segments? + lengthdiff = len(start) - len(end) + # swap shortest first + if lengthdiff > 0: + start, end = end, start + # subdivide the shorter path + for x in range(abs(lengthdiff)): + maxlen = 0 + subpath = 0 + segment = 0 + for y in range(len(start)): + for z in range(1, len(start[y])): + leng = bezlenapprx(start[y][z - 1], start[y][z]) + if leng > maxlen: + maxlen = leng + subpath = y + segment = z + sp1, sp2 = start[subpath][segment - 1:segment + 1] + start[subpath][segment - 1:segment + 1] = cspbezsplit(sp1, sp2) + # if swapped, swap them back + if lengthdiff > 0: + start, end = end, start + + # break paths so that corresponding subpaths have an equal number of segments + s = [[]] + e = [[]] + while start and end: + if start[0] and end[0]: + s[-1].append(start[0].pop(0)) + e[-1].append(end[0].pop(0)) + elif end[0]: + s.append(start.pop(0)) + e[-1].append(end[0][0]) + e.append([end[0].pop(0)]) + elif start[0]: + e.append(end.pop(0)) + s[-1].append(start[0][0]) + s.append([start[0].pop(0)]) + else: + s.append(start.pop(0)) + e.append(end.pop(0)) + + if self.options.dup: + steps = [0] + steps + [1] + # create an interpolated path for each interval + group = self.svg.get_current_layer().add(inkex.Group()) + for time in steps: + interp = [] + # process subpaths + for ssp, esp in zip(s, e): + if not (ssp or esp): + break + interp.append([]) + # process superpoints + for sp, ep in zip(ssp, esp): + if not (sp or ep): + break + interp[-1].append([]) + # process points + for p1, p2 in zip(sp, ep): + if not (sp or ep): + break + interp[-1][-1].append(interppoints(p1, p2, time)) + + # remove final subpath if empty. + if not interp[-1]: + del interp[-1] + + # basic style interpolation + if self.options.style: + basestyle.update(sst.interpolate(est, time)) + for prop in ['stroke', 'fill']: + if isgradient(sst[prop]) and isgradient(est[prop]): + gradid1 = sst[prop][4:-1] + gradid2 = est[prop][4:-1] + grad1 = self.svg.getElementById(gradid1) + grad2 = self.svg.getElementById(gradid2) + newgrad = grad1.interpolate(grad2, time) + stops, orientation = newgrad.stops_and_orientation() + self.svg.defs.add(orientation) + basestyle[prop] = 'url(#{})'.format(orientation.get_id()) + if len(stops): + self.svg.defs.add(stops, orientation) + orientation.set('xlink:href', '#{}'.format(stops.get_id())) + + new = group.add(inkex.PathElement()) + new.style = basestyle + new.path = CubicSuperPath(interp) + + +if __name__ == '__main__': + Interp().run() |