I tried to make the maths background in Part 1 understandable for a general audience, but this Dojo section is going to have to assume you know a few programming terms. If that doesn't sound like your thing, then maybe you're just looking for the string bend calculator.
Lying
Not being a frequent Javascript user and using Dojo for the first time I of course prototyped this as a completely procedural program and then started to refactor it into a better design. I'm going to pretend the final version is how it was meant to be all along. However there are some beginner Javascript mistakes (or Dojo peculiarities) I made along the way and they're included. They're useful for me if no-one else.Aiiieeee (IE)
I'm not anti-Microsoft, but they don't make it easy. This one had me tearing hair out and tidying HTML and Javascript until I found out it that using Dojo from a CDN (content delivery network) just doesn't work with Dojo 1.9. Finding out that loading Dojo was the problem wasn't made any easier by IE 9 not reporting the failed load in web designer mode, IE 10 does tell you about the problem. Changing to Dojo 1.9.1 fixed that.What's that in old money?
Guitar related measures (scale, string gauge) are often given in imperial units, so scale is 24.5 inches rather than 648mm and string gauges are given in thousandths of an inch. Metric on the other hand is much simpler for conversions and calculations. So I wanted to have two controls for each of these that would allow you to adjust in either system and update both boxes.This turns out to be tricky, you set each control to update the other when updated. Now when you update the metric control it updates the imperial control, which wants to update the metric control again, which... etc. While the cycle does stop it leads to weird effects like not being able to set certain metric lengths. Dojo should let you pass a parameter to stop this propagation, but it doesn't work in this situation. The simplest solution is that each box checks whether the other box has focus first and doesn't try to update it if it does.
Incidentally, an inch is now defined to be exactly 25.4mm, so perfect conversion is possible.
Listing nothing
I've made use of some dropdown boxes to provide suggested values for things like density and scale. These are filled from lists provided in the code, mainly of "value" and "name" pairs. Dojo handles null, 0 and null string values the same, which means that even if you need a list of numbers it's best to supply the values as strings, particularly to avoid problems with 0 being interpreted as a default value ("0" instead is okay). That grates a bit on someone more used to strongly type languages, but it's necessary with Javascript.Picking lint
The code got quite a few passes through the useful JSLint, particularly while trying to fix the IE problem above. Some things that are Javascript idioms which you don't do in other C-like languages became apparent:
- Variables in a scope get declared with a single 'var' statement, separating multiple declarations and assignments with a "," rather than making multiple ";" ended statements. This includes defining function names.
- Being statements, anonymous function declarations should have a ";" after the braces (unless the assignment is in a list as above, where a "," might be used).
- Generally linting javascript is very useful since you don't have the same quality of compiler or interpreter warnings available.
Ready: get, set, call
Because there are a lot of variables in the model for string bending that need to be exposed to the interface and update each other in complex ways a lot of getters and setters and callbacks are needed. Writing them all is hard to maintain and makes the JS bulky, there may be other ways to do this, but the solution I came up with is to place this in the class constructor for the guitar string after a list of "this.property=" assignments:var nullfn = function(){}, ii, key, updProps = [ {id: "bendstretch", tgt: "bendlat"}, {id: "bendlat", tgt: "bendlatf"}, {id: "bendlatf", tgt: null} ]; this.buildSet = function (key) { this["set_"+key] = function ( newval ) { this[key]= newval; this["updated_" + key](); }; this["cb_updated_"+key] = nullfn; }; this.buildUpdRecalc = function (id, tgt) { this["updated_"+id] = function () { this["recalc_"+tgt](); this["cb_updated_"+id](); }; }; this.buildUpdOnly = function (id) { this["updated_"+id] = function () { this["cb_updated_"+id](); }; }; for (ii=0; ii < updProps.length; ii++ ) { key = updProps[ii]; this.buildSet(key.id); if (key.tgt) { this.buildUpdRecalc(key.id, key.tgt); } else { this.buildUpdOnly(key.id); } }
Only a few of the properties are shown, but each one simply defines the next property that should be updated. The loop at the end creates a setter and a null callback for each property. When set a property calls a recalculate function for the next property in the chain (if there is one) and the callback function which can be set by the interface. The buildSet, buildUpdRecalc and buildUpdOnly methods are used to add these functions to the class, using the "this[name]" method to address the class properties (variables or methods) by name. Since they're methods, "this" refers to the object being constructed, rather than a separate function scope.
Of course all the "recalc_property" methods still need to be written, and will use the corresponding "set_property" method to do the update, e.g.
recalc_bendforce : function () { this.set_bendforce( this.calcbendforce ( this.tension, this.bendstones )); },
calcbendforce : function ( tension, semitones ) { var bendforce = tension * (Math.pow(2, semitones/6) - 1); return bendforce; }
Stringing it up
The interface is a fairly straightforward HTML page with Dojo input and display boxes attached to elements. To get it to talk to the physics model (which is defined as a Dojo class) each input object can be given a callback to run when the value is updated in the interface and can set a callback on the physics object to allow it to update the interface. Some elements don't depend on anything (like scale), so they don't set the callback in the physics object. Others can't be set and can only be updated as the result of other changes (like bend distance), so they don't use the set method.String tension can both be set directly (bypassing the tension calculations) and updated by other changes:
tension_box is a "dijit/form/NumberSpinner" assigned to the element name "tension":
tension_box = new Spinner({ onChange: tension_updated // other parameters }, "tension");
tension_updated = function () { if (tension_box.focused) {wire.set_tension(tension_box.get("value"));} };
wire.cb_updated_tension = function (){ if (!tension_box.focused) {tension_box.set("value",wire.tension);} };
tension_box.startup();
Progression
In part 1 I mentioned that things were done in terms of tension. You can eliminate calculating the tension if you just want the bend distance, but since it's something that might be interesting (for string sets, or knowing how much harder a certain setup is to bend) instead a lot of the model works by finding the tension and using it to find the other numbers.- Scale, string gauge, density and note are all used to calculate tension.
- The bend tonal distance is used to find how much extra tension is needed.
- Total string length (including past headstock), stiffness, gauge and bend tension are used to find the length stretch.
- Length stretch is used to find the sideways stretch.
- The sideways bend force needed is found using the total tension, sideways stretch and scale length.
Graphs and hair loss
The graphs are done with Dojo's "Chart" widget, using the Lines type ("dojox/charting/plot2d/Lines"). One thing school science lessons should have drummed into you is that you need to label the axes on graphs. And this one isn't very useful:What is the bend force for 25"? It's probably more than 24.5", but we can't even say that for sure, I might have gone crazy (or made a strange mistake) and put the scale upside down. I did ask it for labels (the "title" parameter). The problem turns out to be automatic ranges, if you don't give a "min" and "max" for the axes range, Dojo tries to calculate it from the data series you've given it.
I was starting the charts with an empty series to be filled in on the first update, the "scale length" x axis is okay, we can give it min and max since it doesn't change, but y needs to be automatic. Since Dojo can't guess and hasn't been given a range to start with it doesn't leave enough space for it once it gets the data. The solution is to generate the data when the chart is first created. Final result:
Hosting
Finally a quick note on hosting. At the time of writing it's 2013 and like many people I'm writing on a (free) web service. You can do all kinds of things with plugins, but just try deploying your own Javscript, or (actually worse) your own web page. Getting Javascript hosted by blogger or google sites is possible if you run through hoops, but if you need to go down this road and are happy to put your code under an open source license then Google Code is probably the simplest solution.To use Dojo to do the page layout you need your own .html file, and that rules out most free websites and blogging. I've ended up using paid-for-hosting that I already had, but one interesting alternative is Amazon Web Services S3 which can host static content and provides 5GB free for the first year (though you need a credit card), even after a year hosting a few MB of JS (or even a few hundred) should cost less than a pound per year.
The finished product can be found at http://www.imalone.co.uk/stringbendcalc.html.
No comments:
Post a comment