1 /* 2 Copyright 2008-2026 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@4/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 /** 299 * End value of the slider range. 300 * @memberOf Slider.prototype 301 * @name _smax 302 * @type Number 303 */ 304 p3._smax = smax; 305 306 /** 307 * Start value of the slider range. 308 * @memberOf Slider.prototype 309 * @name _smin 310 * @type Number 311 */ 312 p3._smin = smin; 313 314 /** 315 * Sets the maximum value of the slider. 316 * @memberOf Slider.prototype 317 * @function 318 * @name setMax 319 * @param {Number} val New maximum value 320 * @returns {Object} this object 321 */ 322 p3.setMax = function (val) { 323 this._smax = val; 324 return this; 325 }; 326 327 /** 328 * Sets the value of the slider. This call must be followed 329 * by a board update call. 330 * @memberOf Slider.prototype 331 * @name setValue 332 * @function 333 * @param {Number} val New value 334 * @returns {Object} this object 335 */ 336 p3.setValue = function (val) { 337 var d = this._smax - this._smin; 338 339 if (Math.abs(d) > Mat.eps) { 340 this.position = (val - this._smin) / d; 341 } else { 342 this.position = 0.0; //this._smin; 343 } 344 this.position = Math.max(0.0, Math.min(1.0, this.position)); 345 return this; 346 }; 347 348 /** 349 * Sets the minimum value of the slider. 350 * @memberOf Slider.prototype 351 * @name setMin 352 * @function 353 * @param {Number} val New minimum value 354 * @returns {Object} this object 355 */ 356 p3.setMin = function (val) { 357 this._smin = val; 358 return this; 359 }; 360 361 if (withText) { 362 // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label'); 363 t = board.create('text', [ 364 function () { 365 return (p2.X() - p1.X()) * 0.05 + p2.X(); 366 }, 367 function () { 368 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 369 }, 370 function () { 371 var n, 372 d = p3.evalVisProp('digits'), 373 sl = p3.evalVisProp('suffixlabel'), 374 ul = p3.evalVisProp('unitlabel'), 375 pl = p3.evalVisProp('postlabel'); 376 377 if (d === 2 && p3.evalVisProp('precision') !== 2) { 378 // Backwards compatibility 379 d = p3.evalVisProp('precision'); 380 } 381 382 if (sl !== null) { 383 n = sl; 384 } else if (p3.name && p3.name !== "") { 385 n = p3.name + " = "; 386 } else { 387 n = ""; 388 } 389 390 if (p3.useLocale()) { 391 n += p3.formatNumberLocale(p3.Value(), d); 392 } else { 393 n += Type.toFixed(p3.Value(), d); 394 } 395 396 if (ul !== null) { 397 n += ul; 398 } 399 if (pl !== null) { 400 n += pl; 401 } 402 403 return n; 404 } 405 ], 406 attr.label 407 ); 408 409 /** 410 * The text element to the right of the slider, indicating its current value. 411 * @memberOf Slider.prototype 412 * @name label 413 * @type JXG.Text 414 */ 415 p3.label = t; 416 417 // reset the withlabel attribute 418 p3.visProp.withlabel = true; 419 p3.hasLabel = true; 420 p3.inherits.push(t); 421 t.addParents(p3); 422 p3.addChild(t); 423 } 424 425 /** 426 * Start point of the base line. 427 * @memberOf Slider.prototype 428 * @name point1 429 * @type JXG.Point 430 */ 431 p3.point1 = p1; 432 433 /** 434 * End point of the base line. 435 * @memberOf Slider.prototype 436 * @name point2 437 * @type JXG.Point 438 */ 439 p3.point2 = p2; 440 441 /** 442 * The baseline the glider is bound to. 443 * @memberOf Slider.prototype 444 * @name baseline 445 * @type JXG.Line 446 */ 447 p3.baseline = l1; 448 449 /** 450 * A line on top of the baseline, indicating the slider's progress. 451 * @memberOf Slider.prototype 452 * @name highline 453 * @type JXG.Line 454 */ 455 p3.highline = l2; 456 457 if (withTicks) { 458 // Function to generate correct label texts 459 460 // attr = Type.copyAttributes(attributes, board.options, "slider", 'ticks'); 461 if (!Type.exists(attr.generatelabeltext)) { 462 attr.ticks.generateLabelText = function (tick, zero, value) { 463 var labelText, 464 dFull = p3.point1.Dist(p3.point2), 465 smin = p3._smin, 466 smax = p3._smax, 467 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 468 469 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 470 // Point is zero 471 labelText = '0'; 472 } else { 473 labelText = this.formatLabelText(val); 474 } 475 return labelText; 476 }; 477 } 478 ticks = 2; 479 ti = board.create( 480 "ticks", 481 [ 482 p3.baseline, 483 p3.point1.Dist(p1) / ticks, 484 485 function (tick) { 486 var dFull = p3.point1.Dist(p3.point2), 487 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 488 489 if (dFull < Mat.eps) { 490 return 0; 491 } 492 493 return (d / dFull) * diff + smin; 494 } 495 ], 496 attr.ticks 497 ); 498 499 /** 500 * Ticks give a rough indication about the slider's current value. 501 * @memberOf Slider.prototype 502 * @name ticks 503 * @type JXG.Ticks 504 */ 505 p3.ticks = ti; 506 } 507 508 // override the point's remove method to ensure the removal of all elements 509 p3.remove = function () { 510 if (withText) { 511 board.removeObject(t); 512 } 513 514 board.removeObject(l2); 515 board.removeObject(l1); 516 board.removeObject(p2); 517 board.removeObject(p1); 518 519 Point.prototype.remove.call(p3); 520 }; 521 522 p1.dump = false; 523 p2.dump = false; 524 l1.dump = false; 525 l2.dump = false; 526 if (withText) { 527 t.dump = false; 528 } 529 530 // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER 531 p3.elType = 'slider'; 532 p3.parents = parents; 533 p3.subs = { 534 point1: p1, 535 point2: p2, 536 baseLine: l1, 537 highLine: l2 538 }; 539 p3.inherits.push(p1, p2, l1, l2); 540 // Remove inherits to avoid circular inherits. 541 l1.inherits = []; 542 l2.inherits = []; 543 544 if (withTicks) { 545 ti.dump = false; 546 p3.subs.ticks = ti; 547 p3.inherits.push(ti); 548 } 549 550 p3.getParents = function () { 551 return [ 552 this.point1.coords.usrCoords.slice(1), 553 this.point2.coords.usrCoords.slice(1), 554 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 555 ]; 556 }; 557 558 p3.baseline.on("up", function (evt) { 559 var pos, c; 560 561 if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) { 562 pos = l1.board.getMousePosition(evt, 0); 563 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 564 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 565 p3.triggerEventHandlers(['drag'], [evt]); 566 } 567 }); 568 569 // This is necessary to show baseline, highline and ticks 570 // when opening the board in case the visible attributes are set 571 // to 'inherit'. 572 p3.prepareUpdate().update(); 573 if (!board.isSuspendedUpdate) { 574 p3.updateVisibility().updateRenderer(); 575 p3.baseline.prepareUpdate().updateVisibility().updateRenderer(); // prepareUpdate needed because needsRegularUpdate==false 576 p3.highline.updateVisibility().updateRenderer(); 577 if (withTicks) { 578 p3.prepareUpdate().ticks.updateVisibility().updateRenderer(); 579 } 580 } 581 582 Type.extendInstanceMethodMap(p3, { 583 Value: "Value", 584 setValue: "setValue", 585 smax: "_smax", 586 // Max: "_smax", 587 smin: "_smin", 588 // Min: "_smin", 589 setMax: "setMax", 590 setMin: "setMin", 591 point1: "point1", 592 point2: "point2", 593 baseline: "baseline", 594 highline: "highline", 595 ticks: "ticks", 596 label: "label" 597 }); 598 599 return p3; 600 }; 601 602 JXG.registerElement("slider", JXG.createSlider); 603 604 // export default { 605 // createSlider: JXG.createSlider 606 // }; 607