Доброго часу доби, харбачитник.
У цій статті мені хотілося б розповісти про один придуманий колись алгоритм (або швидше за все - переобретенный велосипед) побудови плавного графіка за заданими точками, використовуючи криві Безьє. Стаття була написана під впливом ось цієї статті і дуже корисного коментаря товариша lany, за що їм окреме спасибі.
Показ завдання
Є масив Y-ків точок, розташованих рівномірно по осі X. Потрібно отримати плавний графік, який проходить через всі задані точки. Приклад на малюнку нижче:
Всіх, кому цікаво, прошу під кат.
Існує ряд стандартних рішень для проведення плавної кривої через точки (з цього приводу багато цікавого написано в вже згаданій статті) таких, як наприклад, інтерполяції сплайнами. Коли на третьому курсі був придуманий цей алгоритм, слово «інтерполяція» вселяло в мене жах, а гуглення за запитом «згладжування графіків» не давало посильних розумінню результатів. Але якось я дійшов до кривих Безьє і вже дуже вони мені сподобалися. Малює швидко, алгоритм інтуїтивно зрозумілий... Що ще треба для щастя. Ну і якось понеслося.
Основна ідея
Розіб'ю ідею на три підпункти, щоб було зрозуміліше і читабельніше.
- Про кривих Безьє добре написано на віки і на javascript.ru. Якщо уважно читати, можна звернути увагу, що крива Безьє виходить з першої точки відносно прямої початкова _ точка-перша _ опорна _ точка. Аналогічно і в кінці - крива заходить відносно прямої остання _ опорна точка-кінцева _ точка. Таким чином виходить, що якщо у нас одна крива закінчується в точці А і зайшла відносно прямої, а інша крива виходить з цієї точки А щодо тієї-ж прямої а, то цей перехід між двома кривими Безьє у нас вийде плавним.
- Виходячи з першого пункту виходить, що у нас опорні точки ліворуч і праворуч відносно точки А повинні лежати на одній прямій. Поміркувавши трохи, було вирішено, що ця пряма повинна бути такою, щоб ∠BAB1=∠CAC1 (малюнок нижче), де точки B1 і C1 - опорні.
- Відстань від точки А до точок B1 і C1 було вирішено взяти рівною половині кроку по X між точками B і A, A і C, і т. д. Мені складно якось обґрунтувати такий вибір, але важливо, щоб ця відстань була менше, ніж крок по X між точками А і B, інакше може вийде щось таке, як на малюнку нижче. Важливо розуміти, що чим більше буде ця відстань, тим більш звивистою буде крива і навпаки. Відстань у половину кроку по X мені здається оптимальною, але тут вже можливі варіанти.
Таким чином виходить, що завдання зводиться до пошуку прямої (B1C1) і, власне, опорних точок B1 і C1, за якими ми потім будемо будувати криві Безьє.
Пошук прямої
Як відомо, пряма на площині виражається формулою y = kx + b, де k - тангенс кута нахилу прямої до осі Х, а b - «висота» перетину прямої і осі Y.
Пошук коефіцієнта k
Скажу наперед, що k = tg ( ) = tg ((в-у-у )/2) = (Sqrt (((YA-YB) 2 + В X2) * ((YA-YC) 2 + В X2) (YA-YB) * (YA-YC)) де (В X Точ( Y* (Y-yb розташовані, X) Y) Y) YA- Y- YC- Y- Y- YC- Y- Y- Y- Y- Y- Y- Y- Y- Нижче представлено математичний доказ правильності формули, але якщо ви не в настрої, то можете його просто пропустити.
Математичне виведення коефіцієнта k
- Нехай кут ∠O1BA=α, а кут ∠O2CA=β.
Тоді ∠BAO1=90o - ∠CAO2=90o, так як △ABO1 і △ACO2 - прямокутні.
- (1) ∠B1AС1=∠B1AB+∠BAO1+∠O1AС+∠CAС1=180o
(2) ∠B1AB=∠C1AC - за умовою
(3) ∠BAO1=90o-α
(4) ∠O1AС=∠CAO2=90o-β
З (1), (2), (3) і (4) виходить, що:
2*∠C1AС+(90o-α)+(90o-β)=180o
2*∠C1AС+180o-α-β=180o
(5) ∠C1AС=(α+β)/2
- (6) ∠C1AС=∠C1AD+∠DAC=φ+∠DAC
(7) ∠DAC=∠O2CA=β - як внутрішні різносторонні кути утворені двома паралельними прямими (AD) і (O2C) і секущею (AC)
З (5), (6) і (7) виходить, що:
∠C1AС=φ+∠DAC
(α+β)/2=φ+β
φ+β=(α+β)/2
(8) φ=(α-β)/2
- k=tg(φ)=tg((α-β)/2)
З △ABO1:
sin(α)=[AO1]/[AB]
cos(α)=[BO1]/[AB]
З △ACO2:
sin(β)=[AO2]/[AC]
cos(β)=[CO2]/[AC]
Під квадратними дужками мається на увазі довгий відрізок (не хотів використовувати вертикальні прямі - сподіваюся, що читач мене пробачить)
- З попереднього підпункту випливає:
- Знаючи, що:
[BO1]=[CO2]=ΔX
[AO1]=YA-YB
[AO2]=YA-YC
[AB]=Sqrt([AO1]2+[BO1]2)=Sqrt((YA-YB)2+ΔX2)
[AC]=Sqrt([AO2]2+[CO2]2)=Sqrt((YA-YC)2+ΔX2)
Отримуємо, що:
І так, k ми знайшли. Забігаючи наперед скажу, що b нам при розрахунках не стане в нагоді. Приступимо до пошуку опорних точок.
Шукаємо опорні точки
Відразу скажу, що: ^ X'= ^ X/2 * Sqrt (1/( 1 + k2)), координати опорної точки праворуч: XC1=XA+ΔX'; YC1 = YA + k * ^ X', і зліва:XB1=XA-ΔX'; YB1=YA-k*ΔX'.
Трохи математики, яка це доводить
З тригонометрії ми пам'ятаємо, що:
З △AC1O:
ΔX'=[AO]=[AC1]*cos(φ).
[C1O]=[AO]*tg(φ)=k*ΔX'
Якщо ми приймаємо [AC1] рівним половині кроку по X між основними точками графіка (точками B і A, A і C, т. д.), то:
І ось, власне:
XC1=XA+ΔX'
XB1=XA-ΔX'
YC1=YA+k*ΔX'
YB1=YA-k*ΔX'
До приємного!
- Від товариша lany (величезне йому за це спасибі і плюс йому до карми) я дізнався, що html5 за допомогою функцій quadraticCurveTo () і bezierCurveTo () вміє малювати по canvas-у криві Безьє сам. Відповідно, алгоритм можна застосувати з JavaScript-ом.
- Приємною особливістю алгоритму є те, що графік передбачувано «випирає» за межі простору точок, через які ми власне проводимо графік. Якщо брати відстань до опорних точок рівними половині кроку по X (відрізок [AC1] на останньому малюнку), то зазору в чверть кроку по X зверху і знизу від меж canvas-а буде достатньо.
Приклад реалізації на JSFiddle
UPDATE:
- Спроби зробити так, щоб опорні точки потрібно було рахувати один раз, а потім, при масштабуванні графіка, просто використовувати їх координати, зазнали невдачі. Все впирається в те, що в розрахунок коефіцієнта k входить і поточний масштаб по X, і поточний масштаб по Y. І витягнути їх з формули не виходить. Товариш quverty мене про це попереджав і виявився правий.
- Хотів би звернути увагу, що кривизну побудованого графіка можна регулювати. Для цього слід змінювати відстань до опорних точок - змінювати коефіцієнт ^ X'= ^ X/2 * Sqrt (1/( 1 + k2)), а саме - знаменник ось цього його шматочка: ΔX/2. Знаменник менше 1 брати не слід (читай третій підпункт пункту «Основна ідея»). Поекспериментувати можна на JSFiddle (129 рядок JavaScript-а).
- Зверніть увагу на реалізацію сплайну Катмулла-Рома товариша ^ vana. І спасибі йому за цей коментар.