Last active
December 21, 2015 12:29
-
-
Save evilstreak/6306024 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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