Make borders with lines and add circles to round it off:
width, color = 20, "green"
radius = width / 2
# triangle verticies
v1 = {x: x1, y: y1}
v2 = {x: x2, y: y2}
v3 = {x: x3, y: y3}
# triangle edges
e1 = {x1: x1, y1: y1, x2: x2, y2: y2}
e2 = {x1: x2, y1: y2, x2: x3, y2: y3}
e3 = {x1: x3, y1: y3, x2: x1, y2: y1}
# lines over the edges
Line.new(width:, color:, **e1),
Line.new(width:, color:, **e2),
Line.new(width:, color:, **e3),
# hide the shame with circles
Circle.new(radius:, color:, **v1),
Circle.new(radius:, color:, **v2),
Circle.new(radius:, color:, **v3)
This was fun for a little bit. But it is so low level, I mean, updates are so difficult.

# Game of triangle. You can rotate, move, and resize it,
# and it has borders.
require "ruby2d"
COLORS = %w[blue aqua teal olive green lime yellow orange red brown fuchsia purple maroon]
UPDATE_CALLBACKS = []
def window_center = [Window.get(:width) / 2, Window.get(:height) / 2]
def sqrt(...) = Math.sqrt(...)
def cos(...) = Math.cos(...)
def sin(...) = Math.sin(...)
def rads(degrees) = degrees * Math::PI / 180
class Collection
def initialize(**shapes_hash)
@shapes_hash = shapes_hash
@shapes = shapes_hash.values
end
def add = @shapes.each(&:add)
def remove = @shapes.each(&:remove)
def contains?(x, y) = @shapes.select { |shape| shape.contains?(x, y) }
def key(shape) = @shapes_hash.rassoc(shape).first
def update_all attached
@shapes_hash.each do |key, shape|
coordinates = attached.send(key)
coordinates.each do |coordinate, value|
shape.send("#{coordinate}=", value)
end
end
end
end
class Triangle < Ruby2D::Triangle
def self.generate(size: 500, rotation: 0, center: window_center, color: COLORS.sample)
radius = size * sqrt(3) / 6
rotation -= 90 # pointy end up
cx, cy = center
coordinates = {
x1: cx + radius * cos(rads(rotation + 0)),
y1: cy + radius * sin(rads(rotation + 0)),
x2: cx + radius * cos(rads(rotation + 120)),
y2: cy + radius * sin(rads(rotation + 120)),
x3: cx + radius * cos(rads(rotation + 240)),
y3: cy + radius * sin(rads(rotation + 240))
}
t = new(color:, **coordinates)
t.on_drag
t
end
def rotate degrees = 1
cx, cy = center.values
verticies.map do |x, y|
[x - cx, y - cy]
end.map do |x, y|
[x * cos(rads(degrees)) - y * sin(rads(degrees)), x * sin(rads(degrees)) + y * cos(rads(degrees))]
end.map do |x, y|
[x + cx, y + cy]
end.each.with_index(1) do |(x, y), index|
send("x#{index}=", x)
send("y#{index}=", y)
end
@borders&.update_all(self)
@points&.update_all(self)
end
def coordinates = {x1:, y1:, x2:, y2:, x3:, y3:}
def verticies = [[x1, y1], [x2, y2], [x3, y3]]
def v1 = {x: x1, y: y1}
def v2 = {x: x2, y: y2}
def v3 = {x: x3, y: y3}
def e1 = {x1: x1, y1: y1, x2: x2, y2: y2}
def e2 = {x1: x2, y1: y2, x2: x3, y2: y3}
def e3 = {x1: x3, y1: y3, x2: x1, y2: y1}
def center = {x: (x1 + x2 + x3) / 3, y: (y1 + y2 + y3) / 3}
def points radius: 15, color: "fuchsia"
@points ||= Collection.new(
v1: Circle.new(radius:, color:, sectors: 16, **v1),
v2: Circle.new(radius:, color:, sectors: 16, **v2),
v3: Circle.new(radius:, color:, sectors: 16, **v3),
center: Circle.new(radius:, color:, sectors: 16, **center)
)
end
def borders width: 10, color: "green"
@borders ||= begin
radius = width / 2.0
Collection.new(
e1: Line.new(width:, color:, **e1),
e2: Line.new(width:, color:, **e2),
e3: Line.new(width:, color:, **e3),
v1: Circle.new(radius:, color:, sectors: 16, **v1),
v2: Circle.new(radius:, color:, sectors: 16, **v2),
v3: Circle.new(radius:, color:, sectors: 16, **v3)
)
end
end
def on_drag
Window.on(:mouse_down) do |event|
@dragging = @points.contains?(event.x, event.y).first if @points
end
Window.on(:mouse_up) { @dragging = nil }
UPDATE_CALLBACKS << proc do
if @dragging
key = points.key(@dragging)
x, y = Window.mouse_x, Window.mouse_y
case key
when :v1
self.x1, self.y1 = x, y
when :v2
self.x2, self.y2 = x, y
when :v3
self.x3, self.y3 = x, y
when :center
cx, cy = center.values
self.x1, self.y1 = (x1 + x - cx), (y1 + y - cy)
self.x2, self.y2 = (x2 + x - cx), (y2 + y - cy)
self.x3, self.y3 = (x3 + x - cx), (y3 + y - cy)
end
@points&.update_all(self)
@borders&.update_all(self)
end
end
self
end
end
class Button
def initialize(name:, x: 0, y: 0)
@text = Text.new(name, x:, y:)
@w = @text.instance_variable_get(:@width)
@h = @text.instance_variable_get(:@height)
end
attr_reader :text, :w, :h
def y= num
text.y = num
end
def on_click
Window.on(:mouse_down) do |event|
yield if @text.contains? event.x, event.y
end
self
end
def on_mouse_down
Window.on(:mouse_down) do |event|
@down = true if @text.contains? event.x, event.y
end
Window.on(:mouse_up) { @down = nil }
UPDATE_CALLBACKS << proc do
yield if @down
end
self
end
end
class Menu
def initialize(*buttons)
@buttons = buttons
heights = @buttons.map(&:h)
widths = @buttons.map(&:w)
Rectangle.new(x: 0, y: 0, width: widths.max, height: heights.sum, color: "maroon", z: -1)
y = 0
@buttons.each do |btn|
btn.y = y
y += btn.h
end
end
end
triangle = Triangle.generate
Menu.new(
Button.new(name: "show points").on_click { triangle.points.add },
Button.new(name: "hide points").on_click { triangle.points.remove },
Button.new(name: "show borders").on_click { triangle.borders(width: 20).add },
Button.new(name: "hide borders").on_click { triangle.borders.remove },
Button.new(name: "rotate").on_mouse_down { triangle.rotate }
)
update do
UPDATE_CALLBACKS.each(&:call)
end
show
# ruby v3.2.2