5edbf026ddc34366f4b706d83c199194dfd72ae5
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, re, bezmisc
21 uuconv = {'in':90.0, 'pt':1.25, 'px':1, 'mm':3.5433070866, 'cm':35.433070866, 'pc':15.0}
22 def numsegs(csp):
23 return sum([len(p)-1 for p in csp])
24 def interpcoord(v1,v2,p):
25 return v1+((v2-v1)*p)
26 def interppoints(p1,p2,p):
27 return [interpcoord(p1[0],p2[0],p),interpcoord(p1[1],p2[1],p)]
28 def pointdistance((x1,y1),(x2,y2)):
29 return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
30 def bezlenapprx(sp1, sp2):
31 return pointdistance(sp1[1], sp1[2]) + pointdistance(sp1[2], sp2[0]) + pointdistance(sp2[0], sp2[1])
32 def tpoint((x1,y1), (x2,y2), t = 0.5):
33 return [x1+t*(x2-x1),y1+t*(y2-y1)]
34 def cspbezsplit(sp1, sp2, t = 0.5):
35 m1=tpoint(sp1[1],sp1[2],t)
36 m2=tpoint(sp1[2],sp2[0],t)
37 m3=tpoint(sp2[0],sp2[1],t)
38 m4=tpoint(m1,m2,t)
39 m5=tpoint(m2,m3,t)
40 m=tpoint(m4,m5,t)
41 return [[sp1[0][:],sp1[1][:],m1], [m4,m,m5], [m3,sp2[1][:],sp2[2][:]]]
42 def cspbezsplitatlength(sp1, sp2, l = 0.5, tolerance = 0.001):
43 bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
44 t = bezmisc.beziertatlength(bez, l, tolerance)
45 return cspbezsplit(sp1, sp2, t)
46 def cspseglength(sp1,sp2, tolerance = 0.001):
47 bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
48 return bezmisc.bezierlength(bez, tolerance)
49 def csplength(csp):
50 total = 0
51 lengths = []
52 for sp in csp:
53 lengths.append([])
54 for i in xrange(1,len(sp)):
55 l = cspseglength(sp[i-1],sp[i])
56 lengths[-1].append(l)
57 total += l
58 return lengths, total
59 def styleunittouu(string):
60 unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
61 param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
63 p = param.match(string)
64 u = unit.search(string)
65 if p:
66 retval = float(p.string[p.start():p.end()])
67 else:
68 retval = 0.0
69 if u:
70 try:
71 return retval * uuconv[u.string[u.start():u.end()]]
72 except KeyError:
73 pass
74 return retval
76 def tweenstylefloat(property, start, end, time):
77 sp = float(start[property])
78 ep = float(end[property])
79 return str(sp + (time * (ep - sp)))
80 def tweenstyleunit(property, start, end, time):
81 sp = styleunittouu(start[property])
82 ep = styleunittouu(end[property])
83 return str(sp + (time * (ep - sp)))
84 def tweenstylecolor(property, start, end, time):
85 sr,sg,sb = parsecolor(start[property])
86 er,eg,eb = parsecolor(end[property])
87 return '#%s%s%s' % (tweenhex(time,sr,er),tweenhex(time,sg,eg),tweenhex(time,sb,eb))
88 def tweenhex(time,s,e):
89 s = float(int(s,16))
90 e = float(int(e,16))
91 retval = hex(int(math.floor(s + (time * (e - s)))))[2:]
92 if len(retval)==1:
93 retval = '0%s' % retval
94 return retval
95 def parsecolor(c):
96 r,g,b = '0','0','0'
97 if c[:1]=='#':
98 if len(c)==4:
99 r,g,b = c[1:2],c[2:3],c[3:4]
100 elif len(c)==7:
101 r,g,b = c[1:3],c[3:5],c[5:7]
102 return r,g,b
104 class Interp(inkex.Effect):
105 def __init__(self):
106 inkex.Effect.__init__(self)
107 self.OptionParser.add_option("-e", "--exponent",
108 action="store", type="float",
109 dest="exponent", default=0.0,
110 help="values other than zero give non linear interpolation")
111 self.OptionParser.add_option("-s", "--steps",
112 action="store", type="int",
113 dest="steps", default=5,
114 help="number of interpolation steps")
115 self.OptionParser.add_option("-m", "--method",
116 action="store", type="int",
117 dest="method", default=2,
118 help="method of interpolation")
119 self.OptionParser.add_option("-d", "--dup",
120 action="store", type="inkbool",
121 dest="dup", default=True,
122 help="duplicate endpaths")
123 self.OptionParser.add_option("--style",
124 action="store", type="inkbool",
125 dest="style", default=True,
126 help="try interpolation of some style properties")
127 def effect(self):
128 exponent = self.options.exponent
129 if exponent>= 0:
130 exponent = 1.0 + exponent
131 else:
132 exponent = 1.0/(1.0 - exponent)
133 steps = [1.0/(self.options.steps + 1.0)]
134 for i in range(self.options.steps - 1):
135 steps.append(steps[0] + steps[-1])
136 steps = [step**exponent for step in steps]
138 paths = {}
139 styles = {}
140 for id in self.options.ids:
141 node = self.selected[id]
142 if node.tagName =='path':
143 paths[id] = cubicsuperpath.parsePath(node.attributes.getNamedItem('d').value)
144 styles[id] = simplestyle.parseStyle(node.attributes.getNamedItem('style').value)
145 else:
146 self.options.ids.remove(id)
148 for i in range(1,len(self.options.ids)):
149 start = copy.deepcopy(paths[self.options.ids[i-1]])
150 end = copy.deepcopy(paths[self.options.ids[i]])
151 sst = copy.deepcopy(styles[self.options.ids[i-1]])
152 est = copy.deepcopy(styles[self.options.ids[i]])
153 basestyle = copy.deepcopy(sst)
155 #prepare for experimental style tweening
156 if self.options.style:
157 dostroke = True
158 dofill = True
159 styledefaults = {'opacity':'1.0', 'stroke-opacity':'1.0', 'fill-opacity':'1.0',
160 'stroke-width':'1.0', 'stroke':'none', 'fill':'none'}
161 for key in styledefaults.keys():
162 sst.setdefault(key,styledefaults[key])
163 est.setdefault(key,styledefaults[key])
164 isnotplain = lambda x: not (x=='none' or x[:1]=='#')
165 if isnotplain(sst['stroke']) or isnotplain(est['stroke']) or (sst['stroke']=='none' and est['stroke']=='none'):
166 dostroke = False
167 if isnotplain(sst['fill']) or isnotplain(est['fill']) or (sst['fill']=='none' and est['fill']=='none'):
168 dofill = False
169 if dostroke:
170 if sst['stroke']=='none':
171 sst['stroke-width'] = '0.0'
172 sst['stroke-opacity'] = '0.0'
173 sst['stroke'] = est['stroke']
174 elif est['stroke']=='none':
175 est['stroke-width'] = '0.0'
176 est['stroke-opacity'] = '0.0'
177 est['stroke'] = sst['stroke']
178 if dofill:
179 if sst['fill']=='none':
180 sst['fill-opacity'] = '0.0'
181 sst['fill'] = est['fill']
182 elif est['fill']=='none':
183 est['fill-opacity'] = '0.0'
184 est['fill'] = sst['fill']
188 if self.options.method == 2:
189 #subdivide both paths into segments of relatively equal lengths
190 slengths, stotal = csplength(start)
191 elengths, etotal = csplength(end)
192 lengths = {}
193 t = 0
194 for sp in slengths:
195 for l in sp:
196 t += l / stotal
197 lengths.setdefault(t,0)
198 lengths[t] += 1
199 t = 0
200 for sp in elengths:
201 for l in sp:
202 t += l / etotal
203 lengths.setdefault(t,0)
204 lengths[t] += -1
205 sadd = [k for (k,v) in lengths.iteritems() if v < 0]
206 sadd.sort()
207 eadd = [k for (k,v) in lengths.iteritems() if v > 0]
208 eadd.sort()
210 t = 0
211 s = [[]]
212 for sp in slengths:
213 if not start[0]:
214 s.append(start.pop(0))
215 s[-1].append(start[0].pop(0))
216 for l in sp:
217 pt = t
218 t += l / stotal
219 if sadd and t > sadd[0]:
220 while sadd and sadd[0] < t:
221 nt = (sadd[0] - pt) / (t - pt)
222 bezes = cspbezsplitatlength(s[-1][-1][:],start[0][0][:], nt)
223 s[-1][-1:] = bezes[:2]
224 start[0][0] = bezes[2]
225 pt = sadd.pop(0)
226 s[-1].append(start[0].pop(0))
227 t = 0
228 e = [[]]
229 for sp in elengths:
230 if not end[0]:
231 e.append(end.pop(0))
232 e[-1].append(end[0].pop(0))
233 for l in sp:
234 pt = t
235 t += l / etotal
236 if eadd and t > eadd[0]:
237 while eadd and eadd[0] < t:
238 nt = (eadd[0] - pt) / (t - pt)
239 bezes = cspbezsplitatlength(e[-1][-1][:],end[0][0][:], nt)
240 e[-1][-1:] = bezes[:2]
241 end[0][0] = bezes[2]
242 pt = eadd.pop(0)
243 e[-1].append(end[0].pop(0))
244 start = s[:]
245 end = e[:]
246 else:
247 #which path has fewer segments?
248 lengthdiff = numsegs(start) - numsegs(end)
249 #swap shortest first
250 if lengthdiff > 0:
251 start, end = end, start
252 #subdivide the shorter path
253 for x in range(abs(lengthdiff)):
254 maxlen = 0
255 subpath = 0
256 segment = 0
257 for y in range(len(start)):
258 for z in range(1, len(start[y])):
259 leng = bezlenapprx(start[y][z-1], start[y][z])
260 if leng > maxlen:
261 maxlen = leng
262 subpath = y
263 segment = z
264 sp1, sp2 = start[subpath][segment - 1:segment + 1]
265 start[subpath][segment - 1:segment + 1] = cspbezsplit(sp1, sp2)
266 #if swapped, swap them back
267 if lengthdiff > 0:
268 start, end = end, start
270 #break paths so that corresponding subpaths have an equal number of segments
271 s = [[]]
272 e = [[]]
273 while start and end:
274 if start[0] and end[0]:
275 s[-1].append(start[0].pop(0))
276 e[-1].append(end[0].pop(0))
277 elif end[0]:
278 s.append(start.pop(0))
279 e[-1].append(end[0][0])
280 e.append([end[0].pop(0)])
281 elif start[0]:
282 e.append(end.pop(0))
283 s[-1].append(start[0][0])
284 s.append([start[0].pop(0)])
285 else:
286 s.append(start.pop(0))
287 e.append(end.pop(0))
289 if self.options.dup:
290 steps = [0] + steps + [1]
291 #create an interpolated path for each interval
292 group = self.document.createElement('svg:g')
293 self.document.documentElement.appendChild(group)
294 for time in steps:
295 interp = []
296 #process subpaths
297 for ssp,esp in zip(s, e):
298 if not (ssp or esp):
299 break
300 interp.append([])
301 #process superpoints
302 for sp,ep in zip(ssp, esp):
303 if not (sp or ep):
304 break
305 interp[-1].append([])
306 #process points
307 for p1,p2 in zip(sp, ep):
308 if not (sp or ep):
309 break
310 interp[-1][-1].append(interppoints(p1, p2, time))
312 #remove final subpath if empty.
313 if not interp[-1]:
314 del interp[-1]
315 new = self.document.createElement('svg:path')
317 #basic style tweening
318 if self.options.style:
319 basestyle['opacity'] = tweenstylefloat('opacity',sst,est,time)
320 if dostroke:
321 basestyle['stroke-opacity'] = tweenstylefloat('stroke-opacity',sst,est,time)
322 basestyle['stroke-width'] = tweenstyleunit('stroke-width',sst,est,time)
323 basestyle['stroke'] = tweenstylecolor('stroke',sst,est,time)
324 if dofill:
325 basestyle['fill-opacity'] = tweenstylefloat('fill-opacity',sst,est,time)
326 basestyle['fill'] = tweenstylecolor('fill',sst,est,time)
327 new.setAttribute('style', simplestyle.formatStyle(basestyle))
328 new.setAttribute('d', cubicsuperpath.formatPath(interp))
329 group.appendChild(new)
331 e = Interp()
332 e.affect()