Code

Added tutorial-basic.it
[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.current_layer.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()