Skip to content

Commit 9c6b3f5

Browse files
committed
matrix math circumcircle
1 parent 947b584 commit 9c6b3f5

File tree

6 files changed

+232
-0
lines changed

6 files changed

+232
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env jruby
2+
require 'propane'
3+
4+
class Circles < Propane::App
5+
load_library :circles
6+
7+
def settings
8+
size(800, 600, P2D)
9+
end
10+
11+
## To be overriden by the Presentation Code.
12+
def setup
13+
sketch_title 'Circles'
14+
color_mode(HSB, 360, 100, 100, 100)
15+
@c = rand(360)
16+
@points = TPoints.new
17+
3.times { @points << TPoint.new(Vec2D.new(rand(5..width - 5), rand(5..height - 5))) }
18+
background 0
19+
ellipse_mode(RADIUS)
20+
end
21+
22+
def draw
23+
fill(0, 0, 0)
24+
no_stroke
25+
rect(0, 0, width, height) if (frame_count % 8_000).zero?
26+
@points.each do |point|
27+
# change direction sometimes
28+
point.direction Vec2D.random if rand > 0.96
29+
point.update
30+
end
31+
32+
# set the style of the circle
33+
@dc = map1d(millis, 0..150_000, 0..360) # slowly changes hue
34+
stroke((@c + @dc) % 360, 50, 100, 5)
35+
no_fill
36+
37+
## verifies if there is a circle and draw it
38+
draw_circle @points unless @points.collinear?
39+
end
40+
41+
def draw_circle(pts)
42+
circumcircle = Circumcircle.new(@points.vec)
43+
circumcircle.calculate
44+
center_point = circumcircle.center
45+
radius = circumcircle.radius
46+
ellipse(center_point.x, center_point.y, radius, radius)
47+
end
48+
end
49+
50+
Circles.new
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
### Dealing with processing coordinate system ###
2+
3+
For library see `library/circles/circles.rb`
4+
5+
If you want to create Math Sketches in processing, you need to deal with peculiar coordinate systems, where the Y-axis is inverted in theory you should be able to do.
6+
7+
```ruby
8+
scale(1, -1)
9+
translate(0, -height)
10+
```
11+
But that will mess up any text (plus you probably need to `push_matrix` and `pop_matrix`) so it is probably simpler to create a parallel coordinate system for the math, and translate that back to the screen (using the processing `map` function or in `propane` and `JRubyArt` use `map1d`).
12+
13+
We have done this in `circumcircle_sketch.rb` or just accept the processing coordinate system as we have with `basic_cirmcumcircle_sketch.rb` (it is much simpler).
14+
15+
### PVector limitations ###
16+
17+
PVector is a 3D vector, that is often used as a 2D vector, to my mind that is just plain wrong. If you evaluate the cross product of a 2D vector you get a float (you may see somewhere that a cross product does not exist for 2D vectors, but it can be useful). The cross product of PVector yields another PVector, so cannot be used in the calculation of the area of the triangle as defined by two vectors _cf_ Vec2D:-
18+
19+
```ruby
20+
a = Vec2D.new(100, 0)
21+
b = Vec2D.new(0, 100)
22+
23+
a.cross(b).abs == 10_000 # or twice the area of the triangle enclosed by a, b
24+
25+
```
26+
Further we can use the cross product in a test for collinearity
27+
28+
```ruby
29+
30+
# given 3 points in 2D space
31+
a = Vec2D.new(0, 0)
32+
b = Vec2D.new(100, 100)
33+
c = Vec2D.new(200, 200)
34+
35+
(a - b).cross(b - c) == 0 # the area of the triangle is zero, so a, b, c are collinear
36+
37+
```
38+
39+
Also because we were able to separate the logic, we were able to confidently re-factor the Barbara Almeida sketch to use Matrix math to determine the circumcenter
40+
41+
### Matrix Math ###
42+
43+
For detailed workings see [Circumcircle at Mathworld Wolfram.com][circumcircle]
44+
45+
46+
a = {{x<sub>1</sub> y<sub>1</sub> 1}, {x<sub>2</sub> y<sub>2</sub> 1}, {x<sub>3</sub> y<sub>3</sub> 1}}
47+
48+
bx = -{{x<sub>1</sub><sup>2</sup> + y<sub>1</sub><sup>2</sup> y<sub>1</sub> 1}, {x<sub>2</sub><sup>2</sup> + y<sub>2</sub><sup>2</sup> y<sub>2</sub> 1}, {x<sub>3</sub><sup>2</sup> + y<sub>3</sub><sup>2</sup> y<sub>3</sub> 1}}
49+
50+
by = {{x<sub>1</sub><sup>2</sup> + y<sub>1</sub><sup>2</sup> x<sub>1</sub> 1}, {x<sub>2</sub><sup>2</sup> + y<sub>2</sub><sup>2</sup> x<sub>2</sub> 1}, {x<sub>3</sub><sup>2</sup> + y<sub>3</sub><sup>2</sup> x<sub>3</sub> 1}}
51+
52+
xo = -bx / 2 * a
53+
54+
yo = -by / 2 * a
55+
56+
57+
[circumcircle]:http://mathworld.wolfram.com/Circumcircle.html
58+
59+
60+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
require_relative 'lib/circumcircle'
2+
require_relative 'lib/t_points'
3+
require_relative 'lib/triangle_point'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Circumcircle from 3 points
2+
require 'matrix'
3+
4+
class Circumcircle
5+
attr_reader :center, :radius, :points
6+
def initialize(points)
7+
@points = points
8+
end
9+
10+
def calculate
11+
@center = Vec2D.new(-(bx / am), -(by / am))
12+
@radius = center.dist(points[2]) # any point would do
13+
end
14+
15+
private
16+
17+
# Matrix math see matrix_math.md and in detail
18+
# http://mathworld.wolfram.com/Circumcircle.html
19+
20+
def am
21+
2 * Matrix[
22+
[points[0].x, points[0].y, 1],
23+
[points[1].x, points[1].y, 1],
24+
[points[2].x, points[2].y, 1]
25+
].determinant
26+
end
27+
28+
def bx
29+
-Matrix[
30+
[points[0].x * points[0].x + points[0].y * points[0].y, points[0].y, 1],
31+
[points[1].x * points[1].x + points[1].y * points[1].y, points[1].y, 1],
32+
[points[2].x * points[2].x + points[2].y * points[2].y, points[2].y, 1]
33+
].determinant
34+
end
35+
36+
def by
37+
Matrix[
38+
[points[0].x * points[0].x + points[0].y * points[0].y, points[0].x, 1],
39+
[points[1].x * points[1].x + points[1].y * points[1].y, points[1].x, 1],
40+
[points[2].x * points[2].x + points[2].y * points[2].y, points[2].x, 1]
41+
].determinant
42+
end
43+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
require 'forwardable'
3+
MAX_POINT = 3
4+
# A collection of a maximum of 3 points in the processing world
5+
# includes a collinearity test using Vec2D
6+
class TPoints
7+
extend Forwardable
8+
def_delegators(:@points, :each, :map, :size, :shift, :clear, :[])
9+
include Enumerable
10+
11+
attr_reader :points
12+
13+
def initialize
14+
@points = []
15+
end
16+
17+
def <<(pt)
18+
points << pt
19+
shift if size > MAX_POINT
20+
end
21+
22+
def collinear?
23+
full? ? (vec[0] - vec[1]).cross(vec[1] - vec[2]).zero? : false
24+
end
25+
26+
def vec
27+
points.map { |point| point.pos }
28+
end
29+
30+
def full?
31+
points.length == MAX_POINT
32+
end
33+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
# particle and triangle point
3+
class TPoint
4+
include Propane::Proxy
5+
attr_reader :pos, :vel, :accel, :xbound, :ybound
6+
7+
def initialize(position)
8+
@pos = position
9+
@vel = Vec2D.new
10+
@accel = Vec2D.random
11+
@xbound = Boundary.new(0, width)
12+
@ybound = Boundary.new(0, height)
13+
end
14+
15+
def direction(acc)
16+
# magnitude of the acceleration is proportional to the angle between
17+
# acceleration and velocity
18+
dif = accel.angle_between(vel)
19+
dif = map1d(dif, 0..PI, 0.1..0.001)
20+
@accel = acc * dif
21+
end
22+
23+
def update
24+
@vel += accel
25+
@vel.set_mag(1.5) { vel.mag > 1.5 }
26+
@pos += vel
27+
check_bounds
28+
end
29+
30+
private
31+
32+
def check_bounds
33+
@vel.x *= -1 if xbound.exclude? pos.x
34+
@vel.y *= -1 if ybound.exclude? pos.y
35+
end
36+
end
37+
38+
# we are looking for excluded values
39+
Boundary = Struct.new(:lower, :upper) do
40+
def exclude?(val)
41+
true unless (lower...upper).cover? val
42+
end
43+
end

0 commit comments

Comments
 (0)