A Tutorial on Callbacks, Factories and Animation — and an Animated Disclosure Widget
-
As always, feel free to ask questions, and let me know of any corrections or improvements. Cheers.
EDIT: I've rewritten this from scratch using a factory. Thanks to @d-healey and @ulrik for
you insightful commentary.EDIT: I can longer fight with forum software's code comment parsing. I am beaten.
/*************************************************************************************** * * * FERRET UI TOOLKIT * * * * Disclosure Triangle * * * ***************************************************************************************/ namespace TriangleWidget { /////////////////////////////// WIDGET FACTORY ///////////////////////////////////// // // Create a factory function to produce widgets. The size is a single // variable because the widget will be square. inline function createWidget(name, x, y, size, colour) { // ------------- Creating the Panel // Call HISE's API to create a panel. local widget = Content.addPanel(name, x, y); // Assign these built-in parameters to the panel. Content.setPropertiesFromJSON(name, { "width": size, "height": size, // Without this, the mouse callback will not execute. "allowCallbacks": "Clicks & Hover" }); // ------------- Creating Public Variables // This angle represents the closed position. widget.data.closedAngle = 90; // This angle respresnts the open position. widget.data.openAngle = 180; // Choose which position the widget should start in. widget.data.currentAngle = widget.data.openAngle;; // Specify which position the starting angle respresents. widget.data.status = "open"; // Choose the rotation algorithm. widget.data.bouncyRotation = false; widget.data.baseColour = colour; // Choose how many milliseconds to schedule the timer callback. widget.data.timingInterface = 2; // Choose the number of animation frames. widget.data.stepSize = 1; // Store the widget size so we do not need a function call. widget.data.size = size; ////////////////////////////// PAINT ROUTINE //////////////////////////////// // // This is the paint routine for the panel, which draws the widget. widget.setPaintRoutine(function(g) { g.setColour(this.data.baseColour); // Humans deal in angles; geometry deals with radians. var currentRadians = Math.toRadians(this.data.currentAngle); // Choose the rotation algorithm. if (bouncyRotation) { // We animate the drawing angle of the triangle. var rotationRadians = currentRadians; } else { // We rotate the panel. g.rotate(currentRadians, [this.data.size/2, this.data.size/2]); var rotationRadians = 0; } // Draw the triangle. g.fillTriangle([0, 0, this.data.size, this.data.size], rotationRadians); }); ///////////////////////////// MOUSE CALLBACK //////////////////////////////// // // Define the function for when the panel is clicked on, making it a button. widget.setMouseCallback(function(event) { if (event.clicked) { // Begin the animation. this.startTimer(this.data.timingInternal); } }); ///////////////////////////// TIMER CALLBACK //////////////////////////////// // // Define the function that is called with each scheduled iteration. widget.setTimerCallback(function() { // Call the routine to animate the triangle. Toggle (this); }); // Return our completed widget now that it has been constructed. return widget; }; ///////////////////////// ANIMATION HELPER FUNCTION ///////////////////////////// // // We define a helper function to animate the triangle. By declaring it inside // the namespace of the widget, we have functionally encapsualted the widget. // However, this function is not part of the widget like the callbacks are. // This function could be four (e.g., putting the increment inside the conditional, // using a ternary operator), but this probably would not be help to explain what // is going on. Maybe there is something better in between, though? inline function Toggle (component) { // If it is open, close it. if (component.data.status == "open") { component.data.currentAngle -= component.data.stepSize; if (component.data.currentAngle <= component.data.closedAngle) { component.stopTimer(); component.data.status = "closed"; } // if it is closed, open it. } else { component.data.currentAngle += component.data.stepSize; if (component.data.currentAngle >= component.data.openAngle) { component.stopTimer(); component.data.status = "open"; } } // Call the paint function for the panel to draw this frame of animation. component.repaint(); }; };
-
@Dr-Bill That's nice
Just be careful not to name your variables "global" when they are not, to remove any confusion
Also,global_closedAngle
should be a constant, andglobal_openAngle
isn't usedThe deformation of the triangle could be avoided if you rotate the canva instead of the triangle angle
-
@ustk Thank you for the feedback.
Would this be a "var" declaration, then? (All the HISE documentation links on variable types lead to 404 pages.)
Rotation: Thanks for the keen eye. At first, I wanted a centred rotation – but I actually like the bouncing behavior better. I should offer a choice, though. (Will do.)
-
@Dr-Bill
const var
or justconst
(same exact thing)Do you want my version?
-
@Dr-Bill
Would this be a "var" declaration, then?
Avoid var like your life depends on it. The only place they should be used is in non-inline functions.
All the HISE documentation links on variable types lead to 404 pages.
Not this one :)
https://docs.hise.audio/scripting/scripting-in-hise/hise-script-coding-standards.html#variables
-
@ustk I have updated the tutorial:
-
Global variables are now var types. (I didn't use const because the user may choose to use them within a context where they may change during programme execution.)
-
The rotation algorithm can be specified as "traditional" or "bouncy".
-
Expanded documentation on specifying the geometry of the triangle.
Thanks again for your comments.
-
-
@Dr-Bill don’t use var but reg for what can be modified
In fact for such a widget you don't need any external variable that does nothing but clutter the script, same for specific functions, everything can be done in a more simple way. No need for using the value of the component, here the data object is more adaptedconst var panel = Content.getComponent("panel"); panel.data.angle = 0; panel.data.state = false; // Paint Routine panel.setPaintRoutine(function(g) { g.setColour(Colours.whitesmoke); g.rotate(Math.toRadians(this.data.angle), [this.getWidth()/2, this.getHeight()/2]); // triangle reduced by 30% to avoid edge collision (sqrt((w*w)+(h*h)) = 0.707) var area = [this.getWidth()*0.15, this.getWidth()*0.15, this.getWidth()*0.7, this.getHeight()*0.7]; g.fillTriangle(area, Math.PI/2); }); // Mouse CB panel.setMouseCallback(function(event) { if (event.clicked) { this.data.state = !this.data.state; this.startTimer(30); } }); // Timer panel.setTimerCallback(function() { if (this.data.state) { if (panel.data.angle < 90) panel.data.angle += 10; else this.stopTimer(); } else { if (panel.data.angle > 0) panel.data.angle -= 10; else this.stopTimer(); } this.repaint(); });
HiseSnippet 1093.3ocsVEtaaaCDlxIpcxacXEXO.bEX.xotNxIsManqaYwIYyXKIF0ocCXnnfQh1hvxjdjzwyqHuy6MX6NJYKm3jzTCL8Cace2cje7ti2oNZUL2XTZhWvoSGwIdele2oRaZqTlPRZuOw6y8OhYrbMMGZuoiXFCOg34s1Og.dAqSbO+yOrGKiIi4kPDxaThX9uJFJrknc18WDYYGxR3mJFtf0Oc21wJYKUlZLvm07iHiXwCX84GyPyp3S7t2AIBqR20xrbCXydpjocSUSj41+FgQbVFGEZR5BKTNLoUpHKoyrypgP7VuS4Ies7S9W5ejHQLGuLB7ENEzROVLF3U41nTyOBJ4s.kVOmROzuarVLxVpA4ym52VBIjdLHTuHUxskT4uW2ukBrPZaLjMfenFDl6Q3yihpSgep8hpfUFUFuQbFmoCAfpUgDfwROmooiXRdF8kzYKUetskZ3HkDDBejS6i.WbuzHgYYMXx9YbvinKgZv.Cf1ikY33Vr4lzNPn0RekZrUH4EFa3VGbAZXuwxXqPIC6Wq56qFzGMHu1HL+OSiIoBHlOTMfCDI.sQqvcK7HlMsgU8JVhfIMg1TgYAJVqN8ObPvQ52DI1zvZatUc5LnelK5mZQr2lur.gsZQ9oSySFGySnmMktczWSsJJ6bkHgxS5yowprLHcqjzPyepsggS1XRsGGltQZsZXbowNQ6TqZ.FcYZNCftJO1HpQymURkOD5NKyZD8sEQidv0rSKXdHti0otHSm1atEbztvkww7wQpwFNs0dkoBGRKVV1YvUvxbA+bH66xGhdzbIn5QDOfm.GL.NnLVOKw+UWA5EyrBjzVrEfNbarbL3hEnjCujONwk4ybpbk8nfLnlkJP+N52FA5CBVRyieIsYDRONTphVTPS0nbVlywBsuO3l1fumdCq+Staqe0bbMeDdeHrLSM+Y14mpjGqr7SJBDUunJ8pp506Z0g2p0P4JtqWi5B9byNFJGO7LttNzpHaLetgPmqK2N7d2s1gw4MYVvPkrsTXOYDuP9PUVB1lCee4lmjhtTvaut89PDG6mVfA1MhqsBjNd6yOGFHk2cMveetY.D8c1VzZi3ceqS6Cl06EyhDAr0222kQI+07QV6p1c5hBSvalDOeuDPN0ciblDT5plLq.1fKeK7digdhLaJLSbod+vTGUx3Ll8xihvYtEJfDxk5+i83kFgc5hyj+HlOEcqymtqT7g9cD13zqmiUtFNBop+O3XwT8G3ePud7XaIAW2+veeUGg+A197YW8glrZATk3e73gcgIUwbX2kPoiAqFpfkq4xQnLFA5xkINg+EdJT1Dk8JT1blRxPVrV8t37qg32M7INDfSR2mIE.euFHSaRbWMA+7iZDQFBeBy6hiwi+SfaPWuOasB9r8J3ySWAed1J3yyWAe1YE74atUevud7GGaUCyuN..cNv0ixy6.ICprbUgj+CT+uN.C
-
@d-healey said in A Tutorial on Callbacks, Animation and Creating Panel Buttons — And an Animated Disclosure Widget:
@Dr-Bill
Would this be a "var" declaration, then?
Avoid var like your life depends on it. The only place they should be used is in non-inline functions.
Please do call me out when I should be using a reg variable.
All the HISE documentation links on variable types lead to 404 pages.
Not this one :)
https://docs.hise.audio/scripting/scripting-in-hise/hise-script-coding-standards.html#variables
I appreciate the link; I guess I'm particular on these things. The text you refer to is an optional style guideline; it's not documentation, nor meant to be. Any technical discussion therein is incidental, incomplete, and not within a referential context.
This is the documentation link for data types is here: ```
https://docs.hise.audio/scripting/scripting-in-hise/javascript.html#data-types -
@ustk Thank you for taking the time to write this code. I've incorporated your colour assignment, as that's simpler than referring the user to the Properties panel—nice thinking. Please understand that I am writing a tutorial; you code is certainly more efficient, but not pedagogical. (I love your use of the data extension.)
Can you please explain what you mean by "…don’t use var but reg for what can be modified"?
-
@Dr-Bill
Can you please explain what you mean by "…don’t use var but reg for what can be modified"?
Always use
reg
for any data that isn't constant (except arrays/objects). The only time you should usevar
is in functions, and that's only because we have no other choice. The reason to avoid var is becausevar
leaks into the script wide namespace. -
-
@Dr-Bill
I originally had the variables outside the functions as reg
Sounds like you had it right for those variables. If they needed to be script-wide then reg was the way to go.
-
@d-healey What's funny is that in my comments explaining why I used reg variables, I named-check you, saying you preferred reg variables. (Look back through the post edits back to the initial post.)
Bottom line is you guys are both are far more knowledge about HISE than me. I appreciate your insights on my tutorials, and help with my forum questions—thank you.
-
@Dr-Bill no worries, my pleasure to contribute
I don’t take what you said badly of course, but I think a method that is more straight forward and doesn’t lead beginners to bad practices can’t be less pedagogical than your method.
I don’t have the time to comment my code today though, excuse me for this… -
@ustk I'll have a think on that for the next tutorial I write - cheers, mate.
-
@Dr-Bill Excuse me if, as a non native speaker, what I wrote offended you in some ways, it was absolutely not meant to do it...
That been said, your demonstration still contains some hiccups if I might say it this way.
I have to go more deeply through your code because tonight I run out of time, but there's one thing that I think is weird.
You are talking about globals, I understand what you mean regarding the tuto, they global in a sense. But global variables are defined usingglobal
and have a special purpose, which is being accessed from other scripts. -
@ustk Not at all, mate. You and what you wrote are awesome; I appreciate the opportunity to learn from someone as knowledgable about HISE as you. I value any critiques you can offer from going through my code - thank you! I'll follow up on any suggestions/corrections you make.