Skip to content

Instantly share code, notes, and snippets.

@evilstreak
Last active December 21, 2015 12:29
Show Gist options
  • Save evilstreak/6306024 to your computer and use it in GitHub Desktop.
Save evilstreak/6306024 to your computer and use it in GitHub Desktop.
class Bezier
TOLERANCE = 0.5 ** 8
def initialize(control_points)
@control_points = control_points
end
def points
@points ||= build_points
end
def length
@length ||= points.each_cons(2).map { |p, q| p.distance(q) }.reduce(:+)
end
def point_at_length(length_along_curve)
return points.first if length_along_curve <= 0
return points.last if length_along_curve >= length
total_length, points = segments.find do |cumulative_length, _|
cumulative_length > length_along_curve
end
segment_length = points.first.distance(points.last)
ratio = (total_length - length_along_curve) / segment_length
x_offset = (points.first.x - points.last.x) * ratio
y_offset = (points.first.y - points.last.y) * ratio
points.last.offset(x_offset, y_offset)
end
private
def build_points
if flat_enough?
[first_point, last_point]
else
left, right = split_curve
left.points + right.points[1..-1]
end
end
def first_point
@control_points.first
end
def last_point
@control_points.last
end
def baseline_length
first_point.distance(last_point)
end
def max_arc_length
@control_points.each_cons(2).map { |p, q| p.distance(q) }.reduce(:+)
end
def flat_enough?
max_arc_length - baseline_length < TOLERANCE
end
def segments
@segments ||= points.each_cons(2).reduce({}) do |memo, (p, q)|
distance = memo.keys.last.to_f + p.distance(q)
memo[distance] = [p, q]
memo
end
end
def split_curve
points = reduce_points(@control_points).reduce([[], []]) do |memo, arc|
memo.first.push(arc.first)
memo.last.unshift(arc.last)
memo
end
points.map { |p| Bezier.new(p) }
end
def reduce_points(points)
if points.count > 1
midpoints = points.each_cons(2).map { |p, q| p.midpoint(q) }
[points] + reduce_points(midpoints)
else
[points]
end
end
end
Point = Struct.new(:x, :y) do
# @returns a new Point offset the given amount from this one
def offset(x_offset, y_offset)
Point.new(x + x_offset, y + y_offset)
end
# @returns the distance between this point and the given point
def distance(other)
Math.hypot(x - other.x, y - other.y)
end
# @returns a new Point point halfway between this point and the given point
def midpoint(other)
x_offset = (other.x - x) / 2.0
y_offset = (other.y - y) / 2.0
offset(x_offset, y_offset)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment