Last active
January 28, 2022 04:51
-
-
Save jason-woolf/48774d11699846ad58ec63fa3019faa5 to your computer and use it in GitHub Desktop.
Programmatically control a Desmos graph
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name desmosPlayer | |
// @namespace http://github.com/jason-woolf | |
// @version 1.0.0 | |
// @description Program a series of graph changes to create animation | |
// @author Jason Woolf (MathyJaphy) | |
// @match https://www.desmos.com/calculator* | |
// @grant none | |
// @run-at document-idle | |
// ==/UserScript== | |
// LICENSE: ISC | |
// Fork of https://gist.github.com/jared-hughes/b62dc3042947d6dcee16a186301227dc | |
"use strict" | |
/***************************************************************************** | |
// Example 1: MathyJaphy Intro/Logo | |
// Graph for this example: https://www.desmos.com/calculator/y3lulgdmir | |
// | |
// This is the animation of my channel logo that I used in this video: | |
// https://youtu.be/sV1NbgNodD0 | |
// Helpful symbolic names for expression ID's | |
const a = 116 | |
const b1 = 139 | |
const b2 = 140 | |
const b3 = 141 | |
const b4 = 142 | |
const f1 = 131 | |
const f2 = 133 | |
const f3 = 136 | |
const f4 = 138 | |
const f5 = 128 | |
const name_angle = 144 | |
const name_size = 145 | |
const boing = 148 | |
const w1 = 775 | |
const w2 = 776 | |
// Define the program | |
const mathyJaphy = [ | |
startSlider(b1, 1000), | |
startSlider(b2, 1000), | |
startSlider(b3, 1000), | |
startSlider(b4, 1000), | |
startSlider(w1), | |
animateValue(w2, 0, 1.5, 0.1, 0, 3000), | |
animateValue(w2, 1.5, 0, 0.07, 0, 1000), | |
stopSlider(w1), | |
startSlider(f1, 1000), | |
startSlider(f2, 1000), | |
startSlider(f3, 1000), | |
startSlider(f4, 1000), | |
startSlider(f5, 3000), | |
[startSlider(name_angle), | |
startSlider(name_size)], | |
pause(2500), | |
startSlider(boing) | |
] | |
// Run it | |
desmosPlayer(mathyJaphy, {graphTitle: "MathyJaphy", debugMode: true}); | |
******************************************************************************/ | |
/***************************************************************************** | |
// Example 2: Binary Counter | |
// Graph for this example: https://www.desmos.com/calculator/o61fs3lfjb | |
// | |
// A simple binary counter with "lights" for each bit. I wanted to see how | |
// intricate a program I could create with the goto/label instructions. | |
// Helpful symbolic names for expression ID's | |
const ones = 8; | |
const twos = 6; | |
const fours = 4; | |
const eights = 2; | |
// Define the program | |
const binaryCounter = [ | |
label("clear eights"), | |
hide(eights), | |
label("clear fours"), | |
hide(fours), | |
label("clear twos"), | |
hide(twos), | |
label("clear ones"), | |
hide(ones), | |
pause(1000), | |
show(ones), | |
pause(1000), | |
show(twos), | |
goto("clear ones", 1), | |
show(fours), | |
goto("clear twos", 1), | |
show(eights), | |
goto("clear fours", 1), | |
goto("clear eights"), | |
] | |
// Run it | |
desmosPlayer(binaryCounter, {graphTitle: "Binary Counter", debugMode: true}); | |
******************************************************************************/ | |
// DesmosPlayer - a module for controlling Desmos graphs for | |
// making animated videos or whatever. | |
// | |
// Function: desmosPlayer | |
// Parameters: program - an array of functions to be executed | |
// in sequence that control expressions | |
// in a Desmos graph. | |
// properties - An object that conveys additional | |
// configuration parameters. | |
// | |
// This is the main function of the DesmosPlayer module. It sets up the | |
// environment needed to run the given program. Create a program by | |
// filling an array with the results of "instruction functions" (see | |
// examples above). Then pass this program to the desmosPlayer function, | |
// along with optional properties: | |
// | |
// graphTitle: A string value that has to match the title of the graph | |
// in order for the system to be activated. | |
// allowSave: If set to true, do not disable the "save" button after | |
// the program has started. It is normally disabled to | |
// prevent accidental modification to the initial state. | |
// debugMode: Set to 'true' to enable back-stepping. | |
// | |
// A "Start" button will be added next to the "Save" button. | |
// Clicking it will start the program and turn it into a "Stop" | |
// button. Clicking it again will stop execution of the program. A | |
// "Reset" button will also appear which when clicked will put the graph and | |
// the running program back to their initial states so that the program can | |
// be run again. Note: if allowSave mode is on and the save button is | |
// accidentally used to overwrite the original graph after the program has | |
// run, hit the "Reset" button and save again. There is also a "Step" | |
// button which will execute one function in the program at a time. If | |
// debugMode is true, then a "Back Step" button also appears after the | |
// program has started, allowing you to undo the effect of the previous | |
// step, all the way back to the start of the program. This is expensive | |
// since it saves the entire graph state after every step. | |
// | |
// Each instruction takes one or more expression ID parameters, which are | |
// not the same as the index of the expression displayed in the expression | |
// list. To get the ID of an expression, select it and press ctrl-Q. The ID | |
// will appear in the console window. Normally these are numbers, but they | |
// could be strings if the expressions were created by your program (see the | |
// set command below). | |
// | |
// Execution of the instructions proceeds automatically from one to the next. | |
// The pause() instruction can be used to insert delays. Also, most | |
// instructions take an optional delay value as the final parameter which | |
// inserts a pause implicitly. | |
// | |
// Instructions can be grouped together using square brackets. Instructions | |
// in such groupings will be run without delays and without giving Desmos a | |
// chance to update its graph until they have all run. This can eliminate | |
// glitches in the graph and make it look as though the instructions ran | |
// simultaneously. For example: | |
// | |
// const testProg = [ | |
// startSlider(1, 5000), | |
// [setValue(1, 0), | |
// setValue(2, 1.0), | |
// setValue(3, 2.5), | |
// hideLabel(3), | |
// showLabel(4)], | |
// startSlider(1, 5000) | |
// ] | |
// | |
// In this hypothetical scenario, a slider runs once to animate something | |
// and then it is reset and a bunch of other elements are altered, hidden | |
// and revealed all at once, then the animation is run again. Without the | |
// grouping, intermediate settings might be visible as glitches. Delays | |
// within the grouped commands are ignored, and animateValue instructions | |
// will go directly to the ending value. It is an error to have a goto | |
// instruction inside a grouping. | |
// | |
// When each instruction is executed, a message is displayed in the console | |
// so it is possible to see what the program is doing. When back-stepping, | |
// the console will show the instruction that was just undone. | |
// | |
// Here is a summary of the instructions. Detailed descriptions appear above | |
// each instruction function in the code below. | |
// | |
// hide (<id>, [<id>, ...]) | |
// show (<id>, [<id>, ...]) | |
// hideLabel (<id>, [<id>, ...]) | |
// showLabel (<id>, [<id>, ...]) | |
// setLabel (<id>, <label-string>) | |
// setValue (<id>, <value>, [<delay>]) | |
// startSlider (<id>, [<delay>]) | |
// stopSlider (<id>, [<delay>]) | |
// animateValue (<id>, <start-value>, <end-value>, <increment>, [<frame-delay>], [<delay>]) | |
// setSliderProperties (<id>, {<properties>}, [<delay>]) | |
// set (<id>, <properties>, [<delay>]) | |
// stop (<message-string>) | |
// pause (<delay>) | |
// label (<label-name-string>) | |
// goto (<label-name-string>, [<repeat-count>]) | |
// Instruction: hide | |
// Parameter: ids - comma separated list of expression id's to hide | |
// Hides all the expressions given as arguments to the instruction. | |
// Equivalent to set(id, {hidden: true}) for each of the given id's. | |
// Moves on to the next instruction immediately. | |
function hide (...ids) { | |
let verify = 1; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
for (let id of ids) { | |
verifyId(id); | |
} | |
} | |
for (let id of ids) { | |
Calc.setExpression({id, hidden: true}); | |
} | |
} | |
func.desc = ["hide", arguments]; | |
return func; | |
} | |
// Instruction: show | |
// Parameter: ids - comma separated list of expression id's | |
// Shows (un-hides) all the expressions given as arguments to the instruction. | |
// Equivalent to set(id, {hidden: false}) for each of the given id's. | |
// Moves on to the next instruction immediately. | |
function show (...ids) { | |
let verify = 1; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
for (let id of ids) { | |
verifyId(id); | |
} | |
} | |
for (let id of ids) { | |
Calc.setExpression({id, hidden: false}); | |
} | |
} | |
func.desc = ["show", arguments]; | |
return func; | |
} | |
// Instruction: hideLabel | |
// parameter: ids - comma separated list of expression id's | |
// Turns off the label of all the expressions given as arguments to | |
// the instruction. Equivalent to set(id, {showLabel: false}) for | |
// each of the given id's. Moves on to the next instruction | |
// immediately. | |
function hideLabel (...ids) { | |
let verify = 1; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
for (let id of ids) { | |
verify(id); | |
} | |
} | |
for (let id of ids) { | |
Calc.setExpression({id, showLabel: false}); | |
} | |
} | |
func.desc = ["hideLabel", arguments]; | |
return func; | |
} | |
// Instruction: showLabel | |
// parameter: ids - comma separated list of expression id's | |
// Turns on the label of all the expressions given as arguments to | |
// the instruction. Equivalent to set(id, {showLabel: true}) for | |
// each of the given id's. Moves on to the next instruction | |
// immediately. | |
function showLabel (...ids) { | |
let verify = 1; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
for (let id of ids) { | |
verifyId(id); | |
} | |
} | |
for (let id of ids) { | |
Calc.setExpression({id, showLabel: true}); | |
} | |
} | |
func.desc = ["showLabel", arguments]; | |
return func; | |
} | |
// Instruction: setLabel | |
// Parameters: id - id of an expression with a label | |
// labelStr - the desired label string | |
// Sets the label of the given expression to the given string. | |
// If the string uses latex, enclose the latex in back-ticks, | |
// as usual. | |
function setLabel (id, labelStr) { | |
let verify = 1; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
verifyId(id); | |
} | |
Calc.setExpression({id, label: labelStr}); | |
return 0; | |
} | |
func.desc = ["setLabel", arguments]; | |
return func; | |
} | |
// Instruction: setValue | |
// Parameters: id - id of a "<name>=<value>" type of expression | |
// value - latex string or number of new value | |
// delay - number of ms to delay before next instruction | |
// Replaces the <value> part of the given expression with the given value. | |
// The value can be any latex expression string or a number. If the | |
// expression has a slider that is playing, it will be stopped first. | |
function setValue (id, value, delay=0) { | |
let verify = 1; | |
const stop_slider = {id, type: 'set-slider-isplaying', isPlaying: 0}; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
verifyId(id); | |
} | |
let name = Calc.getExpressions().filter(e => e.id === id.toString())[0].latex; | |
name = name.slice(0, name.lastIndexOf("=") + 1); | |
if (name[name.length - 1] != '=') { | |
throw "expression does not contain '='"; | |
} | |
const obj = { id, latex: name + value }; | |
Calc.controller.dispatch(stop_slider); | |
Calc.setExpression(obj); | |
return delay; | |
} | |
func.desc = ["setValue", arguments]; | |
return func; | |
} | |
// Instruction: startSlider | |
// Parameters: id - the id of an expression that has a slider | |
// delay - number of ms to delay before next instruction | |
// Equivalent to pressing the play button on a slider when it is not | |
// yet running. | |
function startSlider (id, delay=0) { | |
let verify = 1; | |
const obj = {id, type: 'set-slider-isplaying', isPlaying: 1}; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
verifyId(id); | |
} | |
Calc.controller.dispatch(obj); | |
return delay; | |
} | |
func.desc = ["startSlider", arguments]; | |
return func; | |
} | |
// Instruction: stopSlider | |
// Parameters: id - the id of an expression that has a slider | |
// delay - number of ms to delay before next instruction | |
// Equivalent to pressing the pause button on a slider when it is | |
// already running. | |
function stopSlider (id, delay=0) { | |
let verify = 1; | |
const obj = {id, type: 'set-slider-isplaying', isPlaying: 0}; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
verifyId(id); | |
} | |
Calc.controller.dispatch(obj); | |
return delay; | |
} | |
func.desc = ["stopSlider", arguments]; | |
return func; | |
} | |
// Instruction: animateValue | |
// Parmeters: id - the id of a "<name>=<value>" type of expression | |
// startVal - the starting value for the animation | |
// endVal - the ending value for the animation | |
// interval - the amount to step by on each frame | |
// frameTime - number of ms of explicit delay between frames | |
// delay - number of ms to wait after animation is done | |
// Animates a variable by setting its value to startVal and incrementing or | |
// decrementing it by the given interval until it reaches endVal. Speed | |
// can be controlled by changing the interval and by giving a frameTime value. | |
// This is similar to playing a slider, but allows explicit control over the | |
// speed and the step size, and allows running in reverse. The instruction | |
// blocks until the endVal is reached, unlike startSlider which moves on to | |
// the next instruction after the slider starts. If the expression has a | |
// slider that is playing, it will be stopped first. | |
function animateValue(id, startVal, endVal, interval, frameTime=0, delay=0) { | |
let name; | |
let stop_cond; | |
let verify = 1; | |
if (startVal == endVal) throw "Animate values equal"; | |
if (interval <= 0) throw "Bad interval parameter: " + interval; | |
function animateHelper (val, incr) { | |
if (stop_cond(val, endVal) || animating == 2) { | |
const obj = { id, latex: name + endVal } | |
Calc.setExpression(obj); | |
animating = 0; | |
} else { | |
val += incr | |
const obj = { id, latex: name + val.toFixed(5) }; | |
Calc.setExpression(obj); | |
setTimeout(() => animateHelper(val, incr), frameTime); | |
} | |
} | |
const stop_slider = {id, type: 'set-slider-isplaying', isPlaying: 0}; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
verifyId(id); | |
} | |
Calc.controller.dispatch(stop_slider); | |
name = Calc.getExpressions().filter(e => e.id === id.toString())[0].latex; | |
name = name.slice(0, name.lastIndexOf("=")+1); | |
if (name[name.length - 1] != '=') { | |
throw "expression does not contain '='"; | |
} | |
if (grouping) { | |
// Running as a group, so go straight to the end value | |
stop_cond = (val, endVal) => (true); | |
animateHelper(endVal); | |
} else if (startVal < endVal) { | |
stop_cond = (val, endVal) => (val >= endVal); | |
animating = 1; | |
animateHelper(startVal, interval); | |
} else { | |
stop_cond = (val, endVal) => (val <= endVal); | |
animating = 1; | |
animateHelper(startVal, -interval); | |
} | |
return delay; | |
} | |
func.desc = ["animateValue", arguments]; | |
return func; | |
} | |
// Instruction: setSliderProperties | |
// Parmeters: id - the id of an expression with a slider | |
// properties - the properties to be set | |
// delay - number of ms to wait before next instruction | |
// Sets the given properties of the given slider. The properties are: | |
// min: <latex or number> | |
// max: <latex or number> | |
// step: <latex or number> | |
// period: <time in ms from min to max> | |
// loopMode: "LOOP_FORWARD_REVERSE" or | |
// "LOOP_FORWARD" or | |
// "PLAY_ONCE" or | |
// "PLAY_FOREVER" | |
function setSliderProperties(id, properties, delay=0) { | |
let verify = 1; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
verifyId(id); | |
} | |
if (properties.min !== undefined) { | |
Calc.controller.dispatch({id, type: 'set-slider-minlatex', latex: properties.min.toString()}); | |
} | |
if (properties.max !== undefined) { | |
Calc.controller.dispatch({id, type: 'set-slider-maxlatex', latex: properties.max.toString()}); | |
} | |
if (properties.step !== undefined) { | |
Calc.controller.dispatch({id, type: 'set-slider-steplatex', latex: properties.step.toString()}); | |
} | |
if (properties.period !== undefined) { | |
Calc.controller.dispatch({id, type: 'set-slider-animationperiod', animationPeriod: properties.period}); | |
} | |
if (properties.loopmode !== undefined) { | |
Calc.controller.dispatch({id, type: 'set-slider-loopmode', loopMode: properties.loopmode}); | |
} | |
return delay; | |
} | |
func.desc = ["setSliderLimits", arguments]; | |
return func; | |
} | |
// Instruction: set | |
// Parameter: id - id of an expression to operate on | |
// properties - an object containing property/value pairs | |
// delay - number of ms to delay before next instruction | |
// This is a generic instruction to set any property on the given expression. | |
// See the API documentation of Calculator.setExpression to see what can | |
// be set. Example args parameter: {label: "Hi", showLabel: true}. This | |
// instruction can be used to create a new expression, if the given id does | |
// not currently exist and the 'latex' property is provided. | |
// Note: if using this to set the 'latex' property, this may not have any | |
// effect if the expression's slider is playing (even if it is set to "play once" | |
// and has reached the maximum value). Use stopSlider beforehand, if necessary. | |
function set (id, properties, delay=0) { | |
let verify = 1; | |
const obj = { id, ...properties }; | |
const func = () => { | |
if (verify) { | |
verify = 0; | |
const expr = Calc.getExpressions().filter(e => e.id === id.toString())[0]; | |
if (expr === undefined) { | |
if (properties.latex === undefined) { | |
throw("set function with unknown id without latex property"); | |
} | |
} | |
} | |
Calc.setExpression(obj); | |
return delay; | |
} | |
func.desc = ["set", {1: id, 2: JSON.stringify(properties), 3: delay}]; | |
return func; | |
} | |
// Instruction: stop | |
// Parameter: message - message to display | |
// Causes the program to stop and display the given message string next to | |
// the Start/Stop button. This gives the opportunity to do manual changes to | |
// the graph. The message can give instructions on what to do. The program | |
// will resume when the Start button is clicked. | |
function stop (messageStr) { | |
const func = () => { | |
message.innerHTML = messageStr; | |
startButton.firstChild.innerHTML = 'Start'; | |
running = 0; | |
} | |
func.desc = ["stop", arguments]; | |
return func; | |
} | |
// Instruction: pause | |
// Parameter: delay - number of ms to pause | |
// This introduces a pause of the given duration into the program. Although | |
// most instructions have a built-in ability to delay after running, some do not. | |
function pause (delay) { | |
const func = () => delay; | |
func.desc = ["pause", arguments]; | |
return func; | |
} | |
// Instruction: label | |
// Parameters: name - the name of this label | |
// Gives a name to the point in the instruction stream which | |
// can be the target of a goto instruction. Does not do anything | |
// functional. | |
function label (name) { | |
const labelFunc = () => { | |
// If called internally for filling in the labels object, add | |
// this label to it. Otherwise just return. | |
if (labelIndex >= 0) { | |
labels[name] = labelIndex; | |
} | |
} | |
labelFunc.desc = ["label", arguments]; | |
return labelFunc; | |
} | |
// Instruction: goto | |
// Parameters: name - the name of the label to go to | |
// repeat - the number of times to repeat | |
// Jumps to the specified label and decrements an internal count that | |
// is initialized to the repeat parameter. If the internal count | |
// has reached zero, resets the count to the given repeat value, but | |
// does not jump to the given label. This allows a set of instructions | |
// to be repeated the given number of times before moving on to the | |
// next instruction. If another goto loops back to before this instruction, | |
// it will be ready to repeat again, allowing nested repeat loops to work. | |
// To repeat indefinitely, omit the repeat count parameter or set the | |
// repeat count to -1. | |
function goto (name, repeat=-1) { | |
let count = repeat; | |
let lastReset = 0; | |
const gotoFunc = () => { | |
if (grouping) { | |
throw("\"goto\" not allowed in a statement group"); | |
} | |
if (name in labels) { | |
// If we have hit "reset" since we were last here, | |
// reset the repeat count | |
if (lastReset != resetCount) { | |
count = repeat; | |
lastReset = resetCount; | |
} | |
if (backStepping) { | |
// Reset the count to what it was | |
if (count == repeat) { | |
count = 0; | |
} else { | |
count++; | |
} | |
} else { | |
// Goto label if count>0, otherwise reset the count | |
if (count == 0) { | |
count = repeat; | |
} else { | |
count--; | |
currentIndex = labels[name]; | |
} | |
} | |
} else { | |
disableButton(startButton); | |
disableButton(stepButton); | |
message.innerHTML = "Error: unknown label (\"" + name + "\")"; | |
throw("Unknown label: " + name); | |
} | |
} | |
gotoFunc.desc = ["goto", arguments]; | |
return gotoFunc; | |
} | |
// -------------------------------- | |
// Internal variables and functions | |
// -------------------------------- | |
let Calc; | |
let currentIndex = -1; | |
let resetButton, startButton, message; | |
let stepButton, revStepButton, saveButton; | |
let running = 0, stepping = 0, grouping = 0, animating = 0, backStepping = 0; | |
let steps; | |
let labelIndex = -1; | |
let labels = {}; | |
let debugMode = false; | |
let allowSave = false; | |
let history = []; | |
// Counter that increments when the reset button is | |
// pressed. Used by the goto instruction to reset | |
// its internal count if a reset has occurred since | |
// it was last executed. | |
let resetCount = 0; | |
function testlog (s) { | |
console.log(`%c${s}`, 'font-weight: bold; border: 1px solid black; border-radius: 999px; padding: 3px 5px 3px 5px'); | |
} | |
function steplog (index, undo) { | |
let dash = undo ? "-" : ""; | |
if (steps[index].constructor === Array) { | |
let str = dash + (index + 1) + ": "; | |
const indent = str.length; | |
for (let i of steps[index]) { | |
if (i != steps[index][0]) { | |
str += " ".repeat(indent); | |
} | |
str += i.desc[0] + "(" + Object.values(i.desc[1]) + ")"; | |
if (i != steps[index][steps[index].length-1]) { | |
str += "\n"; | |
} | |
} | |
testlog(str); | |
} else { | |
testlog(dash + (index + 1) + ": " + steps[index].desc[0] + "(" + | |
Object.values(steps[index].desc[1]) + ")"); | |
} | |
} | |
function enableButton (el) { | |
if (el != undefined) { | |
el.firstChild.classList.add('dcg-btn-green'); | |
el.firstChild.classList.remove('disabled-save-btn'); | |
} | |
} | |
function disableButton (el) { | |
if (el != undefined) { | |
el.firstChild.classList.remove('dcg-btn-green'); | |
el.firstChild.classList.add('disabled-save-btn'); | |
} | |
} | |
// Wait for animation to finish | |
function waitForAnimation (func, delay) { | |
const interval = setInterval(() => { | |
if (!animating) { | |
clearInterval(interval); | |
setTimeout(func, delay); | |
} | |
}, 100); | |
} | |
// Tell the animation to stop and then wait for it to do so | |
function cancelAnimation (func) { | |
if (animating == 1) { | |
// Special value of 2 tells animateHelper to stop now! | |
animating = 2; | |
waitForAnimation (func, 0); | |
} | |
} | |
function advance () { | |
setState(currentIndex + 1); | |
} | |
function runStatement(stmt) { | |
let delay = 0; | |
if (stmt.constructor === Array) { | |
let saveGrouping = grouping; | |
grouping = 1; | |
for (let i of stmt) { | |
runStatement(i); | |
} | |
grouping = saveGrouping; | |
} else { | |
delay = stmt(); | |
} | |
if (running && !grouping) { | |
if (animating) { | |
waitForAnimation(advance, delay); | |
} else { | |
setTimeout(advance, delay); | |
} | |
} | |
} | |
function setState (index) { | |
if (!running && !stepping) { | |
return; | |
} | |
if (index >= steps.length) { | |
running = 0; | |
stepping = 0; | |
startButton.firstChild.innerHTML = 'Start'; | |
markState(index); | |
return; | |
} | |
index = Math.max(index, 0); | |
currentIndex = index; | |
steplog(index, false); | |
markState(index); | |
runStatement(steps[index]); | |
if (!allowSave) { | |
disableButton(saveButton); | |
} | |
} | |
function markState (index) { | |
if (index >= 0) { | |
enableButton(resetButton); | |
enableButton(revStepButton); | |
} else { | |
disableButton(resetButton); | |
if (history.length == 0) { | |
disableButton(revStepButton); | |
} | |
} | |
if (index === steps.length) { | |
disableButton(startButton); | |
disableButton(stepButton); | |
} else { | |
enableButton(startButton); | |
enableButton(stepButton); | |
} | |
message.innerHTML = (``); | |
if (index >= 0 && index < steps.length && (debugMode || index == 0) && !backStepping) { | |
history.push({i: index, state: Calc.getState()}); | |
} | |
} | |
function verifyId (id, desc) { | |
const expr = Calc.getExpressions().filter(e => e.id === id.toString())[0]; | |
if (expr === undefined) { | |
throw "undefined id (" + id + ")"; | |
} | |
} | |
// Set up the environment to run the given program | |
function desmosPlayer (program, properties={}) { | |
const interval = setInterval(() => { | |
if (document.querySelector('.save-button') && window.Calc) { | |
Calc = window.Calc; | |
init(); | |
clearInterval(interval); | |
} | |
}, 100); | |
function init () { | |
steps = program; | |
let title = document.querySelector('.dcg-variable-title'); | |
// If a graphName is given, it has to match the graph title | |
if ((properties.graphTitle != undefined) && (properties.graphTitle !== title.innerHTML)) { | |
return; | |
} | |
if (properties.debugMode == true) { | |
debugMode = true; | |
} | |
if (properties.allowSave == true) { | |
allowSave = true; | |
} | |
testlog("desmosPlayer ready"); | |
saveButton = document.querySelector('.save-button'); | |
resetButton = saveButton.cloneNode(true); | |
resetButton.firstChild.innerHTML = 'Reset'; | |
resetButton.addEventListener('click', () => { | |
const func = () => { | |
running = 0; | |
currentIndex = -1; | |
resetCount++; | |
markState(-1); | |
startButton.firstChild.innerHTML = 'Start'; | |
Calc.setState(history[0].state, { allowUndo: true }); | |
history = []; | |
} | |
if (animating) { | |
cancelAnimation(func); | |
} else { | |
func(); | |
} | |
}) | |
saveButton.after(resetButton); | |
startButton = resetButton.cloneNode(true); | |
startButton.firstChild.innerHTML = 'Start'; | |
startButton.addEventListener('click', () => { | |
const func = () => { | |
if (running) { | |
running = 0; | |
startButton.firstChild.innerHTML = 'Start'; | |
} else { | |
running = 1; | |
startButton.firstChild.innerHTML = 'Stop'; | |
setState(currentIndex + 1); | |
} | |
stepping = 0; | |
} | |
if (animating) { | |
cancelAnimation(func, 0); | |
} else { | |
func(); | |
} | |
}) | |
resetButton.after(startButton); | |
enableButton(startButton); | |
stepButton = resetButton.cloneNode(true); | |
stepButton.firstChild.innerHTML = 'Step'; | |
stepButton.addEventListener('click', () => { | |
const func = () => { | |
if (running) { | |
return; | |
} else { | |
stepping = 1; | |
advance(); | |
stepping = 0; | |
} | |
} | |
if (animating) { | |
cancelAnimation(func); | |
} else { | |
func(); | |
} | |
}) | |
startButton.after(stepButton); | |
enableButton(stepButton); | |
if (debugMode) { | |
revStepButton = resetButton.cloneNode(true); | |
revStepButton.firstChild.innerHTML = 'Back Step'; | |
revStepButton.addEventListener('click', () => { | |
const func = () => { | |
if (running) { | |
return; | |
} else if (currentIndex > -1) { | |
let lastState = history.pop(); | |
currentIndex = lastState.i - 1; | |
Calc.setState(lastState.state); | |
steplog(lastState.i, true); | |
backStepping = 1; | |
if (steps[lastState.i].name == "gotoFunc") { | |
steps[lastState.i](); | |
} | |
markState(currentIndex); | |
backStepping = 0; | |
} | |
} | |
if (animating) { | |
cancelAnimation(func); | |
} else { | |
func(); | |
} | |
}) | |
stepButton.after(revStepButton); | |
} | |
message = title.cloneNode(true); | |
message.style.maxWidth = '600px'; | |
const buttonContainer = document.querySelector('.save-btn-container'); | |
buttonContainer.after(message); | |
buttonContainer.style.position = 'relative'; | |
buttonContainer.style.top = '-18px'; | |
document.querySelector('.align-center-container').style.display = 'none'; | |
// Fill in the label object with index values for each label | |
for (labelIndex = 0; labelIndex < steps.length; labelIndex++) { | |
if (steps[labelIndex].name == "labelFunc") { | |
// The label name is known only to the function in the program | |
// It will fill in labels[labelName] if global labelIndex >= 0. | |
steps[labelIndex](); | |
} | |
} | |
labelIndex = -1; | |
const exprList = document.querySelector('.dcg-exppanel-outer'); | |
exprList.addEventListener('keydown', e => { | |
if (e.code === 'KeyQ' && e.ctrlKey) { | |
const selExprId = Calc.selectedExpressionId; | |
if (selExprId == undefined) { | |
testlog("Select an expression"); | |
} else { | |
testlog("id: " + selExprId + " (" | |
+ Calc.getExpressions().filter(e => e.id === selExprId.toString())[0].latex | |
+ ")"); | |
} | |
} | |
}) | |
markState(currentIndex); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Fixed.