Для кубической кривой Безье с обычными четырьмя точками a, b, c и d,
для данного значения т,
как наиболее элегантно найти касательную в этой точке?
Для кубической кривой Безье с обычными четырьмя точками a, b, c и d,
для данного значения т,
как наиболее элегантно найти касательную в этой точке?
Тангенс кривой является просто ее производной. Параметрическое уравнение, которое использует Михаль:
P(t) = (1 - t)^3 * P0 + 3t(1-t)^2 * P1 + 3t^2 (1-t) * P2 + t^3 * P3
должна иметь производную от
dP(t) / dt = -3(1-t)^2 * P0 + 3(1-t)^2 * P1 - 6t(1-t) * P1 - 3t^2 * P2 + 6t(1-t) * P2 + 3t^2 * P3
Что, кстати, кажется неправильным в вашем предыдущем вопросе. Я считаю, что вы используете наклон для квадратичной кривой Безье, а не кубический.
Оттуда должно быть тривиально реализовать функцию C, которая выполняет этот расчет, например, Михал уже предоставил для самой кривой.
Вот полностью протестированный код для копирования и вставки:
Он рисует приближенные точки вдоль кривой и рисует касательные.
bezierInterpolation
находит точки
bezierTangent
находит касательные
Есть две версии bezierInterpolation
поставляемые ниже:
bezierInterpolation
работает отлично.
altBezierInterpolation
точно такой же, НО это написано в расширенной, ясной, объяснительной манере. Это делает арифметику намного проще для понимания.
Используйте любую из этих двух процедур: результаты идентичны.
В обоих случаях используйте bezierTangent
чтобы найти касательные. (Примечание: Michal сказочной кодовой базы здесь.)
Полный пример того, как использовать с drawRect:
также включен.
// MBBezierView.m original BY MICHAL stackoverflow #4058979
#import "MBBezierView.h"
CGFloat bezierInterpolation(
CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d) {
// see also below for another way to do this, that follows the 'coefficients'
// idea, and is a little clearer
CGFloat t2 = t * t;
CGFloat t3 = t2 * t;
return a + (-a * 3 + t * (3 * a - a * t)) * t
+ (3 * b + t * (-6 * b + b * 3 * t)) * t
+ (c * 3 - c * 3 * t) * t2
+ d * t3;
}
CGFloat altBezierInterpolation(
CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
// here an alternative to Michal bezierInterpolation above.
// the result is absolutely identical.
// of course, you could calculate the four 'coefficients' only once for
// both this and the slope calculation, if desired.
CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a );
// it now easy to calculate the point, using those coefficients:
return ( C1*t*t*t + C2*t*t + C3*t + C4 );
}
CGFloat bezierTangent(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
// note that abcd are aka x0 x1 x2 x3
/* the four coefficients ..
A = x3 - 3 * x2 + 3 * x1 - x0
B = 3 * x2 - 6 * x1 + 3 * x0
C = 3 * x1 - 3 * x0
D = x0
and then...
Vx = 3At2 + 2Bt + C */
// first calcuate what are usually know as the coeffients,
// they are trivial based on the four control points:
CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a ); // (not needed for this calculation)
// finally it is easy to calculate the slope element,
// using those coefficients:
return ( ( 3.0 * C1 * t* t ) + ( 2.0 * C2 * t ) + C3 );
// note that this routine works for both the x and y side;
// simply run this routine twice, once for x once for y
// note that there are sometimes said to be 8 (not 4) coefficients,
// these are simply the four for x and four for y,
// calculated as above in each case.
}
@implementation MBBezierView
- (void)drawRect:(CGRect)rect {
CGPoint p1, p2, p3, p4;
p1 = CGPointMake(30, rect.size.height * 0.33);
p2 = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
p3 = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
p4 = CGPointMake(-30 + CGRectGetMaxX(rect), rect.size.height * 0.66);
[[UIColor blackColor] set];
[[UIBezierPath bezierPathWithRect:rect] fill];
[[UIColor redColor] setStroke];
UIBezierPath *bezierPath = [[[UIBezierPath alloc] init] autorelease];
[bezierPath moveToPoint:p1];
[bezierPath addCurveToPoint:p4 controlPoint1:p2 controlPoint2:p3];
[bezierPath stroke];
[[UIColor brownColor] setStroke];
// now mark in points along the bezier!
for (CGFloat t = 0.0; t <= 1.00001; t += 0.05) {
[[UIColor brownColor] setStroke];
CGPoint point = CGPointMake(
bezierInterpolation(t, p1.x, p2.x, p3.x, p4.x),
bezierInterpolation(t, p1.y, p2.y, p3.y, p4.y));
// there, use either bezierInterpolation or altBezierInterpolation,
// identical results for the position
// just draw that point to indicate it...
UIBezierPath *pointPath =
[UIBezierPath bezierPathWithArcCenter:point
radius:5 startAngle:0 endAngle:2*M_PI clockwise:YES];
[pointPath stroke];
// now find the tangent if someone on stackoverflow knows how
CGPoint vel = CGPointMake(
bezierTangent(t, p1.x, p2.x, p3.x, p4.x),
bezierTangent(t, p1.y, p2.y, p3.y, p4.y));
// the following code simply draws an indication of the tangent
CGPoint demo = CGPointMake( point.x + (vel.x*0.3),
point.y + (vel.y*0.33) );
// (the only reason for the .3 is to make the pointers shorter)
[[UIColor whiteColor] setStroke];
UIBezierPath *vp = [UIBezierPath bezierPath];
[vp moveToPoint:point];
[vp addLineToPoint:demo];
[vp stroke];
}
}
@end
to draw that class...
MBBezierView *mm = [[MBBezierView alloc]
initWithFrame:CGRectMake(400,20, 600,700)];
[mm setNeedsDisplay];
[self addSubview:mm];
Вот две процедуры для вычисления приблизительно равноотстоящих точек и их касательных вдоль кубики Безье.
Для ясности и надежности эти процедуры написаны в простейшем, наиболее объяснительном виде.
CGFloat bezierPoint(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a );
return ( C1*t*t*t + C2*t*t + C3*t + C4 );
}
CGFloat bezierTangent(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a );
return ( ( 3.0 * C1 * t* t ) + ( 2.0 * C2 * t ) + C3 );
}
Четыре предварительно рассчитанных значения, C1 C2 C3 C4, иногда называют коэффициентами Безье. (Напомним, что abcd обычно называют четырьмя контрольными точками.)
Конечно, t работает от 0 до 1, например, каждые 0,05.
Просто вызовите эти подпрограммы один раз для X, а затем один раз отдельно для Y.
Надеюсь, это поможет кому-то!
Важные факты:
(1) Это абсолютный факт, что: к сожалению, Apple определенно не имеет способа извлекать точки из UIBezierPath.
(2) Не забывайте, как легко анимировать что-то вдоль UIBezierPath. Гугл много примеров.
(3) Многие спрашивают: "Разве нельзя использовать CGPathApply для извлечения точек из UIBezierPath?" Нет, CGPathApply совершенно не связан: он просто дает вам список ваших инструкций по созданию любого пути (так, "начните здесь", "проведите прямую линию к этой точке" и т.д. И т.д.)
Для программистов игр - как указывает @Engineer, вы можете захотеть получить нормаль касательной, к счастью, в Apple есть встроенная векторная математика:
https://developer.apple.com/documentation/accelerate/simd/working_with_vectors
https://developer.apple.com/documentation/simd/2896658-simd_normalize
Я нашел слишком склонным к ошибкам использовать предоставленные уравнения. Слишком легко пропустить тонкий t или неулокальный кронштейн.
В отличие от этого, Википедия предоставляет гораздо более четкую, чистую, производную ИМХО:
... который легко реализуется в коде как:
3f * oneMinusT * oneMinusT * (p1 - p0)
+ 6f * t * oneMinusT * (p2 - p1)
+ 3f * t * t * (p3 - p2)
(при условии, что у вас есть вектор-минус, настроенный на выбранном вами языке, вопрос не отмечен как ObjC, а iOS теперь имеет несколько доступных языков)
Здесь идет моя реализация Swift.
Который я старался изо всех сил оптимизировать для скорости, устраняя все лишние математические операции. т.е. сделать минимальное количество вызовов математических операций. И используйте наименьшее возможное количество умножений (которые намного дороже сумм).
Существует 0 умножений для создания Безье. Затем 3 умножения, чтобы получить точку Безье. И 2 умножения, чтобы получить касательную к Безье.
struct CubicBezier {
private typealias Me = CubicBezier
typealias Vector = CGVector
typealias Point = CGPoint
typealias Num = CGFloat
typealias Coeficients = (C: Num, S: Num, M: Num, L: Num)
let xCoeficients: Coeficients
let yCoeficients: Coeficients
static func coeficientsOfCurve(from c0: Num, through c1: Num, andThrough c2: Num, to c3: Num) -> Coeficients
{
let _3c0 = c0 + c0 + c0
let _3c1 = c1 + c1 + c1
let _3c2 = c2 + c2 + c2
let _6c1 = _3c1 + _3c1
let C = c3 - _3c2 + _3c1 - c0
let S = _3c2 - _6c1 + _3c0
let M = _3c1 - _3c0
let L = c0
return (C, S, M, L)
}
static func xOrYofCurveWith(coeficients coefs: Coeficients, at t: Num) -> Num
{
let (C, S, M, L) = coefs
return ((C * t + S) * t + M) * t + L
}
static func xOrYofTangentToCurveWith(coeficients coefs: Coeficients, at t: Num) -> Num
{
let (C, S, M, _) = coefs
return ((C + C + C) * t + S + S) * t + M
}
init(from start: Point, through c1: Point, andThrough c2: Point, to end: Point)
{
xCoeficients = Me.coeficientsOfCurve(from: start.x, through: c1.x, andThrough: c2.x, to: end.x)
yCoeficients = Me.coeficientsOfCurve(from: start.y, through: c1.y, andThrough: c2.y, to: end.y)
}
func x(at t: Num) -> Num {
return Me.xOrYofCurveWith(coeficients: xCoeficients, at: t)
}
func y(at t: Num) -> Num {
return Me.xOrYofCurveWith(coeficients: yCoeficients, at: t)
}
func dx(at t: Num) -> Num {
return Me.xOrYofTangentToCurveWith(coeficients: xCoeficients, at: t)
}
func dy(at t: Num) -> Num {
return Me.xOrYofTangentToCurveWith(coeficients: yCoeficients, at: t)
}
func point(at t: Num) -> Point {
return .init(x: x(at: t), y: y(at: t))
}
func tangent(at t: Num) -> Vector {
return .init(dx: dx(at: t), dy: dy(at: t))
}
}
Используйте как:
let bezier = CubicBezier.init(from: .zero, through: .zero, andThrough: .zero, to: .zero)
let point02 = bezier.point(at: 0.2)
let point07 = bezier.point(at: 0.7)
let tangent01 = bezier.tangent(at: 0.1)
let tangent05 = bezier.tangent(at: 0.5)
Я не мог заставить это работать, пока не понял, что для параметрических уравнений (dy/dt)/(dx/dt) = dy/dx