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