dyi 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,612 @@
1
+ # -*- encoding: UTF-8 -*-
2
+
3
+ # Copyright (c) 2009-2011 Sound-F Co., Ltd. All rights reserved.
4
+ #
5
+ # Author:: Mamoru Yuo
6
+ #
7
+ # This file is part of DYI.
8
+ #
9
+ # DYI is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # DYI is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with DYI. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ require 'enumerator'
23
+
24
+ module DYI #:nodoc:
25
+ module Shape #:nodoc:
26
+
27
+ class Base < GraphicalElement
28
+ attr_painting :painting
29
+ attr_font :font
30
+ attr_reader :attributes, :clipping
31
+ attr_reader :parent
32
+ attr_accessor :anchor_href
33
+ attr_accessor :anchor_target
34
+
35
+ ID_REGEXP = /\A[:A-Z_a-z][0-9:A-Z_a-z]*\z/
36
+
37
+ # Draws the shape on a parent element.
38
+ # @param [Element] parent a element that you draw the shape on
39
+ # @return [Shape::Base] receiver itself
40
+ def draw_on(parent)
41
+ raise ArgumentError, "parent is nil" if parent.nil?
42
+ return self if @parent == parent
43
+ raise RuntimeError, "this shape already has a parent" if @parent
44
+ current_node = parent
45
+ loop do
46
+ break if current_node.nil? || current_node.root_element?
47
+ if current_node == self
48
+ raise RuntimeError, "descendants of this shape include itself"
49
+ end
50
+ current_node = current_node.parent
51
+ end
52
+ (@parent = parent).child_elements.push(self)
53
+ self
54
+ end
55
+
56
+ # This method is depricated; use Shape::Base#root_element?
57
+ # @deprecated
58
+ def root_node?
59
+ msg = [__FILE__, __LINE__, ' waring']
60
+ msg << ' DYI::Shape::Base#root_node? is depricated; use DYI::Shape::Base#root_element?'
61
+ warn(msg.join(':'))
62
+ false
63
+ end
64
+
65
+ # @since 1.0.0
66
+ def root_element?
67
+ false
68
+ end
69
+
70
+ def transform
71
+ @transform ||= []
72
+ end
73
+
74
+ def translate(x, y=0)
75
+ x = Length.new(x)
76
+ y = Length.new(y)
77
+ return if x.zero? && y.zero?
78
+ lt = transform.last
79
+ if lt && lt.first == :translate
80
+ lt[1] += x
81
+ lt[2] += y
82
+ transform.pop if lt[1].zero? && lt[2].zero?
83
+ else
84
+ transform.push([:translate, x, y])
85
+ end
86
+ end
87
+
88
+ def scale(x, y=nil, base_point=Coordinate::ZERO)
89
+ y ||= x
90
+ return if x == 1 && y == 1
91
+ base_point = Coordinate.new(base_point)
92
+ translate(base_point.x, base_point.y) if base_point.nonzero?
93
+ lt = transform.last
94
+ if lt && lt.first == :scale
95
+ lt[1] *= x
96
+ lt[2] *= y
97
+ transform.pop if lt[1] == 1 && lt[2] == 1
98
+ else
99
+ transform.push([:scale, x, y])
100
+ end
101
+ translate(- base_point.x, - base_point.y) if base_point.nonzero?
102
+ end
103
+
104
+ def rotate(angle, base_point=Coordinate::ZERO)
105
+ angle %= 360
106
+ return if angle == 0
107
+ base_point = Coordinate.new(base_point)
108
+ translate(base_point.x, base_point.y) if base_point.nonzero?
109
+ lt = transform.last
110
+ if lt && lt.first == :rotate
111
+ lt[1] = (lt[1] + angle) % 360
112
+ transform.pop if lt[1] == 0
113
+ else
114
+ transform.push([:rotate, angle])
115
+ end
116
+ translate(- base_point.x, - base_point.y) if base_point.nonzero?
117
+ end
118
+
119
+ def skew_x(angle, base_point=Coordinate::ZERO)
120
+ angle %= 180
121
+ return if angle == 0
122
+ base_point = Coordinate.new(base_point)
123
+ translate(base_point.x, base_point.y) if base_point.nonzero?
124
+ transform.push([:skewX, angle])
125
+ translate(- base_point.x, - base_point.y) if base_point.nonzero?
126
+ end
127
+
128
+ def skew_y(angle, base_point=Coordinate::ZERO)
129
+ angle %= 180
130
+ return if angle == 0
131
+ base_point = Coordinate.new(base_point)
132
+ translate(base_point.x, base_point.y) if base_point.nonzero?
133
+ lt = transform.last
134
+ transform.push([:skewY, angle])
135
+ translate(- base_point.x, - base_point.y) if base_point.nonzero?
136
+ end
137
+
138
+ def set_clipping(clipping)
139
+ clipping.set_canvas(canvas)
140
+ @clipping = clipping
141
+ end
142
+
143
+ def clear_clipping
144
+ @clipping = nil
145
+ end
146
+
147
+ def set_clipping_shapes(*shapes)
148
+ set_clipping(Drawing::Clipping.new(*shapes))
149
+ end
150
+
151
+ # since 1.0.0
152
+ def animations
153
+ @animations ||= []
154
+ end
155
+
156
+ # @return [Boolean] whether the shape is animated
157
+ # @since 1.0.0
158
+ def animate?
159
+ !(@animations.nil? || @animations.empty?)
160
+ end
161
+
162
+ # Add animation to the shape
163
+ # @param [Animation::Base] animation a animation that the shape is run
164
+ # @return [void]
165
+ # @since 1.0.0
166
+ def add_animation(animation)
167
+ animations << animation
168
+ end
169
+
170
+ # Add animation of painting to the shape
171
+ # @param [Hash] options
172
+ # @option options [Painting] :from the starting painting of the animation
173
+ # @option options [Painting] :to the ending painting of the animation
174
+ # @option options [Number] :duration a simple duration in seconds
175
+ # @option options [Number] :begin_offset a offset that determine the
176
+ # animation begin, in seconds
177
+ # @option options [Event] :begin_event an event that determine the
178
+ # animation begin
179
+ # @option options [Number] :end_offset a offset that determine the
180
+ # animation end, in seconds
181
+ # @option options [Event] :end_event an event that determine the
182
+ # animation end
183
+ # @option options [String] :fill `freeze' or `remove'
184
+ # @return [void]
185
+ # @since 1.0.0
186
+ def add_painting_animation(options)
187
+ add_animation(Animation::PaintingAnimation.new(self, options))
188
+ end
189
+
190
+ # Add animation of transform to the shape
191
+ #
192
+ # @param [Symbol] type a type of transformation which is to have values
193
+ # @param [Hash] options
194
+ # @option options [Number|Array] :from the starting transform of the animation
195
+ # @option options [Number|Array] :to the ending transform of the animation
196
+ # @option options [Number] :duration a simple duration in seconds
197
+ # @option options [Number] :begin_offset a offset that determine the
198
+ # animation begin, in seconds
199
+ # @option options [Event] :begin_event an event that determine the
200
+ # animation begin
201
+ # @option options [Number] :end_offset a offset that determine the
202
+ # animation end, in seconds
203
+ # @option options [Event] :end_event an event that determine the
204
+ # animation end
205
+ # @option options [String] :fill `freeze' or `remove'
206
+ # @return [void]
207
+ # @since 1.0.0
208
+ def add_transform_animation(type, options)
209
+ add_animation(Animation::TransformAnimation.new(self, type, options))
210
+ end
211
+
212
+ # Add animation of painting to the shape
213
+ # @param [Event] an event that is set to the shape
214
+ # @return [void]
215
+ # @since 1.0.0
216
+ def set_event(event)
217
+ super
218
+ canvas.set_event(event)
219
+ end
220
+
221
+ # @since 1.0.0
222
+ def anchor_href=(href)
223
+ anchor_href = href.strip
224
+ @anchor_href = anchor_href.empty? ? nil : anchor_href
225
+ end
226
+
227
+ # @return [Boolean] whether the element has a URI reference
228
+ # @since 1.0.0
229
+ def has_uri_reference?
230
+ @anchor_href ? true : false
231
+ end
232
+
233
+ private
234
+
235
+ def init_attributes(options)
236
+ options = options.clone
237
+ @font = Font.new_or_nil(options.delete(:font))
238
+ @painting = Painting.new_or_nil(options.delete(:painting))
239
+ @anchor_href = options.delete(:anchor_href)
240
+ @anchor_target = options.delete(:anchor_target)
241
+ self.css_class = options.delete(:css_class)
242
+ self.id = options.delete(:id) if options[:id]
243
+ options
244
+ end
245
+ end
246
+
247
+ class Rectangle < Base
248
+ attr_length :width, :height
249
+
250
+ def initialize(left_top, width, height, options={})
251
+ width = Length.new(width)
252
+ height = Length.new(height)
253
+ @lt_pt = Coordinate.new(left_top)
254
+ @lt_pt += Coordinate.new(width, 0) if width < Length::ZERO
255
+ @lt_pt += Coordinate.new(0, height) if height < Length::ZERO
256
+ @width = width.abs
257
+ @height = height.abs
258
+ @attributes = init_attributes(options)
259
+ end
260
+
261
+ def left
262
+ @lt_pt.x
263
+ end
264
+
265
+ def right
266
+ @lt_pt.x + width
267
+ end
268
+
269
+ def top
270
+ @lt_pt.y
271
+ end
272
+
273
+ def bottom
274
+ @lt_pt.y + height
275
+ end
276
+
277
+ def center
278
+ @lt_pt + Coordinate.new(width.quo(2), height.quo(2))
279
+ end
280
+
281
+ def write_as(formatter, io=$>)
282
+ formatter.write_rectangle(self, io, &(block_given? ? Proc.new : nil))
283
+ end
284
+
285
+ class << self
286
+
287
+ public
288
+
289
+ def create_on_width_height(left_top, width, height, options={})
290
+ new(left_top, width, height, options)
291
+ end
292
+
293
+ def create_on_corner(top, right, bottom, left, options={})
294
+ left_top = Coordinate.new([left, right].min, [top, bottom].min)
295
+ width = (Length.new(right) - Length.new(left)).abs
296
+ height = (Length.new(bottom) - Length.new(top)).abs
297
+ new(left_top, width, height, options)
298
+ end
299
+ end
300
+ end
301
+
302
+ class Circle < Base
303
+ attr_coordinate :center
304
+ attr_length :radius
305
+
306
+ def initialize(center, radius, options={})
307
+ @center = Coordinate.new(center)
308
+ @radius = Length.new(radius).abs
309
+ @attributes = init_attributes(options)
310
+ end
311
+
312
+ def left
313
+ @center.x - @radius
314
+ end
315
+
316
+ def right
317
+ @center.x + @radius
318
+ end
319
+
320
+ def top
321
+ @center.y - @radius
322
+ end
323
+
324
+ def bottom
325
+ @center.y + @radius
326
+ end
327
+
328
+ def width
329
+ @radius * 2
330
+ end
331
+
332
+ def height
333
+ @radius * 2
334
+ end
335
+
336
+ def write_as(formatter, io=$>)
337
+ formatter.write_circle(self, io, &(block_given? ? Proc.new : nil))
338
+ end
339
+
340
+ class << self
341
+
342
+ public
343
+
344
+ def create_on_center_radius(center, radius, options={})
345
+ new(center, radius, options)
346
+ end
347
+ end
348
+ end
349
+
350
+ class Ellipse < Base
351
+ attr_coordinate :center
352
+ attr_length :radius_x, :radius_y
353
+
354
+ def initialize(center, radius_x, radius_y, options={})
355
+ @center = Coordinate.new(center)
356
+ @radius_x = Length.new(radius_x).abs
357
+ @radius_y = Length.new(radius_y).abs
358
+ @attributes = init_attributes(options)
359
+ end
360
+
361
+ def left
362
+ @center.x - @radius_x
363
+ end
364
+
365
+ def right
366
+ @center.x + @radius_x
367
+ end
368
+
369
+ def top
370
+ @center.y - @radius_y
371
+ end
372
+
373
+ def bottom
374
+ @center.y + @radius_y
375
+ end
376
+
377
+ def width
378
+ @radius_x * 2
379
+ end
380
+
381
+ def height
382
+ @radius_y * 2
383
+ end
384
+
385
+ def write_as(formatter, io=$>)
386
+ formatter.write_ellipse(self, io, &(block_given? ? Proc.new : nil))
387
+ end
388
+
389
+ class << self
390
+
391
+ public
392
+
393
+ def create_on_center_radius(center, radius_x, radius_y, options={})
394
+ new(center, radius_x, radius_y, options)
395
+ end
396
+ end
397
+ end
398
+
399
+ class Line < Base
400
+ attr_coordinate :start_point, :end_point
401
+
402
+ def initialize(start_point, end_point, options={})
403
+ @start_point = Coordinate.new(start_point)
404
+ @end_point = Coordinate.new(end_point)
405
+ @attributes = init_attributes(options)
406
+ end
407
+
408
+ def left
409
+ [@start_point.x, @end_point.x].min
410
+ end
411
+
412
+ def right
413
+ [@start_point.x, @end_point.x].max
414
+ end
415
+
416
+ def top
417
+ [@start_point.y, @end_point.y].min
418
+ end
419
+
420
+ def bottom
421
+ [@start_point.y, @end_point.y].max
422
+ end
423
+
424
+ def write_as(formatter, io=$>)
425
+ formatter.write_line(self, io, &(block_given? ? Proc.new : nil))
426
+ end
427
+
428
+ class << self
429
+
430
+ public
431
+
432
+ def create_on_start_end(start_point, end_point, options={})
433
+ new(start_point, end_point, options)
434
+ end
435
+
436
+ def create_on_direction(start_point, direction_x, direction_y, options={})
437
+ start_point = Coordinate.new(start_point)
438
+ end_point = start_point + Coordinate.new(direction_x, direction_y)
439
+ new(start_point, end_point, options)
440
+ end
441
+ end
442
+ end
443
+
444
+ class Polyline < Base
445
+
446
+ def initialize(start_point, options={})
447
+ @points = [Coordinate.new(start_point)]
448
+ @attributes = init_attributes(options)
449
+ end
450
+
451
+ def line_to(point, relative=false)
452
+ @points.push(relative ? current_point + point : Coordinate.new(point))
453
+ end
454
+
455
+ def current_point
456
+ @points.last
457
+ end
458
+
459
+ def start_point
460
+ @points.first
461
+ end
462
+
463
+ def points
464
+ @points.dup
465
+ end
466
+
467
+ def undo
468
+ @points.pop if @points.size > 1
469
+ end
470
+
471
+ def left
472
+ @points.min {|a, b| a.x <=> b.x}.x
473
+ end
474
+
475
+ def right
476
+ @points.max {|a, b| a.x <=> b.x}.x
477
+ end
478
+
479
+ def top
480
+ @points.min {|a, b| a.y <=> b.y}.y
481
+ end
482
+
483
+ def bottom
484
+ @points.max {|a, b| a.y <=> b.y}.y
485
+ end
486
+
487
+ def write_as(formatter, io=$>)
488
+ formatter.write_polyline(self, io, &(block_given? ? Proc.new : nil))
489
+ end
490
+ end
491
+
492
+ class Polygon < Polyline
493
+
494
+ def write_as(formatter, io=$>)
495
+ formatter.write_polygon(self, io, &(block_given? ? Proc.new : nil))
496
+ end
497
+ end
498
+
499
+ # @since 1.0.0
500
+ class Image < Rectangle
501
+ attr_reader :file_path
502
+
503
+ def initialize(left_top, width, height, file_path, options={})
504
+ super(left_top, width, height, options)
505
+ @file_path = file_path
506
+ end
507
+
508
+ # @return [Boolean] whether the element has a URI reference
509
+ def has_uri_reference?
510
+ true
511
+ end
512
+
513
+ def write_as(formatter, io=$>)
514
+ formatter.write_image(self, io, &(block_given? ? Proc.new : nil))
515
+ end
516
+ end
517
+
518
+ # @since 1.0.0
519
+ class ImageReference < Image
520
+ def include_external_file?
521
+ true
522
+ end
523
+ end
524
+
525
+ class Text < Base
526
+ UNPRIMITIVE_OPTIONS = [:line_height, :alignment_baseline, :format]
527
+ BASELINE_VALUES = ['baseline', 'top', 'middle', 'bottom']
528
+ DEFAULT_LINE_HEIGHT = 1
529
+ attr_coordinate :point
530
+ attr_coordinate :line_height
531
+ attr_accessor :text
532
+ attr_reader :format
533
+ attr_reader *UNPRIMITIVE_OPTIONS
534
+
535
+ def initialize(point, text=nil, options={})
536
+ @point = Coordinate.new(point || [0,0])
537
+ @text = text
538
+ @attributes = init_attributes(options)
539
+ end
540
+
541
+ def format=(value)
542
+ @format = value && value.to_s
543
+ end
544
+
545
+ def font_height
546
+ font.draw_size
547
+ end
548
+
549
+ def dy
550
+ font_height * (line_height || DEFAULT_LINE_HEIGHT)
551
+ end
552
+
553
+ def formated_text
554
+ if @format
555
+ if @text.kind_of?(Numeric)
556
+ @text.strfnum(@format)
557
+ elsif @text.respond_to?(:strftime)
558
+ @text.strftime(@format)
559
+ else
560
+ @text.to_s
561
+ end
562
+ else
563
+ @text.to_s
564
+ end
565
+ end
566
+
567
+ def write_as(formatter, io=$>)
568
+ formatter.write_text(self, io, &(block_given? ? Proc.new : nil))
569
+ end
570
+
571
+ private
572
+
573
+ def init_attributes(options)
574
+ options = super
575
+ format = options.delete(:format)
576
+ @format = format && format.to_s
577
+ line_height = options.delete(:line_height)
578
+ @line_height = line_height || DEFAULT_LINE_HEIGHT
579
+ options
580
+ end
581
+ end
582
+
583
+ class ShapeGroup < Base
584
+ attr_reader :child_elements
585
+
586
+ def initialize(options={})
587
+ @attributes = init_attributes(options)
588
+ @child_elements = []
589
+ end
590
+
591
+ def width
592
+ Length.new_or_nil(@attributes[:width])
593
+ end
594
+
595
+ def height
596
+ Length.new_or_nil(@attributes[:height])
597
+ end
598
+
599
+ def write_as(formatter, io=$>)
600
+ formatter.write_group(self, io, &(block_given? ? Proc.new : nil))
601
+ end
602
+
603
+ class << self
604
+ public
605
+
606
+ def draw_on(canvas, options = {})
607
+ new(options).draw_on(canvas)
608
+ end
609
+ end
610
+ end
611
+ end
612
+ end