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