Creating Interesting Paths

Starting from a very simple path, let’s see how easy it can be to turn it into something interesting! The approach below is the basic method I used on my recent fx(hash) release, Crisis Worlds.

Note: The code samples use my own private framework. If you have some basic experience with JavaScript and p5js, it should be easy to adapt them. The variable c represents my personal drawing library, and the methods should translate to p5js almost identically. I also use several random number convenience methods, and what they do should be clear from the function name.

First, we’ll create two starting points, an array to hold them, and then a function to draw them.


Below is the output.


We need to connect them with a line. We’ll modify the code in the forEach loop to get the next point and draw a line between the current point and the one after it.


Let’s pull in some code from my last tutorial on generative brushes. But instead of drawing random circles at each step along the line, we’ll use it to build the array of points between the start and end.


Now we have a perfectly plain line between the start and endpoints. That’s no fun! Let’s add code to move each point around before we add it to the array.


You can go crazy with this, and if you do, you’ll see some interesting behavior soon.

To make things easier from here on, let’s extract the point drawing code into a new function.


What if we wanted to further refine and smooth out this line? We need an algorithm to take in all of our rough points and smooth out the corners by adding in new points. The Chaikin Algorithm is the perfect solution for this! Matt DesLauriers has a JavaScript implementation of it that we can use.

Adding the code to our example project and inlining the vec2-copy import, we get this.


Then we run it over our points array and get this. The smoothed line is in green.


Looking nice! What if we want even more smoothing? Just iterate the smoothing function a few times. Let’s modify the chainkinSmooth function to iterate over the input points as many times as we specify.


Running this with 3 iterations returns a much smoother line.


And that’s it! We’ve gone from a simple line from point A to point B and transformed it into a flowing path. Experiment with different values, even polygons, to see what you can create! The more starting points you have, the wilder the paths will be.

Below are some examples with increasing amounts of starting points (pointsToInsert).


Paths like this are perfect for tracing with a natural media brush 😉. I hope this has given you new ideas and new areas to explore in your work!

Please give me a follow on Twitter @nudoru and Instagram @hfaze! If you use this in a project, give me a shout out, I’d love to see what you make!

Below is the full code for what I’ve created above. As stated above, this is for my own framework and will need to be adapted to p5js.

const chaikinSmooth = (points, itr = 1) => {
    const smoothFn = (input, output = []) => {
        const copy = (out, a) => {
            out[0] = a[0];
            out[1] = a[1];
            return out;

        if (input.length > 0) output.push(copy([0, 0], input[0]));

        for (let i = 0; i < input.length - 1; i++) {
            const p0 = input[i];
            const p1 = input[i + 1];
            const p0x = p0[0];
            const p0y = p0[1];
            const p1x = p1[0];
            const p1y = p1[1];

            const Q = [0.75 * p0x + 0.25 * p1x, 0.75 * p0y + 0.25 * p1y];
            const R = [0.25 * p0x + 0.75 * p1x, 0.25 * p0y + 0.75 * p1y];
        if (input.length > 1) output.push(copy([0, 0], input[input.length - 1]));
        return output;

    if (itr === 0) return points;
    const smoothed = smoothFn(points);
    return itr === 1 ? smoothed : chaikinSmooth(smoothed, itr - 1);

const drawPoints = (pointsArray, pColor = 'red', lColor = 'blue') => {
    // Loop over the array and pass in the point and the current index
    pointsArray.forEach((point, idx) => {
        c.fill(pColor);[0], point[1], 5);

        // Draw a line between the current points, and the next one
        // If it's the last point, don't do anything
        const next = idx < pointsArray.length - 1 ? pointsArray[idx + 1] : null;
        if (next) {
            const px1 = point[0];
            const py1 = point[1];
            const px2 = next[0];
            const py2 = next[1];

            c.line(px1, py1, px2, py2);

const draw = () => {
    const cw = c.width; // Width of the canvas
    const ch = c.height; // Height of the canvas
    const m = 200; // Margin

    const x1 = m;
    const x2 = cw - m;
    const y1 = ch / 2 - 50;
    const y2 = ch / 2 + 50;

    const points = [];

    const pointsToInsert = randomWholeBetween(10, 50); // Will insert 1 more than this
    const xIncrement = (x2 - x1) / pointsToInsert;
    const yIncrement = (y2 - y1) / pointsToInsert;

    let currentX = x1;
    let currentY = y1;

    const minOffset = 50;
    const maxOffset = 100;

    for (let i = 0; i <= pointsToInsert; i++) {
        // Only move around the middle points
        if (i > 0 && i < pointsToInsert) {
            // Random radius between the min and max
            const rRadius = randomNumberBetween(minOffset, maxOffset);
            // Random radians between 0 and 2PI, the full circle
            const rRadians = randomNumberBetween(0, Math.PI * 2);
            const offsetX = currentX + rRadius * Math.cos(rRadians);
            const offsetY = currentY + rRadius * Math.sin(rRadians);
            points.push([offsetX, offsetY]);
        } else {
            points.push([currentX, currentY]);

        currentX += xIncrement;
        currentY += yIncrement;

    const smoothPoints = chaikinSmooth(points, 3);

    drawPoints(points, 'red', 'blue');
    drawPoints(smoothPoints, 'green', 'green');

    return false;

Generative Brushes Part 1

I make heavy use of ‘brushes’ in most of my generative pieces. I think they’re crucial for adding an extra dimension and a lot of texture to the piece. Below, I’ll create a simple system that you can add and customize on your projects. This technique can be customized and extended to create dry media looks from pencils to charcoal or take a completely new direction!

Note: The code samples use my own private framework. If you have some basic experience with JavaScript and p5js, it should be easy to adapt them. The variable c represents my personal drawing library, and the methods should translate to p5js almost identically. I also use several random number convenience methods, and what they do should be clear from the function name.

Start with two points and a line

The idea is to take two points and draw many little circles between them. It’s the spacing, arrangement, and color of these tiny shapes that determine the look of the final line and what media it looks like. If you have a complex shape, such as a circle, square, etc., then imagine doing this between each pair of points on that shape.

Let’s start with a basic line.

And the code. Here, I define some basic variables, set the context color, and draw the line to the canvas context.


The next step is to plot points along the path rather than just drawing a line. We’ll start with a dotted line, drawing a circle at each point as we step between the start and endpoints.


If you increase the variable stepsBetweenPoints you’ll end up with a more detailed line. The farther apart the start and end points are, the higher you’ll want this value. Here’s what it looks like with 100 steps.


Adding interest with randomness and noise

If you’ll notice, the radius of the circles is the thickness of the stroke. By using a random number as the radius, the line becomes much more interesting!


This looks like an erratic leaky pen. I’m using this random number utility function to get this value.


Let’s make it better with some noise. In this context, it’s helpful to think of noise as a smooth gradation of random values. As we get the 2d noise values of the points on our line, it travels through and noise space and gradually changes. The frequency of the noise will determine how quickly the values change. Simplex noise with a frequency of 0.005 at 1 amplitude is a very smooth place to start.


Experimenting with this simple approach can yield interesting results! Below, I’ve increased stepsBetweenPoints to 300, changed minRadius to .5, and maxRadius to 5. The result is a wavy ink line.


Adding texture

So what about the old-school airbrush look? For that, we need to draw random points around each of the points in our line. For a great explainer of the best way to get a uniform distribution of random points in a circle, watch this excellent video from nubDotDev. These recent tweets from Yazid and Takawo Shunsuke show several methods for point distribution in a circle. Experiment! We’ll the method below for the rest of this article.


Using that function to get a uniform random radius, we can pair that with random radians between 0 and two PI to draw random circles around our point, and then we have our old-school spray can.


The new function, randomCirclesAroundPoint, creates all of the random circles for us. Thickness is the maximum radius for the points, and density is the number of circles to draw.


To visually illustrate what’s going on, here’s a debug view. The green dot is our stepped point, the center of the circle. The red outline is the thickness or the max radius to create the random points.


Tweaks and ideas

At this point, you have everything needed to create a convincing natural media-drawn line in your projects. You can create a pencil line by tweaking all of the variables we’ve covered so far.


One tip, is to use alpha on the circle’s fill color. Below is that line again, but with a fill alpha of 0.25.


Below, I’ve increased the alpha to 0.5, and changed the min/max radius of the circles to 0.25 and 1. With circles sizes this small, you’ll need to increase the density quite a bit, but the results are even better.


Scaling up the thickness of the line, you can create a charcoal effect.


Another trick I’ve used in my work is to snap about 30% of the circles to a multiple of a whole number. This will give the feeling of the media sticking to a paper or canvas grain. Below, 30% of the circles in the randomCirclesAroundPoint function are snapped to an x or y value that’s a multiple of 3.


The effect is subtle, but it adds more realistic details. Here’s the updated code with this included.


For more complex polygons, like an arc, circle, or square, where the points are close together, the stepsBetweenPoints needs be very low – even 1 in some cases. In my projects, I have all of this wrapped in a function that takes an array of points and the steps, thickness, density, sizes, etc., as options.

Be careful as you start to use this technique in your projects. Drawing too many circles will have a negative performance hit. Experiment with the stepsBetweenPoints and thickness vs density variables. You only need enough to get a convincing look. The closer the points are, the less steps you need between the points. The thinner the line, the less density you need. It’s a good idea to create a function that will lerp these values for you.

I’ve spent a lot of time hunting down performance bottlenecks for it to be a low alpha and very high steps and density. Increasing the alpha let me reduce the other variables and didn’t have a negative impact on the appearance.

A complete function

Below is an example of how this could look in your projects. I’ve taken this code, placed all of the tweaking variables in an object, added checks for different options values, and made it reusable. This is similar to how it looks in my own projects.

Using an object to store and pass the variables makes it easy to change up the looks. You can have one for a pencil, another for charcoal, etc.

const drawLine = (x1, y1, x2, y2, options = {}) => {
    // options defaults, a pencil line
    const {
        stepsBetweenPoints = 10,
        density = 5,
        minThickness = 0.1,
        maxThickness = 2,
        minRadius = 0.25,
        maxRadius = 1,
        noiseFrequency = 0,
        snapPointsPx = 3,
        snapPoinstPct = 0.7,
    } = options;

    const xIncrement = (x2 - x1) / stepsBetweenPoints;
    const yIncrement = (y2 - y1) / stepsBetweenPoints;

    let currentX = x1;
    let currentY = y1;

    // Creates a function that takes x,y as arguments and returns a value from -1 to 1
    // if noiseFrequency === 0 then, it won't be used
    const noiseFn = basicSimplex(noiseFrequency);

    const randomCirclesAroundPoint = (centerX, centerY, thickness, density) => {
        for (let i = 0; i < density; i++) {
            const rndRadius = randomNumberBetweenSq(0, thickness);
            const rndRadians = randomNumberBetween(0, Math.PI * 2);
            let x = centerX + rndRadius * Math.cos(rndRadians);
            let y = centerY + rndRadius * Math.sin(rndRadians);
            const pointRadius = randomNumberBetween(minRadius, maxRadius);

            if (snapPoinstPct && snapPoinstPct < 1) {
                if (randomNumberBetween(0, 1) > snapPoinstPct) x = snapNumber(snapPointsPx, x);
                if (randomNumberBetween(0, 1) > snapPoinstPct) y = snapNumber(snapPointsPx, y);

  , y, pointRadius);

    for (let i = 0; i < stepsBetweenPoints; i++) {
        let rad;
        if (noiseFrequency) {
            rad = (Math.abs(noiseFn(currentX, currentY)) + minThickness) * maxThickness;
        } else {
            rad = randomNumberBetween(minThickness, maxThickness);
        const thickness = rad;
        randomCirclesAroundPoint(currentX, currentY, thickness, density);
        currentX += xIncrement;
        currentY += yIncrement;

const draw = () => {

    // slant the lines
    const xOffset = 30;

    // bounds on the color so it's not too dark or light
    const minColor = 50;
    const maxColor = 150;

    // vary the thickness of the lines
    let minOffset = 0;
    let maxOffset = 0;
    const thicknessOffsetStep = 0.05;

    for (let x = 0; x < canvasWidth; x += 15) {
        const r = randomNumberBetween(minColor, maxColor);
        const g = randomNumberBetween(minColor, maxColor);
        const b = randomNumberBetween(minColor, maxColor);

        drawLine(x - xOffset, 0, x + xOffset, canvasHeight, {
            stepsBetweenPoints: canvasHeight,
            density: 20,
            noiseFrequency: 0,
            minThickness: 1 + minOffset,
            maxThickness: 3 + maxOffset,
            snapPoinstPct: 0,

        minOffset += thicknessOffsetStep;
        maxOffset += thicknessOffsetStep;

I’ll end this article here and continue with simulating paint in a future one. I hope this has given you new ideas and areas to explore in your work!

Please give me a follow on Twitter @nudoru and Instagram @hfaze! If you use this in a project, give me a shout, I’d love to see what you make!

Airdropping on fx(hash): Tips and Tricks

By Nudoru and Abstractment

So, you have an awesome gentk on fx(hash)! Congratulations! Now you want to send copies to your supporters and fans? Here’s our quick guide on how to do it.

The end goal is to get a CSV file that we can drop into and have it do the work of passing out our tokens for us. 

The CSV we’ll want to use for the batch tools site needs 3 columns: wallet IDs, fx(hash) token IDs, and quantity (1 for one per wallet). We’ve gone ahead and created this template for you to make it as easy as possible. When you click this link, it’ll require you to create a copy so that it’s your own unique copy.

Step 1 – Mint your tokens

This is the most manual and time-consuming part of the process. You need to manually mint each token that you want to airdrop. Put on a good show or some tunes to keep you occupied. Keep in mind that your transactions will fail if you try to process more than one mint within a single block. A good baseline is to wait 30 seconds between each mint or just wait for the confirmation to show on the fx(hash) page.  You can also track the Tezos blocks here if you want to move quickly. Once you’re done and they’re all signed by fx(hash), you may want to clear/refresh the metadata in your wallet so that the names and thumbnails are updated.

Step 2 – Get the token IDs

Go to your project’s page on this site: Thanks to @zancan for creating this resource. This site uses the fx(hash) API to display a list of minted editions and the owner. We want to scrape the list of the tokens we minted from this site. 

Open up the JavaScript console and paste this script to the console.  Before hitting enter, change YOUR_fx(hash)_ID to your fx(hash) username on the fourth line of code.

let a=[]; 
document.querySelectorAll('li.token').forEach((e,i) => {
let o = e.querySelector('div.owner').innerText;
if(o === 'YOUR_fx(hash)_ID') {
let lnk = e.querySelector('div.inner a');

As an example, the project ID is 7044 in this sample below.

Voila!  Now you’ll see a list of all of the tokens you hold for this collection.  Copy and paste this list to column A on tab 2 of the Google sheet. We just need the ID number of each mint, and the google sheet will automate extracting this for you.

Note: Sometimes it doesn’t load all of the editions and you’ll need to refresh the page. Make sure that all editions have loaded before pasting the script for this step.

Step 3 – Get the list of lucky wallets

How you determine which wallets will receive an airdrop is up to you, you just need a list of them for column A of our sheet. Use tab 3 in the google sheet for these steps.

If you want to use the owners of previous projects:

  • Visit your project on  Shout out to @mknol for creating this. 
  • Enter your project ID and hit enter.  
  • Click the Owners tab. This entire page is sorted by the number of pieces in the wallet. Scroll to the very bottom of the page and you’ll find a text area of wallet addresses. This list is ordered by the number of pieces in the wallet, so if you want to airdrop to just holders of a specific number of pieces, you can compare the first on this page to determine where to cut it off.
  • Copy and paste the wallet addresses to column A of tab 3 in the Google sheet. Note: In a recent update, the number of editions is after the wallet addresses. You’ll need to manually remove this in the Google sheet.
  • You can pull wallets from multiple projects, just keep copying and pasting to the bottom of the list in the sheet.
    • If you want to remove duplicate wallets so that everyone only gets 1 airdrop, do the following: In Google sheets, select column A, then Data menu > Data cleanup > Remove duplicates.
  • How do you want to assign tokens to the wallets?  
    • Want your largest holders to get lower numbered editions?  If so, you’re done.  
    • Want everything randomized? Shuffle the list: With column A still selected, Data menu > Randomize range. Do this as many times as you want, but I usually do it 3 times for a good shuffle. 
    • Want to pick and choose which wallet gets which pieces?  Just put them in the order that you want and confirm they match on the first tab.

Step 4 – Double-check the sheet

Now, in the first tab (_final for CSV), you should have a Google sheet with 3 columns: a list of wallets, a list of your minted IDs, and a 1 beside each in column C. Make sure all of the data matches up and that columns A and C are the same lengths as column B. 

Export the list: File > Download > Comma Separated Values (.csv).  Open up your CSV to confirm you have just one tab with three columns.

Step 5 – Airdrops!

Now for some magic, thanks to the work of @pureSpider.  

  • Go to and connect your wallet. 
  • Pick “fx(hash) 1.0” in the FA2 Contract drop down.
  • Upload or paste the CSV file.
  • Do a quick double-check of the data.
  • Click Send Transaction, and approve the transaction through your wallet. In a few minutes, you’ll see all of the transactions sent.

Clean Code

I’ve been programming for along time now. My first program was probably back in 5th or 6th grade when I had to get checked off on basic computer skills in elementary school. I think they were pretty progressive for 1989. It would have been as BASIC program on an Apple II and gone something like this:

10 PRINT "Hello!"
20 GOTO 10

I got into this seriously in 8th grade programming games in BASIC on the TRS-80s in the typing lab. Then moved though various lanauges (QuickBasic, Pascal, C++, Lingo, Javascript, ActionScript 0, 1 and 2) before landing where I currently am: an ActionScript 3 developer working with the Flash Platform.

Throughout all of this, I really just concentrated on syntax of the language and getting stuff done. But two years ago, I started using design patterns and looking beyond the code into the technique 9or craft) of coding. And in the past few weeks, I’ve really started thinking seriously about how bad I code and how want to code. In my freshman year of college, my first English assignment was a required paper titled: “My Writing Sucks and And Why.” Now I’m doing this mentally.

I picked up a copy of the excellent Clean Code by Robert C. Martin and have just started to read it. A few weeks from now, I fully intent to be a better programmer and look back on the code that I’ll write today and think: “What the hell was I thinking?!”

Note, I’d consider ActionScript 0 to be what ever the hell we were writing for frame scripts in Flash 4. That was painful.

On using SWCs …

So I’m pretty late to this party. Up until now, I’ve been using 2 methods for sharing assets on my Flash projects:

    1. An assets proxy that a coworker of mine wrote.
    2. Nothing. Ok this doesn’t count.

The assets proxy loads a SWF file and view elements can pull assets from it like


Can it get easier? This does a great job of separating content from code in a reusable way and keeping the main app SWF small, however, you have to wait for the assets SWF to load. And write a lot of boilerplate code to set it up, start it loading and listening/responding to the loaded event.

So what’s another way? Enter the SWC. ActiveTuts has a great tutorial on how to create and use them.

My scenario: I have a collection of learning interactions that have a lot of little clips in the libraries. Many of these are the same across interactions and really should be shared in some way. A lot of them also have class files associated with them since they need a little more functionality than a basis extended Sprite.

These classes were mixed in with my project classes and this created a special case with using a SWC: You have to set it up so that the classes are imported from the SWC instead of the class file in the project folder. It took me a few hours to figure this out – classes specific to assets in your SWC file must not be in the class path of the project file.

So I created a separate directory for all classes needed for my assets SWC and it all started working. For now, I just dumped it all in the lib folder because I can’t think of a better place. The project classes are safely in the src folder, nice and separate.

With this SWC linked in FDT and the SWC added to the library path of the FLA this is a quick snip of my code:

package screen
	// this class is in the SWC, need to import
	import assets.view.WheelList.WheelListItemSprite;

	// stuff

	private function drawList():void
		// stuff
		var item:WheelListItemSprite = new WheelListItemSprite();
		// stuff

The library of my interaction FLA went from over 10 clips to none. And they're all separated and ready for reuse in other interactions.

Nori Bindable Model

Here’s the super simple bindable model class that I’ve created for Nori. I’ve never actually developed a project with Flex, but I really like the idea behind it’s data binding implementation. I tried to create something similar for Flash/AS3.

package com.nudoru.nori.model 
	import flash.utils.Dictionary;
	import org.osflash.signals.Signal;
	import com.nudoru.nori.model.bind.PropertyChangedVO;

	 * Adds data binding functionality to the abstract model
	 * Usage:
	 * 	To set up a property to be bound:
	 * 		public function set bind_prop(value:String):void
	 * 		{
	 * 			bind_prop_field = value;
	 * 			dispatchPropertyChange("bind_prop", bind_prop_field [, old_value]);		// this is the important line
	 * 		}
	 * 	To bind the property:
	 * 		bindable_model.bindProperty("bind_prop", binding_listener_function);	
	 * 		bindable_model.bindtest = "hello!";
	 * 	The binding_listener_function may any function and as long as it takes the new property value as an argument
	 * This class should be subclassed to create more enhanced functionality.
	public class BindableModel extends AbstractModel implements IBindableModel

		 * Signal for simple binding
		protected var _onPropertyChangeSignal	:Signal = new Signal(PropertyChangedVO);

		 * Map of the bindings
		protected var _bindingMap				:Dictionary = new Dictionary(true);

		public function get onPropertyChangeSignal():Signal
			return _onPropertyChangeSignal;

		 * Binds a function to a property name
		 * @param propName Name of the property
		 * @param setter Function to call when the property changes. Must take the property's value as a param
		 * @param overwrite Will remove existing setters and assign a new one
		public function bindProperty(propName:String, setter:Function, overwrite:Boolean = false):void
			if(overwrite) unbind(propName);
				_bindingMap[propName] = setter;
			// if the signal doesn't have any listeners yet, set it up
			if(onPropertyChangeSignal.numListeners < 1)

		 * Remove the bindings for a property
		public function unbind(propName:String):void
				delete _bindingMap[propName];

		 * Determins if the property is bound to anything
		protected function isPropertyBound(propName:String):Boolean
			return (_bindingMap[propName]) ? true : false;

		 * Called from a setter to notify bindings of a change to the value
		protected function dispatchPropertyChange(name:String, value:*, oldvalue:*=undefined):void
				var vo:PropertyChangedVO = new PropertyChangedVO(name, value, oldvalue);

		 * Listener for the onPropertyChangeSignal signal and notifys bound setter
		protected function handlePropertyChanged(changeObject:PropertyChangedVO):void
				var func:Function = _bindingMap[]as Function;, changeObject.value);

		 * Construtor
		public function BindableModel() 


Nori Framework

Lately, I’ve been into learning new things – one of them has been to find out what IOC (Inversion of Control) and DI (Dependency Injection) is all about. As a working project for this, I’ve taken the Cairngorm based framework that I created at work and started to do a major rewrite around IOC. I’m basing most of it on Robotlegs.
Not much to say right now, but I’ll toss up information about it and informative links that I’ve found soon.

Social sim. engine, Part 3 – Progress!

Thanks to a wonderful confluence of events and great timing, I’m able to turn this in to a project for work – there are a handful of solutions in the queue that can use this starting next month – so I’m working full time on it over the month of Feburay.

After a few days, this is what I’ve got: click. It’s a pretty straight forward scenario, but it demonstrates all of the features that I’ve got working.

I’ve been able to keep up with my “play” metaphor and it’s working out great by providing many more opportunities for gaming rather than typical multiple choice branching.

One of the projects brought up a requirement that started a new idea – an inventory system. All I really need to do is allow the learner to reference a screen shot to get some data, but I think it can be taken farther. But more on that later.

Designing learning interactions that no one wants to use

Well, done that!

Multiple Sliders

I had a requirement to for an interaction for learners to rate up to six criteria for a given scenario. I thought of 3 different ways to do it: 1) that, 2) text entry and 3) drop down menus.

Text entry was the first idea. But that’s commonplace – can you call that an idea? I had a slider component that I’d coded for a project a year ago that was never used – so why not? The result, while interesting, fails miserably on many levels with visual clutter being the main one. I still think that it would work for up to 3 or 4 items, but it just doesn’t for this many.

So go back and do something more simple – with drop downs. I’d coded the slider to use the same basic properties as the native AS3 ones so, there wasn’t that much more effort to make this change. It’s much easier for the learner to use and saves a lot of space – so much that the scenario can be on the same screen as the question.

Created a few learning interactions today

When I started Ramen, one of the goals was to allow the page templates/interactions to be used outside of the system – in a Lectora course for example. This has even become more important as my day job standardizes on Lectora as the shell for any tracked learning content. Over the past year, I’ve been able to write a whole lot of really easy to use APIs for creating learning interactions. Creating a new interaction takes just a few hours using the Ramen page template API and borrowing functionality from existing templates.

The biggest benefit of this is quick and easy reusability. Just change the XML file and it’s a new page. I don’t want to even think about how hard  some of these would be to pull of in Lectora. It gets really confusing when the action icons start to pile up.

I’m helping out on a project now that needs a few learning interactions developed – quickly. So I spent today working on these. Here they are in the Ramen player:

These aren’t the fanciest interactions ever created, but not bad for a few hours work.