From ab9176c00df8bd20233335e7ace4750c366f8c5b Mon Sep 17 00:00:00 2001 From: Alex Clink Date: Wed, 16 Feb 2022 00:43:43 -0500 Subject: [PATCH] Add bezier paths --- examples/cubic_bezier.cr | 90 ++++++++++++++++++++++++++++++++++++++++ src/bezier.cr | 1 + src/bezier/cubic.cr | 48 +++++++++++++++++++++ src/bezier/quad.cr | 45 ++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 examples/cubic_bezier.cr create mode 100644 src/bezier.cr create mode 100644 src/bezier/cubic.cr create mode 100644 src/bezier/quad.cr diff --git a/examples/cubic_bezier.cr b/examples/cubic_bezier.cr new file mode 100644 index 0000000..1ecbaa6 --- /dev/null +++ b/examples/cubic_bezier.cr @@ -0,0 +1,90 @@ +require "../src/game" +require "../src/bezier" + +module PF + class CubicBezier < PF::Game + @curve : BezierCubic(Float64) + + @hover_point : Vector2(Float64)*? = nil + @selected_point : Vector2(Float64)*? = nil + + @mouse_pos : Vector2(Int32) = Vector[0, 0] + + def initialize(*args, **kwargs) + super + @curve = BezierCubic.new( + Vector[width * 0.25, height * 0.75], + Vector[width * 0.33, height * 0.5], + Vector[width * 0.66, height * 0.5], + Vector[width * 0.75, height * 0.75] + ) + + @controller = PF::Controller(Keys).new({ + Keys::KEY_1 => "p1", + Keys::KEY_2 => "p2", + Keys::KEY_3 => "p3", + Keys::KEY_4 => "p4", + + Keys::UP => "up", + Keys::LEFT => "left", + Keys::DOWN => "down", + Keys::RIGHT => "right", + }) + end + + def update(dt, event) + @controller.map_event(event) + + case event + when SDL::Event::MouseButton + if event.pressed? && event.button == 1 + @selected_point = @hover_point + end + + if event.released? && event.button == 1 + @selected_point = nil + end + when SDL::Event::MouseMotion + @mouse_pos = Vector[event.x, event.y] // scale + + unless point = @selected_point + @hover_point = @curve.points.find { |p| @mouse_pos.distance(p.value) < 4 } + else + point.value.x = @mouse_pos.x.to_f + point.value.y = @mouse_pos.y.to_f + end + end + end + + def draw + clear + + draw_line(@curve.p0, @curve.p1, Pixel.new(100, 100, 100)) + draw_line(@curve.p3, @curve.p2, Pixel.new(100, 100, 100)) + + point = @curve.p0 + 0.upto(100) do |x| + t = x / 100 + next_point = @curve.at(t) + draw_line(point.to_i, next_point.to_i, Pixel.white) + point = next_point + end + + fill_circle(@curve.p0.to_i, 1, Pixel.blue) + fill_circle(@curve.p1.to_i, 1, Pixel.blue) + fill_circle(@curve.p2.to_i, 1, Pixel.blue) + fill_circle(@curve.p3.to_i, 1, Pixel.blue) + + draw_string("P1", @curve.p0.to_i, Pixel.white) + draw_string("P2", @curve.p1.to_i, Pixel.white) + draw_string("P3", @curve.p2.to_i, Pixel.white) + draw_string("P4", @curve.p3.to_i, Pixel.white) + + if point = @hover_point + draw_circle(point.value.to_i, 5, Pixel.yellow) + end + end + end +end + +engine = PF::CubicBezier.new(500, 500, 2).run! diff --git a/src/bezier.cr b/src/bezier.cr new file mode 100644 index 0000000..2fdf724 --- /dev/null +++ b/src/bezier.cr @@ -0,0 +1 @@ +require "./bezier/*" diff --git a/src/bezier/cubic.cr b/src/bezier/cubic.cr new file mode 100644 index 0000000..1ea98b0 --- /dev/null +++ b/src/bezier/cubic.cr @@ -0,0 +1,48 @@ +require "../bezier" + +module PF + struct BezierCubic(T) + def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number) + (1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t ** 2 * p2 + t ** 3 * p3 + end + + property p0 : Vector2(T) + property p1 : Vector2(T) + property p2 : Vector2(T) + property p3 : Vector2(T) + + def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T), @p3 : Vector2(T)) + end + + def [](index : Int) + points[index] + end + + def points + {pointerof(@p0), pointerof(@p1), pointerof(@p2), pointerof(@p3)} + end + + # Get the point at percentage *t* of the curve + def at(t : Float64) + Vector[ + BezierCubic.point(t, @p0.x, @p1.x, @p2.x, @p3.x), + BezierCubic.point(t, @p0.y, @p1.y, @p2.y, @p3.y), + ] + end + + # Get the length of the curve by calculating the length of line segments + def length(steps : UInt32 = 10) + _length = 0.0 + seg_p0 = Vector[@p0.x, @p0.y] + seg_p1 = uninitialized Vector2(T) + + 0.upto(steps) do |n| + t = n / steps + seg_p1 = at(t) + _length += seg_p0.distance(seg_p1) + seg_p0 = seg_p1 + end + _length + end + end +end diff --git a/src/bezier/quad.cr b/src/bezier/quad.cr new file mode 100644 index 0000000..a16773f --- /dev/null +++ b/src/bezier/quad.cr @@ -0,0 +1,45 @@ +module PF + struct BezierQuad(T) + def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number) + (1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t ** 2 * p2 + end + + property p0 : Vector2(T) + property p1 : Vector2(T) + property p2 : Vector2(T) + + def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T)) + end + + def [](index : Int) + points[index] + end + + def points + {pointerof(@p0), pointerof(@p1), pointerof(@p2)} + end + + # Get the point at percentage *t* of the curve + def at(t : Float64) + Vector[ + BezierQuad.point(t, @p0.x, @p1.x, @p2.x), + BezierQuad.point(t, @p0.y, @p1.y, @p2.y), + ] + end + + # Get the length of the curve by calculating the length of line segments + def length(steps : UInt32 = 10) + _length = 0.0 + seg_p0 = Vector[@p0.x, @p0.y] + seg_p1 = uninitialized Vector2(T) + + 0.upto(steps) do |n| + t = n / steps + seg_p1 = at(t) + _length += seg_p0.distance(seg_p1) + seg_p0 = seg_p1 + end + _length + end + end +end