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 = this.evalVisProp('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 = this.evalVisProp('arrowhead.enabled'), 220 leg, leg_x, leg_y, alpha; 221 222 223 if (showArrow) { 224 // Arrow head style 225 leg = this.evalVisProp('arrowhead.size'); 226 leg_x = leg / board.unitX; 227 leg_y = leg / board.unitY; 228 alpha = this.evalVisProp('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