-
Notifications
You must be signed in to change notification settings - Fork 3
/
kvg2sexp.rb
362 lines (275 loc) · 9.16 KB
/
kvg2sexp.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# -*- coding: utf-8 -*-
# kvg2sexp.rb
#A Point
class Point
attr_accessor :x, :y, :color
def initialize(x,y, color = :black)
@x,@y, @color = x, y, color
end
def to_sexp
#round to 2 decimal places
return "( " + @x.round(2).to_s + " " + @y.round(2).to_s + " )";
end
#Basic point arithmetics
def +(p2)
return Point.new(@x + p2.x, @y + p2.y)
end
def -(p2)
return Point.new(@x - p2.x, @y - p2.y)
end
def dist(p2)
return Math.sqrt((p2.x - @x)**2 + (p2.y - @y)**2)
end
def *(number)
return Point.new(@x * number, @y * number)
end
def to_s
"x:" + @x.to_s + " y:" + @y.to_s + "\n"
end
#to array
def to_a
[@x.round(2), @y.round(2)]
end
def to_xml
"<point x=\"" + (@x * 1000 / 109).to_s + "\" y=\"" + (@y * 1000 / 109).to_s + "\"/>"
end
end
# SVG_M represents the moveto command.
# SVG Syntax is:
# m x y
# It sets the current cursor to the point (x,y).
# As always, capitalization denotes absolute values.
# Takes a Point as argument.
# If given 2 Points, the second argument is treated as the current cursor.
class SVG_M
def initialize(p1, p2 = Point.new(0,0))
@p = p1 + p2;
end
#As there is now real movement, no output is necessary
def to_sexp
return ""
end
def to_points
return []
end
def current_cursor
return @p
end
def to_xml
""
end
end
# SVG_C represents the cubic Bézier curveto command.
# Syntax is:
# c x1 y1 x2 y2 x y
# It sets the current cursor to the point (x,y).
# As always, capitalization denotes absolute values.
# Takes 4 Points as argument, the fourth being the current cursor
# If constructed using SVG_C.relative, the current cursor is added to every
# point.
class SVG_C
def initialize(c1,c2,p,current_cursor)
@c1,@c2,@p,@current_cursor = c1,c2,p,current_cursor
@@c_color = :green
end
def SVG_C.relative(c1,c2,p,current_cursor)
SVG_C.new(c1 + current_cursor, c2 + current_cursor, p + current_cursor, current_cursor)
end
def second_point
@c2
end
# This implements the algorithm found here:
# http://www.cubic.org/docs/bezier.htm
# Takes 2 Points and a factor between 0 and 1
def linear_interpolation(a,b,factor)
xr = a.x + ((b.x - a.x) * factor)
yr = a.y + ((b.y - a.y) * factor)
return Point.new(xr,yr);
end
def switch_color
if @@c_color == :green
@@c_color = :red
elsif @@c_color == :red
@@c_color = :purple
else
@@c_color = :green
end
end
def make_curvepoint(factor)
ab = linear_interpolation(@current_cursor,@c1,factor)
bc = linear_interpolation(@c1,@c2,factor)
cd = linear_interpolation(@c2,@p,factor)
abbc = linear_interpolation(ab,bc,factor)
bccd = linear_interpolation(bc,cd,factor)
return linear_interpolation(abbc,bccd,factor)
end
def length(points)
old_point = @current_cursor;
length = 0.0
factor = points.to_f
(1..points).each {|point|
new_point = make_curvepoint(point/(factor.to_f))
length += old_point.dist(new_point)
old_point = new_point
}
return length
end
# This gives back an array of points on the curve. The argument given
# denotes how the distance between each point.
def make_curvepoint_array(distance)
result = Array.new
l = length(20)
points = l * distance
factor = points.to_f
(0..points).each {|point|
result.push(make_curvepoint(point/(factor.to_f)))
}
return result
end
# This calculates 20 points on a curve and puts them out as a string
def to_sexp
curve_array = to_points
# Why did they call "fold" inject? It's still fold.
return curve_array.inject("") {|result, element|
result + element.to_sexp
}
end
def to_xml
curve_array = to_points
return curve_array.inject("") {|result, element|
result + "\t" + element.to_xml + "\n"
}
end
def to_points
return make_curvepoint_array(0.3)
end
def current_cursor
@p
end
end
# SVG_S represents the smooth curveto command.
# Syntax is:
# s x2 y2 x y
# It sets the current cursor to the point (x,y).
# As always, capitalization denotes absolute values.
# Takes 3 Points as argument, the third being the current cursor
# If constructed using SVG_S.relative, the current cursor is added to every
# point.
class SVG_S < SVG_C
def initialize(c2, p, current_cursor,previous_point)
super(SVG_S.reflect(previous_point,current_cursor), c2, p, current_cursor)
end
# The reflection in this case is rather tricky. Using SVG_C.relative, the
# offset of current_cursor is added to all the positions (except current_cursor).
# The reflected point, however is already calculated in absolute values.
# Because of this, we have to subtract the current_cursor from the reflected
# point, as it is already added later. I think I got the classes somewhat wrong.
# Maybe points should get a field whether they are absolute oder relative?
# Don't know yet. It works now, though!
def SVG_S.relative(c2, p, current_cursor, previous_point)
SVG_C.relative(SVG_S.reflect(previous_point,current_cursor) - current_cursor, c2, p, current_cursor)
end
def SVG_S.reflect(p, mirror)
return mirror + (mirror - p)
end
end
# Stroke represent one stroke, which is a series of SVG commands.
class Stroke
COMMANDS = ["M", "C", "c", "s", "S"]
def initialize(stroke_as_code)
@command_list = parse(stroke_as_code)
end
def to_sexp
return "( " + @command_list.inject("") {|result,element| result + element.to_sexp} + ")"
end
def to_xml
"<stroke>\n" + @command_list.inject("") {|result,element| result + element.to_xml} + "</stroke>\n"
end
def to_points
return @command_list.map{|element| element.to_points}.flatten
end
#to array
#TODO: better implementation using composite pattern
def to_a
to_points.map{|point| point.to_a}
end
def split_elements(line)
# This is magic.
return line.gsub("-",",-").gsub("s",",s,").gsub("S",",S,").gsub("c",",c,").gsub("C",",C,").gsub("M","M,").gsub("[","").gsub(";",",;,").gsub(",,",",").gsub(" ,", ",").gsub(", ", ",").gsub(" ", ",").split(/,/);
end
def parse(stroke_as_code)
elements = split_elements(stroke_as_code)
command_list = Array.new
current_cursor = Point.new(0,0);
while elements != [] do
case elements.slice!(0)
when "M"
x,y = elements.slice!(0..1)
m = SVG_M.new(Point.new(x.to_f,y.to_f))
current_cursor = m.current_cursor
command_list.push(m)
when "C"
x1,y1,x2,y2,x,y = elements.slice!(0..5)
c = SVG_C.new(Point.new(x1.to_f,y1.to_f), Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor)
current_cursor = c.current_cursor
command_list.push(c)
#handle polybezier
unless elements.empty? || COMMANDS.include?(elements.first)
elements.unshift("C")
end
when "c"
x1,y1,x2,y2,x,y = elements.slice!(0..5)
c = SVG_C.relative(Point.new(x1.to_f,y1.to_f), Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor)
current_cursor = c.current_cursor
command_list.push(c)
#handle polybezier
unless elements.empty? || COMMANDS.include?(elements.first)
elements.unshift("c")
end
when "s"
x2,y2,x,y = elements.slice!(0..3)
reflected_point = command_list[-1].second_point
s = SVG_S.relative(Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor, reflected_point)
current_cursor = s.current_cursor
command_list.push(s)
when "S"
x2,y2,x,y = elements.slice!(0..3)
reflected_point = command_list[-1].second_point
s = SVG_S.new(Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor,reflected_point)
current_cursor = s.current_cursor
command_list.push(s)
else
#print "You should not be here\n"
end
end
return command_list
end
end
# SVG_Character represents a whole character. It takes 2 arguments:
# Codepoint is a hex number
# Strokes are svg commands stored in an array
class SVG_Character
def initialize(codepoint, strokes)
@codepoint = codepoint
#convert unicode codepoint to string character
@character = [@codepoint].pack("U")
@strokes = strokes.map{ |stroke| Stroke.new(stroke) }
end
def to_sexp
c = "(character (value " + @character +") (width 109) (height 109) (strokes "
c = @strokes.inject(c) {|result, stroke| result + stroke.to_sexp}
c += ") )"
return c
end
def to_xml
c = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<character><utf8>"
c += @character
c += "</utf8><strokes>"
c += @strokes.inject("") {|result, stroke| result + stroke.to_xml}
c += "</strokes></character>"
return c
end
def to_points
return @strokes.map {|stroke| stroke.to_points}.flatten
end
end