1b28f1bffd9a97dfa1e02d6f1e1bef3735e7f1c6
1 #!/usr/bin/env python
2 '''
3 Copyright (C) 2005 Aaron Spike, aaron@ekips.org
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 '''
19 import inkex, cubicsuperpath, simplestyle, copy, math, bezmisc
21 def numsegs(csp):
22 return sum([len(p)-1 for p in csp])
23 def interpcoord(v1,v2,p):
24 return v1+((v2-v1)*p)
25 def interppoints(p1,p2,p):
26 return [interpcoord(p1[0],p2[0],p),interpcoord(p1[1],p2[1],p)]
27 def pointdistance((x1,y1),(x2,y2)):
28 return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
29 def bezlenapprx(sp1, sp2):
30 return pointdistance(sp1[1], sp1[2]) + pointdistance(sp1[2], sp2[0]) + pointdistance(sp2[0], sp2[1])
31 def tpoint((x1,y1), (x2,y2), t = 0.5):
32 return [x1+t*(x2-x1),y1+t*(y2-y1)]
33 def cspbezsplit(sp1, sp2, t = 0.5):
34 m1=tpoint(sp1[1],sp1[2],t)
35 m2=tpoint(sp1[2],sp2[0],t)
36 m3=tpoint(sp2[0],sp2[1],t)
37 m4=tpoint(m1,m2,t)
38 m5=tpoint(m2,m3,t)
39 m=tpoint(m4,m5,t)
40 return [[sp1[0][:],sp1[1][:],m1], [m4,m,m5], [m3,sp2[1][:],sp2[2][:]]]
41 def cspbezsplitatlength(sp1, sp2, l = 0.5, tolerance = 0.001):
42 bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
43 t = bezmisc.beziertatlength(bez, l, tolerance)
44 return cspbezsplit(sp1, sp2, t)
45 def cspseglength(sp1,sp2, tolerance = 0.001):
46 bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
47 return bezmisc.bezierlength(bez, tolerance)
48 def csplength(csp):
49 total = 0
50 lengths = []
51 for sp in csp:
52 lengths.append([])
53 for i in xrange(1,len(sp)):
54 l = cspseglength(sp[i-1],sp[i])
55 lengths[-1].append(l)
56 total += l
57 return lengths, total
59 def tweenstylefloat(property, start, end, time):
60 sp = float(start[property])
61 ep = float(end[property])
62 return str(sp + (time * (ep - sp)))
63 def tweenstyleunit(property, start, end, time):
64 sp = inkex.unittouu(start[property])
65 ep = inkex.unittouu(end[property])
66 return str(sp + (time * (ep - sp)))
67 def tweenstylecolor(property, start, end, time):
68 sr,sg,sb = parsecolor(start[property])
69 er,eg,eb = parsecolor(end[property])
70 return '#%s%s%s' % (tweenhex(time,sr,er),tweenhex(time,sg,eg),tweenhex(time,sb,eb))
71 def tweenhex(time,s,e):
72 s = float(int(s,16))
73 e = float(int(e,16))
74 retval = hex(int(math.floor(s + (time * (e - s)))))[2:]
75 if len(retval)==1:
76 retval = '0%s' % retval
77 return retval
78 def parsecolor(c):
79 r,g,b = '0','0','0'
80 if c[:1]=='#':
81 if len(c)==4:
82 r,g,b = c[1:2],c[2:3],c[3:4]
83 elif len(c)==7:
84 r,g,b = c[1:3],c[3:5],c[5:7]
85 return r,g,b
87 class Interp(inkex.Effect):
88 def __init__(self):
89 inkex.Effect.__init__(self)
90 self.OptionParser.add_option("-e", "--exponent",
91 action="store", type="float",
92 dest="exponent", default=0.0,
93 help="values other than zero give non linear interpolation")
94 self.OptionParser.add_option("-s", "--steps",
95 action="store", type="int",
96 dest="steps", default=5,
97 help="number of interpolation steps")
98 self.OptionParser.add_option("-m", "--method",
99 action="store", type="int",
100 dest="method", default=2,
101 help="method of interpolation")
102 self.OptionParser.add_option("-d", "--dup",
103 action="store", type="inkbool",
104 dest="dup", default=True,
105 help="duplicate endpaths")
106 self.OptionParser.add_option("--style",
107 action="store", type="inkbool",
108 dest="style", default=True,
109 help="try interpolation of some style properties")
110 def effect(self):
111 exponent = self.options.exponent
112 if exponent>= 0:
113 exponent = 1.0 + exponent
114 else:
115 exponent = 1.0/(1.0 - exponent)
116 steps = [1.0/(self.options.steps + 1.0)]
117 for i in range(self.options.steps - 1):
118 steps.append(steps[0] + steps[-1])
119 steps = [step**exponent for step in steps]
121 paths = {}
122 styles = {}
123 for id in self.options.ids:
124 node = self.selected[id]
125 if node.tag ==inkex.addNS('path','svg'):
126 paths[id] = cubicsuperpath.parsePath(node.get('d'))
127 styles[id] = simplestyle.parseStyle(node.get('style'))
128 else:
129 self.options.ids.remove(id)
131 for i in range(1,len(self.options.ids)):
132 start = copy.deepcopy(paths[self.options.ids[i-1]])
133 end = copy.deepcopy(paths[self.options.ids[i]])
134 sst = copy.deepcopy(styles[self.options.ids[i-1]])
135 est = copy.deepcopy(styles[self.options.ids[i]])
136 basestyle = copy.deepcopy(sst)
138 #prepare for experimental style tweening
139 if self.options.style:
140 dostroke = True
141 dofill = True
142 styledefaults = {'opacity':'1.0', 'stroke-opacity':'1.0', 'fill-opacity':'1.0',
143 'stroke-width':'1.0', 'stroke':'none', 'fill':'none'}
144 for key in styledefaults.keys():
145 sst.setdefault(key,styledefaults[key])
146 est.setdefault(key,styledefaults[key])
147 isnotplain = lambda x: not (x=='none' or x[:1]=='#')
148 if isnotplain(sst['stroke']) or isnotplain(est['stroke']) or (sst['stroke']=='none' and est['stroke']=='none'):
149 dostroke = False
150 if isnotplain(sst['fill']) or isnotplain(est['fill']) or (sst['fill']=='none' and est['fill']=='none'):
151 dofill = False
152 if dostroke:
153 if sst['stroke']=='none':
154 sst['stroke-width'] = '0.0'
155 sst['stroke-opacity'] = '0.0'
156 sst['stroke'] = est['stroke']
157 elif est['stroke']=='none':
158 est['stroke-width'] = '0.0'
159 est['stroke-opacity'] = '0.0'
160 est['stroke'] = sst['stroke']
161 if dofill:
162 if sst['fill']=='none':
163 sst['fill-opacity'] = '0.0'
164 sst['fill'] = est['fill']
165 elif est['fill']=='none':
166 est['fill-opacity'] = '0.0'
167 est['fill'] = sst['fill']
171 if self.options.method == 2:
172 #subdivide both paths into segments of relatively equal lengths
173 slengths, stotal = csplength(start)
174 elengths, etotal = csplength(end)
175 lengths = {}
176 t = 0
177 for sp in slengths:
178 for l in sp:
179 t += l / stotal
180 lengths.setdefault(t,0)
181 lengths[t] += 1
182 t = 0
183 for sp in elengths:
184 for l in sp:
185 t += l / etotal
186 lengths.setdefault(t,0)
187 lengths[t] += -1
188 sadd = [k for (k,v) in lengths.iteritems() if v < 0]
189 sadd.sort()
190 eadd = [k for (k,v) in lengths.iteritems() if v > 0]
191 eadd.sort()
193 t = 0
194 s = [[]]
195 for sp in slengths:
196 if not start[0]:
197 s.append(start.pop(0))
198 s[-1].append(start[0].pop(0))
199 for l in sp:
200 pt = t
201 t += l / stotal
202 if sadd and t > sadd[0]:
203 while sadd and sadd[0] < t:
204 nt = (sadd[0] - pt) / (t - pt)
205 bezes = cspbezsplitatlength(s[-1][-1][:],start[0][0][:], nt)
206 s[-1][-1:] = bezes[:2]
207 start[0][0] = bezes[2]
208 pt = sadd.pop(0)
209 s[-1].append(start[0].pop(0))
210 t = 0
211 e = [[]]
212 for sp in elengths:
213 if not end[0]:
214 e.append(end.pop(0))
215 e[-1].append(end[0].pop(0))
216 for l in sp:
217 pt = t
218 t += l / etotal
219 if eadd and t > eadd[0]:
220 while eadd and eadd[0] < t:
221 nt = (eadd[0] - pt) / (t - pt)
222 bezes = cspbezsplitatlength(e[-1][-1][:],end[0][0][:], nt)
223 e[-1][-1:] = bezes[:2]
224 end[0][0] = bezes[2]
225 pt = eadd.pop(0)
226 e[-1].append(end[0].pop(0))
227 start = s[:]
228 end = e[:]
229 else:
230 #which path has fewer segments?
231 lengthdiff = numsegs(start) - numsegs(end)
232 #swap shortest first
233 if lengthdiff > 0:
234 start, end = end, start
235 #subdivide the shorter path
236 for x in range(abs(lengthdiff)):
237 maxlen = 0
238 subpath = 0
239 segment = 0
240 for y in range(len(start)):
241 for z in range(1, len(start[y])):
242 leng = bezlenapprx(start[y][z-1], start[y][z])
243 if leng > maxlen:
244 maxlen = leng
245 subpath = y
246 segment = z
247 sp1, sp2 = start[subpath][segment - 1:segment + 1]
248 start[subpath][segment - 1:segment + 1] = cspbezsplit(sp1, sp2)
249 #if swapped, swap them back
250 if lengthdiff > 0:
251 start, end = end, start
253 #break paths so that corresponding subpaths have an equal number of segments
254 s = [[]]
255 e = [[]]
256 while start and end:
257 if start[0] and end[0]:
258 s[-1].append(start[0].pop(0))
259 e[-1].append(end[0].pop(0))
260 elif end[0]:
261 s.append(start.pop(0))
262 e[-1].append(end[0][0])
263 e.append([end[0].pop(0)])
264 elif start[0]:
265 e.append(end.pop(0))
266 s[-1].append(start[0][0])
267 s.append([start[0].pop(0)])
268 else:
269 s.append(start.pop(0))
270 e.append(end.pop(0))
272 if self.options.dup:
273 steps = [0] + steps + [1]
274 #create an interpolated path for each interval
275 group = inkex.etree.SubElement(self.current_layer,inkex.addNS('g','svg'))
276 for time in steps:
277 interp = []
278 #process subpaths
279 for ssp,esp in zip(s, e):
280 if not (ssp or esp):
281 break
282 interp.append([])
283 #process superpoints
284 for sp,ep in zip(ssp, esp):
285 if not (sp or ep):
286 break
287 interp[-1].append([])
288 #process points
289 for p1,p2 in zip(sp, ep):
290 if not (sp or ep):
291 break
292 interp[-1][-1].append(interppoints(p1, p2, time))
294 #remove final subpath if empty.
295 if not interp[-1]:
296 del interp[-1]
298 #basic style tweening
299 if self.options.style:
300 basestyle['opacity'] = tweenstylefloat('opacity',sst,est,time)
301 if dostroke:
302 basestyle['stroke-opacity'] = tweenstylefloat('stroke-opacity',sst,est,time)
303 basestyle['stroke-width'] = tweenstyleunit('stroke-width',sst,est,time)
304 basestyle['stroke'] = tweenstylecolor('stroke',sst,est,time)
305 if dofill:
306 basestyle['fill-opacity'] = tweenstylefloat('fill-opacity',sst,est,time)
307 basestyle['fill'] = tweenstylecolor('fill',sst,est,time)
308 attribs = {'style':simplestyle.formatStyle(basestyle),'d':cubicsuperpath.formatPath(interp)}
309 new = inkex.etree.SubElement(group,inkex.addNS('path','svg'), attribs)
311 e = Interp()
312 e.affect()