summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/doc/latex/figput/javascript/widgets.js
diff options
context:
space:
mode:
Diffstat (limited to 'Master/texmf-dist/doc/latex/figput/javascript/widgets.js')
-rw-r--r--Master/texmf-dist/doc/latex/figput/javascript/widgets.js2256
1 files changed, 2256 insertions, 0 deletions
diff --git a/Master/texmf-dist/doc/latex/figput/javascript/widgets.js b/Master/texmf-dist/doc/latex/figput/javascript/widgets.js
new file mode 100644
index 00000000000..06dff9a8b45
--- /dev/null
+++ b/Master/texmf-dist/doc/latex/figput/javascript/widgets.js
@@ -0,0 +1,2256 @@
+"use strict";
+/*
+Widget management code.
+
+Typically, widget management is done with a tightly siloed hierarchy, with
+widgets in containers, which are in containers, etc. There is a hierarchy
+like that here (though rather flat since I don't need much), but there's also
+a global registry of widgets alongside the hierarchy. This makes widgets
+easier to work with for the author of the latex document.
+
+I've also avoided any kind of clever abstraction around the widget concept.
+This can mean that there's a certain amount of boilerplate (not DRY), but
+it also means that things aren't tangled up. Widgets can be individually
+modified without worries about side-effects.
+
+All widgets use a register() method rather than a constructor. This
+is because the user should be able to specify a widget repeatedly
+without actually creating a new one every time it's specified.
+
+So, the user should say something like
+let w = RandomWidgetType.register(arguments);
+to create a widget from the drawing code. If register() has never been
+called for the particular widget, then a new widget *is* created and
+a reference to it is placed in global storage. If this widget was created
+earlier, then the reference to it is taken from global storage and returned.
+So, register() is something like an object factory, but it won't make the
+same object more than once.
+
+Every widget is distinguished by its type and the (name of the) figure it
+belongs to. In addition, if a figure has several widgets of the same type,
+then the user must provide an optional name for that particular widget. For
+example, if there are three ButtonWidgets for a given figure, then they
+might be created by
+let b1 = ButtonWidget.register(whatever,"first");
+let b2 = ButtonWidget.register(whatever,"second");
+let b3 = ButtonWidget.register(whatever,"third");
+
+-------------------------
+
+One of the ticklish issues is how to associate widgets with their
+figures. In most languages, this problem is solved by explicitly using
+"this" somehow. In Java, you might say something like
+new Widget(this);
+to indicate that the owner of the widget is the class from which the widget
+was constructed. I would rather not do that because it's the kind of
+boilerplate arcana that the user shouldn't have to think about.
+
+JS provides a couple of ways to determine who made a call to a particular
+function. The easiest way is like this
+
+function example() {
+ let caller = example.caller.name;
+ console.log(caller);
+}
+
+This should print the name of the function that invoked example().
+
+Another way is very easy to do, but it's been deprecated. See
+https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/caller
+Even though it is deprecated, every browser supports it, according to the
+above link. Also, my guess is that this is very commonly used since
+it's so handy. They won't be getting rid of it any time soon.
+
+If they do get rid of caller.name, then another way to obtain this
+information is by creating a bogus Error and examining the call stack:
+
+function example() {
+
+ let stack = new Error().stack;
+ let caller = stack.split('\n')[1].trim();
+
+ // caller is now inforamtion about the function calling this one,
+ // including the file name and a bunch of other stuff I don't care about.
+ // I just want to know the name of the function.
+ // It *appears* (but not extensively tested) that the function name
+ // occurs before the first '@'.;
+ let callingFcn = caller.split('@')[0];
+ console.log(callingFcn);
+}
+
+I wanted to use caller.name, even though it is deprecated, because it is
+so much easier. Unfortunately, it is not allowed in strict mode.
+Since everything that appears within a class is strict, that makes its use
+awkward. I tried to get around this by defining a top-level function (outside
+any class) that does nothing but return the value I want, but that doesn't
+work either. It seems like, as soon as you enter a class, the data you want
+is stripped off (or something). Also, now that I've moved to TypeScript any
+violation of "strict" is even harder. Since I'll have to define a seperate class
+anyway, go ahead and use the Error-based approach.
+
+To top it off, the method via generating an Error is sensitive to the
+broswer because different browsers format this information differently.
+
+-------------------
+
+BUG: Future Widgets...?
+
+Because LoopWidget takes such a crazy number of options, it would be nice
+to have several widgets that merely call LoopWidget. This wouldn't really
+be any different, but would simplify things for the user. Basically, these
+would have a reduced set of arguments to register() and just hand everything
+off to LoopWidget. They would be a mere glue skeleton. Same goes for
+OpenAnimWidget.
+
+Image Building Widget
+ For something like (say) a Mandelbrot set. This takes time to generate
+ and you might want to draw it in an open-ended way. It's not reasonable
+ to allow the user to move through time and see the state of the image
+ at different time steps. The widget should store an off-screen image
+ and making the animation "run faster" would mean calling it more frequently
+ so that it can do more calculation. So it would use some of the same
+ animation infrastructure, but in a different way. The most that makes
+ sense here is a pause/run (to reduce computational load) and something
+ to increase or decrease the load. This would often be something for which
+ it does not make sense to generate tikz. Any JS rendering of a mandelbrot
+ set (say) would be terrible for inclusion in a book. You'd generate a
+ printed figure like that in some other way, even if an on-line animation
+ would be instructive.
+
+Various restrictions on how a DraggableDot can be moved would be handy.
+Restricting to a particular line or arc wouldn't be hard, but restricting to
+fall on a given Path2D would be hard. The JS implementation of Path2D is
+poor, and I would need to reimplement the entire thing from the
+ground up. I might need to do some of that anyway for the best tikz output
+(I did some stuff along those lines already).
+
+Draggable Line
+ Similar to Draggable dot. In fact, it's not clear that this needs its own
+ widget. A line is determined by two points, so the user could use a
+ draggable dot and just draw the line himself. The only advantage I can
+ see to a draggable *line* is that the user could mouse-down on any
+ portion of the line. In fact, that could be done with DraggableDot since
+ the "dot" could be an entire line.
+
+Scroll bar for number selection.
+ Similar to a spinner, but the user drags a dot along a line.
+ Not clear whether a numerical value should appear.
+
+Checkboxes and Radio buttons
+ Gack
+
+Drop-down menu
+ Stuff like this gets more fiddly. If you have a drop-down menu and
+ it has a vertical scroll bar for multiple selections, then it's even worse.
+
+*/
+// There is only one of these, so everything is static. It manages all
+// the widgets in the program.
+class WidgetManager {
+ static register(w) {
+ this.theList.push(w);
+ if (WidgetManager.theWidgets.has(w.betterOwner))
+ WidgetManager.theWidgets.get(w.betterOwner).push(w);
+ else
+ WidgetManager.theWidgets.set(w.betterOwner, [w]);
+ }
+ static knownWidget(o, t, n) {
+ // If there is a widget with the given owner (o), type (t) and name(n)
+ // in theList, then return it; return null otherwise.
+ // BUG: Using a string as owner feels particularly bad.
+ for (let i = 0; i < this.theList.length; i++) {
+ let curW = this.theList[i];
+ if (o !== curW.owner)
+ continue;
+ if (t !== curW.type)
+ continue;
+ if (n === curW.name)
+ return curW;
+ }
+ return null;
+ }
+ static mouseDown(theFig, x, y) {
+ // theFig should match one of the Widget object's owner fields.
+ // (x,y) is given relative to he origin of the figure.
+ if (WidgetManager.theWidgets.has(theFig) === false)
+ return;
+ // The only thing to do here is to pass the event to each widget and
+ // see if it wants it. I suppose that I *could* store the area for
+ // each widget, and do an initial test here, but this is easier.
+ let wlist = WidgetManager.theWidgets.get(theFig);
+ for (let i = 0; i < wlist.length; i++) {
+ if (wlist[i].mouseDown(x, y) === true)
+ // First come, first serve.
+ return;
+ }
+ }
+ static mouseMove(theFig, x, y) {
+ // As above.
+ if (WidgetManager.mouseOwner === null)
+ return;
+ if (WidgetManager.theWidgets.has(theFig) === false)
+ return;
+ // We only accept mouse moves if the mouse is on the figure that
+ // "owns" the mouse event.
+ if (WidgetManager.mouseOwner.betterOwner !== theFig)
+ return;
+ WidgetManager.mouseOwner.mouseMove(x, y);
+ }
+ static mouseUp(theFig, x, y) {
+ // As above.
+ if (WidgetManager.mouseOwner === null)
+ return;
+ if (WidgetManager.theWidgets.has(theFig) === false)
+ return;
+ if (WidgetManager.mouseOwner.betterOwner !== theFig)
+ // Mouse was released over *a* figure, but not the figure with
+ // the owning widget. Tell the correct widget about the release, using
+ // bogus coordinates so that the mouse-up is sure to be off the widget.
+ WidgetManager.mouseOwner.mouseUp(10000000000000, 10000000000000);
+ //console.log("up up");
+ WidgetManager.mouseOwner.mouseUp(x, y);
+ }
+}
+// All the widgets known to the program.
+// Each of these is an instance of the Widget class.
+// BUG: I could get rid of this and just use theWidgets. It's redundant,
+// and theWidgets is actually easier.
+// BUG: Yes, get rid of this.
+WidgetManager.theList = [];
+// Also a complete list of all widgets, but indexed by the figure
+// to which the widget belongs. This makes it easier to pass events
+// to the proper recipent. So this is a hash map taking a figure name
+// (as a string) to an array of Widget objects.
+WidgetManager.theWidgets = new Map();
+// This is BOGUS. Certain path operations require a CanvasRenderingContext2D,
+// even when the question is one of pure abstract geometry, like
+// isPointInPath(), and the only way to get one of these is from a canvas.
+// Using the visible canvas and ctx for this is prone to all kinds of misuse
+// and mistakes, so create a bogus ctx here.
+// BUG: The long-term solution is not to rely on js for this at all. Write
+// my own code for Bezier curves and the like. I'm partway there already.
+WidgetManager.bogusCanvas = document.createElement('canvas');
+WidgetManager.bogusCtx = WidgetManager.bogusCanvas.getContext('2d');
+// Whether a widget "owns" a recent mouse-down event. This is needed
+// for things like dragging. If this is null, then the next mouse-down
+// is up for grabs. Otherwise, it points to the relevant Widget object.
+WidgetManager.mouseOwner = null;
+// This isn't being used yet, but it will be needed to handle keyboard
+// events. The idea is that a widget ""takes ownership" of a mouse-down
+// with mouseOwner, and that widget is the same one that has focus for
+// any future keyboard events too. The only real difference is that
+// focusOwner is "stickier." This goes to null whenever a user clicks
+// on something other than a widget.
+WidgetManager.focusOwner = null;
+function getCaller() {
+ // Irritating function to get around strict mode. See the comment at the
+ // top of the file. I want the caller of the thing that calls this.
+ //
+ // BUG: I was never entirely happy with this, and now that it depends
+ // on the particular browser, I am even less happy about it. It's a
+ // question of the lesser of two evils: this function or requiring
+ // the user to provide a boilerplate 'this' or the like.
+ let stack = new Error().stack;
+ // The exact format of the information in stack depends on the particular
+ // browser. Firefox produces something like this:
+ //
+ // getCaller@http://localhost:8000/widgets.js:375:15
+ // register@http://localhost:8000/widgets.js:1557:18
+ // geartest01@http://localhost:8000/geartest01.js:142:26
+ // render@http://localhost:8000/layout.js:628:11
+ // renderFrame@http://localhost:8000/widgets.js:538:13
+ // etc.
+ //
+ // While MS Edge/Chrome produces
+ //
+ // Error
+ // at getCaller (widgets.js:375:15)
+ // at Function.register (widgets.js:1557:18)
+ // at geartest01 (geartest01.js:142:26)
+ // at FigurePanel.render (layout.js:628:5)
+ // etc.
+ //
+ // Depending on the Browser, stack needs to be parsed differently.
+ // theBrowser was defined in main.js.
+ // The digit value (e.g., '2') indicates how many steps back in the the
+ // stack to go.
+ if (theBrowser === "Firefox") {
+ let caller = stack.split('\n')[2].trim();
+ // caller is now information about a function in the call stack,
+ // including the file name and a bunch of other stuff I don't care about.
+ // I just want to know the name of the function.
+ // The function name occurs before the first '@'.
+ let callingFcn = caller.split('@')[0];
+ return callingFcn;
+ }
+ else if (theBrowser === "Chrome") {
+ let caller = stack.split('\n')[3].trim();
+ // Here, space-deliminting works better.
+ let callingFcn = caller.split(' ')[1];
+ // But this may return something like 'FigurePanel.bezier'; what I want
+ // is just 'bezier'.
+ if (callingFcn.indexOf('.') > -1)
+ callingFcn = callingFcn.split('.')[1];
+ return callingFcn;
+ }
+ else {
+ console.log("IMPOSSIBLE ERROR DUE TO UNKNOWN BROWSER TYPE!!!");
+ return "";
+ }
+}
+// Base class for all widgets. No code outside this file should
+// ever access this class directly. It's not abstract because there
+// are certain actions common to every case in the constructor.
+class Widget {
+ constructor(owner, type, x, y, scale, hide, name) {
+ // Every widget is owned by a particular figure. The "owner" is
+ // the name of the figure function, just as in latex.
+ // BUG: Change this to the AugmentedDrawingFunction. Thus, get rid
+ // of this and use betterOwner.
+ this.owner = "";
+ // JS isn't very good about types, so it's clearer to tag sub-classes with
+ // the name of that sub-class rather than mess with typeof or whatever.
+ // This should be "LoopWidget" or whatever the name is of the sub-class.
+ this.type = "";
+ // Name to distinguish this widget from all others of the same
+ // type that belong to the same figure. So, the triple (owner,type,name)
+ // fully distinguishes this Widget from all others.
+ //
+ // In principle, this variable could have been avoided as a user-provided
+ // value and he wouldn't have to come up with an 'extra' name for the widget,
+ // but it wouldn't be easy. The WidgetManager (or something) would have to
+ // come up with a unique ID and *that* would require that the user invoke
+ // something like a "starting to create widgets" and "done creating widgets"
+ // commands. Overall, this seems less fussy for him.
+ this.name = "";
+ // The (x,y) is where the widget should be drawn relative to the
+ // rectangle of the figure. Often, widgetX will be negative to put the
+ // widget in the margin of the page.
+ // It's tempting to call these fields x and y, but it would
+ // be easy to accidentally reuse those names.
+ this.widgetX = 0;
+ this.widgetY = 0;
+ // To scale the drawing of a widget up or down.
+ this.scale = 1.0;
+ // Occassionally, it may make sense to hide a widget. This is different
+ // than being non-visible because the widget is off-screen. If the
+ // widget has hide == true, then it is *never* shown. For example, the
+ // way animations work, you have to have an animation widget to run the
+ // animation, even if you don't want to see the widget.
+ this.hide = true;
+ // Due to the fact that this tracks the owner of the widget, and how
+ // it is done, it is IMPORTANT that no sub-class has its own constructor.
+ this.owner = owner;
+ this.type = type;
+ this.widgetX = x;
+ this.widgetY = y;
+ this.scale = scale;
+ this.hide = hide;
+ this.name = name;
+ this.betterOwner = getAugmentedFunction(owner);
+ // When a widget is contructed it must be registered in a global list.
+ WidgetManager.register(this);
+ }
+ draw(ctx) {
+ // Every sub-class must implement this method.
+ console.log("Called Widget.draw()!!!");
+ }
+ // BUG: These methods that are "never" supposed to be called
+ // will be called for the widgets based on the DOM, like ButtonWidget.
+ // That's fine -- don't panic! I want to get rid of these DOM-based
+ // widgets anyway. DOM = cesspool.
+ mouseDown(x, y) {
+ // Every sub-class must implement this method.
+ // Return true iff the widget wants to "take ownership" of this event.
+ console.log("Called Widget.mouseDown()!!! " + this.name);
+ return false;
+ }
+ mouseMove(x, y) {
+ // As above, but returns nothing.
+ console.log("Called Widget.mouseMove()!!!");
+ }
+ mouseUp(x, y) {
+ // As above, but returns nothing.
+ console.log("Called Widget.mouseUp()!!!");
+ }
+}
+// Animations require some infrastructure.
+// BUG: Maybe this stuff should be in AnimationWidget.
+function doAnimation(theWidget) {
+ // This is called to render a frame of an animation. It is generated via
+ // the usual event-loop, so we schedule it just as we do for things like
+ // mouse-downs and scroll events.
+ // theWidget is the one that "runs" the animation, like a LoopWidget.
+ let id = Events.getID();
+ doAnimationGuts(id, theWidget);
+}
+async function doAnimationGuts(id, theWidget) {
+ // Scheduling is handled in a way similar to doScroll() in main.ts.
+ await Events.waitMyTurn(id);
+ await renderFrame(theWidget);
+ // Advance to the next frame.
+ theWidget.curStep += theWidget.stepsPerFrame;
+ theWidget.advanceFrame();
+ // Don't forget this or the program is bricked!
+ Events.allDone(id);
+}
+async function renderFrame(theWidget) {
+ // Calls the code to render the relevant figure. It renders the *entire*
+ // figure, widget and all.
+ //
+ // NOTE: This is used for animations, but it is also used to ensure
+ // that any change to a widget (and resulting changes to a figure)
+ // is shown.
+ //
+ // BUG: It's tempting to mention this in the user manual since users might
+ // find it useful. OTOH, that shouldn't be encouraged, and this is the kind
+ // of thing that might change in a later version.
+ // The "owner" is the function (from the latex document) that created
+ // theWidget.
+ let myFunc = getAugmentedFunction(theWidget.owner);
+ let fpc = myFunc.figurePanelClass;
+ // This is generally synchronous, but it doesn't hurt anything to tack an
+ // async on here. Maybe somebody will write one that *is* asynchronous.
+ await fpc.render();
+}
+function getFigureRect(theWidget) {
+ // Returns the width and height of the rectangle of the widget.
+ // The height is in two parts: the height above the x-axis, and the height
+ // below the x-axis.
+ // The units are pdf points, and the width is relative to the left
+ // margin. So the width matches the usual coordinate system for drawing
+ // the figure, and this width is equal to the text width, as reported
+ // by latex. So x in the range [0,width] should be limited to the area
+ // below the text.
+ let myFunc = getAugmentedFunction(theWidget.owner);
+ let fpc = myFunc.figurePanelClass;
+ let answer = { w: fpc.textWidth, ha: fpc.h - fpc.lowerPadding, hb: fpc.lowerPadding };
+ return answer;
+}
+// Base class for widgets that run animations. See LoopAnimWidget and
+// OpenAnimWidget. There is a fair amount of overlap between the two types
+// of animation class, and it is tempting to pull more stuff up to this level for
+// DRY reasons, but it seems cleaner and clearer to limit this to what's needed
+// to run the animations with doAnimation() and related functions.
+class AnimationWidget extends Widget {
+ constructor() {
+ super(...arguments);
+ // Animations run as a series of frames, and this is the frame being displayed.
+ // This value may be open-ended or it may "loop back" so that animation repeats.
+ this.curStep = 0;
+ // How much to advance the above with each frame -- an integer. Animations
+ // can be made to run faster by increasing this value, thereby skipping frames.
+ this.stepsPerFrame = 1;
+ // The process id for the call to setInterval().
+ this.animID = 0;
+ }
+ advanceFrame() {
+ // This is why the class is abstract. It moves curStep to the next frame,
+ // however that should be done for the particular animation.
+ console.log("Calling abstract AnimationWidget.advanceFrame()!");
+ }
+}
+// A LoopWidget is to be used when an animiation runs in a repeating loop.
+// You must have one of these for an animation to run, even if the widget
+// itself is invisible.
+class LoopAnimWidget extends AnimationWidget {
+ constructor() {
+ super(...arguments);
+ // These are as passed to register(). See that method for a description.
+ // They really shouldn't be touched outside this class. The boolean values
+ // are whether certain elements of the widget are visible (and hence
+ // available for interaction).
+ this.steps = 100;
+ this.start = 0;
+ this.timeStep = 20;
+ this.visSteps = true;
+ this.visFastSlow = true;
+ this.visPauseRun = true;
+ this.visCircle = true;
+ this.triGrab = true;
+ // This stuff is very much private.
+ // These are stored when the figure is drawn so that mouse events can find
+ // what was clicked more easily. It's simpler than recalculating with
+ // every event.
+ this.pCircle = null;
+ this.pUpStep = null;
+ this.pDownStep = null;
+ this.pFaster = null;
+ this.pSlower = null;
+ this.pPauseRun = null;
+ // The states of various parts of the widget; e.g., whether the pause
+ // or run icon is present, whether a part is "half-way clicked,"" etc.
+ // I'm using 'a' for 'active' and 's' for 'selected.'
+ //
+ // BUG: If I want to get *really* fancy, then I need another set of
+ // flags to indicate that the mouse *was* clicked on something, so it
+ // is "selected," but the user moved the mouse away from the item without
+ // a mouse-up, so that selected item should be drawn in normal color, not
+ // the highlighted color (sColor). If the mouse is moved back over the
+ // selected item, then the color can go back to being the selection color.
+ this.aRunning = true;
+ this.sCircle = false;
+ this.sPauseRun = false;
+ this.sFaster = false;
+ this.sSlower = false;
+ this.sUpStep = false;
+ this.sDownStep = false;
+ }
+ static register(ctx, x, y, scale, visWidget, steps, start, timeStep, visSteps, visFastSlow, visPauseRun, visCircle, triGrab, name) {
+ // BUG: Add an argument for the size of the steps so that
+ // curStep can be incremented by more than 1?
+ // This is used something like a constructor. It either creates a new
+ // LoopWidget and returns it, or returns one that was created earlier.
+ // Many of these arguments are the same as for Widget.constructor().
+ // In addition, we have
+ // * ctx is assumed to have a t-matrix prepared to properly draw
+ // the widget.
+ // * steps is the number of steps required to form a loop -- when it
+ // "rolls over" or the "steps per revolution."
+ // * start is the starting step, which will usually be zero.
+ // * timeStep, in milliseconds, is the time from frame to frame.
+ // It seems like anything less than about 10ms is pointless.
+ // My guess (?) is that the rate of event generation is throttled
+ // somehow. It could be that my various layers of management are
+ // slowing things down, but I don't think so. 10ms is an eternity
+ // on modern hardware. The eye can only follow about 20 frames per
+ // second, at most, or 50ms per frame, so this is no big deal.
+ // * visWidget is whether the widget is visible at all -- same as the
+ // vis argument to Widget.construtor().
+ // * visSteps is whether the time step controls (at the top) are visible.
+ // * visFastSlow is whether the faster/slower controls are visible (at
+ // the bottom)
+ // * visPauseRun is whether the pause/run controls are visible
+ // * visCircle is for the circle (with triangular indicator).
+ // * triGrab is whether the user is allowed to grab the indicator
+ // triangle and control the animation by dragging it. The indicator
+ // triangle is always there, but it might not be grabable.
+ // Note that if visCircle == false, then triGrab is implicitly false
+ // since the triangle isn't visible either.
+ // * name is as in Widget.constructor()
+ //
+ // There appears to be a tacit assumption that time is measured in
+ // integer steps, but fractional values are fine. So they aren't really
+ // time "steps," but more like time increments.
+ //
+ // BUG: Maybe I should have different classes for some of these choices.
+ // There are just too many. These could all use (internally) the
+ // same class, just not with such a crazy number of options.
+ // Instead of messing with LoopWidget.name or something, be explicit.
+ let type = "LoopWidget";
+ // Something like this line must appear with every regester() method
+ // for each widget.
+ let caller = getCaller();
+ // I am forcing the type here, but if the wrong type is returned,
+ // then there are bigger problems.
+ let w = WidgetManager.knownWidget(caller, type, name);
+ if (w != null) {
+ // Widget is known, but it needs to be drawn too.
+ w.draw(ctx);
+ return w;
+ }
+ // Got here, so this widget is not already known, and must be created.
+ // This class has NO construtor, by design, so this falls through to the
+ // super-class Widget constructor.
+ // Careful: Internally, I use an "is hidden" flag, but the user passes in
+ // an "is visible" flag.
+ // BUG: Change the names to be consistent.
+ w = new LoopAnimWidget(caller, type, x, y, scale, !visWidget, name);
+ // Now the additional stuff. This is what would be in a constructor
+ // if this class had one.
+ w.steps = steps;
+ w.start = start;
+ w.timeStep = timeStep;
+ w.visSteps = visSteps;
+ w.visFastSlow = visFastSlow;
+ w.visPauseRun = visPauseRun;
+ w.visCircle = visCircle;
+ w.triGrab = triGrab;
+ w.curStep = w.start;
+ // Note the existence of this widget for the future.
+ WidgetManager.register(w);
+ // This is a special case because it's an animiation. The animation
+ // needs to be scheduled. It may make sense not to call setInterval()
+ // immediately; rather, it may be better to call setTimeout() so that
+ // setInterval() is called after a brief pause. It depends on how these
+ // two work. Try an immediate call; it *should* be fine.
+ w.animID = setInterval(doAnimation, w.timeStep, w);
+ // Before returning the widget, it must be drawn.
+ w.draw(ctx);
+ return w;
+ }
+ advanceFrame() {
+ // This kind of animation repeats.
+ this.curStep += this.stepsPerFrame;
+ if (this.curStep >= this.steps)
+ this.curStep -= this.steps;
+ }
+ draw(ctx) {
+ // This is drawn with (0,0) at the center of the circle. The ctx must be
+ // shifted and scaled based on where the user wants the widget relative
+ // to the larger drawing area.
+ if (this.hide === true)
+ return;
+ // Don't attempt to draw to a tikz file.
+ if (ctx instanceof CTX)
+ return;
+ let saveT = ctx.getTransform();
+ ctx.translate(this.widgetX, this.widgetY);
+ ctx.scale(this.scale, this.scale);
+ var p = new Path2D();
+ // These values determine where the entire drawing is. (cx,cy) is the
+ // center of the circular thing, with radius r.
+ // In unscaled terms, it's clear that cx should equal r to make the
+ // widget but up against x = 0. It's messier for the y-coordinate and you
+ // need to work backwards from what the values defined below. The total
+ // height is 2r (the circle), plus circWidth (circle line thickness),
+ // plus 2 * arrowHeight (the go fast/go slow things). Then we need to
+ // add the stuff on the top: upperGap, plus stepHeight, plus stepThick.
+ // This is silly and probably confusing to the user. Just place the
+ // widget relative to the center of the circle. In other words, set
+ // (cx,cy) = (0,0). The user just needs adjust accordingly, and
+ // exactly what he wants to do will be influenced by whether the upper
+ // and lower sub-controls are present.
+ var r = 40;
+ var cx = 0;
+ var cy = 0;
+ var circWidth = 3;
+ if (this.visCircle == true) {
+ // Draw the circle. (cx,cy) is center r and r are the two axes of the
+ // elipse. 0 is that the ellipse isn't rotated, and the last two are
+ // the start and end angle.
+ p.ellipse(cx, cy, r, r, 0, 0, 2 * Math.PI);
+ // Note this circle for reference by mouse events.
+ this.pCircle = new Path2D(p);
+ ctx.lineWidth = circWidth;
+ if (this.sCircle == true)
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ else
+ ctx.strokeStyle = "black";
+ ctx.stroke(p);
+ ctx.lineWidth = 1;
+ // A little triangle to point to the location within the animation loop.
+ // This value is in radians, in [0,2pi).
+ // Location of indicator triangle around the perimeter of the circle.
+ // Minus so that it travels clock-wise, which seems to be our natural expectation.
+ var loc = -2 * Math.PI * this.curStep / this.steps;
+ var triHeight = 10;
+ // This is half the full angle at the outer point.
+ var triAngle = Math.PI / 20;
+ p = new Path2D();
+ var x = cx + (r - circWidth / 2) * Math.cos(loc);
+ var y = cy + (r - circWidth / 2) * Math.sin(loc);
+ p.moveTo(x, y);
+ x = cx + (r - triHeight) * Math.cos(loc + triAngle);
+ y = cy + (r - triHeight) * Math.sin(loc + triAngle);
+ p.lineTo(x, y);
+ x = cx + (r - triHeight) * Math.cos(loc - triAngle);
+ y = cy + (r - triHeight) * Math.sin(loc - triAngle);
+ p.lineTo(x, y);
+ p.closePath();
+ ctx.strokeStyle = "red";
+ ctx.stroke(p);
+ ctx.strokeStyle = "black";
+ }
+ // Next, some controls at the bottom for going faster/slower and
+ // pausing/running.
+ // First, a pair of '>' for going faster.
+ let arrowOffset = 18;
+ let lowerGap = 6;
+ let arrowHeight = 7;
+ let arrowDepth = 4;
+ let arrowPairSpace = 3;
+ let arrowThick = 1.25;
+ if (this.visFastSlow === true) {
+ ctx.lineWidth = arrowThick;
+ if (this.sFaster == true)
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ else
+ ctx.strokeStyle = "black";
+ p = new Path2D();
+ p.moveTo(cx + arrowOffset, cy - r - circWidth / 2 - lowerGap);
+ p.lineTo(cx + arrowOffset + arrowDepth, cy - r - circWidth / 2 - lowerGap - arrowHeight);
+ p.lineTo(cx + arrowOffset, cy - r - circWidth / 2 - lowerGap - 2 * arrowHeight);
+ ctx.stroke(p);
+ // You can't just shift a path. Needs to be rebuilt.
+ // BUG: I need to add that ability to my FPath class.
+ // Maybe I have already?
+ p = new Path2D();
+ p.moveTo(cx + arrowOffset + arrowPairSpace, cy - r - circWidth / 2 - lowerGap);
+ p.lineTo(cx + arrowOffset + arrowDepth + arrowPairSpace, cy - r - circWidth / 2 - lowerGap - arrowHeight);
+ p.lineTo(cx + arrowOffset + arrowPairSpace, cy - r - circWidth / 2 - lowerGap - 2 * arrowHeight);
+ ctx.stroke(p);
+ // A rectangle for the clickable area.
+ this.pFaster = new Path2D();
+ this.pFaster.rect(cx + arrowOffset - arrowThick, cy - r - circWidth / 2 - lowerGap - 2 * arrowHeight, arrowPairSpace + arrowDepth + 2 * arrowThick, 2 * arrowHeight);
+ // ctx.strokeStyle = 'green';
+ // ctx.stroke(this.pFaster);
+ // Same idea: '<' to go slower.
+ if (this.sSlower == true)
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ else
+ ctx.strokeStyle = "black";
+ p = new Path2D();
+ p.moveTo(cx - arrowOffset, cy - r - circWidth / 2 - lowerGap);
+ p.lineTo(cx - arrowOffset - arrowDepth, cy - r - circWidth / 2 - lowerGap - arrowHeight);
+ p.lineTo(cx - arrowOffset, cy - r - circWidth / 2 - lowerGap - 2 * arrowHeight);
+ ctx.stroke(p);
+ p = new Path2D();
+ p.moveTo(cx - arrowOffset - arrowPairSpace, cy - r - circWidth / 2 - lowerGap);
+ p.lineTo(cx - arrowOffset - arrowDepth - arrowPairSpace, cy - r - circWidth / 2 - lowerGap - arrowHeight);
+ p.lineTo(cx - arrowOffset - arrowPairSpace, cy - r - circWidth / 2 - lowerGap - 2 * arrowHeight);
+ ctx.stroke(p);
+ // And the clickable area.
+ this.pSlower = new Path2D();
+ this.pSlower.rect(cx - arrowOffset - arrowPairSpace - arrowDepth - arrowThick, cy - r - circWidth / 2 - lowerGap - 2 * arrowHeight, arrowPairSpace + arrowDepth + 2 * arrowThick, 2 * arrowHeight);
+ // ctx.strokeStyle = 'green';
+ // ctx.stroke(this.pSlower);
+ }
+ ctx.lineWidth = 1;
+ if (this.visPauseRun === true) {
+ // A || or triangle for pause or run.
+ let pauseSpace = 3.25;
+ let pauseThick = 1.5;
+ let pauseHeight = 2 * arrowHeight;
+ let runThick = 1.5;
+ let runLeftRight = 5;
+ let runHeight = 2 * arrowHeight;
+ if (this.sPauseRun == true)
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ else
+ ctx.strokeStyle = "black";
+ if (this.aRunning === true) {
+ // The animation is running, so show || to allow pausing.
+ ctx.lineWidth = pauseThick;
+ p = new Path2D();
+ p.moveTo(cx + pauseSpace, cy - r - circWidth / 2 - lowerGap);
+ p.lineTo(cx + pauseSpace, cy - r - circWidth / 2 - lowerGap - pauseHeight);
+ ctx.stroke(p);
+ p = new Path2D();
+ p.moveTo(cx - pauseSpace, cy - r - circWidth / 2 - lowerGap);
+ p.lineTo(cx - pauseSpace, cy - r - circWidth / 2 - lowerGap - pauseHeight);
+ ctx.stroke(p);
+ ctx.lineWidth = 1;
+ }
+ else {
+ // Animation is paused, so show triangle to run it again.
+ ctx.lineWidth = runThick;
+ p = new Path2D();
+ p.moveTo(cx - runLeftRight, cy - r - circWidth / 2 - lowerGap);
+ p.lineTo(cx - runLeftRight, cy - r - circWidth / 2 - lowerGap - runHeight);
+ p.lineTo(cx + runLeftRight, cy - r - circWidth / 2 - lowerGap - runHeight / 2);
+ p.closePath();
+ ctx.stroke(p);
+ }
+ // Either way (paused or running), we need the clickable area.
+ // This area is too generous for the "run" triangle, because
+ // I use the same rectangle for "pause" and "run," but no big deal.
+ this.pPauseRun = new Path2D();
+ this.pPauseRun.rect(cx - runLeftRight - runThick, cy - r - circWidth / 2 - lowerGap - runHeight - runThick, 2 * runLeftRight + 2 * runThick, runHeight + 2 * runThick);
+ // ctx.strokeStyle = "blue";
+ // ctx.lineWidth = 0.5;
+ // ctx.stroke(this.pPauseRun);
+ // ctx.strokeStyle = "black";
+ }
+ ctx.lineWidth = 1;
+ // Now some symbols above the circle for adjusting the step size.
+ if (this.visSteps === true) {
+ // Up and down arrows.
+ let stepSpace = 20;
+ let stepThick = 2.0;
+ let upperGap = 8;
+ let stepHeight = 15;
+ let stepArrowHeight = 8;
+ let stepArrowWidth = 5;
+ ctx.lineWidth = stepThick;
+ if (this.sDownStep == true) {
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ ctx.fillStyle = LoopAnimWidget.sColor;
+ }
+ else {
+ ctx.strokeStyle = "black";
+ ctx.fillStyle = "black";
+ }
+ // Vertical line
+ p = new Path2D();
+ p.moveTo(cx - stepSpace, cy + r + circWidth / 2 + upperGap);
+ p.lineTo(cx - stepSpace, cy + r + circWidth / 2 + upperGap + stepHeight);
+ ctx.stroke(p);
+ // Arrow head
+ p = new Path2D();
+ p.moveTo(cx - stepSpace, cy + r + circWidth / 2 + upperGap - stepThick);
+ p.lineTo(cx - stepSpace + stepArrowWidth, cy + r + circWidth / 2 + upperGap + stepArrowHeight - stepThick);
+ p.lineTo(cx - stepSpace - stepArrowWidth, cy + r + circWidth / 2 + upperGap + stepArrowHeight - stepThick);
+ p.closePath();
+ ctx.fill(p);
+ // Clickable area for down arrow.
+ this.pDownStep = new Path2D();
+ this.pDownStep.rect(cx - stepSpace - stepArrowWidth, cy + r + circWidth / 2 + upperGap - stepThick, 2 * stepArrowWidth, stepHeight + stepThick);
+ // ctx.strokeStyle = "blue";
+ // ctx.lineWidth = 0.5;
+ // ctx.stroke(this.pDownStep);
+ // ctx.strokeStyle = "black";
+ // ctx.lineWidth = stepThick;
+ // Again, to the right, arrow head up.
+ if (this.sUpStep == true) {
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ ctx.fillStyle = LoopAnimWidget.sColor;
+ }
+ else {
+ ctx.strokeStyle = "black";
+ ctx.fillStyle = "black";
+ }
+ p = new Path2D();
+ p.moveTo(cx + stepSpace, cy + r + circWidth / 2 + upperGap);
+ p.lineTo(cx + stepSpace, cy + r + circWidth / 2 + upperGap + stepHeight);
+ ctx.stroke(p);
+ p = new Path2D();
+ p.moveTo(cx + stepSpace, cy + r + circWidth / 2 + upperGap + stepHeight + stepThick);
+ p.lineTo(cx + stepSpace + stepArrowWidth, cy + r + circWidth / 2 + upperGap + stepHeight - stepArrowHeight + stepThick);
+ p.lineTo(cx + stepSpace - stepArrowWidth, cy + r + circWidth / 2 + upperGap + stepHeight - stepArrowHeight + stepThick);
+ p.closePath();
+ ctx.fill(p);
+ // Clickable area for up arrow.
+ this.pUpStep = new Path2D();
+ this.pUpStep.rect(cx + stepSpace - stepArrowWidth, cy + r + circWidth / 2 + upperGap, 2 * stepArrowWidth, stepHeight + stepThick);
+ // ctx.strokeStyle = "blue";
+ // ctx.lineWidth = 0.5;
+ // ctx.stroke(this.pUpStep);
+ // ctx.strokeStyle = "black";
+ ctx.lineWidth = 1;
+ // A little step icon.
+ ctx.strokeStyle = "black";
+ ctx.fillStyle = "black";
+ stepThick = 1.5;
+ let stepSize = 6;
+ ctx.lineWidth = stepThick;
+ // Steps made as one path, starting at upper-left
+ p = new Path2D();
+ p.moveTo(cx - stepSize, cy + r + circWidth / 2 + upperGap + 2 * stepSize);
+ p.lineTo(cx, cy + r + circWidth / 2 + upperGap + 2 * stepSize);
+ p.lineTo(cx, cy + r + circWidth / 2 + upperGap + stepSize);
+ p.lineTo(cx + stepSize, cy + r + circWidth / 2 + upperGap + stepSize);
+ p.lineTo(cx + stepSize, cy + r + circWidth / 2 + upperGap);
+ ctx.stroke(p);
+ }
+ ctx.lineWidth = 1;
+ // Used this to verify the constants. The rectangle barely encloses
+ // the widget.
+ //ctx.strokeRect(
+ // -LoopWidget.Radius,-LoopWidget.Radius - LoopWidget.TopHeight,
+ // 2*LoopWidget.Radius,
+ // 2*LoopWidget.Radius + LoopWidget.TopHeight + LoopWidget.BottomHeight);
+ ctx.setTransform(saveT);
+ }
+ mouseDown(x, y) {
+ // (x,y) is given in coordinates relative to the owning figure.
+ // Return true iff these coordinates apply to this widget.
+ if (this.hide === true)
+ return false;
+ // Adjust coordinates relative to what the draw() methods uses.
+ // This way we can compare (x,y) to what is on the screen.
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ // The widget also has a scale, which must be taken into account.
+ WidgetManager.bogusCtx.resetTransform();
+ // There is isPointInPath() and isPointInStroke().
+ // It seems that isPointInPath() works on an abstract geometric basis;
+ // the lineWidth of the ctx doesn't matter. OTOH, isPointInStroke()
+ // is affected by the lineWidth -- as it must be to work in any
+ // reasonable way.
+ //
+ // Note also that isPointInPath() defaults to the non-zero winding rule.
+ // Pass "evenodd" as the final argument for that winding rule.
+ // Check the pause/run area first since it should be "on top of"
+ // the circle area.
+ if (this.pPauseRun !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pPauseRun, x, y);
+ if (isin === true) {
+ this.sPauseRun = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // And the run faster area.
+ if (this.pFaster !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pFaster, x, y);
+ if (isin === true) {
+ this.sFaster = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // The run slower area.
+ if (this.pSlower !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pSlower, x, y);
+ if (isin === true) {
+ this.sSlower = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // The longer step area.
+ if (this.pUpStep !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pUpStep, x, y);
+ if (isin === true) {
+ this.sUpStep = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // The shorter step area.
+ if (this.pDownStep !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pDownStep, x, y);
+ if (isin === true) {
+ this.sDownStep = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // Last thing to check since it should be "underneath" everything.
+ if (this.pCircle !== null) {
+ // If the user clicked near the circle, then set the indicator and
+ // current step to that position. Be generous with the clickable area.
+ WidgetManager.bogusCtx.lineWidth = 15;
+ let isin = WidgetManager.bogusCtx.isPointInStroke(this.pCircle, x, y);
+ if (isin === true) {
+ // Act on this click and take ownership for future draggging.
+ // Want an angle in [0,2pi].
+ this.sCircle = true;
+ // Again, minus since clockwise.
+ let alpha = -Math.atan2(y, x);
+ if (alpha < 0)
+ alpha += 2 * Math.PI;
+ this.curStep = Math.floor(this.steps * alpha / (2 * Math.PI));
+ WidgetManager.mouseOwner = this;
+ // The appearance of the widget has changed.
+ renderFrame(this);
+ return true;
+ }
+ }
+ return false;
+ }
+ mouseMove(x, y) {
+ // As above.
+ if (this.hide === true)
+ return;
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ WidgetManager.bogusCtx.resetTransform();
+ if (this.sCircle === true) {
+ // What I will do it check that the mouse is "close enough" to the
+ // circle, but it can be a *long* ways away.
+ WidgetManager.bogusCtx.lineWidth = 40;
+ let isin = WidgetManager.bogusCtx.isPointInStroke(this.pCircle, x, y);
+ if (isin == false)
+ return;
+ // Minus to make clockwise.
+ let alpha = -Math.atan2(y, x);
+ if (alpha < 0)
+ alpha += 2 * Math.PI;
+ this.curStep = Math.floor(this.steps * alpha / (2 * Math.PI));
+ // The appearance of the widget may have changed.
+ renderFrame(this);
+ }
+ // BUG: I might (?) want colors to change based on what the mouse
+ // is over. See the BUG comment that goes with aRunning, sCircle, etc.,
+ // at the top of the class.
+ }
+ mouseUp(x, y) {
+ // As above.
+ if (this.hide === true)
+ return;
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ WidgetManager.bogusCtx.resetTransform();
+ // The mouse is up, so nothing can remain selected. In most cases,
+ // releasing the mouse over the selected item means that something
+ // must be done since the "button" was properly pressed.
+ this.sCircle = false;
+ if (this.sPauseRun) {
+ // Did they *release* the mouse over the pause/run area?
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pPauseRun, x, y);
+ if (isin === true) {
+ // Start/stop the animation.
+ if (this.aRunning === true)
+ // Currently running. Pause it.
+ clearInterval(this.animID);
+ else
+ // Currently paused. Restart it.
+ this.animID = setInterval(doAnimation, this.timeStep, this);
+ // Change the pause/run icon too.
+ if (this.aRunning === true)
+ this.aRunning = false;
+ else
+ this.aRunning = true;
+ }
+ this.sPauseRun = false;
+ }
+ if (this.sFaster === true) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pFaster, x, y);
+ if (isin === true) {
+ // Make the animation run a bit faster by reducing the frame-to-
+ // frame time step -- the animation speed. This could be done in
+ // a lot of different ways, by using a factor of 1.4 seems about
+ // right.
+ this.timeStep /= 1.4;
+ if (this.timeStep < 1)
+ this.timeStep = 1;
+ // Stop the animation and restart it at the new speed,
+ // but only if it is currently running. It restarts always, but
+ // don't try to halt it if it's not running.
+ if (this.aRunning === true)
+ clearInterval(this.animID);
+ this.animID = setInterval(doAnimation, this.timeStep, this);
+ this.aRunning = true;
+ }
+ this.sFaster = false;
+ }
+ if (this.sSlower === true) {
+ // Just as above, but make it go slower.
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pSlower, x, y);
+ if (isin === true) {
+ this.timeStep *= 1.4;
+ // More than a second per frame is silly.
+ if (this.timeStep > 1000)
+ this.timeStep = 1000;
+ if (this.aRunning === true)
+ clearInterval(this.animID);
+ this.animID = setInterval(doAnimation, this.timeStep, this);
+ this.aRunning = true;
+ }
+ this.sSlower = false;
+ }
+ if (this.sUpStep === true) {
+ // Make the number of time increments per frame larger.
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pUpStep, x, y);
+ if (isin === true) {
+ // Use a smaller ratio here. Conceptually, it seems like
+ // this should be an integer, but it really doesn't have to be.
+ this.stepsPerFrame *= 1.25;
+ // Fewer than 3 frames per cycle seems silly. For most animations,
+ // you'd proably want at least 10 or 20, at a minimum.
+ if (this.stepsPerFrame > this.steps / 3)
+ this.stepsPerFrame = this.steps / 3;
+ }
+ this.sUpStep = false;
+ }
+ if (this.sDownStep === true) {
+ // As above.
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pDownStep, x, y);
+ if (isin === true) {
+ this.stepsPerFrame /= 1.25;
+ // I am tempted to put a lower bound on this, but it's 'not
+ // absolutely necessary.
+ }
+ this.sDownStep = false;
+ }
+ // The appearance of the widget may have changed.
+ renderFrame(this);
+ }
+}
+// These are useful to the user to help properly place things.
+// These values are given with scale equal to 1, and are worked
+// out from the actual drawing code.
+// The TopHeight is the amount above the circle that is used for the
+// time-step controls, and BottomHeight is the amount used for the
+// faster/slower, pause/run contols. If you don't want the circle at
+// all, then it's a little awkward to work out placement, but it works.
+// There is some imprecision here due to line thickneses, but very close.
+LoopAnimWidget.Radius = 41.5;
+LoopAnimWidget.TopHeight = 24.5;
+LoopAnimWidget.BottomHeight = 21.0;
+// When things are "selected," draw them in this color.
+LoopAnimWidget.sColor = "blue";
+// An OpenAnimWidget is for an open-ended animation that doesn't loop back
+// on itself and repeat. In many respects, it's similar to a LoopWidget.
+// It looks different because it's a long bar, sort of like a scroll bar.
+// It's so similar that there are minimal (for me) comments. See
+// LoopWidgets for certain details.
+class OpenAnimWidget extends AnimationWidget {
+ constructor() {
+ super(...arguments);
+ // These things are important to the animation drawing code, and are
+ // meant to be public(ish).
+ // These are as passed to register().
+ this.barLength = 100;
+ this.timeStep = 25;
+ this.decay = 1.0001;
+ this.visSteps = true;
+ this.visFastSlow = true;
+ this.visPauseRun = true;
+ this.visBar = true;
+ this.barGrab = true;
+ // As for LoopWidth, width modest changes:
+ // Clickable areas:
+ this.pBar = null;
+ this.pDot = null;
+ this.pFaster = null;
+ this.pSlower = null;
+ this.pPauseRun = null;
+ this.pUpStep = null;
+ this.pDownStep = null;
+ // States of parts:
+ //
+ // BUG: If I want to get *really* fancy, then I need another set of
+ // flags to indicate that the mouse *was* clicked on something, so it
+ // is "selected," but the user moved the mouse away from the item without
+ // a mouse-up, so that selected item should be drawn in normal color, not
+ // the highlighted color (sColor). If the mouse is moved back over the
+ // selected item, then the color can go back to being the selection color.
+ this.aRunning = true;
+ this.sDot = false;
+ this.sFaster = false;
+ this.sSlower = false;
+ this.sPauseRun = false;
+ this.sUpStep = false;
+ this.sDownStep = false;
+ }
+ static register(ctx, x, y, scale, width, visWidget, timeStep, decay, visSteps, visFastSlow, visPauseRun, visBar, barGrab, name) {
+ // This is used something like a constructor. It either creates a new
+ // LoopWidget and returns it, or returns one that was created earlier.
+ // Many of these arguments are the same as for Widget.constructor().
+ // In addition, we have
+ // * ctx is assumed to have a t-matrix prepared to properly draw
+ // the widget.
+ // * width is the length of the indicator bar. If you want both the
+ // controls (time steps and fast/slow), then this should be at least
+ // 200 so that the controls don't stick out. Of course, this is the
+ // unscaled size, and you can make the entire thing smaller with the
+ // scale argument.
+ // * timeStep, in milliseconds, is the time from frame to frame.
+ // * decay is complicated. The value of this.curStep must be mapped to
+ // the linear bar, which is not infinite.
+ // We need a map from [0,infty) to [0,width). Define
+ // f(s) = 1 - 1/a^s
+ // This maps [0,infty) to [0,1), provided that a > 1. Then
+ // g(s) = w f(s)
+ // is the function we want. But what about a? The closer a is to 1,
+ // the faster g(s) will approach w. Typically, you'll want a to be
+ // something like 1.001 or 1.0001, depending on the number of steps
+ // in your animation. The decay argment determines a:
+ // a = 1 + 1/decay, so you'll usually want decay to be somewhere in the
+ // range from 100 to (maybe) 100,000. It depends how big the steps
+ // are and how long you want the animiation to run. For comparison,
+ // decay = 1,000 puts f(2000) = 0.86 and f(5000) = 0.99, while
+ // decay = 10,000 puts f(2000) = 0.18, f(5000) = 0.39,
+ // f(20,000) = 0.86. You can also work backwards. If you want
+ // f(n) = x, where x is in [0,1), like x = 85%, then you want
+ // 1 / (1 + 1/a)^n = x, or
+ // a = 1 / [ x^(1/n) - 1 ]
+ // That's not so informative, but you can write it as
+ // a = 1 / [ exp(-ln(x)/n) - 1 ]
+ // If we take x \approx 0.86 so that ln(x) = -0.15 (exactly), then
+ // a = 1 / [ exp(-0.15/n) - 1 ]
+ // Plug in the value for n at which you want to have reached the
+ // 86% level, and you get a.
+ // For brevity, in the code, I use this.decay as the value, a,
+ // discussed above.
+ // BUG: I feel like I made an algebra mistake, but that's the idea.
+ // * visWidget is whether the widget is visible at all -- same as the
+ // vis argument to Widget.construtor().
+ // * visSteps is whether the time step controls (at the right) are visible.
+ // * visFastSlow is whether the faster/slower controls are visible (at
+ // the left)
+ // * visPauseRun is whether the pause/run controls are visible.
+ // * visBar is for the progress bar (with dot indicator).
+ // * barGrab is whether the user is allowed to grab the dot indicator
+ // triangle and control the animation by dragging it.
+ // * name is as in Widget.constructor()
+ //
+ // BUG: Maybe I should have different classes for some of these choices.
+ // There are just too many. These could all use (internally) the
+ // same class, just not with such a crazy number of options.
+ // As for LoopWidget.
+ let type = "OpenAnimWidget";
+ let caller = getCaller();
+ let w = WidgetManager.knownWidget(caller, type, name);
+ if (w != null) {
+ w.draw(ctx);
+ return w;
+ }
+ // Got here, so this widget is not already known, and must be created.
+ w = new OpenAnimWidget(caller, type, x, y, scale, !visWidget, name);
+ // Adjust the length for the scale so that if the user asks for
+ // a bar that is X px long, he gets it. So, the scale adjusts the size
+ // of the "bits", not the total size.
+ w.barLength = width / scale;
+ w.timeStep = timeStep;
+ w.visSteps = visSteps;
+ w.visFastSlow = visFastSlow;
+ w.visPauseRun = visPauseRun;
+ w.visBar = visBar;
+ w.barGrab = barGrab;
+ // For internal use, we convert the given decay to the value we use
+ // for exponentiation.
+ w.decay = 1 + 1 / decay;
+ // Note the existence of this widget for the future.
+ WidgetManager.register(w);
+ // This is a special case because it's an animiation, as with LoopWidget.
+ w.animID = setInterval(doAnimation, w.timeStep, w);
+ // Before returning the widget, it must be drawn.
+ w.draw(ctx);
+ return w;
+ }
+ advanceFrame() {
+ // This animation is open-ended; curStep grows without limit (up to
+ // E500 or whatever it is).
+ this.curStep += this.stepsPerFrame;
+ }
+ draw(ctx) {
+ // This is drawn with (0,0) at the lower-right corner.
+ if (this.hide === true)
+ return;
+ // Don't draw the widget to a tikz file.
+ if (ctx instanceof CTX)
+ return;
+ let saveT = ctx.getTransform();
+ ctx.translate(this.widgetX, this.widgetY);
+ ctx.scale(this.scale, this.scale);
+ var p = new Path2D();
+ if (this.visBar == true) {
+ // These *could* be made accessible to the user, but we already
+ // have a heap of arguments to create this thing.
+ let indDotRadius = 5.0;
+ let barThick = 3.0;
+ let indDotThick = 2.0;
+ // Draw the indicator bar and dot.
+ // Bar first.
+ p.moveTo(0, indDotRadius + OpenAnimWidget.ControlsHeight);
+ p.lineTo(this.barLength, indDotRadius + OpenAnimWidget.ControlsHeight);
+ // Note this circle for reference by mouse events.
+ if (this.barGrab === true)
+ this.pBar = new Path2D(p);
+ ctx.lineWidth = barThick;
+ ctx.stroke(p);
+ // Now the dot.
+ p = new Path2D();
+ let dx = Math.pow(this.decay, this.curStep);
+ dx = this.barLength * (1 - 1 / dx);
+ p.ellipse(dx, indDotRadius + OpenAnimWidget.ControlsHeight, indDotRadius, indDotRadius, 0, 0, 2 * Math.PI);
+ if (this.barGrab === true)
+ this.pDot = new Path2D(p);
+ if (this.sDot == true)
+ ctx.fillStyle = OpenAnimWidget.sColor;
+ else
+ ctx.fillStyle = "red";
+ ctx.fill(p);
+ ctx.lineWidth = indDotThick;
+ ctx.strokeStyle = "black";
+ ctx.stroke(p);
+ }
+ // There may be controls under the bar for faster/slower, pause/run
+ // and larger/smaller steps. Whatever of these is present, they should be
+ // centered, which is a pain. The total width of the pause/run controls
+ // is 40.5, obtained by checking the size of the box necessary to
+ // exactly enclose the controls. Height of that box is 14. I've set
+ // things up so that the height of the "step size" controls is also 14,
+ // and the width of that part is 32. These *could* be expressed in
+ // terms of the various constants defined below, but hard-coding is
+ // easier.
+ // These controls are drawn relative to their individual centers,
+ // so the shifting is done relative to those centers and their widths.
+ let pauseRunWidth = 40;
+ let stepsWidth = 32;
+ let intraGap = 8;
+ // Here (compared to LoopWidget), I use cx and cy to shift the parts
+ // of the control down and right. The right-shift is used for centering
+ // Changing the t-matrix of ctx would work too.
+ let cy = OpenAnimWidget.BarHeight - 4;
+ let cx = this.barLength / 2;
+ // We position the right bit, based on whether the left bit is present.
+ if (this.visSteps === true)
+ cx += intraGap + (pauseRunWidth / 2);
+ // Used for both fast/slow "chevrons" and for up/down arrows.
+ let arrowHeight = 7;
+ if (this.visFastSlow === true) {
+ // I made this a little tighter than for LoopWidget.
+ let arrowOffset = 12; //18;
+ let arrowDepth = 4;
+ let arrowPairSpace = 3;
+ let arrowThick = 1.25;
+ // First, a pair of '>' for going faster.
+ ctx.lineWidth = arrowThick;
+ if (this.sFaster == true)
+ ctx.strokeStyle = OpenAnimWidget.sColor;
+ else
+ ctx.strokeStyle = "black";
+ p = new Path2D();
+ p.moveTo(cx + arrowOffset, cy);
+ p.lineTo(cx + arrowOffset + arrowDepth, cy + arrowHeight);
+ p.lineTo(cx + arrowOffset, cy + 2 * arrowHeight);
+ ctx.stroke(p);
+ // You can't just shift a path. Needs to be rebuilt.
+ p = new Path2D();
+ p.moveTo(cx + arrowOffset + arrowPairSpace, cy);
+ p.lineTo(cx + arrowOffset + arrowDepth + arrowPairSpace, cy + arrowHeight);
+ p.lineTo(cx + arrowOffset + arrowPairSpace, cy + 2 * arrowHeight);
+ ctx.stroke(p);
+ // A rectangle for the clickable area.
+ this.pFaster = new Path2D();
+ this.pFaster.rect(cx + arrowOffset - arrowThick, cy, arrowPairSpace + arrowDepth + 2 * arrowThick, 2 * arrowHeight);
+ /*
+ // BUG: testing
+ ctx.strokeStyle = "green";
+ ctx.lineWidth = 0.5;
+ ctx.stroke(this.pFaster);
+ ctx.strokeStyle = "black";
+ ctx.lineWidth = arrowThick;
+ */
+ // Same idea: '<' to go slower.
+ if (this.sSlower == true)
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ else
+ ctx.strokeStyle = "black";
+ p = new Path2D();
+ p.moveTo(cx - arrowOffset, cy);
+ p.lineTo(cx - arrowOffset - arrowDepth, cy + arrowHeight);
+ p.lineTo(cx - arrowOffset, cy + 2 * arrowHeight);
+ ctx.stroke(p);
+ p = new Path2D();
+ p.moveTo(cx - arrowOffset - arrowPairSpace, cy);
+ p.lineTo(cx - arrowOffset - arrowDepth - arrowPairSpace, cy + arrowHeight);
+ p.lineTo(cx - arrowOffset - arrowPairSpace, cy + 2 * arrowHeight);
+ ctx.stroke(p);
+ // And the clickable area.
+ this.pSlower = new Path2D();
+ this.pSlower.rect(cx - arrowOffset - arrowPairSpace - arrowDepth - arrowThick, cy, arrowPairSpace + arrowDepth + 2 * arrowThick, 2 * arrowHeight);
+ /*
+ // BUG: testing
+ ctx.strokeStyle = "yellow";
+ ctx.lineWidth = 0.5;
+ ctx.stroke(this.pSlower);
+ ctx.strokeStyle = "black";
+ */
+ /*
+ // BUG: test box around entire thing.
+ p = new Path2D();
+ p.rect(cx - arrowOffset - arrowPairSpace - arrowDepth - arrowThick,cy,
+ 2*(arrowOffset + arrowPairSpace + arrowDepth + arrowThick),
+ 2*arrowHeight);
+
+ //let temp = 2*(arrowOffset + arrowPairSpace + arrowDepth + arrowThick);
+ //console.log("val: " +temp);
+
+ ctx.strokeStyle = "red";
+ ctx.stroke(p);
+ */
+ ctx.strokeStyle = "black";
+ }
+ ctx.lineWidth = 1;
+ if (this.visPauseRun === true) {
+ // A || or triangle for pause or run.
+ let pauseSpace = 3.25;
+ let pauseThick = 1.5;
+ let pauseHeight = 2 * arrowHeight;
+ let runThick = 1.5;
+ let runLeftRight = 5;
+ let runHeight = 2 * arrowHeight;
+ if (this.sPauseRun == true)
+ ctx.strokeStyle = LoopAnimWidget.sColor;
+ else
+ ctx.strokeStyle = "black";
+ if (this.aRunning === true) {
+ // The animation is running, so show || to allow pausing.
+ ctx.lineWidth = pauseThick;
+ p = new Path2D();
+ p.moveTo(cx + pauseSpace, cy);
+ p.lineTo(cx + pauseSpace, cy + pauseHeight);
+ ctx.stroke(p);
+ p = new Path2D();
+ p.moveTo(cx - pauseSpace, cy);
+ p.lineTo(cx - pauseSpace, cy + pauseHeight);
+ ctx.stroke(p);
+ ctx.lineWidth = 1;
+ }
+ else {
+ // Animation is paused, so show triangle to run it again.
+ ctx.lineWidth = runThick;
+ p = new Path2D();
+ p.moveTo(cx - runLeftRight, cy);
+ p.lineTo(cx - runLeftRight, cy + runHeight);
+ p.lineTo(cx + runLeftRight, cy + runHeight / 2);
+ p.closePath();
+ ctx.stroke(p);
+ }
+ // Either way (paused or running), we need the clickable area.
+ // This area is too generous for the "run" triangle, because
+ // I use the same rectangle for "pause" and "run," but no big deal.
+ this.pPauseRun = new Path2D();
+ this.pPauseRun.rect(cx - runLeftRight - runThick, cy - runThick, 2 * runLeftRight + 2 * runThick, runHeight + 2 * runThick);
+ /*
+ // BUG: testing
+ ctx.strokeStyle = "blue";
+ ctx.lineWidth = 0.5;
+ ctx.stroke(this.pPauseRun);
+ ctx.strokeStyle = "black";
+ */
+ }
+ // And position the left controls, based on whether the ones on
+ // the right are present.
+ cy += 12.5;
+ cx = this.barLength / 2;
+ if ((this.visPauseRun === true) || (this.visFastSlow === true))
+ cx -= intraGap + stepsWidth / 2;
+ ctx.lineWidth = 1;
+ // Symbols for adjusting the step size.
+ if (this.visSteps === true) {
+ // Up and down arrows. This is a little smaller than for LoopWidget,
+ // so that the height matches the paure/run controls. I also
+ // tightened up the spacing a bit.
+ // Note that I am also using stepThick as a proxy for adjustment
+ // of the fact that the tip of the arrow head is a little tall.
+ let stepSpace = 12;
+ let stepThick = 2.0;
+ let stepHeight = 12.5;
+ let stepArrowHeight = 5.0;
+ let stepArrowWidth = 4.0;
+ ctx.lineWidth = stepThick;
+ if (this.sDownStep === true) {
+ ctx.strokeStyle = OpenAnimWidget.sColor;
+ ctx.fillStyle = OpenAnimWidget.sColor;
+ }
+ else {
+ ctx.strokeStyle = "black";
+ ctx.fillStyle = "black";
+ }
+ // Vertical line
+ p = new Path2D();
+ p.moveTo(cx - stepSpace, cy);
+ p.lineTo(cx - stepSpace, cy - stepHeight);
+ ctx.stroke(p);
+ // Arrow head
+ p = new Path2D();
+ p.moveTo(cx - stepSpace, cy + stepThick);
+ p.lineTo(cx - stepSpace + stepArrowWidth, cy - stepArrowHeight + stepThick);
+ p.lineTo(cx - stepSpace - stepArrowWidth, cy - stepArrowHeight + stepThick);
+ p.closePath();
+ ctx.fill(p);
+ // Clickable area for down arrow.
+ this.pDownStep = new Path2D();
+ this.pDownStep.rect(cx - stepSpace - stepArrowWidth, cy - stepHeight, 2 * stepArrowWidth, stepHeight + stepThick);
+ /*
+ // BUG: testing
+ ctx.strokeStyle = "blue";
+ ctx.lineWidth = 0.5;
+ ctx.stroke(this.pDownStep);
+ ctx.strokeStyle = "black";
+ ctx.lineWidth = stepThick;
+ */
+ // Again, to the right, arrow head up.
+ if (this.sUpStep === true) {
+ ctx.strokeStyle = OpenAnimWidget.sColor;
+ ctx.fillStyle = OpenAnimWidget.sColor;
+ }
+ else {
+ ctx.strokeStyle = "black";
+ ctx.fillStyle = "black";
+ }
+ p = new Path2D();
+ p.moveTo(cx + stepSpace, cy + stepThick);
+ p.lineTo(cx + stepSpace, cy + stepThick - stepHeight);
+ ctx.stroke(p);
+ p = new Path2D();
+ p.moveTo(cx + stepSpace, cy - stepHeight);
+ p.lineTo(cx + stepSpace + stepArrowWidth, cy - stepHeight + stepArrowHeight);
+ p.lineTo(cx + stepSpace - stepArrowWidth, cy - stepHeight + stepArrowHeight);
+ p.closePath();
+ ctx.fill(p);
+ // Clickable area for up arrow.
+ this.pUpStep = new Path2D();
+ this.pUpStep.rect(cx + stepSpace - stepArrowWidth, cy - stepHeight, 2 * stepArrowWidth, stepHeight + stepThick);
+ /*
+ // BUG: testing
+ ctx.strokeStyle = "blue";
+ ctx.lineWidth = 0.5;
+ ctx.stroke(this.pUpStep);
+ ctx.strokeStyle = "black";
+ */
+ ctx.lineWidth = 1;
+ // A little step icon.
+ ctx.strokeStyle = "black";
+ ctx.fillStyle = "black";
+ stepThick = 1.5;
+ let stepSize = 5;
+ ctx.lineWidth = stepThick;
+ // Steps made as one path, starting at upper-left
+ p = new Path2D();
+ p.moveTo(cx - stepSize, cy - 2 * stepSize);
+ p.lineTo(cx, cy - 2 * stepSize);
+ p.lineTo(cx, cy - stepSize);
+ p.lineTo(cx + stepSize, cy - stepSize);
+ p.lineTo(cx + stepSize, cy);
+ ctx.stroke(p);
+ /*
+ // BUG: Testing box around it all.
+ p = new Path2D();
+ p.rect(cx - stepSpace - stepArrowWidth,cy - stepHeight,
+ 2*(stepSpace + stepArrowWidth),
+ stepHeight + stepThick);
+
+ ctx.strokeStyle = "red";
+ ctx.lineWidth = 0.5;
+ ctx.stroke(p);
+
+ //let temp = 2*(stepSpace + stepArrowWidth);
+ //let temp = stepHeight + stepThick;
+ //console.log("val: " + temp);
+ */
+ ctx.strokeStyle = "black";
+ }
+ ctx.lineWidth = 1;
+ ctx.setTransform(saveT);
+ }
+ mouseDown(x, y) {
+ // (x,y) is given in coordinates relative to the owning figure.
+ // Return true iff these coordinates apply to this widget.
+ // BUG: This is almost identical to LoopWidget. DRY?
+ if (this.hide === true)
+ return false;
+ // Adjust coordinates relative to what the draw() methods uses.
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ // The widget also has a scale, which must be taken into account.
+ WidgetManager.bogusCtx.resetTransform();
+ // The run faster area.
+ if (this.pFaster !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pFaster, x, y);
+ if (isin === true) {
+ this.sFaster = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // The run slower area.
+ if (this.pSlower !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pSlower, x, y);
+ if (isin === true) {
+ this.sSlower = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // The pause/run area.
+ if (this.pPauseRun !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pPauseRun, x, y);
+ if (isin === true) {
+ this.sPauseRun = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // The longer step area.
+ if (this.pUpStep !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pUpStep, x, y);
+ if (isin === true) {
+ this.sUpStep = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // The shorter step area.
+ if (this.pDownStep !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pDownStep, x, y);
+ if (isin === true) {
+ this.sDownStep = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ // Last thing to check since it should be "underneath" everything,
+ // although I don't think there's the same kind of overlap that
+ // there was for LoopWidget.
+ if (this.pDot !== null) {
+ // See whether the user clicked on or near the dot. He must click
+ // on the dot, not at some random point along the bar.
+ WidgetManager.bogusCtx.lineWidth = 2;
+ let isin = WidgetManager.bogusCtx.isPointInStroke(this.pDot, x, y);
+ if (isin === false)
+ isin = WidgetManager.bogusCtx.isPointInPath(this.pDot, x, y);
+ if (isin === true) {
+ this.sDot = true;
+ // Move the dot (slightly) so that it is centered at (x,y).
+ // We don't actually "move the dot;" instead we adjust
+ // this.curStep to put the dot where we want it. That is, we
+ // invert g(s) = w (1-1/a^s). See the discussion, in register(),
+ // of this function. We have
+ // x = w (1 - 1/a^s)
+ // a^s = w / (w - x)
+ // s = log_a [ w / (w - x) ]
+ // And recall that log_a (z) = ln(z) / ln(a).
+ let ratio = this.barLength / (this.barLength - x);
+ let s = Math.log(ratio) / Math.log(this.decay);
+ if (s < 0)
+ s = 0;
+ this.curStep = s;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ return false;
+ }
+ mouseMove(x, y) {
+ // As above.
+ if (this.hide === true)
+ return;
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ WidgetManager.bogusCtx.resetTransform();
+ if (this.sDot === true) {
+ // What I will do it check that the mouse is "close enough" to the
+ // bar, but it can be a long ways away.
+ WidgetManager.bogusCtx.lineWidth = 20;
+ let isin = WidgetManager.bogusCtx.isPointInStroke(this.pBar, x, y);
+ if (isin == false)
+ return;
+ let ratio = this.barLength / (this.barLength - x);
+ let s = Math.log(ratio) / Math.log(this.decay);
+ if (s < 0)
+ s = 0;
+ this.curStep = s;
+ // The appearance of the widget may have changed.
+ renderFrame(this);
+ }
+ // BUG: I might (?) want colors to change based on what the mouse
+ // is over. See the BUG comment that goes with aRunning, sCircle, etc.,
+ // at the top of the class.
+ }
+ mouseUp(x, y) {
+ // As above.
+ if (this.hide === true)
+ return;
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ WidgetManager.bogusCtx.resetTransform();
+ // The mouse is up, so nothing can remain selected. In most cases,
+ // releasing the mouse over the selected item means that something
+ // must be done since the "button" was properly pressed.
+ this.sDot = false;
+ if (this.sFaster === true) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pFaster, x, y);
+ if (isin === true) {
+ this.timeStep /= 1.4;
+ if (this.timeStep < 1)
+ this.timeStep = 1;
+ // Stop the animation and restart it at the new speed
+ if (this.aRunning === true)
+ clearInterval(this.animID);
+ this.animID = setInterval(doAnimation, this.timeStep, this);
+ this.aRunning = true;
+ }
+ this.sFaster = false;
+ }
+ if (this.sSlower === true) {
+ // Just as above, but make it go slower.
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pSlower, x, y);
+ if (isin === true) {
+ this.timeStep *= 1.4;
+ // More than a second per frame is silly.
+ if (this.timeStep > 1000)
+ this.timeStep = 1000;
+ if (this.aRunning === true)
+ clearInterval(this.animID);
+ this.animID = setInterval(doAnimation, this.timeStep, this);
+ this.aRunning = true;
+ }
+ this.sSlower = false;
+ }
+ if (this.sPauseRun) {
+ // Did they *release* the mouse over the pause/run area?
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pPauseRun, x, y);
+ if (isin === true) {
+ // Start/stop the animation.
+ if (this.aRunning === true)
+ // Currently running. Pause it.
+ clearInterval(this.animID);
+ else
+ // Currently paused. Restart it.
+ this.animID = setInterval(doAnimation, this.timeStep, this);
+ // Change the pause/run icon too.
+ if (this.aRunning === true)
+ this.aRunning = false;
+ else
+ this.aRunning = true;
+ }
+ this.sPauseRun = false;
+ }
+ if (this.sUpStep === true) {
+ // Make the number of time increments per frame larger.
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pUpStep, x, y);
+ if (isin === true) {
+ // Use a smaller ratio here. Conceptually, it seems like
+ // this should be an integer, but it really doesn't have to be.
+ // Unlike LoopWidget, there's no upper limit on the number
+ // of steps per frame.
+ this.stepsPerFrame *= 1.25;
+ }
+ this.sUpStep = false;
+ }
+ if (this.sDownStep === true) {
+ // As above.
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pDownStep, x, y);
+ if (isin === true)
+ this.stepsPerFrame /= 1.25;
+ this.sDownStep = false;
+ }
+ // The appearance of the widget may have changed.
+ renderFrame(this);
+ }
+}
+// These are useful to the user to help properly place things.
+// The widget is placed based on the lower-left corner, and the
+// user can specify the width. The BarHeight is the height of the
+// bar portion -- it's essentially the radius of the indicator dot --
+// and ControlsHeight is for the controls.
+OpenAnimWidget.BarHeight = 6.0;
+OpenAnimWidget.ControlsHeight = 20.0;
+// This is useful to help the user place stuff above the control.
+OpenAnimWidget.TotalHeight = 32.0;
+// When things are "selected," draw them in this color.
+OpenAnimWidget.sColor = "blue";
+// This is to allow dragging points around. After the much more complicated
+// animation widgets, this is a lot easier. One difference is that dots
+// are not drawn automatically; the user must call the widget's draw()
+// method. This is because the order of drawing may matter -- what should
+// be on top?
+//
+// At one point, this was more flexible, but -- see DraggableDrawWidget below
+// -- it seems better to keep this widget simple (just dots).
+class DraggableDotWidget extends Widget {
+ constructor() {
+ super(...arguments);
+ // The clickable area for the dot, and whether it is selected.
+ // pDot : Path2D | null = null;
+ this.pDot = null;
+ this.selected = false;
+ // The default radius of a dot.
+ this.dotRadius = 3.0;
+ }
+ static register(ctx, x, y, name) {
+ // As with other widgets. Note that there is no scale since it doesn't
+ // make sense here.
+ let type = "DraggableDotWidget";
+ let caller = getCaller();
+ let w = WidgetManager.knownWidget(caller, type, name);
+ if (w != null) {
+ return w;
+ }
+ // Got here, so this widget is not already known, and must be created.
+ // The 'false' here means that we always assume the dot is visible. Leting
+ // this be invisible would be pointless, although it is hidable by
+ // directly changing the Widget.hide field.
+ w = new DraggableDotWidget(caller, type, x, y, 1.0, false, name);
+ // Note the existence of this widget for the future.
+ WidgetManager.register(w);
+ // Note that we do *not* draw this widget here.
+ return w;
+ }
+ draw(ctx) {
+ // Because these widgets might be drawn for tikz output, this uses
+ // FPath instead of Path2D.
+ //
+ // BUG: As a rule, I don't think people will want most widgets (like
+ // LoopWidget) to be drawn to the paper version, but I suppose it should
+ // be possible. *I* would like it for documentation purposes.
+ if (this.hide === true)
+ return;
+ // Adjusting the coordinates this way feels a little weird, but it's
+ // how the other widgets work, and it's actually easier.
+ let saveT = ctx.getTransform();
+ ctx.translate(this.widgetX, this.widgetY);
+ ctx.scale(this.scale, this.scale);
+ // BUG: Somehow, sometimes using FPath puts hair on these dots??
+ // It looks like it happens where the segments meet. There's probably
+ // some algebra mistake. Either that or the JS implementation of
+ // bezier curves sucks. For now, I do not use bezier curves, and
+ // leave it as an ellipse internally.
+ let p = new FPath();
+ //let p = new Path2D();
+ let r = this.dotRadius;
+ p.ellipse(0, 0, r, r, 0, 0, 2 * Math.PI, true);
+ //this.pDot = new Path2D(p);
+ // this.pDot = new FPath(p);
+ this.pDot = p;
+ if (this.selected === true)
+ ctx.fillStyle = DraggableDotWidget.sColor;
+ else
+ ctx.fillStyle = "red";
+ ctx.fill(p);
+ ctx.setTransform(saveT);
+ }
+ mouseDown(x, y) {
+ if (this.hide === true)
+ return false;
+ // Adjust coordinates relative to what the draw() methods uses.
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ // The widget also has a scale, which must be taken into account.
+ WidgetManager.bogusCtx.resetTransform();
+ if (this.pDot !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pDot, x, y);
+ if (isin === true) {
+ this.selected = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ return false;
+ }
+ mouseMove(x, y) {
+ // As above.
+ if (this.hide === true)
+ return;
+ // Restrict to be within the figure area, at the least.
+ // By default (the code here, not this.drawSelFcn, whatever that might
+ // do), the dot doesn't get "lost," but it can be drawn partially outside
+ // the figure proper, leaving "crumbs" that aren't erased until you
+ // scroll the page.
+ let wh = getFigureRect(this);
+ // Rrestrict to the entire figure rectangle, tightened up a bit.
+ wh.w -= 2 * this.dotRadius;
+ wh.ha -= 2 * this.dotRadius;
+ wh.hb -= 2 * this.dotRadius;
+ // Note the minus with hb. The "height below" is a postive value, but
+ // we are comparing to a potentially negative y.
+ if (x <= this.dotRadius)
+ return;
+ if (x >= wh.w)
+ return;
+ if (y <= -wh.hb)
+ return;
+ if (y >= wh.ha)
+ return;
+ if (this.selected === true) {
+ this.widgetX = x;
+ this.widgetY = y;
+ renderFrame(this);
+ }
+ }
+ mouseUp(x, y) {
+ if (this.hide === true)
+ return;
+ // This is more round-about because of the possibility that the
+ // mouse-up occured outside the figure area. We need to reach
+ // the renderFrame() line whatever happens.
+ if (this.selected === true) {
+ this.selected = false;
+ let wh = getFigureRect(this);
+ wh.w -= 2 * this.dotRadius;
+ wh.ha -= 2 * this.dotRadius;
+ wh.hb -= 2 * this.dotRadius;
+ // Note the minus with hb. The "height below" is a postive value, but
+ // we are comparing to a potentially negative y.
+ if ((x > this.dotRadius) && (x < wh.w) &&
+ // (y > this.dotRadius) && (y < wh.h))
+ (y > -wh.hb) && (y < wh.ha)) {
+ this.widgetX = x;
+ this.widgetY = y;
+ }
+ renderFrame(this);
+ }
+ }
+}
+// When things are "selected," draw them in this color.
+DraggableDotWidget.sColor = "blue";
+class DraggableDrawWidget extends Widget {
+ constructor() {
+ super(...arguments);
+ // The clickable area for the "dot" (which could have any shape)
+ // and whether it is selected.
+ this.pDot = null;
+ this.selected = false;
+ // The user must provide these functions.
+ // Note the litle cheat to make the ts compiler shut its yapper.
+ this.drawFcn = null;
+ this.drawSelFcn = null;
+ this.testPosFcn = null;
+ }
+ static register(ctx, x, y, drawFcn, drawSelFcn, testPosFcn, name) {
+ // The drawFcn should be defined to draw whatevever it wants.
+ // It should take the ctx as the sole argument, and return a path
+ // such that a click in the path (using ctx.isPointInPath()).
+ //
+ // The drawSelFcn is similar, but is used for drawing when the
+ // item is selected -- so that the user can change the color or
+ // whatever.
+ //
+ // The testPosFcn receives (x,y) as an argument, along with the (w,h)
+ // of the figure area (in pdf points) and should return
+ // true (point is acceptable) or false (not acceptable).
+ let type = "DraggableDrawWidget";
+ let caller = getCaller();
+ let w = WidgetManager.knownWidget(caller, type, name);
+ if (w != null) {
+ return w;
+ }
+ // Got here, so this widget is not already known, and must be created.
+ // The 'false' here means that we always assume the dot is visible. Leting
+ // this be invisible would be pointless, although it is hidable by
+ // directly changing the Widget.hide field.
+ w = new DraggableDrawWidget(caller, type, x, y, 1.0, false, name);
+ w.drawFcn = drawFcn;
+ w.drawSelFcn = drawSelFcn;
+ w.testPosFcn = testPosFcn;
+ // Note the existence of this widget for the future.
+ WidgetManager.register(w);
+ return w;
+ }
+ draw(ctx) {
+ // Because these widgets might be drawn for tikz output, this uses
+ // FPath instead of Path2D.
+ //
+ // BUG: As a rule, I don't think people will want most widgets (like
+ // LoopWidget) to be drawn to the paper version, but I suppose it should
+ // be possible. *I* would like it for documentation purposes.
+ if (this.hide === true)
+ return;
+ // Adjusting the coordinates this way feels a little weird, but it's
+ // how the other widgets work, and it's actually easier.
+ let saveT = ctx.getTransform();
+ ctx.translate(this.widgetX, this.widgetY);
+ ctx.scale(this.scale, this.scale);
+ if (this.selected === true)
+ this.pDot = this.drawSelFcn(ctx);
+ else
+ this.pDot = this.drawFcn(ctx);
+ ctx.setTransform(saveT);
+ }
+ mouseDown(x, y) {
+ //console.log("dot down");
+ if (this.hide === true)
+ return false;
+ // Adjust coordinates relative to what the draw() methods uses.
+ x -= this.widgetX;
+ y -= this.widgetY;
+ x /= this.scale;
+ y /= this.scale;
+ WidgetManager.bogusCtx.resetTransform();
+ if (this.pDot !== null) {
+ let isin = WidgetManager.bogusCtx.isPointInPath(this.pDot, x, y);
+ if (isin === true) {
+ this.selected = true;
+ WidgetManager.mouseOwner = this;
+ renderFrame(this);
+ return true;
+ }
+ }
+ return false;
+ }
+ mouseMove(x, y) {
+ // As above.
+ if (this.hide === true)
+ return;
+ // Restrict to be within the figure area, at the least.
+ // By default (the code here, not this.drawSelFcn, whatever that might
+ // do), the dot doesn't get "lost," but it can be drawn partially outside
+ // the figure proper, leaving "crumbs" that aren't erased until you
+ // scroll the page.
+ let wh = getFigureRect(this);
+ if (this.testPosFcn(x, y, wh.w, wh.ha, wh.hb) === false)
+ return;
+ if (this.selected === true) {
+ this.widgetX = x;
+ this.widgetY = y;
+ renderFrame(this);
+ }
+ }
+ mouseUp(x, y) {
+ if (this.hide === true)
+ return;
+ // This is more round-about because of the possibility that the
+ // mouse-up occured outside the figure area. We need to reach
+ // the renderFrame() line whatever happens.
+ if (this.selected === true) {
+ this.selected = false;
+ let wh = getFigureRect(this);
+ if (this.testPosFcn(x, y, wh.w, wh.ha, wh.hb) === true) {
+ this.widgetX = x;
+ this.widgetY = y;
+ }
+ renderFrame(this);
+ }
+ }
+}
+// When things are "selected," draw them in this color.
+DraggableDrawWidget.sColor = "blue";
+// A numerical value widget. This uses an HTML <input type = "number">
+// thing by putting it on top of the canvas. Using HTML this way is not the
+// direction I want to go, but I do want to see if it's feasible and what's
+// involved. Certain widgets that already exist in HTML are probably not
+// worth building from scratch as "pure canvas" widgets. Also, this
+// allows me to delay dealing with keyboard events.
+//
+// The idea is to put the HTML widget on top of the canvas, with absolute
+// placement.
+//
+// NOTE: Firefox generates a warning about "ansynchronous panning." Somehow
+// it sees that I'm doing something tricky and warns about it. As far as I
+// can tell, I'm not doing anything likely to be deprecated or problematic
+// in the future. Edge doesn't complain.
+class NumberInputWidget extends Widget {
+ constructor() {
+ super(...arguments);
+ // An HTML <input type = "number"> thing. This is the HTML DOM element,
+ // like from document.getElementById() or createElement("input").
+ this.theWidget = null;
+ }
+ static register(ctx, x, y, v, name) {
+ // Even fewer arguments than usual.
+ // v = initial value;
+ let type = "NumberInputWidget";
+ let caller = getCaller();
+ let w = WidgetManager.knownWidget(caller, type, name);
+ if (w != null) {
+ w.draw(ctx);
+ return w;
+ }
+ // Got here, so this widget is not already known, and must be created.
+ w = new NumberInputWidget(caller, type, x, y, 1.0, false, name);
+ // Note the existence of this widget for the future.
+ WidgetManager.register(w);
+ // The usual syntax for this within HTML is
+ // <input type="number" id="something" name="whatever" min="10" max="100">
+ // There are some other possible settings too. See
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
+ //
+ // BUG: There's some funny business here that I don't like at all.
+ // These DOM elements are placed relative to the body as a whole,
+ // not the canvas. So, I need to know where the figure containing
+ // this widget falls on the entire document. The body height is adjusted
+ // when I tweak the scroll bars (mainbody.style.height and width),
+ // and the widget needs to be placed relative to that.
+ //
+ // As a result, every time the user zooms (or resizes the window),
+ // every widget placed in the DOM this way needs to be repositioned.
+ // One reasonable way to deal with that is to recalculate the position
+ // every time draw() is called.
+ //
+ // So this isn't really a BUG; it works as intended. But using
+ // HTML this way is an entirely different (and unpleasant) approach.
+ w.theWidget = document.createElement("input");
+ w.theWidget.setAttribute("type", "number");
+ w.theWidget.value = v.toString();
+ w.theWidget.style.position = "absolute";
+ w.theWidget.style.display = "block";
+ w.theWidget.style.left = 400 + "px";
+ w.theWidget.style.top = 900 + "px";
+ w.theWidget.style.width = 50 + "px";
+ w.theWidget.style.height = 10 + "px";
+ w.theWidget.style.zIndex = "99";
+ document.body.appendChild(w.theWidget);
+ // The owner will want to redraw it's figure when this changes.
+ w.theWidget.onchange = function () {
+ // console.log("change");
+ let myFunc = getAugmentedFunction(w.owner);
+ let fpc = myFunc.figurePanelClass;
+ fpc.render();
+ };
+ // Before returning the widget, it must be drawn.
+ w.draw(ctx);
+ return w;
+ }
+ getValue() {
+ // Return the numerical value.
+ //
+ // BUG: This seems to return a string -- whatever is in the field.
+ // The caller will typically need to do parseInt() or parseFloat()
+ // on the result. If this widget were better, then it would know
+ // what it's supposed to return and limit the possible things it can hold.
+ // BUG: I should probably have getter functions for everything in
+ // all widgets instead of having the user access fields directly.
+ // So, define LoopWidget.getCurStep(), etc.
+ // It doesn't *really* matter, but it makes it clearer to the user
+ // what he's supposed to have access to.
+ return this.theWidget.value;
+ }
+ draw(ctx) {
+ // This widget is really an HTML DOM element, so this function doesn't
+ // actually draw the widget. It repositions the element in the DOM.
+ // See the discussion in register().
+ // BUG: make accessing this information a function, like I did
+ // for getFigureRect().
+ let myFunc = getAugmentedFunction(this.owner);
+ let fpc = myFunc.figurePanelClass;
+ let totalV = fpc.totalV;
+ // The vertical position is relatively easy, but the horizontal position
+ // requires a calculation similar to fullRender() since the page
+ // is centered. Also, the vertical position must be given in LH coordinates
+ // relative to the entire document.
+ // BUG: Make this calculation a function used in main.js too.
+ // Can I combine with what's done for mouse events there too?
+ // In fact, this is more like the mouse calculation than like fullRender().
+ let vpos = fpc.totalV + fpc.h - this.widgetY;
+ // let vpos = totalV + this.widgetY;
+ // BUG: I don't like reaching into the DOM this way to get the canvas,
+ // but what is the alternative?
+ let hpos = this.widgetX;
+ let canvas = document.getElementById("pdf_renderer");
+ let visWidth = document.documentElement.clientWidth;
+ let totWidth = FullPanel.getFullWidth();
+ if (visWidth > totWidth) {
+ // No horizontal scroll bar. Center it.
+ let canvasCenter = canvas.width / 2;
+ let docCenter = FullPanel.getFullWidth() / 2;
+ canvasCenter = canvasCenter / PDFDocument.getZoom();
+ hpos = hpos + (canvasCenter - docCenter);
+ }
+ else {
+ // Shift according to the horizontal scroll bar.
+ hpos = hpos - window.scrollX;
+ }
+ hpos += fpc.margin;
+ //console.log("wid at " +hpos+ " " +vpos);
+ // Don't forget the stupid "px"!
+ this.theWidget.style.top = vpos + "px";
+ this.theWidget.style.left = hpos + "px";
+ }
+}
+// Another DOM-based widget. A simple button.
+class ButtonWidget extends Widget {
+ constructor() {
+ super(...arguments);
+ // An HTML <input type = "button"> thing.
+ this.theWidget = null;
+ // Sometimes you want to treat the button as a boolean
+ // Each time the button is clicked, this toggles.
+ this.clickState = false;
+ // And this is set to true whenever the button is clicked.
+ this.resetState = false;
+ }
+ static register(ctx, x, y, text, name) {
+ // Even fewer arguments than usual.
+ let type = "ButtonWidget";
+ let caller = getCaller();
+ let w = WidgetManager.knownWidget(caller, type, name);
+ if (w != null) {
+ w.draw(ctx);
+ return w;
+ }
+ // Got here, so this widget is not already known, and must be created.
+ w = new ButtonWidget(caller, type, x, y, 1.0, false, name);
+ // Note the existence of this widget for the future.
+ WidgetManager.register(w);
+ // The usual syntax for this within HTML is
+ // <button type="button" id="something" name="whatever">
+ //
+ // BUG: There's some funny business here that I don't like at all.
+ // These DOM elements are placed relative to the body as a whole,
+ // not the canvas. So, I need to know where the figure containing
+ // this widget falls on the entire document. The body height is adjusted
+ // when I tweak the scroll bars (mainbody.style.height and width),
+ // and the widget needs to be placed relative to that.
+ //
+ // As a result, every time the user zooms (or resizes the window),
+ // every widget placed in the DOM this way needs to be repositioned.
+ // One reasonable way to deal with that is to recalculate the position
+ // every time draw() is called.
+ //
+ // So this isn't really a BUG; it works as intended. But using
+ // HTML this way is an entirely different (and unpleasant) approach.
+ w.theWidget = document.createElement("button");
+ w.theWidget.setAttribute("type", "button");
+ w.theWidget.style.fontSize = "10px";
+ w.theWidget.textContent = text;
+ w.theWidget.style.position = "absolute";
+ w.theWidget.style.display = "block";
+ w.theWidget.style.left = 400 + "px";
+ w.theWidget.style.top = 900 + "px";
+ //w.theWidget.style.width = 100+"px"; // Width automatic, based on text.
+ w.theWidget.style.height = 18 + "px";
+ w.theWidget.style.zIndex = "99";
+ document.body.appendChild(w.theWidget);
+ // Need to hear about clicks....
+ // BUG: I would have thought that this would work, but I don't think that
+ // the doClick() method is seeing the correct class instance or something.
+ //w.theWidget.addEventListener('click',w.doClick,false);
+ // Instead, define the action here.
+ w.theWidget.addEventListener('click', () => {
+ if (w.clickState === false)
+ w.clickState = true;
+ else {
+ w.clickState = false;
+ w.resetState = true;
+ }
+ // If the figure is part of an animation, then it will be drawn taking
+ // into account this click in the next frame. If the figure is *not*
+ // an animation, then we need to refresh.
+ let myFunc = getAugmentedFunction(w.owner);
+ let fpc = myFunc.figurePanelClass;
+ fpc.render();
+ }, false);
+ // Before returning the widget, it must be drawn.
+ w.draw(ctx);
+ return w;
+ }
+ doClick() {
+ // BUG: This doesn't work. See above.
+ if (this.clickState === false)
+ this.clickState = true;
+ else
+ this.clickState = false;
+ //console.log("swapped click");
+ }
+ draw(ctx) {
+ // This widget is really an HTML DOM element, so this function doesn't
+ // actually draw the widget. It repositions the element in the DOM.
+ // See the discussion in register().
+ let myFunc = getAugmentedFunction(this.owner);
+ let fpc = myFunc.figurePanelClass;
+ // The horizontal position requires a calculation similar to fullRender()
+ // since the page is centered and the vertical position must be given
+ // in LH coordinates relative to the entire document.
+ // BUG: Make this calculation a function used in main.js too.
+ // Can I combine with what's done for mouse events there too?
+ // In fact, this is more like the mouse calculation than like fullRender().
+ let vpos = fpc.totalV + fpc.h - this.widgetY;
+ let hpos = this.widgetX;
+ // BUG: Again, with the DOM access.
+ let canvas = document.getElementById("pdf_renderer");
+ let visWidth = document.documentElement.clientWidth;
+ let totWidth = FullPanel.getFullWidth();
+ if (visWidth > totWidth) {
+ // No horizontal scroll bar. Center it.
+ let canvasCenter = canvas.width / 2;
+ let docCenter = FullPanel.getFullWidth() / 2;
+ canvasCenter = canvasCenter / PDFDocument.getZoom();
+ hpos = hpos + (canvasCenter - docCenter);
+ }
+ else {
+ // Shift according to the horizontal scroll bar.
+ hpos = hpos - window.scrollX;
+ }
+ hpos += fpc.margin;
+ // It seems that these must be placed using style.top, not style.bottom.
+ // The clientHeight seems to be the height of the text in the button,
+ // which style.height is the actually button. Don't forget the stupid "px"!
+ let w = this.theWidget;
+ w.style.top = (vpos - parseInt(w.style.height, 10)) + "px";
+ w.style.left = hpos + "px";
+ }
+}