Skip to content

Commit 37a28e9

Browse files
committed
circumcircle sketch
1 parent 9f1dd8b commit 37a28e9

File tree

5 files changed

+226
-0
lines changed

5 files changed

+226
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env jruby
2+
require 'propane'
3+
# Loosely based on a sketch by Bárbara Almeida
4+
# https://www.openprocessing.org/sketch/179567
5+
# Here is a sketch that clearly demonstrates some of the most egregious parts of
6+
# vanilla processing:-
7+
# PVector is over complicated and overloads 2D and 3D functionality, cf Vec2D
8+
# JRubyArt and propane which behaves as a 2D vector (cross product is a float)
9+
# and can easily be used in chain calculations.
10+
# The inverted Y axis in processing requires remapping to use in graphs etc.
11+
# The map method of processing can be used to do this remapping, however in many
12+
# languages (python, ruby etc) map is a keyword with alternative usage.
13+
# For this reason in JRubyArt and propane we have replaced vanilla processing
14+
# map with map1d.
15+
16+
##################################################
17+
# Usage click on screen with mouse to generate a point, repeat as required
18+
# to create a triangle, and the circumcircle will be drawn unless the points are
19+
# collinear. Additional clicks erase the first point and add a new point. To
20+
# demonstrate collinear test press 'c' or 'C', press space-bar to clear
21+
##################################################
22+
class CircumcircleSketch < Propane::App
23+
load_library :circle
24+
25+
attr_reader :point, :font, :points, :circle, :center
26+
27+
def settings
28+
size 800, 800
29+
# pixel_density(2) # for HiDpi screens
30+
# smooth see https://processing.org/reference/smooth_.html
31+
end
32+
33+
def setup
34+
sketch_title 'Circumcircle Through Three Points'
35+
@point = MathPoint.new
36+
@font = create_font('Arial', 16, true)
37+
@points = TPoints.new
38+
ellipse_mode RADIUS
39+
@renderer = AppRender.new(self)
40+
end
41+
42+
def draw
43+
graph_paper
44+
point.display
45+
points.map(&:display)
46+
return unless circle
47+
center.display
48+
no_fill
49+
stroke 200
50+
render_triangle(points)
51+
ellipse(center.screen_x, center.screen_y, circle.radius, circle.radius)
52+
end
53+
54+
def mouse_pressed
55+
points << MathPoint.new.from_sketch(mouse_x, mouse_y)
56+
return unless points.full?
57+
@circle = Circumcircle.new(points.array)
58+
circle.calculate
59+
@center = MathPoint.new(circle.center)
60+
end
61+
62+
def render_triangle(points)
63+
begin_shape
64+
points.each do |point|
65+
vertex(point.screen_x, point.screen_y)
66+
end
67+
end_shape(CLOSE)
68+
end
69+
70+
def key_pressed
71+
case key
72+
when ' '
73+
points.clear
74+
when 'c', 'C'
75+
(0..400).step(200) do |i|
76+
points << MathPoint.new(Vec2D.new(i, i))
77+
end
78+
puts 'collinear points' if points.collinear?
79+
end
80+
end
81+
82+
def graph_paper
83+
background(0)
84+
cell_size = 20 # size of the grid
85+
dim = cell_size * 2
86+
(dim..width - dim).step(cell_size) do |i|
87+
stroke(75, 135, 155)
88+
line(i, dim, i, height - dim)
89+
end
90+
(dim..height - dim).step(cell_size) do |i|
91+
line(dim, i, width - dim, i)
92+
end
93+
end
94+
end
95+
96+
CircumcircleSketch.new
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
require_relative 'lib/circumcircle'
2+
require_relative 'lib/math_point'
3+
require_relative 'lib/points'
4+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
require 'matrix'
3+
4+
# Circumcircle from 3 points
5+
class Circumcircle
6+
attr_reader :center, :radius, :points
7+
def initialize(points)
8+
@points = points
9+
end
10+
11+
def calculate
12+
@center = Vec2D.new(-(bx / am), -(by / am))
13+
@radius = center.dist(points[2]) # points[2] = c
14+
end
15+
16+
private
17+
18+
# Matrix math see matrix_math.md and in detail
19+
# http://mathworld.wolfram.com/Circumcircle.html
20+
21+
def am
22+
2 * Matrix[
23+
*points.map { |pt| [pt.x, pt.y, 1] }
24+
].determinant
25+
end
26+
27+
def bx
28+
-Matrix[
29+
*points.map { |pt| [pt.x * pt.x + pt.y * pt.y, pt.y, 1] }
30+
].determinant
31+
end
32+
33+
def by
34+
Matrix[
35+
*points.map { |pt| [pt.x * pt.x + pt.y * pt.y, pt.x, 1] }
36+
].determinant
37+
end
38+
end
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
# In processing the Y-axis is inverted, plus we have a fake origin for display
3+
# pos is the position in the Math World, which we need to translate to sketch
4+
class MathPoint
5+
include Propane::Proxy # to access sketch methods, including height
6+
OR = 40.0
7+
FORM = '(%d, %d)'.freeze
8+
attr_reader :pos, :from_mouse
9+
def initialize(pos = Vec2D.new)
10+
@pos = pos
11+
@from_mouse = false
12+
end
13+
14+
def from_sketch(x, y)
15+
@from_mouse = true
16+
@pos = Vec2D.new(x - OR, map1d(y, height - OR..-OR, 0..height))
17+
self
18+
end
19+
20+
def xpos
21+
pos.x
22+
end
23+
24+
def ypos
25+
pos.y
26+
end
27+
28+
def screen_x
29+
pos.x + OR
30+
end
31+
32+
def screen_y
33+
map1d(pos.y, 0..height, height - OR..-OR)
34+
end
35+
36+
def display
37+
fill 200
38+
no_stroke
39+
ellipse(screen_x, screen_y, 2.5, 2.5)
40+
display_text
41+
return unless from_mouse
42+
no_fill
43+
stroke(200, 0, 0)
44+
ellipse(screen_x, screen_y, 5, 5)
45+
end
46+
47+
private
48+
49+
def display_text
50+
from_mouse ? fill(255, 255, 0) : fill(0, 255, 0)
51+
text_font(font, 12)
52+
text(format(FORM, xpos.round, ypos.round), screen_x + 5, screen_y - 5)
53+
end
54+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
require 'forwardable'
3+
MAX_POINT = 3
4+
# A collection of a maximum of 3 points in a Math World
5+
# includes a collinearity test, and a map to simple Vec2D array
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+
return false unless full?
24+
(array[0] - array[1]).cross(array[1] - array[2]).zero?
25+
end
26+
27+
def array
28+
points.map(&:pos)
29+
end
30+
31+
def full?
32+
points.length == MAX_POINT
33+
end
34+
end

0 commit comments

Comments
 (0)