Skip to content

Instantly share code, notes, and snippets.

@jason-woolf
Last active January 28, 2022 04:51
Show Gist options
  • Save jason-woolf/48774d11699846ad58ec63fa3019faa5 to your computer and use it in GitHub Desktop.
Save jason-woolf/48774d11699846ad58ec63fa3019faa5 to your computer and use it in GitHub Desktop.
Programmatically control a Desmos graph
// ==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);
}
}
@jared-hughes
Copy link

Hello, the correct link to the original gist is https://gist.github.com/jared-hughes/b62dc3042947d6dcee16a186301227dc.

@jason-woolf
Copy link
Author

Hello, the correct link to the original gist is https://gist.github.com/jared-hughes/b62dc3042947d6dcee16a186301227dc.

Fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment