1 /* 2 Copyright 2008-2025 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true*/ 33 /*jslint nomen: true, plusplus: true*/ 34 35 /** 36 * @fileoverview The geometry object slider is defined in this file. Slider stores all 37 * style and functional properties that are required to draw and use a slider on 38 * a board. 39 */ 40 41 import JXG from "../jxg.js"; 42 import Mat from "../math/math.js"; 43 import Const from "../base/constants.js"; 44 import Coords from "../base/coords.js"; 45 import Type from "../utils/type.js"; 46 import Point from "../base/point.js"; 47 48 /** 49 * @class A slider can be used to choose values from a given range of numbers. 50 * @pseudo 51 * @name Slider 52 * @augments Glider 53 * @constructor 54 * @type JXG.Point 55 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 56 * @param {Array_Array_Array} start,end,range The first two arrays give the start and the end where the slider is drawn 57 * on the board. The third array gives the start and the end of the range the slider operates as the first resp. the 58 * third component of the array. The second component of the third array gives its start value. 59 * 60 * @example 61 * // Create a slider with values between 1 and 10, initial position is 5. 62 * var s = board.create('slider', [[1, 2], [3, 2], [1, 5, 10]]); 63 * </pre><div class="jxgbox" id="JXGcfb51cde-2603-4f18-9cc4-1afb452b374d" style="width: 200px; height: 200px;"></div> 64 * <script type="text/javascript"> 65 * (function () { 66 * var board = JXG.JSXGraph.initBoard('JXGcfb51cde-2603-4f18-9cc4-1afb452b374d', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false}); 67 * var s = board.create('slider', [[1, 2], [3, 2], [1, 5, 10]]); 68 * })(); 69 * </script><pre> 70 * @example 71 * // Create a slider taking integer values between 1 and 5. Initial value is 3. 72 * var s = board.create('slider', [[1, 3], [3, 1], [0, 3, 5]], { 73 * snapWidth: 1, 74 * minTicksDistance: 60, 75 * drawLabels: false 76 * }); 77 * </pre><div class="jxgbox" id="JXGe17128e6-a25d-462a-9074-49460b0d66f4" style="width: 200px; height: 200px;"></div> 78 * <script type="text/javascript"> 79 * (function () { 80 * var board = JXG.JSXGraph.initBoard('JXGe17128e6-a25d-462a-9074-49460b0d66f4', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false}); 81 * var s = board.create('slider', [[1, 3], [3, 1], [1, 3, 5]], { 82 * snapWidth: 1, 83 * minTicksDistance: 60, 84 * drawLabels: false 85 * }); 86 * })(); 87 * </script><pre> 88 * @example 89 * // Draggable slider 90 * var s1 = board.create('slider', [[-3, 1], [2, 1],[-10, 1, 10]], { 91 * visible: true, 92 * snapWidth: 2, 93 * point1: {fixed: false}, 94 * point2: {fixed: false}, 95 * baseline: {fixed: false, needsRegularUpdate: true} 96 * }); 97 * 98 * </pre><div id="JXGbfc67817-2827-44a1-bc22-40bf312e76f8" class="jxgbox" style="width: 300px; height: 300px;"></div> 99 * <script type="text/javascript"> 100 * (function() { 101 * var board = JXG.JSXGraph.initBoard('JXGbfc67817-2827-44a1-bc22-40bf312e76f8', 102 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 103 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 104 * visible: true, 105 * snapWidth: 2, 106 * point1: {fixed: false}, 107 * point2: {fixed: false}, 108 * baseline: {fixed: false, needsRegularUpdate: true} 109 * }); 110 * 111 * })(); 112 * 113 * </script><pre> 114 * 115 * @example 116 * // Set the slider by clicking on the base line: attribute 'moveOnUp' 117 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 118 * snapWidth: 2, 119 * moveOnUp: true // default value 120 * }); 121 * 122 * </pre><div id="JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc" class="jxgbox" style="width: 300px; height: 300px;"></div> 123 * <script type="text/javascript"> 124 * (function() { 125 * var board = JXG.JSXGraph.initBoard('JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc', 126 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 127 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 128 * snapWidth: 2, 129 * moveOnUp: true // default value 130 * }); 131 * 132 * })(); 133 * 134 * </script><pre> 135 * 136 * @example 137 * // Set colors 138 * var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], { 139 * 140 * baseline: { strokeColor: 'blue'}, 141 * highline: { strokeColor: 'red'}, 142 * fillColor: 'yellow', 143 * label: {fontSize: 24, strokeColor: 'orange'}, 144 * name: 'xyz', // Not shown, if suffixLabel is set 145 * suffixLabel: 'x = ', 146 * postLabel: ' u' 147 * 148 * }); 149 * 150 * </pre><div id="JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401" class="jxgbox" style="width: 300px; height: 300px;"></div> 151 * <script type="text/javascript"> 152 * (function() { 153 * var board = JXG.JSXGraph.initBoard('JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401', 154 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 155 * var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], { 156 * 157 * baseline: { strokeColor: 'blue'}, 158 * highline: { strokeColor: 'red'}, 159 * fillColor: 'yellow', 160 * label: {fontSize: 24, strokeColor: 'orange'}, 161 * name: 'xyz', // Not shown, if suffixLabel is set 162 * suffixLabel: 'x = ', 163 * postLabel: ' u' 164 * 165 * }); 166 * 167 * })(); 168 * 169 * </script><pre> 170 * 171 * @example 172 * // Create a "frozen" slider 173 * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], { 174 * name:'a', 175 * frozen: true 176 * }); 177 * 178 * </pre><div id="JXG23afea4f-2e91-4006-a505-2895033cf1fc" class="jxgbox" style="width: 300px; height: 300px;"></div> 179 * <script type="text/javascript"> 180 * (function() { 181 * var board = JXG.JSXGraph.initBoard('JXG23afea4f-2e91-4006-a505-2895033cf1fc', 182 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 183 * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], { 184 * name:'a', 185 * frozen: true 186 * }); 187 * 188 * })(); 189 * 190 * </script><pre> 191 * 192 * @example 193 * // Use MathJax for slider label (don't forget to load MathJax) 194 * var s = board.create('slider', [[-3, 2], [2, 2], [-10, 1, 10]], { 195 * name: 'A^{(2)}', 196 * suffixLabel: '\\(A^{(2)} = ', 197 * unitLabel: ' \\;km/h ', 198 * postLabel: '\\)', 199 * label: {useMathJax: true} 200 * }); 201 * 202 * </pre><div id="JXG76e78c5f-3598-4d44-b43f-1d78fd15302c" class="jxgbox" style="width: 300px; height: 300px;"></div> 203 * <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" id="MathJax-script"></script> 204 * <script type="text/javascript"> 205 * (function() { 206 * var board = JXG.JSXGraph.initBoard('JXG76e78c5f-3598-4d44-b43f-1d78fd15302c', 207 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 208 * // Use MathJax for slider label (don't forget to load MathJax) 209 * var s = board.create('slider', [[-3,2], [2,2],[-10,1,10]], { 210 * name: 'A^{(2)}', 211 * suffixLabel: '\\(A^{(2)} = ', 212 * unitLabel: ' \\;km/h ', 213 * postLabel: '\\)', 214 * label: {useMathJax: true} 215 * }); 216 * 217 * })(); 218 * 219 * </script><pre> 220 * 221 * 222 */ 223 JXG.createSlider = function (board, parents, attributes) { 224 var pos0, pos1, 225 smin, start, smax, diff, 226 p1, p2, p3, l1, l2, 227 ticks, ti, t, 228 startx, starty, 229 withText, withTicks, 230 snapValues, snapValueDistance, 231 snapWidth, sw, s, 232 attr; 233 234 attr = Type.copyAttributes(attributes, board.options, "slider"); 235 withTicks = attr.withticks; 236 withText = attr.withlabel; 237 snapWidth = attr.snapwidth; 238 snapValues = attr.snapvalues; 239 snapValueDistance = attr.snapvaluedistance; 240 241 // Start point 242 p1 = board.create("point", parents[0], attr.point1); 243 244 // End point 245 p2 = board.create("point", parents[1], attr.point2); 246 //g = board.create('group', [p1, p2]); 247 248 // Base line 249 l1 = board.create("segment", [p1, p2], attr.baseline); 250 251 // This is required for a correct projection of the glider onto the segment below 252 l1.updateStdform(); 253 254 pos0 = p1.coords.usrCoords.slice(1); 255 pos1 = p2.coords.usrCoords.slice(1); 256 smin = parents[2][0]; 257 start = parents[2][1]; 258 smax = parents[2][2]; 259 diff = smax - smin; 260 261 sw = Type.evaluate(snapWidth); 262 s = sw === -1 ? 263 start : 264 Math.round((start - smin)/ sw) * sw + smin; 265 // Math.round(start / sw) * sw; 266 startx = pos0[0] + ((pos1[0] - pos0[0]) * (s - smin)) / (smax - smin); 267 starty = pos0[1] + ((pos1[1] - pos0[1]) * (s - smin)) / (smax - smin); 268 269 // glider point 270 // attr = Type.copyAttributes(attributes, board.options, "slider"); 271 // overwrite this in any case; the sliders label is a special text element, not the gliders label. 272 // this will be set back to true after the text was created (and only if withlabel was true initially). 273 attr.withlabel = false; 274 // gliders set snapwidth=-1 by default (i.e. deactivate them) 275 p3 = board.create("glider", [startx, starty, l1], attr); 276 p3.setAttribute({ snapwidth: snapWidth, snapvalues: snapValues, snapvaluedistance: snapValueDistance }); 277 278 // Segment from start point to glider point: highline 279 // attr = Type.copyAttributes(attributes, board.options, "slider", "highline"); 280 l2 = board.create("segment", [p1, p3], attr.highline); 281 282 /** 283 * Returns the current slider value. 284 * @memberOf Slider.prototype 285 * @name Value 286 * @function 287 * @returns {Number} 288 */ 289 p3.Value = function () { 290 var d = this._smax - this._smin, 291 ev_sw = this.evalVisProp('snapwidth'); 292 293 return ev_sw === -1 294 ? this.position * d + this._smin 295 : Math.round((this.position * d) / ev_sw) * ev_sw + this._smin; 296 }; 297 298 p3.methodMap = Type.deepCopy(p3.methodMap, { 299 Value: "Value", 300 setValue: "setValue", 301 smax: "_smax", 302 // Max: "_smax", 303 smin: "_smin", 304 // Min: "_smin", 305 setMax: "setMax", 306 setMin: "setMin", 307 point1: "point1", 308 point2: "point2", 309 baseline: "baseline", 310 highline: "highline", 311 ticks: "ticks", 312 label: "label" 313 }); 314 315 /** 316 * End value of the slider range. 317 * @memberOf Slider.prototype 318 * @name _smax 319 * @type Number 320 */ 321 p3._smax = smax; 322 323 /** 324 * Start value of the slider range. 325 * @memberOf Slider.prototype 326 * @name _smin 327 * @type Number 328 */ 329 p3._smin = smin; 330 331 /** 332 * Sets the maximum value of the slider. 333 * @memberOf Slider.prototype 334 * @function 335 * @name setMax 336 * @param {Number} val New maximum value 337 * @returns {Object} this object 338 */ 339 p3.setMax = function (val) { 340 this._smax = val; 341 return this; 342 }; 343 344 /** 345 * Sets the value of the slider. This call must be followed 346 * by a board update call. 347 * @memberOf Slider.prototype 348 * @name setValue 349 * @function 350 * @param {Number} val New value 351 * @returns {Object} this object 352 */ 353 p3.setValue = function (val) { 354 var d = this._smax - this._smin; 355 356 if (Math.abs(d) > Mat.eps) { 357 this.position = (val - this._smin) / d; 358 } else { 359 this.position = 0.0; //this._smin; 360 } 361 this.position = Math.max(0.0, Math.min(1.0, this.position)); 362 return this; 363 }; 364 365 /** 366 * Sets the minimum value of the slider. 367 * @memberOf Slider.prototype 368 * @name setMin 369 * @function 370 * @param {Number} val New minimum value 371 * @returns {Object} this object 372 */ 373 p3.setMin = function (val) { 374 this._smin = val; 375 return this; 376 }; 377 378 if (withText) { 379 // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label'); 380 t = board.create('text', [ 381 function () { 382 return (p2.X() - p1.X()) * 0.05 + p2.X(); 383 }, 384 function () { 385 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 386 }, 387 function () { 388 var n, 389 d = p3.evalVisProp('digits'), 390 sl = p3.evalVisProp('suffixlabel'), 391 ul = p3.evalVisProp('unitlabel'), 392 pl = p3.evalVisProp('postlabel'); 393 394 if (d === 2 && p3.evalVisProp('precision') !== 2) { 395 // Backwards compatibility 396 d = p3.evalVisProp('precision'); 397 } 398 399 if (sl !== null) { 400 n = sl; 401 } else if (p3.name && p3.name !== "") { 402 n = p3.name + " = "; 403 } else { 404 n = ""; 405 } 406 407 if (p3.useLocale()) { 408 n += p3.formatNumberLocale(p3.Value(), d); 409 } else { 410 n += Type.toFixed(p3.Value(), d); 411 } 412 413 if (ul !== null) { 414 n += ul; 415 } 416 if (pl !== null) { 417 n += pl; 418 } 419 420 return n; 421 } 422 ], 423 attr.label 424 ); 425 426 /** 427 * The text element to the right of the slider, indicating its current value. 428 * @memberOf Slider.prototype 429 * @name label 430 * @type JXG.Text 431 */ 432 p3.label = t; 433 434 // reset the withlabel attribute 435 p3.visProp.withlabel = true; 436 p3.hasLabel = true; 437 } 438 439 /** 440 * Start point of the base line. 441 * @memberOf Slider.prototype 442 * @name point1 443 * @type JXG.Point 444 */ 445 p3.point1 = p1; 446 447 /** 448 * End point of the base line. 449 * @memberOf Slider.prototype 450 * @name point2 451 * @type JXG.Point 452 */ 453 p3.point2 = p2; 454 455 /** 456 * The baseline the glider is bound to. 457 * @memberOf Slider.prototype 458 * @name baseline 459 * @type JXG.Line 460 */ 461 p3.baseline = l1; 462 463 /** 464 * A line on top of the baseline, indicating the slider's progress. 465 * @memberOf Slider.prototype 466 * @name highline 467 * @type JXG.Line 468 */ 469 p3.highline = l2; 470 471 if (withTicks) { 472 // Function to generate correct label texts 473 474 // attr = Type.copyAttributes(attributes, board.options, "slider", "ticks"); 475 if (!Type.exists(attr.generatelabeltext)) { 476 attr.ticks.generateLabelText = function (tick, zero, value) { 477 var labelText, 478 dFull = p3.point1.Dist(p3.point2), 479 smin = p3._smin, 480 smax = p3._smax, 481 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 482 483 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 484 // Point is zero 485 labelText = "0"; 486 } else { 487 labelText = this.formatLabelText(val); 488 } 489 return labelText; 490 }; 491 } 492 ticks = 2; 493 ti = board.create( 494 "ticks", 495 [ 496 p3.baseline, 497 p3.point1.Dist(p1) / ticks, 498 499 function (tick) { 500 var dFull = p3.point1.Dist(p3.point2), 501 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 502 503 if (dFull < Mat.eps) { 504 return 0; 505 } 506 507 return (d / dFull) * diff + smin; 508 } 509 ], 510 attr.ticks 511 ); 512 513 /** 514 * Ticks give a rough indication about the slider's current value. 515 * @memberOf Slider.prototype 516 * @name ticks 517 * @type JXG.Ticks 518 */ 519 p3.ticks = ti; 520 } 521 522 // override the point's remove method to ensure the removal of all elements 523 p3.remove = function () { 524 if (withText) { 525 board.removeObject(t); 526 } 527 528 board.removeObject(l2); 529 board.removeObject(l1); 530 board.removeObject(p2); 531 board.removeObject(p1); 532 533 Point.prototype.remove.call(p3); 534 }; 535 536 p1.dump = false; 537 p2.dump = false; 538 l1.dump = false; 539 l2.dump = false; 540 if (withText) { 541 t.dump = false; 542 } 543 544 // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER 545 p3.elType = "slider"; 546 p3.parents = parents; 547 p3.subs = { 548 point1: p1, 549 point2: p2, 550 baseLine: l1, 551 highLine: l2 552 }; 553 p3.inherits.push(p1, p2, l1, l2); 554 // Remove inherits to avoid circular inherits. 555 l1.inherits = []; 556 l2.inherits = []; 557 558 if (withTicks) { 559 ti.dump = false; 560 p3.subs.ticks = ti; 561 p3.inherits.push(ti); 562 } 563 564 p3.getParents = function () { 565 return [ 566 this.point1.coords.usrCoords.slice(1), 567 this.point2.coords.usrCoords.slice(1), 568 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 569 ]; 570 }; 571 572 p3.baseline.on("up", function (evt) { 573 var pos, c; 574 575 if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) { 576 pos = l1.board.getMousePosition(evt, 0); 577 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 578 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 579 p3.triggerEventHandlers(['drag'], [evt]); 580 } 581 }); 582 583 // This is necessary to show baseline, highline and ticks 584 // when opening the board in case the visible attributes are set 585 // to 'inherit'. 586 p3.prepareUpdate().update(); 587 if (!board.isSuspendedUpdate) { 588 p3.updateVisibility().updateRenderer(); 589 p3.baseline.updateVisibility().updateRenderer(); 590 p3.highline.updateVisibility().updateRenderer(); 591 if (withTicks) { 592 p3.ticks.updateVisibility().updateRenderer(); 593 } 594 } 595 596 return p3; 597 }; 598 599 JXG.registerElement("slider", JXG.createSlider); 600 601 // export default { 602 // createSlider: JXG.createSlider 603 // }; 604