1 /*
  2     Copyright 2008-2024
  3         Matthias Ehmann,
  4         Carsten Miller,
  5         Alfred Wassermann
  6 
  7     This file is part of JSXGraph.
  8 
  9     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 10 
 11     You can redistribute it and/or modify it under the terms of the
 12 
 13       * GNU Lesser General Public License as published by
 14         the Free Software Foundation, either version 3 of the License, or
 15         (at your option) any later version
 16       OR
 17       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 18 
 19     JSXGraph is distributed in the hope that it will be useful,
 20     but WITHOUT ANY WARRANTY; without even the implied warranty of
 21     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 22     GNU Lesser General Public License for more details.
 23 
 24     You should have received a copy of the GNU Lesser General Public License and
 25     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 26     and <https://opensource.org/licenses/MIT/>.
 27  */
 28 
 29 /*global JXG: true, define: true*/
 30 /*jslint nomen: true, plusplus: true*/
 31 
 32 /**
 33  * @fileoverview Implementation of vector fields and slope fields.
 34  */
 35 
 36 import JXG from "../jxg.js";
 37 import Type from "../utils/type.js";
 38 
 39 /**
 40  * @class  A vector field on a plane can be visualized as a collection of arrows
 41  * with given magnitudes and directions, each attached to a point on the plane.
 42  * <p>
 43  * Plot a vector field either given by two functions f1(x, y) and f2(x,y) or by a function f(x, y) returning an array of size 2.
 44  *
 45  * @pseudo
 46  * @name Vectorfield
 47  * @augments JXG.Curve
 48  * @constructor
 49  * @type JXG.Curve
 50  * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown.
 51  * Parameter options:
 52  * @param {Array|Function|String} F Either an array containing two functions f1(x, y) and f2(x, y) or function f(x, y) returning an array of length 2.
 53  * @param {Array} xData Array of length 3 containing start value for x, number of steps, end value of x. The vector field will contain
 54  * (number of steps) + 1 vectors in direction of x.
 55  * @param {Array} yData Array of length 3 containing start value for y, number of steps, end value of y. The vector field will contain
 56  * (number of steps) + 1 vectors in direction of y.
 57  *
 58  * @example
 59  * // Defining functions
 60  * var fx = (x, y) => Math.sin(y);
 61  * var fy = (x, y) => Math.cos(x);
 62  *
 63  * var field = board.create('vectorfield', [
 64  *         [fx, fy],    // Defining function
 65  *         [-6, 25, 6], // Horizontal mesh
 66  *         [-5, 20, 5], // Vertical mesh
 67  *     ]);
 68  *
 69  * </pre><div id="JXGa2040e30-48ea-47d4-9840-bd24cd49150b" class="jxgbox" style="width: 500px; height: 500px;"></div>
 70  * <script type="text/javascript">
 71  *     (function() {
 72  *         var board = JXG.JSXGraph.initBoard('JXGa2040e30-48ea-47d4-9840-bd24cd49150b',
 73  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
 74  *     // Defining functions
 75  *     var fx = (x, y) => Math.sin(y);
 76  *     var fy = (x, y) => Math.cos(x);
 77  *
 78  *     var field = board.create('vectorfield', [
 79  *             [fx, fy],    // Defining function
 80  *             [-6, 25, 6], // Horizontal mesh
 81  *             [-5, 20, 5], // Vertical mesh
 82  *         ]);
 83  *
 84  *     })();
 85  *
 86  * </script><pre>
 87  *
 88  * @example
 89  * // Slider to control length of vectors
 90  * var s = board.create('slider', [[-3, 7], [3, 7], [0, 0.33, 1]], {name: 'length'});
 91  * // Slider to control number of steps
 92  * var stepsize = board.create('slider', [[-3, 6], [3, 6], [1, 20, 100]], {name: 'steps', snapWidth: 1});
 93  *
 94  * // Defining functions
 95  * var fx = (x, y) => 0.2 * y;
 96  * var fy = (x, y) => 0.2 * (Math.cos(x) - 2) * Math.sin(x);
 97  *
 98  * var field = board.create('vectorfield', [
 99  *         [fx, fy],        // Defining function
100  *         [-6, () => stepsize.Value(), 6], // Horizontal mesh
101  *         [-5, () => stepsize.Value(), 5], // Vertical mesh
102  *     ], {
103  *         highlightStrokeColor: JXG.palette.blue, // Make highlighting invisible
104  *
105  *         scale: () => s.Value(), // Scaling of vectors
106  *
107  *         arrowHead: {
108  *             enabled: true,
109  *             size: 8,  // Pixel length of arrow head
110  *             angle: Math.PI / 16
111  *         }
112  * });
113  *
114  * </pre><div id="JXG9196337e-66f0-4d09-8065-11d88c4ff140" class="jxgbox" style="width: 500px; height: 500px;"></div>
115  * <script type="text/javascript">
116  *     (function() {
117  *         var board = JXG.JSXGraph.initBoard('JXG9196337e-66f0-4d09-8065-11d88c4ff140',
118  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
119  *     // Slider to control length of vectors
120  *     var s = board.create('slider', [[-3, 7], [3, 7], [0, 0.33, 1]], {name: 'length'});
121  *     // Slider to control number of steps
122  *     var stepsize = board.create('slider', [[-3, 6], [3, 6], [1, 20, 100]], {name: 'steps', snapWidth: 1});
123  *
124  *     // Defining functions
125  *     var fx = (x, y) => 0.2 * y;
126  *     var fy = (x, y) => 0.2 * (Math.cos(x) - 2) * Math.sin(x);
127  *
128  *     var field = board.create('vectorfield', [
129  *             [fx, fy],        // Defining function
130  *             [-6, () => stepsize.Value(), 6], // Horizontal mesh
131  *             [-5, () => stepsize.Value(), 5], // Vertical mesh
132  *         ], {
133  *             highlightStrokeColor: JXG.palette.blue, // Make highlighting invisible
134  *
135  *             scale: () => s.Value(), // Scaling of vectors
136  *
137  *             arrowHead: {
138  *                 enabled: true,
139  *                 size: 8,  // Pixel length of arrow head
140  *                 angle: Math.PI / 16
141  *             }
142  *     });
143  *
144  *     })();
145  *
146  * </script><pre>
147  *
148  */
149 JXG.createVectorField = function (board, parents, attributes) {
150     var el, attr;
151 
152     if (!(parents.length >= 3 &&
153         (Type.isArray(parents[0]) || Type.isFunction(parents[0]) || Type.isString(parents[0])) &&
154         (Type.isArray(parents[1]) && parents[1].length === 3) &&
155         (Type.isArray(parents[2]) && parents[2].length === 3)
156     )) {
157         throw new Error(
158             "JSXGraph: Can't create vector field with parent types " +
159             "'" + typeof parents[0] + "', " +
160             "'" + typeof parents[1] + "', " +
161             "'" + typeof parents[2] + "'."
162         );
163     }
164 
165     attr = Type.copyAttributes(attributes, board.options, 'vectorfield');
166 
167     /**
168      * @type {JXG.Curve}
169      * @ignore
170      */
171     el = board.create('curve', [[], []], attr);
172     el.elType = 'vectorfield';
173 
174     /**
175      * Set the defining functions of vector field.
176      * @memberOf Vectorfield
177      * @name setF
178      * @function
179      * @param {Array|Function} func Either an array containing two functions f1(x, y) and f2(x, y) or function f(x, y) returning an array of length 2.
180      * @returns {Object} Reference to the vector field object.
181      *
182      * @example
183      * field.setF([(x, y) => Math.sin(y), (x, y) => Math.cos(x)]);
184      * board.update();
185      *
186      */
187     el.setF = function (func, varnames) {
188         var f0, f1;
189         if (Type.isArray(func)) {
190             f0 = Type.createFunction(func[0], this.board, varnames);
191             f1 = Type.createFunction(func[1], this.board, varnames);
192             /**
193              * @ignore
194              */
195             this.F = function (x, y) { return [f0(x, y), f1(x, y)]; };
196         } else {
197             this.F = Type.createFunction(func, el.board, varnames);
198         }
199         return this;
200     };
201 
202     el.setF(parents[0], 'x, y');
203     el.xData = parents[1];
204     el.yData = parents[2];
205 
206     el.updateDataArray = function () {
207         var x, y, i, j,
208             scale = this.evalVisProp('scale'),
209             start_x = Type.evaluate(this.xData[0]),
210             steps_x = Type.evaluate(this.xData[1]),
211             end_x = Type.evaluate(this.xData[2]),
212             delta_x = (end_x - start_x) / steps_x,
213 
214             start_y = Type.evaluate(this.yData[0]),
215             steps_y = Type.evaluate(this.yData[1]),
216             end_y = Type.evaluate(this.yData[2]),
217             delta_y = (end_y - start_y) / steps_y,
218             v, theta, phi1, phi2,
219 
220             showArrow = this.evalVisProp('arrowhead.enabled'),
221             leg, leg_x, leg_y, alpha;
222 
223 
224         if (showArrow) {
225             // Arrow head style
226             leg = this.evalVisProp('arrowhead.size');
227             leg_x = leg / board.unitX;
228             leg_y = leg / board.unitY;
229             alpha = this.evalVisProp('arrowhead.angle');
230         }
231 
232         this.dataX = [];
233         this.dataY = [];
234 
235         for (i = 0, x = start_x; i <= steps_x; x += delta_x, i++) {
236             for (j = 0, y = start_y; j <= steps_y; y += delta_y, j++) {
237                 v = this.F(x, y);
238                 v[0] *= scale;
239                 v[1] *= scale;
240 
241                 Type.concat(this.dataX, [x, x + v[0], NaN]);
242                 Type.concat(this.dataY, [y, y + v[1], NaN]);
243 
244                 if (showArrow && Math.abs(v[0]) + Math.abs(v[1]) > 0.0) {
245                     // Arrow head
246                     theta = Math.atan2(v[1], v[0]);
247                     phi1 = theta + alpha;
248                     phi2 = theta - alpha;
249                     Type.concat(this.dataX, [x + v[0] - Math.cos(phi1) * leg_x, x + v[0], x + v[0] - Math.cos(phi2) * leg_x, NaN]);
250                     Type.concat(this.dataY, [y + v[1] - Math.sin(phi1) * leg_y, y + v[1], y + v[1] - Math.sin(phi2) * leg_y, NaN]);
251                 }
252             }
253         }
254     };
255 
256     el.methodMap = Type.deepCopy(el.methodMap, {
257         setF: "setF"
258     });
259 
260     return el;
261 };
262 
263 JXG.registerElement("vectorfield", JXG.createVectorField);
264 
265 /**
266  * @class A slope field is a graphical representation of the solutions
267  * to a first-order differential equation of a scalar function.
268  * <p>
269  * Plot a slope field given by a function f(x, y) returning a number.
270  *
271  * @pseudo
272  * @name Slopefield
273  * @augments Vectorfield
274  * @constructor
275  * @type JXG.Curve
276  * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown.
277  * Parameter options:
278  * @param {Function|String} F Function f(x, y) returning a number.
279  * @param {Array} xData Array of length 3 containing start value for x, number of steps, end value of x. The slope field will contain
280  * (number of steps) + 1 vectors in direction of x.
281  * @param {Array} yData Array of length 3 containing start value for y, number of steps, end value of y. The slope field will contain
282  * (number of steps) + 1 vectors in direction of y.
283  * @example
284  * var field = board.create('slopefield', [
285  *     (x, y) => x * x - x - 2,
286  *     [-6, 25, 6], // Horizontal mesh
287  *     [-5, 20, 5]  // Vertical mesh
288  * ]);
289  *
290  * </pre><div id="JXG8a2ee562-eea1-4ce0-91ca-46b71fc7543d" class="jxgbox" style="width: 500px; height: 500px;"></div>
291  * <script type="text/javascript">
292  *     (function() {
293  *         var board = JXG.JSXGraph.initBoard('JXG8a2ee562-eea1-4ce0-91ca-46b71fc7543d',
294  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
295  *     var field = board.create('slopefield', [
296  *         (x, y) => x * x - x - 2,
297  *         [-6, 25, 6], [-5, 20, 5]
298  *     ]);
299  *
300  *     })();
301  *
302  * </script><pre>
303  *
304  * @example
305  * // Slider to control length of vectors
306  * var s = board.create('slider', [[-3, 7], [3, 7], [0, 0.33, 1]], {name: 'length'});
307  * // Slider to control number of steps
308  * var stepsize = board.create('slider', [[-3, 6], [3, 6], [1, 20, 100]], {name: 'steps', snapWidth: 1});
309  *
310  * var field = board.create('slopefield', [
311  *     (x, y) => x * x - y * y,
312  *     [-6, () => stepsize.Value(), 6],
313  *     [-5, () => stepsize.Value(), 5]],
314  *     {
315  *         strokeWidth: 1.5,
316  *         highlightStrokeWidth: 0.5,
317  *         highlightStrokeColor: JXG.palette.blue,
318  *
319  *         scale: () => s.Value(),
320  *
321  *         arrowHead: {
322  *             enabled: false,
323  *             size: 8,
324  *             angle: Math.PI / 16
325  *         }
326  *     });
327  *
328  * </pre><div id="JXG1ec9e4d7-6094-4d2b-b72f-4efddd514f55" class="jxgbox" style="width: 500px; height: 500px;"></div>
329  * <script type="text/javascript">
330  *     (function() {
331  *         var board = JXG.JSXGraph.initBoard('JXG1ec9e4d7-6094-4d2b-b72f-4efddd514f55',
332  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
333  *     // Slider to control length of vectors
334  *     var s = board.create('slider', [[-3, 7], [3, 7], [0, 0.33, 1]], {name: 'length'});
335  *     // Slider to control number of steps
336  *     var stepsize = board.create('slider', [[-3, 6], [3, 6], [1, 20, 100]], {name: 'steps', snapWidth: 1});
337  *
338  *     var field = board.create('slopefield', [
339  *         (x, y) => x * x - y * y,
340  *         [-6, () => stepsize.Value(), 6],
341  *         [-5, () => stepsize.Value(), 5]],
342  *         {
343  *             strokeWidth: 1.5,
344  *             highlightStrokeWidth: 0.5,
345  *             highlightStrokeColor: JXG.palette.blue,
346  *
347  *             scale: () => s.Value(),
348  *
349  *             arrowHead: {
350  *                 enabled: false,
351  *                 size: 8,
352  *                 angle: Math.PI / 16
353  *             }
354  *         });
355  *
356  *     })();
357  *
358  * </script><pre>
359  *
360  */
361 JXG.createSlopeField = function (board, parents, attributes) {
362     var el, f, attr;
363 
364     if (!(parents.length >= 3 &&
365         (Type.isFunction(parents[0]) || Type.isString(parents[0])) &&
366         (Type.isArray(parents[1]) && parents[1].length === 3) &&
367         (Type.isArray(parents[2]) && parents[2].length === 3)
368     )) {
369         throw new Error(
370             "JSXGraph: Can't create slope field with parent types " +
371             "'" + typeof parents[0] + "', " +
372             "'" + typeof parents[1] + "', " +
373             "'" + typeof parents[2] + "'."
374         );
375     }
376 
377     f = Type.createFunction(parents[0], board, 'x, y');
378     parents[0] = function (x, y) {
379         var z = f(x, y),
380             nrm = Math.sqrt(1 + z * z);
381         return [1 / nrm, z / nrm];
382     };
383     attr = Type.copyAttributes(attributes, board.options, 'slopefield');
384     /**
385      * @type {JXG.Curve}
386      * @ignore
387      */
388     el = board.create('vectorfield', parents, attr);
389     el.elType = 'slopefield';
390 
391     /**
392      * Set the defining functions of slope field.
393      * @name Slopefield#setF
394      * @function
395      * @param {Function} func Function f(x, y) returning a number.
396      * @returns {Object} Reference to the slope field object.
397      *
398      * @example
399      * field.setF((x, y) => x * x + y * y);
400      * board.update();
401      *
402      */
403     el.setF = function (func, varnames) {
404         var f = Type.createFunction(func, el.board, varnames);
405 
406         /**
407          * @ignore
408          */
409         this.F = function (x, y) {
410             var z = f(x, y),
411                 nrm = Math.sqrt(1 + z * z);
412             return [1 / nrm, z / nrm];
413         };
414     };
415 
416     el.methodMap = Type.deepCopy(el.methodMap, {
417         setF: "setF"
418     });
419 
420     return el;
421 };
422 
423 JXG.registerElement("slopefield", JXG.createSlopeField);
424