sevgi-graphics 0.94.0 → 0.98.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +275 -2
- data/LICENSE +672 -3
- data/README.md +8 -8
- data/lib/sevgi/graphics/attribute.rb +271 -54
- data/lib/sevgi/graphics/auxiliary/canvas.rb +137 -58
- data/lib/sevgi/graphics/auxiliary/content.rb +200 -57
- data/lib/sevgi/graphics/auxiliary/margin.rb +33 -14
- data/lib/sevgi/graphics/auxiliary/paper.rb +118 -86
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +69 -0
- data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
- data/lib/sevgi/graphics/auxiliary.rb +3 -0
- data/lib/sevgi/graphics/document/base.rb +6 -2
- data/lib/sevgi/graphics/document/default.rb +1 -1
- data/lib/sevgi/graphics/document.rb +370 -124
- data/lib/sevgi/graphics/element.rb +143 -33
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +143 -33
- data/lib/sevgi/graphics/mixtures/duplicate.rb +76 -22
- data/lib/sevgi/graphics/mixtures/export.rb +58 -12
- data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
- data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
- data/lib/sevgi/graphics/mixtures/include.rb +40 -5
- data/lib/sevgi/graphics/mixtures/inkscape.rb +215 -46
- data/lib/sevgi/graphics/mixtures/rdf.rb +79 -7
- data/lib/sevgi/graphics/mixtures/render.rb +88 -19
- data/lib/sevgi/graphics/mixtures/save.rb +79 -26
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -9
- data/lib/sevgi/graphics/mixtures/tile.rb +88 -64
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
- data/lib/sevgi/graphics/mixtures/validate.rb +14 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +66 -24
- data/lib/sevgi/graphics/mixtures.rb +19 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +127 -0
- data/lib/sevgi/graphics.rb +75 -28
- metadata +10 -6
|
@@ -3,21 +3,36 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# DSL helpers for defining one SVG template and repeating it through `use` elements.
|
|
7
|
+
#
|
|
8
|
+
# Use {Sevgi::Sundries::Tile} instead when Ruby code needs inspectable repeated geometry or row/column bounds
|
|
9
|
+
# rather than SVG references.
|
|
10
|
+
# @see Sevgi::Sundries::Tile
|
|
11
|
+
# @see https://sevgi.roktas.dev/sundries/#choose-a-layout-model Choosing a layout model
|
|
8
12
|
module Tile
|
|
9
|
-
#
|
|
13
|
+
# Stable prefix used for generated tile CSS classes.
|
|
10
14
|
PREFIX = "tile"
|
|
11
15
|
|
|
12
16
|
# Builds a two-dimensional tile grid.
|
|
17
|
+
# Each use id has the form `id-row-column`, with one-based row and column numbers. Generated classes identify
|
|
18
|
+
# the one-based row and column and mark their first and last positions. A block defines the referenced template
|
|
19
|
+
# as a group under `defs` before the uses are added.
|
|
20
|
+
# @example Define and customize a tile grid
|
|
21
|
+
# customize = proc { |use, x:, y:, nx:, ny:| use[:opacity] = (x + y + 1).fdiv(nx + ny) }
|
|
22
|
+
# Sevgi::Graphics.SVG(:minimal) do
|
|
23
|
+
# Tile("dot", nx: 2, dx: 10, ny: 2, dy: 10, proc: customize) { circle r: 2 }
|
|
24
|
+
# end
|
|
13
25
|
# @param id [String] referenced template id
|
|
14
26
|
# @param nx [Integer] number of columns
|
|
15
|
-
# @param dx [Numeric] horizontal spacing
|
|
16
|
-
# @param ox [Numeric] horizontal offset
|
|
27
|
+
# @param dx [Numeric] finite horizontal spacing, normalized before coordinates are rendered
|
|
28
|
+
# @param ox [Numeric] finite horizontal offset, normalized before coordinates are rendered
|
|
17
29
|
# @param ny [Integer] number of rows
|
|
18
|
-
# @param dy [Numeric] vertical spacing
|
|
19
|
-
# @param oy [Numeric] vertical offset
|
|
20
|
-
# @param proc [Proc, nil] optional
|
|
30
|
+
# @param dy [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
31
|
+
# @param oy [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
32
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, y:, nx:, ny:)`, with
|
|
33
|
+
# zero-based coordinates and total counts; the callback may mutate the element and its return value is ignored
|
|
34
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
35
|
+
# @yieldreturn [Object] ignored block result
|
|
21
36
|
# @return [Sevgi::Graphics::Element] self
|
|
22
37
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
23
38
|
def Tile(
|
|
@@ -31,14 +46,9 @@ module Sevgi
|
|
|
31
46
|
proc: nil,
|
|
32
47
|
&block
|
|
33
48
|
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# rubocop:disable Style/NestedTernaryOperator
|
|
38
|
-
# for pretty kwargs handling
|
|
39
|
-
x.zero? ? (y.zero? ? {} : {y:}) : (y.zero? ? {x:} : {x:, y:})
|
|
40
|
-
# rubocop:enable Style/NestedTernaryOperator
|
|
41
|
-
end
|
|
49
|
+
id, nx, dx, ox, ny, dy, oy, callback = Helper
|
|
50
|
+
.normalize(id:, nx:, dx:, ox:, ny:, dy:, oy:, proc:)
|
|
51
|
+
.values_at(:id, :nx, :dx, :ox, :ny, :dy, :oy, :proc)
|
|
42
52
|
|
|
43
53
|
defs { g(id:, &block) } if block
|
|
44
54
|
|
|
@@ -50,32 +60,36 @@ module Sevgi
|
|
|
50
60
|
cs = Helper.classify(as: "col", index: x, upper: nx)
|
|
51
61
|
|
|
52
62
|
element = use(
|
|
53
|
-
id: [
|
|
54
|
-
href: "##{
|
|
63
|
+
id: [id, y + 1, x + 1].join("-"),
|
|
64
|
+
href: "##{id}",
|
|
55
65
|
class: [*rs, *cs].join(" "),
|
|
56
|
-
**
|
|
66
|
+
**Helper.coordinates(
|
|
67
|
+
x: Scalar.number((x * dx) + ox, context: "tile", field: :x),
|
|
68
|
+
y: Scalar.number((y * dy) + oy, context: "tile", field: :y)
|
|
69
|
+
)
|
|
57
70
|
)
|
|
58
|
-
|
|
71
|
+
callback&.call(element, x:, y:, nx:, ny:)
|
|
59
72
|
end
|
|
60
73
|
end
|
|
61
74
|
end
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
# Builds a one-dimensional horizontal tile row.
|
|
78
|
+
# Each use id has the form `id-column`, with a one-based column number. Generated classes identify the column and
|
|
79
|
+
# mark its first and last positions. A block defines the referenced template as a group under `defs` before the
|
|
80
|
+
# uses are added.
|
|
65
81
|
# @param id [String] referenced template id
|
|
66
82
|
# @param n [Integer] number of instances
|
|
67
|
-
# @param d [Numeric] horizontal spacing
|
|
68
|
-
# @param o [Numeric] horizontal offset
|
|
69
|
-
# @param proc [Proc, nil] optional
|
|
83
|
+
# @param d [Numeric] finite horizontal spacing, normalized before coordinates are rendered
|
|
84
|
+
# @param o [Numeric] finite horizontal offset, normalized before coordinates are rendered
|
|
85
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, n:)`, with a zero-based column
|
|
86
|
+
# and total count; the callback may mutate the element and its return value is ignored
|
|
87
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
88
|
+
# @yieldreturn [Object] ignored block result
|
|
70
89
|
# @return [Sevgi::Graphics::Element] self
|
|
71
90
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
72
91
|
def TileX(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
|
|
73
|
-
Helper.
|
|
74
|
-
|
|
75
|
-
href, coords = id, proc do |x|
|
|
76
|
-
# for pretty kwargs handling
|
|
77
|
-
x.zero? ? {} : {x:}
|
|
78
|
-
end
|
|
92
|
+
id, n, d, o, callback = Helper.normalize(id:, n:, d:, o:, proc:).values_at(:id, :n, :d, :o, :proc)
|
|
79
93
|
|
|
80
94
|
defs { g(id:, &block) } if block
|
|
81
95
|
|
|
@@ -84,31 +98,32 @@ module Sevgi
|
|
|
84
98
|
cs = Helper.classify(as: "col", index: x, upper: n)
|
|
85
99
|
|
|
86
100
|
element = use(
|
|
87
|
-
id: [
|
|
88
|
-
href: "##{
|
|
101
|
+
id: [id, x + 1].join("-"),
|
|
102
|
+
href: "##{id}",
|
|
89
103
|
class: cs.join(" "),
|
|
90
|
-
**
|
|
104
|
+
**Helper.coordinates(x: Scalar.number((x * d) + o, context: "tile", field: :x))
|
|
91
105
|
)
|
|
92
|
-
|
|
106
|
+
callback&.call(element, x:, n:)
|
|
93
107
|
end
|
|
94
108
|
end
|
|
95
109
|
end
|
|
96
110
|
|
|
97
111
|
# Builds a one-dimensional vertical tile column.
|
|
112
|
+
# Each use id has the form `id-row`, with a one-based row number. Generated classes identify the row and mark its
|
|
113
|
+
# first and last positions. A block defines the referenced template as a group under `defs` before the uses are
|
|
114
|
+
# added.
|
|
98
115
|
# @param id [String] referenced template id
|
|
99
116
|
# @param n [Integer] number of instances
|
|
100
|
-
# @param d [Numeric] vertical spacing
|
|
101
|
-
# @param o [Numeric] vertical offset
|
|
102
|
-
# @param proc [Proc, nil] optional
|
|
117
|
+
# @param d [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
118
|
+
# @param o [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
119
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, y:, n:)`, with a zero-based row and
|
|
120
|
+
# total count; the callback may mutate the element and its return value is ignored
|
|
121
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
122
|
+
# @yieldreturn [Object] ignored block result
|
|
103
123
|
# @return [Sevgi::Graphics::Element] self
|
|
104
124
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
105
125
|
def TileY(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
|
|
106
|
-
Helper.
|
|
107
|
-
|
|
108
|
-
href, coords = id, proc do |y|
|
|
109
|
-
# for pretty kwargs handling
|
|
110
|
-
y.zero? ? {} : {y:}
|
|
111
|
-
end
|
|
126
|
+
id, n, d, o, callback = Helper.normalize(id:, n:, d:, o:, proc:).values_at(:id, :n, :d, :o, :proc)
|
|
112
127
|
|
|
113
128
|
defs { g(id:, &block) } if block
|
|
114
129
|
|
|
@@ -117,12 +132,12 @@ module Sevgi
|
|
|
117
132
|
rs = Helper.classify(as: "row", index: y, upper: n)
|
|
118
133
|
|
|
119
134
|
element = use(
|
|
120
|
-
id: [
|
|
121
|
-
href: "##{
|
|
135
|
+
id: [id, y + 1].join("-"),
|
|
136
|
+
href: "##{id}",
|
|
122
137
|
class: rs.join(" "),
|
|
123
|
-
**
|
|
138
|
+
**Helper.coordinates(y: Scalar.number((y * d) + o, context: "tile", field: :y))
|
|
124
139
|
)
|
|
125
|
-
|
|
140
|
+
callback&.call(element, y:, n:)
|
|
126
141
|
end
|
|
127
142
|
end
|
|
128
143
|
end
|
|
@@ -132,18 +147,14 @@ module Sevgi
|
|
|
132
147
|
module Helper
|
|
133
148
|
extend self
|
|
134
149
|
|
|
150
|
+
FINITE = %i[d dx dy o ox oy].freeze
|
|
151
|
+
|
|
135
152
|
# Argument validators for tile helpers.
|
|
136
153
|
ASSERTION = {
|
|
137
154
|
id: proc { |name, value| "Argument '#{name}' must be a string" unless value.is_a?(::String) },
|
|
138
155
|
n: proc { |name, value| positive_integer_issue(name, value) },
|
|
139
156
|
nx: proc { |name, value| positive_integer_issue(name, value) },
|
|
140
157
|
ny: proc { |name, value| positive_integer_issue(name, value) },
|
|
141
|
-
d: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
142
|
-
dx: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
143
|
-
dy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
144
|
-
o: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
145
|
-
ox: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
146
|
-
oy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
147
158
|
proc: proc { |name, value| "Argument '#{name}' must be a proc" unless value.nil? || value.is_a?(::Proc) }
|
|
148
159
|
}.freeze
|
|
149
160
|
|
|
@@ -155,22 +166,33 @@ module Sevgi
|
|
|
155
166
|
"Argument '#{name}' must be a positive integer" unless value.is_a?(::Integer) && value.positive?
|
|
156
167
|
end
|
|
157
168
|
|
|
158
|
-
# Validates tile arguments.
|
|
169
|
+
# Validates and normalizes tile arguments.
|
|
159
170
|
# @param kwargs [Hash] tile arguments
|
|
160
|
-
# @return [
|
|
171
|
+
# @return [Hash] independent arguments with spacing and offsets normalized to SVG numbers
|
|
161
172
|
# @raise [Sevgi::ArgumentError] when an argument is missing or invalid
|
|
162
|
-
def
|
|
163
|
-
kwargs.
|
|
164
|
-
|
|
173
|
+
def normalize(**kwargs)
|
|
174
|
+
kwargs.to_h do |name, value|
|
|
175
|
+
ArgumentError.("Argument '#{name}' required") if value == Undefined
|
|
176
|
+
[name, normalize_value(name, value)]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
end
|
|
180
|
+
def coordinates(**coordinates)
|
|
181
|
+
coordinates.reject { |_, value| value.zero? }
|
|
182
|
+
end
|
|
170
183
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
def normalize_value(name, value)
|
|
185
|
+
return number(name, value) if FINITE.include?(name)
|
|
186
|
+
return value unless (assertion = ASSERTION[name])
|
|
187
|
+
return value unless (issue = assertion.call(name, value))
|
|
188
|
+
|
|
189
|
+
ArgumentError.(issue)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def number(name, value)
|
|
193
|
+
Scalar.number(value, context: "tile", field: name)
|
|
194
|
+
rescue ::Sevgi::ArgumentError
|
|
195
|
+
ArgumentError.("Argument '#{name}' must be a finite real number")
|
|
174
196
|
end
|
|
175
197
|
|
|
176
198
|
# Returns positional tile CSS classes.
|
|
@@ -185,6 +207,8 @@ module Sevgi
|
|
|
185
207
|
classes << "#{PREFIX}-#{as}-last" if index + 1 == upper
|
|
186
208
|
end
|
|
187
209
|
end
|
|
210
|
+
|
|
211
|
+
private :normalize_value, :number
|
|
188
212
|
end
|
|
189
213
|
|
|
190
214
|
private_constant :Helper
|
|
@@ -4,6 +4,12 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for SVG transform attributes.
|
|
7
|
+
#
|
|
8
|
+
# Transform calls are appended in call order and return the element, so they can be composed.
|
|
9
|
+
# @example Compose transforms on one element
|
|
10
|
+
# Sevgi::Graphics.SVG(:minimal) do
|
|
11
|
+
# rect(width: 8, height: 4).Translate(12, 6).Rotate(15, 4, 2)
|
|
12
|
+
# end
|
|
7
13
|
module Transform
|
|
8
14
|
# Aligns an inner box inside an outer box.
|
|
9
15
|
# @param position [Symbol, String, nil] alignment name
|
|
@@ -11,12 +17,15 @@ module Sevgi
|
|
|
11
17
|
# @param outer [#width, #height, nil] outer box
|
|
12
18
|
# @return [Sevgi::Graphics::Element] self
|
|
13
19
|
# @raise [Sevgi::ArgumentError] when alignment is unsupported
|
|
20
|
+
# @raise [Sevgi::ArgumentError] when a box dimension is not a finite real number
|
|
14
21
|
def Align(position, inner:, outer:)
|
|
15
22
|
return self unless position && inner && outer
|
|
16
23
|
|
|
17
24
|
case position.to_sym
|
|
18
25
|
when :center
|
|
19
|
-
|
|
26
|
+
dimensions = [inner.width, inner.height, outer.width, outer.height]
|
|
27
|
+
iw, ih, ow, oh = dimensions.map { Scalar.number(it, context: "alignment", field: :dimension) }
|
|
28
|
+
Translate((ow - iw) / 2.0, (oh - ih) / 2.0)
|
|
20
29
|
else
|
|
21
30
|
ArgumentError.("Unsupported alignment: #{position}")
|
|
22
31
|
end
|
|
@@ -26,7 +35,7 @@ module Sevgi
|
|
|
26
35
|
# @return [Sevgi::Graphics::Element] self
|
|
27
36
|
def Flip
|
|
28
37
|
tap do
|
|
29
|
-
attributes[:"transform#{
|
|
38
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(-1, -1)"
|
|
30
39
|
end
|
|
31
40
|
end
|
|
32
41
|
|
|
@@ -34,7 +43,7 @@ module Sevgi
|
|
|
34
43
|
# @return [Sevgi::Graphics::Element] self
|
|
35
44
|
def FlipX
|
|
36
45
|
tap do
|
|
37
|
-
attributes[:"transform#{
|
|
46
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(-1, 1)"
|
|
38
47
|
end
|
|
39
48
|
end
|
|
40
49
|
|
|
@@ -42,36 +51,40 @@ module Sevgi
|
|
|
42
51
|
# @return [Sevgi::Graphics::Element] self
|
|
43
52
|
def FlipY
|
|
44
53
|
tap do
|
|
45
|
-
attributes[:"transform#{
|
|
54
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(1, -1)"
|
|
46
55
|
end
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
# Appends a six-value SVG matrix transform.
|
|
50
|
-
# @param values [Array<Numeric>] matrix values
|
|
59
|
+
# @param values [Array<Numeric>] finite matrix values, normalized to SVG numbers
|
|
51
60
|
# @return [Sevgi::Graphics::Element] self
|
|
52
|
-
# @raise [Sevgi::ArgumentError] when
|
|
61
|
+
# @raise [Sevgi::ArgumentError] when six finite real values are not supplied
|
|
53
62
|
def Matrix(*values)
|
|
54
63
|
tap do
|
|
55
64
|
ArgumentError.("Incorrect transform matrix (six values required): #{values}") if values.size != 6
|
|
65
|
+
values = Scalar.numbers(values, context: "matrix")
|
|
56
66
|
|
|
57
|
-
attributes[:"transform#{
|
|
67
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "matrix(#{values.join(" ")})"
|
|
58
68
|
end
|
|
59
69
|
end
|
|
60
70
|
|
|
61
71
|
# Appends an SVG rotate transform.
|
|
62
|
-
# @param a [Numeric] angle in degrees
|
|
63
|
-
# @param origin [Array<Numeric>] optional x and y origin
|
|
72
|
+
# @param a [Numeric] finite angle in degrees, normalized to an SVG number
|
|
73
|
+
# @param origin [Array<Numeric>] optional finite x and y origin, normalized to SVG numbers
|
|
64
74
|
# @return [Sevgi::Graphics::Element] self
|
|
65
|
-
# @raise [Sevgi::ArgumentError] when origin is not two coordinates
|
|
75
|
+
# @raise [Sevgi::ArgumentError] when angle or origin is not finite real, or origin is not two coordinates
|
|
66
76
|
def Rotate(a, *origin)
|
|
67
77
|
tap do
|
|
68
78
|
if !origin.empty? && origin.size != 2
|
|
69
79
|
ArgumentError.("Incorrect origin (two coordinates required): #{origin}")
|
|
70
80
|
end
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
angle = Scalar.number(a, context: "rotation", field: :angle)
|
|
83
|
+
origin = Scalar.numbers(origin, context: "rotation")
|
|
73
84
|
|
|
74
|
-
|
|
85
|
+
next if angle.zero?
|
|
86
|
+
|
|
87
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "rotate(#{[angle, *origin].join(", ")})"
|
|
75
88
|
end
|
|
76
89
|
end
|
|
77
90
|
|
|
@@ -88,56 +101,83 @@ module Sevgi
|
|
|
88
101
|
def RotateLeft(...) = Rotate(-90, ...)
|
|
89
102
|
|
|
90
103
|
# Appends an SVG scale transform.
|
|
91
|
-
# @param x [Numeric] x scale, or uniform scale when y is nil
|
|
92
|
-
# @param y [Numeric, nil] y scale
|
|
104
|
+
# @param x [Numeric] finite x scale, or uniform scale when y is nil
|
|
105
|
+
# @param y [Numeric, nil] finite y scale
|
|
93
106
|
# @return [Sevgi::Graphics::Element] self
|
|
107
|
+
# @raise [Sevgi::ArgumentError] when a scale is not a finite real number
|
|
94
108
|
def Scale(x, y = nil)
|
|
95
109
|
tap do
|
|
96
|
-
|
|
110
|
+
x = Scalar.number(x, context: "scale", field: :x)
|
|
111
|
+
y = Scalar.number(y, context: "scale", field: :y) unless y.nil?
|
|
112
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(#{(y.nil? ? [x] : [x, y]).join(", ")})"
|
|
97
113
|
end
|
|
98
114
|
end
|
|
99
115
|
|
|
100
116
|
# Appends a skew transform through an SVG matrix.
|
|
101
|
-
# @param ax [Numeric] x skew angle in degrees
|
|
102
|
-
# @param ay [Numeric] y skew angle in degrees
|
|
117
|
+
# @param ax [Numeric] finite x skew angle in degrees, normalized before calculation
|
|
118
|
+
# @param ay [Numeric] finite y skew angle in degrees, normalized before calculation
|
|
103
119
|
# @return [Sevgi::Graphics::Element] self
|
|
120
|
+
# @raise [Sevgi::ArgumentError] when an angle is not a finite real number
|
|
104
121
|
def Skew(ax, ay)
|
|
122
|
+
ax = Scalar.number(ax, context: "skew", field: :x)
|
|
123
|
+
ay = Scalar.number(ay, context: "skew", field: :y)
|
|
105
124
|
Matrix(1.0, ::Math.tan(ay / 180.0 * ::Math::PI), ::Math.tan(ax / 180.0 * ::Math::PI), 1.0, 0.0, 0.0)
|
|
106
125
|
end
|
|
107
126
|
|
|
108
127
|
# Appends an SVG skewX transform.
|
|
109
|
-
# @param a [Numeric] angle in degrees
|
|
128
|
+
# @param a [Numeric] finite angle in degrees, normalized to an SVG number
|
|
110
129
|
# @return [Sevgi::Graphics::Element] self
|
|
130
|
+
# @raise [Sevgi::ArgumentError] when angle is not a finite real number
|
|
111
131
|
def SkewX(a)
|
|
112
132
|
tap do
|
|
113
|
-
|
|
133
|
+
angle = Scalar.number(a, context: "skew", field: :x)
|
|
134
|
+
next if angle.zero?
|
|
114
135
|
|
|
115
|
-
attributes[:"transform#{
|
|
136
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "skewX(#{angle})"
|
|
116
137
|
end
|
|
117
138
|
end
|
|
118
139
|
|
|
119
140
|
# Appends an SVG skewY transform.
|
|
120
|
-
# @param a [Numeric] angle in degrees
|
|
141
|
+
# @param a [Numeric] finite angle in degrees, normalized to an SVG number
|
|
121
142
|
# @return [Sevgi::Graphics::Element] self
|
|
143
|
+
# @raise [Sevgi::ArgumentError] when angle is not a finite real number
|
|
122
144
|
def SkewY(a)
|
|
123
145
|
tap do
|
|
124
|
-
|
|
146
|
+
angle = Scalar.number(a, context: "skew", field: :y)
|
|
147
|
+
next if angle.zero?
|
|
125
148
|
|
|
126
|
-
attributes[:"transform#{
|
|
149
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "skewY(#{angle})"
|
|
127
150
|
end
|
|
128
151
|
end
|
|
129
152
|
|
|
130
|
-
# Appends an SVG translate transform.
|
|
131
|
-
# @param x [Numeric] x translation
|
|
132
|
-
# @param y [Numeric, nil] y translation
|
|
153
|
+
# Appends an SVG translate transform. One argument translates only the x axis.
|
|
154
|
+
# @param x [Numeric] finite x translation, normalized to an SVG number
|
|
155
|
+
# @param y [Numeric, nil] finite y translation, normalized to an SVG number
|
|
133
156
|
# @return [Sevgi::Graphics::Element] self
|
|
157
|
+
# @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
|
|
134
158
|
def Translate(x, y = nil)
|
|
135
159
|
tap do
|
|
136
|
-
|
|
160
|
+
dx = Scalar.number(x, context: "translation", field: :x)
|
|
161
|
+
dy = Scalar.number(y, context: "translation", field: :y) unless y.nil?
|
|
162
|
+
next if dx.zero? && (dy.nil? || dy.zero?)
|
|
137
163
|
|
|
138
|
-
attributes[:"transform#{
|
|
164
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "translate(#{(dy.nil? ? [dx] : [dx, dy]).join(" ")})"
|
|
139
165
|
end
|
|
140
166
|
end
|
|
167
|
+
|
|
168
|
+
# Appends an x-axis SVG translate transform.
|
|
169
|
+
# @param x [Numeric] finite x translation
|
|
170
|
+
# @return [Sevgi::Graphics::Element] self
|
|
171
|
+
# @raise [Sevgi::ArgumentError] when x is not a finite real number
|
|
172
|
+
# @see #Translate
|
|
173
|
+
def TranslateX(x) = Translate(x)
|
|
174
|
+
|
|
175
|
+
# Appends a y-axis SVG translate transform.
|
|
176
|
+
# @param y [Numeric] finite y translation
|
|
177
|
+
# @return [Sevgi::Graphics::Element] self
|
|
178
|
+
# @raise [Sevgi::ArgumentError] when y is not a finite real number
|
|
179
|
+
# @see #Translate
|
|
180
|
+
def TranslateY(y) = Translate(0, y)
|
|
141
181
|
end
|
|
142
182
|
end
|
|
143
183
|
end
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
-
# DSL helpers for floating text, comments, and inherited
|
|
6
|
+
# DSL helpers for floating text, comments, and inherited non-rendering context.
|
|
7
7
|
module Underscore
|
|
8
|
+
CONTEXT = :"#{Attributes::META_PREFIX}context"
|
|
9
|
+
private_constant :CONTEXT
|
|
10
|
+
|
|
8
11
|
# Creates a floating content element.
|
|
9
12
|
# @param contents [Array<Object>] text or content objects
|
|
10
13
|
# @return [Sevgi::Graphics::Element] floating element
|
|
@@ -15,9 +18,9 @@ module Sevgi
|
|
|
15
18
|
# Adds an XML comment.
|
|
16
19
|
# @param comment [Object] comment text
|
|
17
20
|
# @return [Sevgi::Graphics::Element] floating comment element
|
|
18
|
-
# @raise [Sevgi::ArgumentError] when comment
|
|
21
|
+
# @raise [Sevgi::ArgumentError] when comment cannot be stringified as valid XML or would form malformed markup
|
|
19
22
|
def Comment(comment)
|
|
20
|
-
comment = comment
|
|
23
|
+
comment = XML.text(comment, context: "XML comment")
|
|
21
24
|
|
|
22
25
|
ArgumentError.("XML comment must not contain '--'") if comment.include?("--")
|
|
23
26
|
ArgumentError.("XML comment must not end with '-'") if comment.end_with?("-")
|
|
@@ -25,16 +28,22 @@ module Sevgi
|
|
|
25
28
|
_(Content.verbatim("<!-- #{comment} -->"))
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
# Merges
|
|
31
|
+
# Merges `-context` metadata from the document root, ancestors, and this element.
|
|
29
32
|
# Only the direct root-to-self ancestor chain participates; sibling subtrees are ignored. When the same key is
|
|
30
|
-
# present on multiple chain elements, the nearest element to the receiver wins.
|
|
31
|
-
#
|
|
33
|
+
# present on multiple chain elements, the nearest element to the receiver wins. Context remains available in
|
|
34
|
+
# memory but is omitted from rendered SVG. It is unrelated to the `_:` XML namespace syntax preserved by
|
|
35
|
+
# Derender.
|
|
36
|
+
# @return [Hash] merged context
|
|
37
|
+
# @example Inherit drawing context without rendering it
|
|
38
|
+
# Sevgi::Graphics.SVG "-context": {tone: "tomato"} do
|
|
39
|
+
# rect fill: Ancestral()[:tone]
|
|
40
|
+
# end
|
|
32
41
|
def Ancestral
|
|
33
42
|
chain = []
|
|
34
43
|
TraverseUp { |element| chain.unshift(element) }
|
|
35
44
|
|
|
36
45
|
{}.tap do |result|
|
|
37
|
-
chain.each { |element| result.merge!(element[
|
|
46
|
+
chain.each { |element| result.merge!(element[CONTEXT]) if element.has?(CONTEXT) }
|
|
38
47
|
end
|
|
39
48
|
end
|
|
40
49
|
end
|
|
@@ -4,13 +4,25 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for SVG standard validation.
|
|
7
|
+
#
|
|
8
|
+
# @!method CData
|
|
9
|
+
# Returns text content used as SVG character data.
|
|
10
|
+
# @return [String, nil] joined text content or nil
|
|
11
|
+
# @!method NS?(name)
|
|
12
|
+
# Reports whether a namespace is available on this element or an ancestor.
|
|
13
|
+
# @param name [Symbol, String] namespace suffix without xmlns:
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
# @!method Validate
|
|
16
|
+
# Validates this subtree against SVG standard metadata when that component is available.
|
|
17
|
+
# @return [Sevgi::Graphics::Element, nil] self after validation, or nil without sevgi/standard
|
|
18
|
+
# @raise [Sevgi::ValidationError] when SVG validation fails
|
|
7
19
|
module Validate
|
|
8
20
|
# Returns text content used as SVG character data.
|
|
9
21
|
# @return [String, nil] joined text content or nil
|
|
10
22
|
def CData
|
|
11
23
|
return if !contents || contents.empty?
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
contents.join("\n")
|
|
14
26
|
end
|
|
15
27
|
|
|
16
28
|
# Reports whether a namespace is available on this element or an ancestor.
|
|
@@ -29,7 +41,7 @@ module Sevgi
|
|
|
29
41
|
Traverse do |element|
|
|
30
42
|
Standard.conform(
|
|
31
43
|
element.name,
|
|
32
|
-
attributes: element.attributes.
|
|
44
|
+
attributes: element.attributes.keys,
|
|
33
45
|
cdata: element.CData(),
|
|
34
46
|
elements: element.children.map(&:name)
|
|
35
47
|
)
|