Code

5edbf026ddc34366f4b706d83c199194dfd72ae5
[inkscape.git] / share / extensions / interp.py
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
75         
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]
137                         
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'] 
186                                         
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
269                         
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))
288         
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()