Refactor Bezier, small improvements

This commit is contained in:
Alex Clink 2022-02-20 00:37:08 -05:00
parent 2c22abf2ea
commit 94df341310
10 changed files with 157 additions and 132 deletions

View file

@ -3,7 +3,13 @@ require "../src/bezier"
module PF module PF
class CubicBezier < PF::Game class CubicBezier < PF::Game
@curve : BezierCubic(Float64) FONT_COLOR = Pixel.new(0xFFFFFFFF)
POINT_COLOR = Pixel.new(0xFF0000FF)
CTL_COLOR = Pixel.new(0x505050FF)
CURVE_COLOR = Pixel.new(0x0077FFFF)
SEL_COLOR = Pixel.new(0xFFFF00FF)
@curve : Bezier::Cubic(Float64)
@hover_point : Vector2(Float64)*? = nil @hover_point : Vector2(Float64)*? = nil
@selected_point : Vector2(Float64)*? = nil @selected_point : Vector2(Float64)*? = nil
@ -12,29 +18,15 @@ module PF
def initialize(*args, **kwargs) def initialize(*args, **kwargs)
super super
@curve = BezierCubic.new( @curve = Bezier::Cubic.new(
Vector[width * 0.25, height * 0.75], Vector[width * 0.25, height * 0.7],
Vector[width * 0.33, height * 0.5], Vector[width * 0.33, height * 0.3],
Vector[width * 0.66, height * 0.5], Vector[width * 0.66, height * 0.3],
Vector[width * 0.75, height * 0.75] Vector[width * 0.75, height * 0.7]
) )
@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 end
def update(dt, event) def update(dt, event)
@controller.map_event(event)
case event case event
when SDL::Event::MouseButton when SDL::Event::MouseButton
if event.pressed? && event.button == 1 if event.pressed? && event.button == 1
@ -59,29 +51,24 @@ module PF
def draw def draw
clear clear
draw_line(@curve.p0, @curve.p1, Pixel.new(100, 100, 100)) draw_line(@curve.p0, @curve.p1, CTL_COLOR)
draw_line(@curve.p3, @curve.p2, Pixel.new(100, 100, 100)) draw_line(@curve.p3, @curve.p2, CTL_COLOR)
point = @curve.p0 draw_string("Length: " + @curve.length.round(2).to_s, 5, 5, FONT_COLOR)
0.upto(100) do |x| draw_curve(@curve, CURVE_COLOR)
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.p0.to_i, 2, POINT_COLOR)
fill_circle(@curve.p1.to_i, 1, Pixel.blue) fill_circle(@curve.p1.to_i, 2, POINT_COLOR)
fill_circle(@curve.p2.to_i, 1, Pixel.blue) fill_circle(@curve.p2.to_i, 2, POINT_COLOR)
fill_circle(@curve.p3.to_i, 1, Pixel.blue) fill_circle(@curve.p3.to_i, 2, POINT_COLOR)
draw_string("P1", @curve.p0.to_i, Pixel.white) draw_string("P1", @curve.p0.to_i, color: FONT_COLOR)
draw_string("P2", @curve.p1.to_i, Pixel.white) draw_string("P2", @curve.p1.to_i, color: FONT_COLOR)
draw_string("P3", @curve.p2.to_i, Pixel.white) draw_string("P3", @curve.p2.to_i, color: FONT_COLOR)
draw_string("P4", @curve.p3.to_i, Pixel.white) draw_string("P4", @curve.p3.to_i, color: FONT_COLOR)
if point = @hover_point if point = @hover_point
draw_circle(point.value.to_i, 5, Pixel.yellow) draw_circle(point.value.to_i, 5, SEL_COLOR)
end end
end end
end end

View file

@ -15,7 +15,7 @@ class Wind
property position : PF::Vector2(Float64) property position : PF::Vector2(Float64)
property strength : PF::Vector2(Float64) property strength : PF::Vector2(Float64)
def initialize(@position, @strength) def initialize(@position, @strength = PF::Vector[rand(-5.0..5.0), rand(-5.0..5.0)])
end end
end end
@ -33,7 +33,7 @@ class Wind
while y < @height while y < @height
x = step / 2 x = step / 2
while x < @width while x < @width
@gusts << Gust.new(PF::Vector[x, y], PF::Vector[rand(-1.0..1.0), rand(-1.0..1.0)]) @gusts << Gust.new(PF::Vector[x, y])
x += step x += step
end end
y += step y += step
@ -48,11 +48,11 @@ class Flake
property velocity : PF::Vector2(Float64) property velocity : PF::Vector2(Float64)
def initialize(@position, @shape = rand(0_u8..2_u8), @z_pos = rand(0.0..1.0), velocity : PF::Vector2(Float64)? = nil) def initialize(@position, @shape = rand(0_u8..2_u8), @z_pos = rand(0.0..1.0), velocity : PF::Vector2(Float64)? = nil)
@velocity = velocity || PF::Vector[rand(-2.0..2.0), rand(0.0..20.0)] @velocity = velocity || PF::Vector[rand(-2.0..2.0), rand(10.0..20.0)]
end end
def update(dt) def update(dt)
@velocity.y = @velocity.y + 1.0 * dt @velocity.y = @velocity.y + 5.0 * dt
@position += @velocity * dt @position += @velocity * dt
end end
end end
@ -66,6 +66,9 @@ class Snow < PF::Game
super super
@wind = Wind.new(@width, @height) @wind = Wind.new(@width, @height)
500.times do
@flakes << Flake.new(position: PF::Vector[rand(0.0..@width.to_f64), rand(0.0..@height.to_f64)])
end
clear(0, 0, 15) clear(0, 0, 15)
end end

View file

@ -1 +1,24 @@
module PF
module Bezier
alias Curve = Quad(Float64) | Cubic(Float64)
module Aproximations
# Get the length of the curve by calculating the length of line segments
# Increase *steps* for accuracy
def length(steps : UInt32 = 10)
_length = 0.0
seg_p0 = Vector[@p0.x, @p0.y]
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
end
require "./bezier/*" require "./bezier/*"

View file

@ -1,48 +1,57 @@
require "../bezier" require "../bezier"
module PF module PF
struct BezierCubic(T) module Bezier
def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number) struct Cubic(T)
(1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t ** 2 * p2 + t ** 3 * p3 include Aproximations
end
property p0 : Vector2(T) def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number)
property p1 : Vector2(T) (1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t ** 2 * p2 + t ** 3 * p3
property p2 : Vector2(T) end
property p3 : Vector2(T)
def self.derivative(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number)
def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T), @p3 : Vector2(T)) 3 * (1 - t) ** 2 * (p1 - p0) + 6 * (1 - t) * t * (p2 - p1) + 3 * t ** 2 * (p3 - p2)
end end
def [](index : Int) def self.second_derivative(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number)
points[index] 6 * (1 - t) * (p2 - 2 * p1 + p0) + 6 * t * (p3 - 2 * p2 + p1)
end end
def points property p0 : Vector2(T)
{pointerof(@p0), pointerof(@p1), pointerof(@p2), pointerof(@p3)} property p1 : Vector2(T)
end property p2 : Vector2(T)
property p3 : Vector2(T)
# Get the point at percentage *t* of the curve
def at(t : Float64) def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T), @p3 : Vector2(T))
Vector[ end
BezierCubic.point(t, @p0.x, @p1.x, @p2.x, @p3.x),
BezierCubic.point(t, @p0.y, @p1.y, @p2.y, @p3.y), def points
] {pointerof(@p0), pointerof(@p1), pointerof(@p2), pointerof(@p3)}
end end
# Get the length of the curve by calculating the length of line segments # Get the point at percentage *t* < 0 < 1 of the curve
def length(steps : UInt32 = 10) def at(t : Float64)
_length = 0.0 Vector[
seg_p0 = Vector[@p0.x, @p0.y] T.new(self.class.point(t, @p0.x, @p1.x, @p2.x, @p3.x)),
seg_p1 = uninitialized Vector2(T) T.new(self.class.point(t, @p0.y, @p1.y, @p2.y, @p3.y)),
]
0.upto(steps) do |n| end
t = n / steps
seg_p1 = at(t) # Get the tangent to a point at *t* < 0 < 1 on the spline
_length += seg_p0.distance(seg_p1) def tangent(t : Float64)
seg_p0 = seg_p1 Vector[
T.new(self.class.derivative(t, @p0.x, @p1.x, @p2.x, @p3.x)),
T.new(self.class.derivative(t, @p0.y, @p1.y, @p2.y, @p3.y)),
].normalized
end
# Get the normal to a point at *t* < 0 < 1 on the spline
def normal(t : Float64)
Vector[
T.new(self.class.derivative(t, @p0.y, @p1.y, @p2.y, @p3.y)),
T.new(-self.class.derivative(t, @p0.x, @p1.x, @p2.x, @p3.x)),
].normalized
end end
_length
end end
end end
end end

View file

@ -1,45 +1,30 @@
module PF module PF
struct BezierQuad(T) module Bezier
def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number) struct Quad(T)
(1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t ** 2 * p2 include Aproximations
end
property p0 : Vector2(T) def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number)
property p1 : Vector2(T) (1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t ** 2 * p2
property p2 : Vector2(T) end
def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T)) property p0 : Vector2(T)
end property p1 : Vector2(T)
property p2 : Vector2(T)
def [](index : Int)
points[index] def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T))
end end
def points def points
{pointerof(@p0), pointerof(@p1), pointerof(@p2)} {pointerof(@p0), pointerof(@p1), pointerof(@p2)}
end end
# Get the point at percentage *t* of the curve # Get the point at percentage *t* of the curve
def at(t : Float64) def at(t : Float64)
Vector[ Vector[
BezierQuad.point(t, @p0.x, @p1.x, @p2.x), self.class.point(t, @p0.x, @p1.x, @p2.x),
BezierQuad.point(t, @p0.y, @p1.y, @p2.y), self.class.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 end
_length
end end
end end
end end

View file

@ -17,7 +17,7 @@ module PF
property running = true property running = true
property screen : Sprite property screen : Sprite
delegate :draw_point, :draw_line, :scan_line, :draw_circle, :draw_triangle, :draw_rect, :draw_shape, delegate :draw_point, :draw_line, :draw_curve, :scan_line, :draw_circle, :draw_triangle, :draw_rect, :draw_shape,
:fill_triangle, :fill_rect, :fill_circle, :fill_shape, :draw_string, to: @screen :fill_triangle, :fill_rect, :fill_circle, :fill_shape, :draw_string, to: @screen
@fps_lasttime : Float64 = Time.monotonic.total_milliseconds # the last recorded time. @fps_lasttime : Float64 = Time.monotonic.total_milliseconds # the last recorded time.

View file

@ -39,10 +39,10 @@ module PF
property r : UInt8, g : UInt8, b : UInt8, a : UInt8 property r : UInt8, g : UInt8, b : UInt8, a : UInt8
def initialize(rgba : UInt32) def initialize(rgba : UInt32)
@r = ((rgba >> 24) & 0xFF).to_u8 @r = (rgba >> 24).to_u8!
@g = ((rgba >> 16) & 0xFF).to_u8 @g = (rgba >> 16).to_u8!
@b = ((rgba >> 8) & 0xFF).to_u8 @b = (rgba >> 8).to_u8!
@a = (rgba & 0xFF).to_u8 @a = (rgba).to_u8!
end end
def initialize(@r : UInt8 = 255, @g : UInt8 = 255, @b : UInt8 = 255, @a : UInt8 = 255) def initialize(@r : UInt8 = 255, @g : UInt8 = 255, @b : UInt8 = 255, @a : UInt8 = 255)
@ -73,6 +73,7 @@ module PF
end end
def to_u32 def to_u32
value = uninitialized UInt32
value = @r.to_u32 << 24 value = @r.to_u32 << 24
value |= @g.to_u32 << 16 value |= @g.to_u32 << 16
value |= @b.to_u32 << 8 value |= @b.to_u32 << 8

View file

@ -117,7 +117,8 @@ module PF
end end
# Sample a color with alhpa # Sample a color with alhpa
def sample(x : Int, y : Int, alpha = true) def sample(x : Int, y : Int, alpha : Boolean)
return sample(x, y) unless alpha
raw_pixel = pixel_pointer(x, y).value raw_pixel = pixel_pointer(x, y).value
r = uninitialized UInt8 r = uninitialized UInt8
@ -130,13 +131,13 @@ module PF
end end
# ditto # ditto
def sample(point : Vector2(Int), alpha = true) def sample(point : Vector2(Int), alpha : Boolean)
sample(point.x, point.y, true) sample(point.x, point.y, alpha)
end end
# Get the pointer to a pixel # Get the pointer to a pixel
def pixel_pointer(x : Int32, y : Int32) def pixel_pointer(x : Int32, y : Int32)
target = @surface.pixels + (y * @surface.pitch) + (x * 4) target = @surface.pixels + (y * @surface.pitch) + (x * sizeof(UInt32))
target.as(Pointer(UInt32)) target.as(Pointer(UInt32))
end end
end end

17
src/sprite/draw_curve.cr Normal file
View file

@ -0,0 +1,17 @@
module PF
class Sprite
def draw_curve(curve : Bezier::Cubic, samples : Int = 100, pixel : Pixel = Pixel.new)
point = curve.p0
0.upto(samples) do |x|
t = x / samples
next_point = curve.at(t)
draw_line(point.to_i, next_point.to_i, pixel)
point = next_point
end
end
def draw_curve(curve : Bezier::Cubic, pixel : Pixel)
draw_curve(curve, pixel: pixel)
end
end
end

View file

@ -94,10 +94,9 @@ module PF
CHAR_WIDTH = 7 CHAR_WIDTH = 7
CHAR_HEIGHT = 8 CHAR_HEIGHT = 8
def draw_string(msg : String, x : Int, y : Int, color : Pixel = Pixel.black, bg : Pixel? = nil) def draw_string(msg : String, x : Int, y : Int, color : Pixel = Pixel.new, bg : Pixel? = nil, leading : Int = 2)
cur_y = 0 cur_y = 0
cur_x = 0 cur_x = 0
leading = 2
msg.chars.each do |c| msg.chars.each do |c|
if c == '\n' if c == '\n'