      1 /*
      2     Copyright 2008-2025
      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