vector_salad 0.0.1
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 +7 -0
- data/.gitignore +16 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +89 -0
- data/Rakefile +2 -0
- data/bin/vector_salad +72 -0
- data/examples/birthday.png +0 -0
- data/examples/birthday.rb +45 -0
- data/examples/birthday.svg +630 -0
- data/examples/boolean_operations.png +0 -0
- data/examples/boolean_operations.rb +23 -0
- data/examples/boolean_operations.svg +151 -0
- data/examples/bp_logo.png +0 -0
- data/examples/bp_logo.rb +18 -0
- data/examples/bp_logo.svg +25 -0
- data/examples/bunny_card.png +0 -0
- data/examples/bunny_card.rb +219 -0
- data/examples/bunny_card.svg +134 -0
- data/examples/chill.png +0 -0
- data/examples/chill.rb +16 -0
- data/examples/chill.svg +86 -0
- data/examples/circle_line_segments.png +0 -0
- data/examples/circle_line_segments.rb +11 -0
- data/examples/circle_line_segments.svg +28 -0
- data/examples/circles.png +0 -0
- data/examples/circles.rb +14 -0
- data/examples/circles.svg +11 -0
- data/examples/clip_operations.png +0 -0
- data/examples/clip_operations.rb +14 -0
- data/examples/clip_operations.svg +8 -0
- data/examples/cog_menu.png +0 -0
- data/examples/cog_menu.rb +32 -0
- data/examples/cog_menu.svg +37 -0
- data/examples/cubic_bezier_handles.png +0 -0
- data/examples/cubic_bezier_handles.rb +21 -0
- data/examples/cubic_bezier_handles.svg +14 -0
- data/examples/cubic_circle.png +0 -0
- data/examples/cubic_circle.rb +26 -0
- data/examples/cubic_circle.svg +29 -0
- data/examples/face.png +0 -0
- data/examples/face.rb +4 -0
- data/examples/face.svg +10 -0
- data/examples/flower.png +0 -0
- data/examples/flower.rb +23 -0
- data/examples/flower.svg +207 -0
- data/examples/fox.png +0 -0
- data/examples/fox.rb +110 -0
- data/examples/fox.svg +31 -0
- data/examples/fresh_vector_salad_gui.png +0 -0
- data/examples/galaxies.png +0 -0
- data/examples/galaxies.rb +60 -0
- data/examples/galaxies.svg +5806 -0
- data/examples/gold_stars.png +0 -0
- data/examples/gold_stars.rb +9 -0
- data/examples/gold_stars.svg +12 -0
- data/examples/paths.png +0 -0
- data/examples/paths.rb +87 -0
- data/examples/paths.svg +13 -0
- data/examples/pepsi_logo.png +0 -0
- data/examples/pepsi_logo.rb +21 -0
- data/examples/pepsi_logo.svg +10 -0
- data/examples/polygons.png +0 -0
- data/examples/polygons.rb +9 -0
- data/examples/polygons.svg +13 -0
- data/examples/quadratic_bezier_handle.png +0 -0
- data/examples/quadratic_bezier_handle.rb +18 -0
- data/examples/quadratic_bezier_handle.svg +13 -0
- data/examples/rects.png +0 -0
- data/examples/rects.rb +10 -0
- data/examples/rects.svg +11 -0
- data/examples/simple_path.png +0 -0
- data/examples/simple_path.rb +29 -0
- data/examples/simple_path.svg +8 -0
- data/examples/space.png +0 -0
- data/examples/space.rb +171 -0
- data/examples/space.svg +46453 -0
- data/examples/spiro_nodes.png +0 -0
- data/examples/spiro_nodes.rb +20 -0
- data/examples/spiro_nodes.svg +13 -0
- data/examples/squares.png +0 -0
- data/examples/squares.rb +14 -0
- data/examples/squares.svg +11 -0
- data/examples/stars.png +0 -0
- data/examples/stars.rb +3 -0
- data/examples/stars.svg +30006 -0
- data/examples/transforms.png +0 -0
- data/examples/transforms.rb +58 -0
- data/examples/transforms.svg +121 -0
- data/examples/triangles.png +0 -0
- data/examples/triangles.rb +8 -0
- data/examples/triangles.svg +9 -0
- data/lib/contracts_contracts.rb +32 -0
- data/lib/vector_salad.rb +5 -0
- data/lib/vector_salad/canvas.rb +27 -0
- data/lib/vector_salad/dsl.rb +41 -0
- data/lib/vector_salad/export_with_magic.rb +29 -0
- data/lib/vector_salad/exporters/base_exporter.rb +92 -0
- data/lib/vector_salad/exporters/svg_exporter.rb +174 -0
- data/lib/vector_salad/interpolate.rb +57 -0
- data/lib/vector_salad/magic.rb +17 -0
- data/lib/vector_salad/mixins/at.rb +28 -0
- data/lib/vector_salad/shape_proxy.rb +14 -0
- data/lib/vector_salad/standard_shapes/basic_shape.rb +29 -0
- data/lib/vector_salad/standard_shapes/circle.rb +64 -0
- data/lib/vector_salad/standard_shapes/clip.rb +51 -0
- data/lib/vector_salad/standard_shapes/custom.rb +28 -0
- data/lib/vector_salad/standard_shapes/difference.rb +28 -0
- data/lib/vector_salad/standard_shapes/exclusion.rb +28 -0
- data/lib/vector_salad/standard_shapes/flip.rb +24 -0
- data/lib/vector_salad/standard_shapes/hexagon.rb +15 -0
- data/lib/vector_salad/standard_shapes/intersection.rb +28 -0
- data/lib/vector_salad/standard_shapes/iso_tri.rb +39 -0
- data/lib/vector_salad/standard_shapes/jitter.rb +33 -0
- data/lib/vector_salad/standard_shapes/move.rb +24 -0
- data/lib/vector_salad/standard_shapes/multi_path.rb +82 -0
- data/lib/vector_salad/standard_shapes/n.rb +112 -0
- data/lib/vector_salad/standard_shapes/oval.rb +51 -0
- data/lib/vector_salad/standard_shapes/path.rb +249 -0
- data/lib/vector_salad/standard_shapes/pentagon.rb +15 -0
- data/lib/vector_salad/standard_shapes/polygon.rb +37 -0
- data/lib/vector_salad/standard_shapes/rect.rb +34 -0
- data/lib/vector_salad/standard_shapes/rotate.rb +24 -0
- data/lib/vector_salad/standard_shapes/scale.rb +34 -0
- data/lib/vector_salad/standard_shapes/square.rb +34 -0
- data/lib/vector_salad/standard_shapes/transform.rb +20 -0
- data/lib/vector_salad/standard_shapes/triangle.rb +15 -0
- data/lib/vector_salad/standard_shapes/union.rb +28 -0
- data/lib/vector_salad/version.rb +3 -0
- data/vector_salad.gemspec +34 -0
- metadata +262 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'vector_salad/standard_shapes/transform'
|
2
|
+
|
3
|
+
module VectorSalad
|
4
|
+
module StandardShapes
|
5
|
+
class Jitter < Transform
|
6
|
+
# Moves the contained shapes by the specified x and y amounts relatively.
|
7
|
+
#
|
8
|
+
# Examples:
|
9
|
+
#
|
10
|
+
# move(50, -10) do
|
11
|
+
# triangle(30, at: [50, -50])
|
12
|
+
# pentagon(40, at: [50, -100])
|
13
|
+
# end
|
14
|
+
|
15
|
+
# Jitter the position of nodes in a Path randomly.
|
16
|
+
# @param max The maximum offset
|
17
|
+
# @param min The minimum offset (default 0)
|
18
|
+
# @param fn The quantization number of sides
|
19
|
+
Contract Num, { min: Maybe[Num], fn: Maybe[Fixnum] } => Path
|
20
|
+
Contract Num, { min: Maybe[Num],
|
21
|
+
fn: Maybe[Fixnum],
|
22
|
+
canvas: VectorSalad::Canvas
|
23
|
+
}, Proc => Any
|
24
|
+
def initialize(max, min: 0, fn: nil, canvas:, **_options, &block)
|
25
|
+
instance_eval(&block) # inner_canvas is populated
|
26
|
+
|
27
|
+
@canvas.each do |shape|
|
28
|
+
canvas << shape.jitter(max, min: min, fn: fn)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "vector_salad/standard_shapes/transform"
|
2
|
+
|
3
|
+
module VectorSalad
|
4
|
+
module StandardShapes
|
5
|
+
class Move < Transform
|
6
|
+
# Moves the contained shapes by the specified x and y amounts relatively.
|
7
|
+
#
|
8
|
+
# Examples:
|
9
|
+
#
|
10
|
+
# move(50, -10) do
|
11
|
+
# triangle(30, at: [50, -50])
|
12
|
+
# pentagon(40, at: [50, -100])
|
13
|
+
# end
|
14
|
+
Contract Coord, Coord, { canvas: VectorSalad::Canvas }, Proc => Any
|
15
|
+
def initialize(x, y, canvas:, **_options, &block)
|
16
|
+
instance_eval(&block) # inner_canvas is populated
|
17
|
+
|
18
|
+
@canvas.each do |shape|
|
19
|
+
canvas << shape.move(x, y)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'vector_salad/standard_shapes/basic_shape'
|
2
|
+
require 'vector_salad/standard_shapes/n'
|
3
|
+
require 'vector_salad/interpolate'
|
4
|
+
|
5
|
+
module VectorSalad
|
6
|
+
module StandardShapes
|
7
|
+
class MultiPath < BasicShape
|
8
|
+
attr_reader :paths, :closed
|
9
|
+
|
10
|
+
# A MultiPath is a collection of Paths.
|
11
|
+
# It is mainly the result of {Clip} operations.
|
12
|
+
# See {Path} for details on constructing paths.
|
13
|
+
#
|
14
|
+
# Examples:
|
15
|
+
# new(
|
16
|
+
# Path.n([0,0], [0,300], [300,300], [300,0], [0,0]),
|
17
|
+
# Path.n([100,100], [200,100], [200,200], [100,200], [100,100])
|
18
|
+
# )
|
19
|
+
# new(
|
20
|
+
# [[0,0], [0,300], [300,300], [300,0], [0,0]],
|
21
|
+
# [[100,100], [200,100], [200,200], [100,200], [100,100]]
|
22
|
+
# )
|
23
|
+
Contract Args[Or[Array, Path]], { closed: Maybe[Bool] } => MultiPath
|
24
|
+
def initialize(*paths, closed: true, **options)
|
25
|
+
paths.each do |path|
|
26
|
+
path = path.instance_of?(Array) ? Path.new(*path) : path
|
27
|
+
@paths = Array(@paths) << path
|
28
|
+
end
|
29
|
+
|
30
|
+
@closed = closed
|
31
|
+
@options = options
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Move the shape relatively.
|
36
|
+
Contract Num, Num => MultiPath
|
37
|
+
def move(x, y)
|
38
|
+
each_send(:move, x, y)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Rotates the MultiPath by the specified angle about the origin.
|
42
|
+
Contract Num => MultiPath
|
43
|
+
def rotate(angle)
|
44
|
+
each_send(:rotate, angle)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Scale a MultiPath by multiplier about the origin.
|
48
|
+
Contract Num => MultiPath
|
49
|
+
def scale(multiplier)
|
50
|
+
each_send(:scale, multiplier)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_path
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_cubic_path
|
58
|
+
self.class.new(*@paths.map(&:to_cubic_path), **@options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_simple_path
|
62
|
+
self.class.new(*@paths.map(&:to_simple_path), **@options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_multi_path
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_a
|
70
|
+
@paths.map(&:to_a)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def each_send(method, *args, &block)
|
76
|
+
self.class.new(*@paths.map do |path|
|
77
|
+
path.send(method, *args, &block)
|
78
|
+
end, closed: @closed, **@options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require "vector_salad/mixins/at"
|
2
|
+
|
3
|
+
module VectorSalad
|
4
|
+
module StandardShapes
|
5
|
+
class N
|
6
|
+
include VectorSalad::Mixins::At
|
7
|
+
include Contracts
|
8
|
+
|
9
|
+
attr_accessor :x, :y, :type
|
10
|
+
|
11
|
+
Contract nil, nil, :mirror => N
|
12
|
+
def initialize(x, y, type)
|
13
|
+
create(x, y, type)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
# A node is the simplest primitive but useless on its own. Use nodes to
|
18
|
+
# build up a path (see Path). A node is a point in space with x and y
|
19
|
+
# coordinates. The coordinates must be nil if the node type is :mirror,
|
20
|
+
# else they must be numeric.
|
21
|
+
#
|
22
|
+
# A node also has a type, the simplest is :node for corners.
|
23
|
+
# To create node types other than :node see the shorthand class methods.
|
24
|
+
#
|
25
|
+
# Examples:
|
26
|
+
# new(50, 100)
|
27
|
+
# new(50, 100, :cubic)
|
28
|
+
# new(nil, nil, :mirror)
|
29
|
+
Contract Coord, Coord,
|
30
|
+
Maybe[*%i(node quadratic cubic g2 g4 left right)] => N
|
31
|
+
def initialize(x, y, type = :node)
|
32
|
+
create(x, y, type)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Shorthand for calling `new` to create a node.
|
37
|
+
# The type defaults to :node for basic corner or line segment nodes.
|
38
|
+
# See the documentation for `new` for usage.
|
39
|
+
Contract Maybe[Coord], Maybe[Coord], Maybe[Symbol] => N
|
40
|
+
def self.n(x, y, type = :node)
|
41
|
+
new(x, y, type)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Creates a :mirrored type node. It makes it easier to make smooth curves.
|
45
|
+
# The reflection is based on the cubic or quadratic node
|
46
|
+
# before the last standard :node so there must be one.
|
47
|
+
Contract None => N
|
48
|
+
def self.m
|
49
|
+
new(nil, nil, :mirror)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Shorthand that creates a :cubic type bezier curve handle node.
|
53
|
+
# This "off curve" node isn't part of the path but distorts pulling it.
|
54
|
+
# Two :cubic nodes must come between two :node type nodes. E.g. n c c n.
|
55
|
+
# As the interaction of two cubic nodes distorts the line segment this
|
56
|
+
# can be quite difficult to imagine, see :quadratic or :spiro for
|
57
|
+
# easier alternatives to make curves.
|
58
|
+
Contract Coord, Coord => N
|
59
|
+
def self.c(x, y)
|
60
|
+
new(x, y, :cubic)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Shorthand that creates a :quadratic type bezier curve handle node.
|
64
|
+
# This "off curve" node isn't part of the path but distorts pulling it.
|
65
|
+
# One :quadratic node must come between two :node type nodes. E.g. n q n.
|
66
|
+
# Only one quadratic node is needed to make a curve, however :spiro type
|
67
|
+
# nodes are even easier for making smooth curves.
|
68
|
+
Contract Coord, Coord => N
|
69
|
+
def self.q(x, y)
|
70
|
+
new(x, y, :quadratic)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Shorthand that creates a smooth spiro :g2 node.
|
74
|
+
# Spiro nodes are "on curve" so the path bends through them, finding the
|
75
|
+
# smoothest possible route. They are perfect for making organic curves.
|
76
|
+
# There are :g2 and :g4 spiro node types;
|
77
|
+
# :g2 is most robust and a good all rounder.
|
78
|
+
Contract Coord, Coord => N
|
79
|
+
def self.s(x, y)
|
80
|
+
new(x, y, :g2)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Shorthand that creates a smooth spiro :g4 node.
|
84
|
+
# There are :g2 and :g4 spiro node types;
|
85
|
+
# :g4 is smoothest but has longer distance effects and may sometimes fail.
|
86
|
+
Contract Coord, Coord => N
|
87
|
+
def self.g(x, y)
|
88
|
+
new(x, y, :g4)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Shorthand that creates a :right spiro node.
|
92
|
+
# It joins a straight line segment on the left to a curve on the right.
|
93
|
+
Contract Coord, Coord => N
|
94
|
+
def self.r(x, y)
|
95
|
+
new(x, y, :right)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Shorthand that creates a :left spiro node.
|
99
|
+
# It joins a straight line segment on the right to a curve on the left.
|
100
|
+
Contract Coord, Coord => N
|
101
|
+
def self.l(x, y)
|
102
|
+
new(x, y, :left)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def create(x, y, type)
|
108
|
+
@x, @y, @type = x, y, type
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "vector_salad/standard_shapes/path"
|
2
|
+
require "vector_salad/standard_shapes/n"
|
3
|
+
require "vector_salad/mixins/at"
|
4
|
+
|
5
|
+
module VectorSalad
|
6
|
+
module StandardShapes
|
7
|
+
class Oval < BasicShape
|
8
|
+
include VectorSalad::Mixins::At
|
9
|
+
attr_reader :width, :height
|
10
|
+
|
11
|
+
# Create an oval.
|
12
|
+
#
|
13
|
+
# Examples:
|
14
|
+
# new(100, 200)
|
15
|
+
Contract Pos, Pos, {} => Oval
|
16
|
+
def initialize(width, height, **options)
|
17
|
+
@options = options
|
18
|
+
@width, @height = width, height
|
19
|
+
@x, @y = 0, 0
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_path
|
24
|
+
# http://stackoverflow.com/a/13338311
|
25
|
+
# c = 4 * (Math.sqrt(2) - 1) / 3
|
26
|
+
# c = 0.5522847498307936
|
27
|
+
#
|
28
|
+
# http://spencermortensen.com/articles/bezier-circle/
|
29
|
+
c = 0.551915024494
|
30
|
+
dw = c * @width
|
31
|
+
dh = c * @height
|
32
|
+
Path.new(
|
33
|
+
N.n(@x + @width, @y),
|
34
|
+
N.c(@x + @width, @y + dh),
|
35
|
+
N.c(@x + dw, @y + @height),
|
36
|
+
N.n(@x, @y + @height),
|
37
|
+
N.c(@x - dw, @y + @height),
|
38
|
+
N.c(@x - @width, @y + dh),
|
39
|
+
N.n(@x - @width, @y),
|
40
|
+
N.c(@x - @width, @y - dh),
|
41
|
+
N.c(@x - dw, @y - @height),
|
42
|
+
N.n(@x, @y - @height),
|
43
|
+
N.c(@x + dw, @y - @height),
|
44
|
+
N.c(@x + @width, @y - dh),
|
45
|
+
N.n(@x + @width, @y),
|
46
|
+
@options
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'spiro'
|
2
|
+
|
3
|
+
require 'vector_salad/standard_shapes/basic_shape'
|
4
|
+
require 'vector_salad/standard_shapes/n'
|
5
|
+
require 'vector_salad/interpolate'
|
6
|
+
|
7
|
+
module VectorSalad
|
8
|
+
module StandardShapes
|
9
|
+
class Path < BasicShape
|
10
|
+
attr_reader :nodes, :closed
|
11
|
+
|
12
|
+
# The simplest shape primitive, all shapes can be represented as a Path.
|
13
|
+
# A path is made up of N nodes and these nodes can have different types
|
14
|
+
# (see N).
|
15
|
+
#
|
16
|
+
# Examples:
|
17
|
+
# new([0,0], [0,1], [1,1])
|
18
|
+
#
|
19
|
+
# @param nodes x,y coordinate arrays or N node instances
|
20
|
+
# @param closed whether the path is open or closed
|
21
|
+
Contract Args[Or[Array, N]], { closed: Maybe[Bool] } => Path
|
22
|
+
def initialize(*nodes, closed: true, **options)
|
23
|
+
@nodes = []
|
24
|
+
nodes.each_index do |i|
|
25
|
+
node = nodes[i].class == Array ? N.new(*nodes[i]) : nodes[i]
|
26
|
+
if i == 0 && ![:node, :g2, :g4, :left, :right].include?(node.type)
|
27
|
+
fail 'First node in a path must be :node or :spiro type.'
|
28
|
+
end
|
29
|
+
case node.type
|
30
|
+
when :cubic
|
31
|
+
unless nodes[i - 1].type == :node ||
|
32
|
+
(nodes[i - 2].type == :node && nodes[i - 1].type == :cubic)
|
33
|
+
fail ':cubic nodes must follow a :node and at most 1 other :cubic.'
|
34
|
+
end
|
35
|
+
when :quadratic
|
36
|
+
unless nodes[i - 1].type == :node
|
37
|
+
fail ':quadratic nodes must follow a :node.'
|
38
|
+
end
|
39
|
+
when :mirror
|
40
|
+
if nodes[i - 1].type == :node &&
|
41
|
+
(nodes[i - 2].type == :quadratic || nodes[i - 2].type == :cubic)
|
42
|
+
pivot = nodes[i - 1]
|
43
|
+
source = nodes[i - 2]
|
44
|
+
|
45
|
+
dx = pivot.x - source.x
|
46
|
+
dy = pivot.y - source.y
|
47
|
+
node[pivot.x + dx, pivot.y + dy]
|
48
|
+
|
49
|
+
node.type = source.type
|
50
|
+
else
|
51
|
+
fail ':reflect nodes must be preceeded by a :node with a
|
52
|
+
:quadratic or :cubic before that.'
|
53
|
+
end
|
54
|
+
when :node
|
55
|
+
end
|
56
|
+
@nodes << node
|
57
|
+
end
|
58
|
+
|
59
|
+
@closed = closed
|
60
|
+
@options = options
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Move the shape relatively.
|
65
|
+
Contract Num, Num => Path
|
66
|
+
def move(x, y)
|
67
|
+
Path.new(
|
68
|
+
*to_path.nodes.map do |node|
|
69
|
+
node.move(x, y)
|
70
|
+
end,
|
71
|
+
closed: @closed,
|
72
|
+
**@options
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Flips on the x axis.
|
77
|
+
Contract None => Path
|
78
|
+
def flip_x
|
79
|
+
flip(:x)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Flips on the y axis.
|
83
|
+
Contract None => Path
|
84
|
+
def flip_y
|
85
|
+
flip(:y)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Flips the path on the specified axis.
|
89
|
+
#
|
90
|
+
# Examples:
|
91
|
+
# flip(:x)
|
92
|
+
# flip(:y)
|
93
|
+
Contract Or[:x, :y] => Path
|
94
|
+
def flip(axis)
|
95
|
+
x = axis == :y ? -1 : 1
|
96
|
+
y = axis == :x ? -1 : 1
|
97
|
+
|
98
|
+
Path.new(
|
99
|
+
*to_path.nodes.map { |n| N.new(n.x * x, n.y * y, n.type) },
|
100
|
+
closed: @closed, **@options
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Rotates the Path by the specified angle about the origin.
|
105
|
+
# Examples:
|
106
|
+
# rotate(90)
|
107
|
+
# rotate(-45)
|
108
|
+
Contract Num => Path
|
109
|
+
def rotate(angle)
|
110
|
+
theta = angle / 180.0 * Math::PI
|
111
|
+
|
112
|
+
# http://stackoverflow.com/a/786508
|
113
|
+
# p'x = cos(theta) * (px-ox) - sin(theta) * (py-oy) + ox
|
114
|
+
# p'y = sin(theta) * (px-ox) + cos(theta) * (py-oy) + oy
|
115
|
+
Path.new(
|
116
|
+
*to_path.nodes.map do |n|
|
117
|
+
N.new(
|
118
|
+
Math.cos(theta) * n.x - Math.sin(theta) * n.y,
|
119
|
+
Math.sin(theta) * n.x + Math.cos(theta) * n.y,
|
120
|
+
n.type
|
121
|
+
)
|
122
|
+
end, closed: @closed, **@options
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Scale a Path by multiplier about the origin.
|
127
|
+
# Supply just 1 multiplier to scale evenly, or x and y multipliers
|
128
|
+
# to stretch or squash the axies.
|
129
|
+
# @param x_multiplier 1 is no change, 2 is double size, 0.5 is half, etc.
|
130
|
+
Contract Num, Maybe[Num] => Path
|
131
|
+
def scale(x_multiplier, y_multiplier = x_multiplier)
|
132
|
+
Path.new(
|
133
|
+
*to_path.nodes.map do |n|
|
134
|
+
N.new(
|
135
|
+
n.x * x_multiplier,
|
136
|
+
n.y * y_multiplier,
|
137
|
+
n.type
|
138
|
+
)
|
139
|
+
end, closed: @closed, **@options
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Jitter the position of nodes in a Path randomly.
|
144
|
+
# @param max The maximum offset
|
145
|
+
# @param min The minimum offset (default 0)
|
146
|
+
# @param fn The quantization number of sides
|
147
|
+
Contract Num, { min: Maybe[Num], fn: Maybe[Fixnum] } => Path
|
148
|
+
def jitter(max, min: 0, fn: nil)
|
149
|
+
Path.new(
|
150
|
+
*to_simple_path(fn).nodes.map do |n|
|
151
|
+
r = Random.rand(min..max)
|
152
|
+
a = Random.rand(0..Math::PI * 2)
|
153
|
+
x = r * Math.cos(a)
|
154
|
+
y = r * Math.sin(a)
|
155
|
+
n.move(x, y)
|
156
|
+
end, closed: @closed, **@options
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_path
|
161
|
+
self
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_bezier_path
|
165
|
+
path = to_path
|
166
|
+
spiro = false
|
167
|
+
flat_path = path.nodes.map do |n|
|
168
|
+
spiro = true if spiro || [:g2, :g4, :left, :right].include?(n.type)
|
169
|
+
[n.x, n.y, n.type]
|
170
|
+
end
|
171
|
+
if spiro
|
172
|
+
flat_spline_path = Spiro.spiros_to_splines(flat_path, @closed)
|
173
|
+
if flat_spline_path.nil?
|
174
|
+
fail 'Spiro failed, try different coordinates or using G2 nodes.'
|
175
|
+
else
|
176
|
+
path = Path.new(*flat_spline_path.map do |n|
|
177
|
+
N.new(n[0], n[1], n[2])
|
178
|
+
end, closed: @closed, **@options)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
path
|
182
|
+
end
|
183
|
+
|
184
|
+
def to_cubic_path
|
185
|
+
path = to_bezier_path.nodes
|
186
|
+
cubic_path = []
|
187
|
+
quadratic_last = false
|
188
|
+
path.each_index do |i|
|
189
|
+
n = path[i]
|
190
|
+
if quadratic_last
|
191
|
+
n0 = path[i - 2]
|
192
|
+
q = path[i - 1]
|
193
|
+
|
194
|
+
# CP1 = QP0 + 2/3 * (QP1-QP0)
|
195
|
+
# CP2 = QP2 + 2/3 * (QP1-QP2)
|
196
|
+
third = 2 / 3.0
|
197
|
+
cubic_path << N.c(
|
198
|
+
n0.x + third * (q.x - n0.x),
|
199
|
+
n0.y + third * (q.y - n0.y)
|
200
|
+
)
|
201
|
+
cubic_path << N.c(
|
202
|
+
n.x + third * (q.x - n.x),
|
203
|
+
n.y + third * (q.y - n.y)
|
204
|
+
)
|
205
|
+
cubic_path << n
|
206
|
+
|
207
|
+
quadratic_last = false
|
208
|
+
elsif n.type == :quadratic
|
209
|
+
quadratic_last = true
|
210
|
+
else
|
211
|
+
cubic_path << n
|
212
|
+
end
|
213
|
+
end
|
214
|
+
Path.new(*cubic_path, closed: @closed, **@options)
|
215
|
+
end
|
216
|
+
|
217
|
+
def to_simple_path(*_)
|
218
|
+
# convert bezier curves and spiro splines
|
219
|
+
path = to_cubic_path.nodes
|
220
|
+
|
221
|
+
nodes = []
|
222
|
+
path.each_index do |i|
|
223
|
+
case path[i].type
|
224
|
+
when :node
|
225
|
+
if path[i - 1].type == :cubic
|
226
|
+
curve = path[i - 3..i].map(&:at)
|
227
|
+
nodes += VectorSalad::Interpolate.new.casteljau(curve)
|
228
|
+
else
|
229
|
+
nodes << path[i]
|
230
|
+
end
|
231
|
+
when :cubic
|
232
|
+
else
|
233
|
+
fail "Only :node and :cubic nodes in a path can be converted
|
234
|
+
to a simple path, was #{path[i].type}."
|
235
|
+
end
|
236
|
+
end
|
237
|
+
Path.new(*nodes, closed: @closed, **@options)
|
238
|
+
end
|
239
|
+
|
240
|
+
def to_multi_path
|
241
|
+
MultiPath.new(self)
|
242
|
+
end
|
243
|
+
|
244
|
+
def to_a
|
245
|
+
nodes.map(&:at)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|