globally change all use of "frame" to refer to audio into "sample".
[ardour.git] / libs / canvas / framed_curve.cc
1 /*
2     Copyright (C) 2013 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <cmath>
21 #include <exception>
22 #include <algorithm>
23
24 #include "canvas/framed_curve.h"
25
26 using namespace ArdourCanvas;
27 using std::min;
28 using std::max;
29
30 FramedCurve::FramedCurve (Canvas* c)
31         : PolyItem (c)
32         , n_samples (0)
33         , points_per_segment (16)
34         , curve_fill (Inside)
35 {
36 }
37
38 FramedCurve::FramedCurve (Item* parent)
39         : PolyItem (parent)
40         , n_samples (0)
41         , points_per_segment (16)
42         , curve_fill (Inside)
43 {
44 }
45
46 /** When rendering the curve, we will always draw a fixed number of straight
47  * line segments to span the x-axis extent of the curve. More segments:
48  * smoother visual rendering. Less rendering: closer to a visibily poly-line
49  * render.
50  */
51 void
52 FramedCurve::set_points_per_segment (uint32_t n)
53 {
54         /* this only changes our appearance rather than the bounding box, so we
55            just need to schedule a redraw rather than notify the parent of any
56            changes
57         */
58         points_per_segment = max (n, (uint32_t) 3);
59         interpolate ();
60         redraw ();
61 }
62
63 void
64 FramedCurve::compute_bounding_box () const
65 {
66         PolyItem::compute_bounding_box ();
67
68         /* possibly add extents of any point indicators here if we ever do that */
69 }
70
71 void
72 FramedCurve::set (Points const& p)
73 {
74         PolyItem::set (p);
75         interpolate ();
76 }
77
78 void
79 FramedCurve::interpolate ()
80 {
81         Points curve_points = _points;
82
83         if (curve_points.size()) {
84                 curve_points.erase (curve_points.begin());
85         }
86         samples.clear ();
87
88         if (_points.size() == 3) {
89                 samples.push_back (curve_points.front());
90                 samples.push_back (curve_points.back());
91                 n_samples = 2;
92         } else {
93                 InterpolatedCurve::interpolate (curve_points, points_per_segment, CatmullRomCentripetal, false, samples);
94                 n_samples = samples.size();
95         }
96 }
97
98 void
99 FramedCurve::render (Rect const & area, Cairo::RefPtr<Cairo::Context> context) const
100 {
101         if (!_outline || _points.size() < 3 || !_bounding_box) {
102                 return;
103         }
104
105         Rect self = item_to_window (_bounding_box);
106         Rect d = self.intersection (area);
107         assert (d);
108         Rect draw = d;
109
110         /* Our approach is to always draw n_segments across our total size.
111          *
112          * This is very inefficient if we are asked to only draw a small
113          * section of the curve. For now we rely on cairo clipping to help
114          * with this.
115          */
116
117         /* x-axis limits of the curve, in window space coordinates */
118
119         Duple w1 = item_to_window (Duple (_points.front().x, 0.0));
120         Duple w2 = item_to_window (Duple (_points.back().x, 0.0));
121
122         /* clamp actual draw to area bound by points, rather than our bounding box which is slightly different */
123
124         context->save ();
125         context->rectangle (draw.x0, draw.y0, draw.width(), draw.height());
126         context->clip ();
127
128         /* expand drawing area by several pixels on each side to avoid cairo stroking effects at the boundary.
129            they will still occur, but cairo's clipping will hide them.
130         */
131
132         draw = draw.expand (4.0);
133
134         /* now clip it to the actual points in the curve */
135
136         if (draw.x0 < w1.x) {
137                 draw.x0 = w1.x;
138         }
139
140         if (draw.x1 >= w2.x) {
141                 draw.x1 = w2.x;
142         }
143
144         setup_outline_context (context);
145         if (_points.size() == 3) {
146
147                 /* straight line */
148
149                 Duple window_space;
150                 Points::const_iterator it = _points.begin();
151
152                 Duple first_point = Duple (0.0, 0.0);
153                 Duple last_point = Duple (0.0, 0.0);
154
155                 window_space = item_to_window (*it);
156                 if (window_space.x <= draw.x0) {
157                         first_point = Duple (draw.x0, window_space.y);
158                 } else {
159                         first_point = Duple (window_space.x, window_space.y);
160                 }
161                 context->move_to (first_point.x, first_point.y);
162
163                 ++it;
164                 window_space = item_to_window (*it, false);
165                 if (window_space.x <= draw.x0) {
166                         context->line_to (draw.x0, window_space.y);
167                 } else {
168                         context->line_to (window_space.x, window_space.y);
169                 }
170
171                 window_space = item_to_window (_points.back(), false);
172                 if (window_space.x >= draw.x1) {
173                         last_point = Duple (draw.x1, window_space.y);
174                 } else {
175                         last_point = Duple (window_space.x, window_space.y);
176                 }
177
178                 context->line_to (last_point.x, last_point.y);
179
180                 switch (curve_fill) {
181                         case None:
182                                 context->stroke();
183                                 break;
184                         case Inside:
185                                 context->stroke_preserve ();
186                                 window_space = item_to_window (Duple(0.0, draw.height()));
187                                 context->line_to (last_point.x, window_space.y);
188                                 window_space = item_to_window (Duple(0.0, draw.height()));
189                                 context->line_to (first_point.x, window_space.y);
190                                 context->close_path();
191                                 setup_fill_context(context);
192                                 context->fill ();
193                                 break;
194                         case Outside:
195                                 context->stroke_preserve ();
196                                 window_space = item_to_window (Duple(_points.back().x, 0.0));
197                                 context->line_to (last_point.x, window_space.y);
198                                 window_space = item_to_window (Duple(_points.front().x, 0.0));
199                                 context->line_to (first_point.x, window_space.y);
200                                 context->close_path();
201                                 setup_fill_context(context);
202                                 context->fill ();
203                                 break;
204                 }
205         } else {
206                 /* curve of at least 3 points */
207
208                 /* find left and right-most sample */
209                 Duple window_space;
210                 Points::size_type left = 0;
211                 Points::size_type right = n_samples - 1;
212
213                 for (Points::size_type idx = 0; idx < n_samples - 1; ++idx) {
214                         window_space = item_to_window (Duple (samples[idx].x, 0.0));
215                         if (window_space.x >= draw.x0) {
216                                 break;
217                         }
218                         left = idx;
219                 }
220
221                 for (Points::size_type idx = left; idx < n_samples - 1; ++idx) {
222                         window_space = item_to_window (Duple (samples[idx].x, 0.0));
223                         if (window_space.x > draw.x1) {
224                                 right = idx;
225                                 break;
226                         }
227                 }
228
229                 const Duple first_sample = Duple (samples[left].x, samples[left].y);
230
231                 /* move to the first sample's x and the draw height */
232                 window_space = item_to_window (Duple (first_sample.x, draw.height()));
233                 context->move_to (window_space.x, window_space.y);
234
235                 /* draw line to first sample and then between samples */
236                 for (uint32_t idx = left; idx <= right; ++idx) {
237                         window_space = item_to_window (Duple (samples[idx].x, samples[idx].y), false);
238                         context->line_to (window_space.x, window_space.y);
239                 }
240
241                 /* a redraw may have been requested between the last sample and the last point.
242                    if so, draw a line to the last _point.
243                 */
244                 Duple last_sample = Duple (samples[right].x, samples[right].y);
245
246                 if (draw.x1 > last_sample.x) {
247                         last_sample = Duple (_points.back().x, _points.back().y);
248                         window_space = item_to_window (last_sample, false);
249                         context->line_to (window_space.x, window_space.y);
250                 }
251
252                 switch (curve_fill) {
253                         case None:
254                                 context->stroke();
255                                 break;
256                         case Inside:
257                                 context->stroke_preserve ();
258                                 /* close the sample, possibly using the last _point's x rather than samples[right].x */
259                                 window_space = item_to_window (Duple (last_sample.x, draw.height()));
260                                 context->line_to (window_space.x, window_space.y);
261                                 window_space = item_to_window (Duple (first_sample.x, draw.height()));
262                                 context->line_to (window_space.x, window_space.y);
263                                 context->close_path();
264                                 setup_fill_context(context);
265                                 context->fill ();
266                                 break;
267                         case Outside:
268                                 context->stroke_preserve ();
269                                 window_space = item_to_window (Duple (last_sample.x, 0.0));
270                                 context->line_to (window_space.x, window_space.y);
271                                 window_space = item_to_window (Duple (first_sample.x, 0.0));
272                                 context->line_to (window_space.x, window_space.y);
273                                 context->close_path();
274                                 setup_fill_context(context);
275                                 context->fill ();
276                                 break;
277                 }
278         }
279         context->restore ();
280
281 #if 0
282         /* add points */
283         setup_outline_context (context);
284         for (Points::const_iterator p = _points.begin(); p != _points.end(); ++p) {
285                 Duple window_space (item_to_window (*p));
286                 context->arc (window_space.x, window_space.y, 5.0, 0.0, 2 * M_PI);
287                 context->stroke ();
288         }
289 #endif
290 }
291
292 bool
293 FramedCurve::covers (Duple const & pc) const
294 {
295         Duple point = window_to_item (pc);
296
297         /* O(N) N = number of points, and not accurate */
298
299         for (Points::const_iterator p = _points.begin(); p != _points.end(); ++p) {
300
301                 const Coord dx = point.x - (*p).x;
302                 const Coord dy = point.y - (*p).y;
303                 const Coord dx2 = dx * dx;
304                 const Coord dy2 = dy * dy;
305
306                 if ((dx2 < 2.0 && dy2 < 2.0) || (dx2 + dy2 < 4.0)) {
307                         return true;
308                 }
309         }
310
311         return false;
312 }