1 /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; -*- */
2 /* ***** BEGIN LICENSE BLOCK *****
3  * This file is part of openfx-supportext <https://github.com/devernay/openfx-supportext>,
4  * Copyright (C) 2013-2018 INRIA
5  *
6  * openfx-supportext is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * openfx-supportext is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with openfx-supportext.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>
18  * ***** END LICENSE BLOCK ***** */
19 
20 /*
21  * OFX generic rectangle interact with 4 corner points + center point and 4 mid-points.
22  * You can use it to define any rectangle in an image resizable by the user.
23  */
24 
25 #include "ofxsRamp.h"
26 
27 #include <cmath>
28 #include <cfloat> // DBL_MAX
29 
30 #ifdef __APPLE__
31 #include <OpenGL/gl.h>
32 #else
33 #include <GL/gl.h>
34 #endif
35 #define POINT_TOLERANCE 6
36 #define POINT_SIZE 5
37 
38 namespace OFX {
39 static inline
40 void
crossProd(const Ofx3DPointD & u,const Ofx3DPointD & v,Ofx3DPointD * w)41 crossProd(const Ofx3DPointD& u,
42           const Ofx3DPointD& v,
43           Ofx3DPointD* w)
44 {
45     w->x = u.y * v.z - u.z * v.y;
46     w->y = u.z * v.x - u.x * v.z;
47     w->z = u.x * v.y - u.y * v.x;
48 }
49 
50 // round to the closest int, 1/10 int, etc
51 // this make parameter editing easier
52 // pscale is args.pixelScale.x / args.renderScale.x;
53 // pscale10 is the power of 10 below pscale
54 static inline
55 double
fround(double val,double pscale)56 fround(double val,
57        double pscale)
58 {
59     double pscale10 = std::pow( 10., std::floor( std::log10(pscale) ) );
60 
61     return pscale10 * std::floor(val / pscale10 + 0.5);
62 }
63 
64 bool
draw(const DrawArgs & args)65 RampInteractHelper::draw(const DrawArgs &args)
66 {
67     const double time = args.time;
68 
69     if ( !_interactOpen->getValueAtTime(time) ) {
70         return false;
71     }
72     RampTypeEnum type = (RampTypeEnum)_type->getValueAtTime(time);
73     bool noramp = (type == eRampTypeNone);
74     if (noramp) {
75         return false;
76     }
77     OfxRGBColourD color = { 0.8, 0.8, 0.8 };
78     _interact->getSuggestedColour(color);
79     const OfxPointD &pscale = args.pixelScale;
80     GLdouble projection[16];
81     glGetDoublev( GL_PROJECTION_MATRIX, projection);
82     GLint viewport[4];
83     glGetIntegerv(GL_VIEWPORT, viewport);
84     OfxPointD shadow; // how much to translate GL_PROJECTION to get exactly one pixel on screen
85     shadow.x = 2. / (projection[0] * viewport[2]);
86     shadow.y = 2. / (projection[5] * viewport[3]);
87 
88     OfxPointD p[2];
89     if (_state == eInteractStateIdle) {
90         _point0->getValueAtTime(time, p[0].x, p[0].y);
91         _point1->getValueAtTime(time, p[1].x, p[1].y);
92     } else {
93         p[0] = _point0DragPos;
94         p[1] = _point1DragPos;
95     }
96 
97     ///Clamp points to the rod
98     OfxRectD rod = _dstClip->getRegionOfDefinition(time);
99 
100     // A line is represented by a 3-vector (a,b,c), and its equation is (a,b,c).(x,y,1)=0
101     // The intersection of two lines is given by their cross-product: (wx,wy,w) = (a,b,c)x(a',b',c').
102     // The line passing through 2 points is obtained by their cross-product: (a,b,c) = (x,y,1)x(x',y',1)
103     // The two lines passing through p0 and p1 and orthogonal to p0p1 are:
104     // (p1.x - p0.x, p1.y - p0.y, -p0.x*(p1.x-p0.x) - p0.y*(p1.y-p0.y)) passing through p0
105     // (p1.x - p0.x, p1.y - p0.y, -p1.x*(p1.x-p0.x) - p1.y*(p1.y-p0.y)) passing through p1
106     // the four lines defining the RoD are:
107     // (1,0,-x1) [x=x1]
108     // (1,0,-x2) [x=x2]
109     // (0,1,-y1) [x=y1]
110     // (0,1,-y2) [y=y2]
111     const Ofx3DPointD linex1 = {1, 0, -rod.x1};
112     const Ofx3DPointD linex2 = {1, 0, -rod.x2};
113     const Ofx3DPointD liney1 = {0, 1, -rod.y1};
114     const Ofx3DPointD liney2 = {0, 1, -rod.y2};
115     Ofx3DPointD line[2];
116     OfxPointD pline1[2];
117     OfxPointD pline2[2];
118 
119     // line passing through p0
120     line[0].x = p[1].x - p[0].x;
121     line[0].y = p[1].y - p[0].y;
122     line[0].z = -p[0].x * (p[1].x - p[0].x) - p[0].y * (p[1].y - p[0].y);
123     // line passing through p1
124     line[1].x = p[1].x - p[0].x;
125     line[1].y = p[1].y - p[0].y;
126     line[1].z = -p[1].x * (p[1].x - p[0].x) - p[1].y * (p[1].y - p[0].y);
127     // for each line...
128     for (int i = 0; i < 2; ++i) {
129         // compute the intersection with the four lines
130         Ofx3DPointD interx1, interx2, intery1, intery2;
131 
132         crossProd(line[i], linex1, &interx1);
133         crossProd(line[i], linex2, &interx2);
134         crossProd(line[i], liney1, &intery1);
135         crossProd(line[i], liney2, &intery2);
136         if ( (interx1.z != 0.) && (interx2.z != 0.) ) {
137             // initialize pline1 to the intersection with x=x1, pline2 with x=x2
138             pline1[i].x = interx1.x / interx1.z;
139             pline1[i].y = interx1.y / interx1.z;
140             pline2[i].x = interx2.x / interx2.z;
141             pline2[i].y = interx2.y / interx2.z;
142             if ( ( (pline1[i].y > rod.y2) && (pline2[i].y > rod.y2) ) ||
143                  ( ( pline1[i].y < rod.y1) && ( pline2[i].y < rod.y1) ) ) {
144                 // line doesn't intersect rectangle, don't draw it.
145                 pline1[i].x = p[i].x;
146                 pline1[i].y = p[i].y;
147                 pline2[i].x = p[i].x;
148                 pline2[i].y = p[i].y;
149             } else if (pline1[i].y < pline2[i].y) {
150                 // y is an increasing function of x, test the two other endpoints
151                 if ( (intery1.z != 0.) && (intery1.x / intery1.z > pline1[i].x) ) {
152                     pline1[i].x = intery1.x / intery1.z;
153                     pline1[i].y = intery1.y / intery1.z;
154                 }
155                 if ( (intery2.z != 0.) && (intery2.x / intery2.z < pline2[i].x) ) {
156                     pline2[i].x = intery2.x / intery2.z;
157                     pline2[i].y = intery2.y / intery2.z;
158                 }
159             } else {
160                 // y is an decreasing function of x, test the two other endpoints
161                 if ( (intery2.z != 0.) && (intery2.x / intery2.z > pline1[i].x) ) {
162                     pline1[i].x = intery2.x / intery2.z;
163                     pline1[i].y = intery2.y / intery2.z;
164                 }
165                 if ( (intery1.z != 0.) && (intery1.x / intery1.z < pline2[i].x) ) {
166                     pline2[i].x = intery1.x / intery1.z;
167                     pline2[i].y = intery1.y / intery1.z;
168                 }
169             }
170         } else {
171             // initialize pline1 to the intersection with y=y1, pline2 with y=y2
172             pline1[i].x = intery1.x / intery1.z;
173             pline1[i].y = intery1.y / intery1.z;
174             pline2[i].x = intery2.x / intery2.z;
175             pline2[i].y = intery2.y / intery2.z;
176             if ( ( (pline1[i].x > rod.x2) && (pline2[i].x > rod.x2) ) ||
177                  ( ( pline1[i].x < rod.x1) && ( pline2[i].x < rod.x1) ) ) {
178                 // line doesn't intersect rectangle, don't draw it.
179                 pline1[i].x = p[i].x;
180                 pline1[i].y = p[i].y;
181                 pline2[i].x = p[i].x;
182                 pline2[i].y = p[i].y;
183             }
184         }
185     }
186 
187     //glPushAttrib(GL_ALL_ATTRIB_BITS); // caller is responsible for protecting attribs
188 
189     glEnable(GL_LINE_STIPPLE);
190     glEnable(GL_LINE_SMOOTH);
191     glDisable(GL_POINT_SMOOTH);
192     glEnable(GL_BLEND);
193     glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE);
194     glLineWidth(1.5f);
195     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
196     glLineStipple(2, 0xAAAA);
197 
198     glPointSize(POINT_SIZE);
199 
200     // Draw everything twice
201     // l = 0: shadow
202     // l = 1: drawing
203     for (int l = 0; l < 2; ++l) {
204         // shadow (uses GL_PROJECTION)
205         glMatrixMode(GL_PROJECTION);
206         int direction = (l == 0) ? 1 : -1;
207         // translate (1,-1) pixels
208         glTranslated(direction * shadow.x, -direction * shadow.y, 0);
209         glMatrixMode(GL_MODELVIEW); // Modelview should be used on Nuke
210 
211         for (int i = 0; i < 2; ++i) {
212             bool dragging = _state == (i == 0 ? eInteractStateDraggingPoint0 : eInteractStateDraggingPoint1);
213             glBegin(GL_POINTS);
214             if (dragging) {
215                 glColor3f(0.f * l, 1.f * l, 0.f * l);
216             } else {
217                 glColor3f( (float)color.r * l, (float)color.g * l, (float)color.b * l );
218             }
219             glVertex2d(p[i].x, p[i].y);
220             glEnd();
221 
222             glBegin(GL_LINES);
223             glColor3f( (float)color.r * l, (float)color.g * l, (float)color.b * l );
224             glVertex2d(pline1[i].x, pline1[i].y);
225             glVertex2d(pline2[i].x, pline2[i].y);
226             glEnd();
227 
228             double xoffset = 5 * pscale.x;
229             double yoffset = 5 * pscale.y;
230             TextRenderer::bitmapString(p[i].x + xoffset, p[i].y + yoffset, i == 0 ? kParamRampPoint0Label : kParamRampPoint1Label);
231         }
232     }
233 
234     //glPopAttrib();
235 
236     return true;
237 } // RampInteractHelper::draw
238 
239 static bool
isNearby(const OfxPointD & p,double x,double y,double tolerance,const OfxPointD & pscale)240 isNearby(const OfxPointD& p,
241          double x,
242          double y,
243          double tolerance,
244          const OfxPointD& pscale)
245 {
246     return std::fabs(p.x - x) <= tolerance * pscale.x &&  std::fabs(p.y - y) <= tolerance * pscale.y;
247 }
248 
249 bool
penMotion(const PenArgs & args)250 RampInteractHelper::penMotion(const PenArgs &args)
251 {
252     const double time = args.time;
253 
254     if ( !_interactOpen->getValueAtTime(time) ) {
255         return false;
256     }
257     RampTypeEnum type = (RampTypeEnum)_type->getValueAtTime(time);
258     bool noramp = (type == eRampTypeNone);
259     if (noramp) {
260         return false;
261     }
262 
263     const OfxPointD &pscale = args.pixelScale;
264     OfxPointD p0, p1;
265     if (_state != eInteractStateIdle) {
266         p0 = _point0DragPos;
267         p1 = _point1DragPos;
268     } else {
269         _point0->getValueAtTime(time, p0.x, p0.y);
270         _point1->getValueAtTime(time, p1.x, p1.y);
271     }
272     bool valuesChanged = false;
273     OfxPointD delta;
274     delta.x = args.penPosition.x - _lastMousePos.x;
275     delta.y = args.penPosition.y - _lastMousePos.y;
276 
277     if (_state == eInteractStateDraggingPoint0) {
278         _point0DragPos.x += delta.x;
279         _point0DragPos.y += delta.y;
280         valuesChanged = true;
281     } else if (_state == eInteractStateDraggingPoint1) {
282         _point1DragPos.x += delta.x;
283         _point1DragPos.y += delta.y;
284         valuesChanged = true;
285     }
286 
287     if ( (_state != eInteractStateIdle) && _interactiveDrag && valuesChanged ) {
288         if (_state == eInteractStateDraggingPoint0) {
289             _point0->setValue( fround(_point0DragPos.x, pscale.x), fround(_point0DragPos.y, pscale.y) );
290         } else if (_state == eInteractStateDraggingPoint1) {
291             _point1->setValue( fround(_point1DragPos.x, pscale.x), fround(_point1DragPos.y, pscale.y) );
292         }
293     }
294 
295     if (valuesChanged) {
296         _interact->requestRedraw();
297     }
298 
299     _lastMousePos = args.penPosition;
300 
301     return valuesChanged;
302 } // RampInteractHelper::penMotion
303 
304 bool
penDown(const PenArgs & args)305 RampInteractHelper::penDown(const PenArgs &args)
306 {
307     const double time = args.time;
308 
309     if ( !_interactOpen->getValueAtTime(time) ) {
310         return false;
311     }
312     RampTypeEnum type = (RampTypeEnum)_type->getValueAtTime(time);
313     bool noramp = (type == eRampTypeNone);
314     if (noramp) {
315         return false;
316     }
317 
318     const OfxPointD &pscale = args.pixelScale;
319     OfxPointD p0, p1;
320     if (_state != eInteractStateIdle) {
321         p0 = _point0DragPos;
322         p1 = _point1DragPos;
323     } else {
324         _point0->getValueAtTime(time, p0.x, p0.y);
325         _point1->getValueAtTime(time, p1.x, p1.y);
326         _interactive->getValueAtTime(time, _interactiveDrag);
327     }
328 
329     bool didSomething = false;
330 
331     if ( isNearby(args.penPosition, p0.x, p0.y, POINT_TOLERANCE, pscale) ) {
332         _state = eInteractStateDraggingPoint0;
333         didSomething = true;
334     } else if ( isNearby(args.penPosition, p1.x, p1.y, POINT_TOLERANCE, pscale) ) {
335         _state = eInteractStateDraggingPoint1;
336         didSomething = true;
337     } else {
338         _state = eInteractStateIdle;
339     }
340 
341     _point0DragPos = p0;
342     _point1DragPos = p1;
343     _lastMousePos = args.penPosition;
344 
345     if (didSomething) {
346         _interact->requestRedraw();
347     }
348 
349     return didSomething;
350 }
351 
352 bool
penUp(const PenArgs & args)353 RampInteractHelper::penUp(const PenArgs &args)
354 {
355     const double time = args.time;
356 
357     if ( !_interactOpen->getValueAtTime(time) ) {
358         return false;
359     }
360     RampTypeEnum type = (RampTypeEnum)_type->getValueAtTime(time);
361     bool noramp = (type == eRampTypeNone);
362     if (noramp) {
363         return false;
364     }
365 
366     bool didSomething = false;
367     const OfxPointD &pscale = args.pixelScale;
368 
369     if ( !_interactiveDrag && (_state != eInteractStateIdle) ) {
370         if (_state == eInteractStateDraggingPoint0) {
371             // round newx/y to the closest int, 1/10 int, etc
372             // this make parameter editing easier
373 
374             _point0->setValue( fround(_point0DragPos.x, pscale.x), fround(_point0DragPos.y, pscale.y) );
375             didSomething = true;
376         } else if (_state == eInteractStateDraggingPoint1) {
377             _point1->setValue( fround(_point1DragPos.x, pscale.x), fround(_point1DragPos.y, pscale.y) );
378             didSomething = true;
379         }
380     } else if (_state != eInteractStateIdle) {
381         _interact->requestRedraw();
382     }
383 
384     _state = eInteractStateIdle;
385 
386     return didSomething;
387 }
388 
389 /** @brief Called when the interact is loses input focus */
390 void
loseFocus(const FocusArgs &)391 RampInteractHelper::loseFocus(const FocusArgs & /*args*/)
392 {
393     _interactiveDrag = false;
394     _state = eInteractStateIdle;
395 }
396 
397 void
ofxsRampDescribeParams(ImageEffectDescriptor & desc,PageParamDescriptor * page,GroupParamDescriptor * group,RampTypeEnum defaultType,bool isOpen,bool oldParams)398 ofxsRampDescribeParams(ImageEffectDescriptor &desc,
399                        PageParamDescriptor *page,
400                        GroupParamDescriptor *group,
401                        RampTypeEnum defaultType,
402                        bool isOpen,
403                        bool oldParams)
404 {
405     // type
406     {
407         ChoiceParamDescriptor* param = desc.defineChoiceParam(oldParams ? kParamRampTypeOld : kParamRampType);
408         param->setLabelAndHint(kParamRampTypeLabel);
409         assert(param->getNOptions() == eRampTypeLinear);
410         param->appendOption(kParamRampTypeOptionLinear);
411         assert(param->getNOptions() == eRampTypePLinear);
412         param->appendOption(kParamRampTypeOptionPLinear);
413         assert(param->getNOptions() == eRampTypeEaseIn);
414         param->appendOption(kParamRampTypeOptionEaseIn);
415         assert(param->getNOptions() == eRampTypeEaseOut);
416         param->appendOption(kParamRampTypeOptionEaseOut);
417         assert(param->getNOptions() == eRampTypeSmooth);
418         param->appendOption(kParamRampTypeOptionSmooth);
419         assert(param->getNOptions() == eRampTypeNone);
420         param->appendOption(kParamRampTypeOptionNone);
421         param->setDefault(defaultType);
422         param->setAnimates(false);
423         if (group) {
424             param->setParent(*group);
425         }
426         if (page) {
427             page->addChild(*param);
428         }
429     }
430 
431     // point0
432     {
433         Double2DParamDescriptor* param = desc.defineDouble2DParam(oldParams ? kParamRampPoint0Old : kParamRampPoint0);
434         param->setLabel(kParamRampPoint0Label);
435         param->setDoubleType(eDoubleTypeXYAbsolute);
436         param->setDefaultCoordinateSystem(eCoordinatesCanonical); // Nuke defaults to Normalized for XY and XYAbsolute!
437         param->setDefault(100., 100.);
438         param->setRange(-DBL_MAX, -DBL_MAX, DBL_MAX, DBL_MAX); // Resolve requires range and display range or values are clamped to (-1,1)
439         param->setDisplayRange(-10000, -10000, 10000, 10000); // Resolve requires display range or values are clamped to (-1,1)
440         //param->setUseHostNativeOverlayHandle(true);
441         if (group) {
442             param->setParent(*group);
443         }
444         if (page) {
445             page->addChild(*param);
446         }
447     }
448 
449 
450     // color0
451     {
452         RGBAParamDescriptor* param = desc.defineRGBAParam(oldParams ? kParamRampColor0Old : kParamRampColor0);
453         param->setLabel(kParamRampColor0Label);
454         param->setDefault(0, 0, 0, 0);
455         if (group) {
456             param->setParent(*group);
457         }
458         if (page) {
459             page->addChild(*param);
460         }
461     }
462 
463     // point1
464     {
465         Double2DParamDescriptor* param = desc.defineDouble2DParam(oldParams ? kParamRampPoint1Old : kParamRampPoint1);
466         param->setLabel(kParamRampPoint1Label);
467         param->setDoubleType(eDoubleTypeXYAbsolute);
468         param->setDefaultCoordinateSystem(eCoordinatesCanonical); // Nuke defaults to Normalized for XY and XYAbsolute!
469         param->setDefault(100., 200.);
470         param->setRange(-DBL_MAX, -DBL_MAX, DBL_MAX, DBL_MAX); // Resolve requires range and display range or values are clamped to (-1,1)
471         param->setDisplayRange(-10000, -10000, 10000, 10000); // Resolve requires display range or values are clamped to (-1,1)
472         //param->setUseHostNativeOverlayHandle(true);
473         if (group) {
474             param->setParent(*group);
475         }
476         if (page) {
477             page->addChild(*param);
478         }
479     }
480 
481     // color1
482     {
483         RGBAParamDescriptor* param = desc.defineRGBAParam(oldParams ? kParamRampColor1Old : kParamRampColor1);
484         param->setLabel(kParamRampColor1Label);
485         param->setDefault(1., 1., 1., 1. );
486         if (group) {
487             param->setParent(*group);
488         }
489         if (page) {
490             page->addChild(*param);
491         }
492     }
493 
494     // interactOpen
495     {
496         BooleanParamDescriptor* param = desc.defineBooleanParam(kParamRampInteractOpen);
497         param->setLabelAndHint(kParamRampInteractOpenLabel);
498         param->setDefault(isOpen); // open by default
499         param->setIsSecretAndDisabled(true); // secret by default, but this can be changed for specific hosts
500         param->setAnimates(false);
501         if (group) {
502             param->setParent(*group);
503         }
504         if (page) {
505             page->addChild(*param);
506         }
507     }
508 
509 
510     // interactive
511     {
512         BooleanParamDescriptor* param = desc.defineBooleanParam(oldParams ? kParamRampInteractiveOld : kParamRampInteractive);
513         param->setLabelAndHint(kParamRampInteractiveLabel);
514         param->setEvaluateOnChange(false);
515         if (group) {
516             param->setParent(*group);
517         }
518         if (page) {
519             page->addChild(*param);
520         }
521     }
522 } // ofxsRampDescribeParams
523 } // namespace OFX
524