#!/usr/bin/env python ''' 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ''' import inkex, cubicsuperpath, simplestyle, copy, math, bezmisc, simpletransform def numsegs(csp): return sum([len(p)-1 for p in csp]) def interpcoord(v1,v2,p): return v1+((v2-v1)*p) def interppoints(p1,p2,p): return [interpcoord(p1[0],p2[0],p),interpcoord(p1[1],p2[1],p)] def pointdistance((x1,y1),(x2,y2)): return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2)) def bezlenapprx(sp1, sp2): return pointdistance(sp1[1], sp1[2]) + pointdistance(sp1[2], sp2[0]) + pointdistance(sp2[0], sp2[1]) def tpoint((x1,y1), (x2,y2), t = 0.5): return [x1+t*(x2-x1),y1+t*(y2-y1)] def cspbezsplit(sp1, sp2, t = 0.5): m1=tpoint(sp1[1],sp1[2],t) m2=tpoint(sp1[2],sp2[0],t) m3=tpoint(sp2[0],sp2[1],t) m4=tpoint(m1,m2,t) m5=tpoint(m2,m3,t) m=tpoint(m4,m5,t) return [[sp1[0][:],sp1[1][:],m1], [m4,m,m5], [m3,sp2[1][:],sp2[2][:]]] def cspbezsplitatlength(sp1, sp2, l = 0.5, tolerance = 0.001): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) t = bezmisc.beziertatlength(bez, l, tolerance) return cspbezsplit(sp1, sp2, t) def cspseglength(sp1,sp2, tolerance = 0.001): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) return bezmisc.bezierlength(bez, tolerance) def csplength(csp): total = 0 lengths = [] for sp in csp: lengths.append([]) for i in xrange(1,len(sp)): l = cspseglength(sp[i-1],sp[i]) lengths[-1].append(l) total += l return lengths, total def tweenstylefloat(property, start, end, time): sp = float(start[property]) ep = float(end[property]) return str(sp + (time * (ep - sp))) def tweenstylecolor(property, start, end, time): sr,sg,sb = parsecolor(start[property]) er,eg,eb = parsecolor(end[property]) return '#%s%s%s' % (tweenhex(time,sr,er),tweenhex(time,sg,eg),tweenhex(time,sb,eb)) def tweenhex(time,s,e): s = float(int(s,16)) e = float(int(e,16)) retval = hex(int(math.floor(s + (time * (e - s)))))[2:] if len(retval)==1: retval = '0%s' % retval return retval def parsecolor(c): r,g,b = '0','0','0' if c[:1]=='#': if len(c)==4: r,g,b = c[1:2],c[2:3],c[3:4] elif len(c)==7: r,g,b = c[1:3],c[3:5],c[5:7] return r,g,b class Interp(inkex.Effect): def __init__(self): inkex.Effect.__init__(self) self.OptionParser.add_option("-e", "--exponent", action="store", type="float", dest="exponent", default=0.0, help="values other than zero give non linear interpolation") self.OptionParser.add_option("-s", "--steps", action="store", type="int", dest="steps", default=5, help="number of interpolation steps") self.OptionParser.add_option("-m", "--method", action="store", type="int", dest="method", default=2, help="method of interpolation") self.OptionParser.add_option("-d", "--dup", action="store", type="inkbool", dest="dup", default=True, help="duplicate endpaths") self.OptionParser.add_option("--style", action="store", type="inkbool", dest="style", default=True, help="try interpolation of some style properties") def tweenstyleunit(self, property, start, end, time): # moved here so we can call 'unittouu' sp = self.unittouu(start[property]) ep = self.unittouu(end[property]) return str(sp + (time * (ep - sp))) def effect(self): exponent = self.options.exponent if exponent>= 0: exponent = 1.0 + exponent 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] paths = {} styles = {} for id in self.options.ids: node = self.selected[id] if node.tag ==inkex.addNS('path','svg'): paths[id] = cubicsuperpath.parsePath(node.get('d')) styles[id] = simplestyle.parseStyle(node.get('style')) trans = node.get('transform') if trans: simpletransform.applyTransformToPath(simpletransform.parseTransform(trans), paths[id]) else: self.options.ids.remove(id) for i in range(1,len(self.options.ids)): start = copy.deepcopy(paths[self.options.ids[i-1]]) end = copy.deepcopy(paths[self.options.ids[i]]) sst = copy.deepcopy(styles[self.options.ids[i-1]]) est = copy.deepcopy(styles[self.options.ids[i]]) basestyle = copy.deepcopy(sst) if basestyle.has_key('stroke-width'): basestyle['stroke-width'] = self.tweenstyleunit('stroke-width',sst,est,0) #prepare for experimental style tweening if self.options.style: dostroke = True dofill = True styledefaults = {'opacity':'1.0', 'stroke-opacity':'1.0', 'fill-opacity':'1.0', 'stroke-width':'1.0', 'stroke':'none', 'fill':'none'} for key in styledefaults.keys(): sst.setdefault(key,styledefaults[key]) est.setdefault(key,styledefaults[key]) isnotplain = lambda x: not (x=='none' or x[:1]=='#') if isnotplain(sst['stroke']) or isnotplain(est['stroke']) or (sst['stroke']=='none' and est['stroke']=='none'): dostroke = False if isnotplain(sst['fill']) or isnotplain(est['fill']) or (sst['fill']=='none' and est['fill']=='none'): dofill = False if dostroke: 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 dofill: 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.iteritems() if v < 0] sadd.sort() eadd = [k for (k,v) in lengths.iteritems() 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 = numsegs(start) - numsegs(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 = inkex.etree.SubElement(self.current_layer,inkex.addNS('g','svg')) 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 tweening if self.options.style: basestyle['opacity'] = tweenstylefloat('opacity',sst,est,time) if dostroke: basestyle['stroke-opacity'] = tweenstylefloat('stroke-opacity',sst,est,time) basestyle['stroke-width'] = self.tweenstyleunit('stroke-width',sst,est,time) basestyle['stroke'] = tweenstylecolor('stroke',sst,est,time) if dofill: basestyle['fill-opacity'] = tweenstylefloat('fill-opacity',sst,est,time) basestyle['fill'] = tweenstylecolor('fill',sst,est,time) attribs = {'style':simplestyle.formatStyle(basestyle),'d':cubicsuperpath.formatPath(interp)} new = inkex.etree.SubElement(group,inkex.addNS('path','svg'), attribs) if __name__ == '__main__': e = Interp() e.affect() # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99