task[1]
0344034.
BDCM
.Advanced Interactive Design
::task[1]
task[1]: Interactive application screen design for online store
todo:
- Research concept & come up with an idea
- Create wireframe design
- Create flow chart
- Create prototype
research:
The first thing done was to get on Behance & start looking for app storefronts.
Derived commonalities
The common denominators extrapolated from all the visuals collected seem to be the following:
- Bright whites
- Sans-serif (modern looking?)
- Rounded corners (more friendly?)
- Clear visual hierarchy whilst maintaining relatively high visual density
- Prominent focus on visuals (imagery, illustrations)
process:
ideation:
With some inspo in the stirring pot, it came time to ideate. I want to make something that would be clean & elegant, which whilst a deviation from my usual work, I feel can encapsulate the goals the above apps try to achieve much easier. I mean, bright whites? Fancy stores burn retinas for breakfast. Sans serif? What's cleaner than boring type? Rounded corners? Straight people can move over anyways.
Thus, I decided to do a jewellery storefront. Now with 100% less money laundering! Okay, just kidding, there will be no such guarantees. How'd you think I'd make my app elegant & clean?
With the idea out of the way, here comes the bumping about to get a semi-working design.
branding:
I find myself working better when I have a vision & direction for what the app will look like. Thus, quickly coming up with a sorta temporary brand, I present luminance.
Logo
Palette
#CAD3C8
#2C3A47
#FD7272
#FC427B
Typography
sketches:
Hand sketch
Starting out with an infinite canvas using Milton, I vomited all my ideas onto the medium. This ended up with a 4-main page app, with categorisation, search, & a cart.
Wireframe
Flow
implementation:
Tree-stopping
I started with a simple timeline, as taught in the tutorials, with labels & such for navigation. When starting its implementation, there came the first sorta snag — stopping movie clips. What was taught was to use a this.stop()
on the frames I'd want the timeline to stop. However, this would only stop the current-level timeline. What I wanted was a function that would traverse through the tree of timelines to stop all of them, so I wouldn't need to add a stopper for every single movie clip.
This wouldn't be too much of a problem, I thought, a simple recursive loop through this.children
, calling child.stop()
. However, as I continued using this function, there slowly popped up edge cases that would be beyond the wildest imaginations of any sadist. First of all, apparently all three types of symbols — movie clips, graphics, & buttons — all derive from the createjs.MovieClip
class, which funnily enough contains the .play()
method, even if it makes no sense (like on a button, which will just loop through the states of it).
Unfortunately, for the button situation, there actually isn't a simple prop, getter, or method to access to find out whether an object is a button. The closest thing that I found was just to check for whether an symbol's cursor was a pointer. A hack, but hey, it works.
With that edge-case patched, I also found that I would like to keep some symbols isolated from this traversal, & have them control their own playing lifecycle. This meant I were to implement a declareIsolated(ctx)
function, which would just add a symbol into a WeakSet
, enabling the traversal function to skip accessing these symbols.
Tree-playing
With all my symbols stopped, now I needed a way to restart them. Initially I just had a mirroring function that called this.play()
during traversal. However, this would resume play from the wrong timeline location the most of the time. I needed a solution a little more smart; I needed a solution that would be a replacement for this.gotoAndPlay(frame)
rather than this.play()
.
One snag though, initially everything was well, just passing the single frame parameter & using that frame to gotoAndPlay
all the children. However, this would make all frames absolute, & relative to the exportRoot
rather than the position of the symbol relative to its parent. For this, I'd need to find a way to find to squeeze that info out from somewhere. After a long, long time inspecting every single property of every single object in the chrome console, I found this internal variable _stepHead
on each of the this.timeline.tweens
property, which was a linked list of the registered states in the tween & their durations. Amongst them would appear another internal _off
which would determined if the layer appeared on the timeline. Looping through it & reducing an offset down from the d
(duration) prop, whilst checking for _off
to be false
, would land me the relative frame where I needed to call child.gotoAndPlay(frame)
from.
Fonts
After a little bit of implementation work, eventually I got to adding some text. Previewing them showed a something unfortunately gruelling — fonts don't work. A little googling told me that Adobe Animate didn't support font embedding for Canvas-based projects. Bizarre. There was also a mismatch in name between the font referenced inside Animate & what was installed on the system ("Camaro" vs "Camaro Sans"), making the lack of font-embedding immediately obvious.
This was however, just an easy fix. Just converting the font into woff
& serialising it into base64 enabled me to insert it into the DOM through a style tag at runtime. Yay, arbitrary JavaScript!
This method would also eventually be extended to include the "Material Icons Two Tone" icon font I was using for all the icons in the app. Whilst I probably could've gotten away with linking it directly from Google Fonts (& have a lot leaner of a HTML load), I didn't feel like thinking about whether there would be a race condition between the remote CSS loading & the Canvas code creating font bitmaps 🤷.
Add this to the "Global" > "Script" section in Adobe Animate, & replace
{font name}
&{url}
with ur own stuff (::
{
const style = document
.createElement('style');
document.head.appendChild(style);
style.innerHTML = `
@font-face {
font-family: '{font name}';
font-style: normal;
font-weight: 700;
src: url({url});
}
`
}
Querying
Apparently, Adobe Animate sometimes decides to merge instance by their names, whilst sometimes it doesn't, when it appears multiple times on a timeline. What are the precise conditions for this? God knows. This means just accessing this
using the instance name sometimes doesn't return all the instances bearing the same name.
This comes a querying function, to scoop through all the props on this
, finding every object deriving from createjs.MovieClip
, & matching their name to what is requested (either a string or regex). The name of this function? $
, cuz jQuery has ruined a whole generation. Initially, it would just be a simple function that I'd call, like the other util functions already written, however having to write $(this, /^btn\_\_bruh/)
is a lot more annoying than this.$(/^btn\_\_bruh/)
. Fortunately, with some prototypical inheritance knowledge, I just threw it on the prototype of createjs.MovieClip
& called it a day. Whilst there are better methods to do this (foreshadowing), this was the easiest way & I couldn't really be bothered (at the time).
Running things once
Whilst attaching listeners onto the buttons & debugging their contents, I noticed every time I'd reach the frame where my controller would attach those listeners, they would run again. This meant I'd end up with tremendous numbers of listeners all doing the same thing, which was not only a memory sore, it was also super confusing for debugging.
Having already looked into the generated JavaScript source from Adobe Animate, I found that the frame actions were actually wrapped into their own functions. This gave me the mischievous idea to just return out of it if the controller has already run. All that lead me to create the once(ctx)
function, where it return true
the first time it's called with the symbol & false
every subsequent time.
Inter-symbol exports/imports
After a little while, I came to the point of composition where I needed to pass data between symbols. Whilst coming up with names of what I'd call the function, I found that package
is a reserved word in AS3, which means it's purple! So I went with that. With a signature of package(ctx)
, it would return an object to allow arbitrary data, with less worry for namespace collisions. Other symbols would be able to get a reference to their child symbols & get the contents they export as well.
Store-age
With imports & exports done, now I'd need some sort of store which would fire an event every time the value it contained, changes. If I'd export/import these objects, I'd be able to update arbitrary symbols when a value is changed externally, achieving ✨reactivity✨. Luckily, this pattern is extremely common in the JavaScript land, & something I've been implementing & reimplementing for many many projects. I decided to just write one real quickly, based on svelte
's implementation.
Yeah this section could've just been a footnote.
Unblocking content-blocked pointer events
It was around the time I started adding buttons that I found the text above my button bases would obscure the mouse events onto them. Whilst this made perfect sense, it was tremendously annoying. I needed a way to disable mouse events for anything that wasn't a button.
Initially using a dumb loop to just check whether a symbol was a button, disabling the rest, it borked the buttons anyways. Eventually I found out disabling mouseEnabled
for a symbol which contains a button, would disable all pointer events for its children as well. Finally, using an in-order depth first search, determining whether a parent has a button/button-bearing child, I'd disable pointer events for all of them without.
However, after all that, there was one final, annoyance. Since I needed to run this function on, ideally, every single symbol, it needed to start from exportRoot
. However, exportRoot
isn't available in the global script context, due to the fact all arbitrary global code is executed before Adobe Animate's initialisation. I could, find a way to hook into the init functions of the AdobeAn
namespace, or... I could just spin the event loop with an async function checking for whether exportRoot
is available (hehe).
OO-ification
At this point, there were a lot functions which took in the current context as the first argument, which started being annoying, since they should've just been methods on the prototypes themselves. Falling down this rabbit hole, I decided I would start attaching all the functions I've written previously onto the createjs.MovieClip
prototype.
Initially, I started out with some prototypical shenanigans, replacing the prototype of createjs.MovieClip
, defining property descriptors, hooking into the constructor, bunch of nasty things that produces tonnes of undefined behaviour for createjs
. Unfortunately, these were all unsuccessful, due to the fact that the objects deriving from createjs.MovieClip
were actually instantiated before the global script code. However, there was one single lead, the fact that the prototypes of derived objects still point to the prototype of createjs.MovieClip
. Thus, I could in theory, use a pattern sort of like C#'s *Extensions
classes to attach methods, whilst still maintaining relatively good DX since I'd still be using classes & not individual functions.
There were, however, a few major caveats that I had to bash my head in to figure out:
- Private properties (
this.#foo
) cannot be accessed - Constructors will not be called
- Properties on the prototype will be static (same reference shared amongst deriving classes), even though they're accessible from the
this
context
This meant I'd have to find & hook into a custom initialisation function to construct properties per instance. Luckily, symbols generated from Adobe Animate always end with a call to createjs.MovieClip.prototype._renderFirstFrame()
, thus by holding onto a reference to the old function & overriding it, I was able to get my pseudo-constructor called every single time.
With all the engineering done to set up the extensions, I started porting over the functions into methods, with many miscellaneous changes along the way to improve DX:
- Make
isButton
a getter - Rename
declareIsolated(ctx)
toctx.isolate()
& add the correspondingctx.deisolate()
- Expose
isIsolated
as a getter - Make
package
a property - Rewrite mechanism that changed the pseudo-route of the project based on labels, to use a store
Everything went (sorta) well! Just the usual JavaScript refactoring woes, as a developer which is used to both TypeScript & IntelliSense's creature comforts. All was okay except for...
Running things once, but better
So... this was a hyperfixation, sorta. The main reason why this was sorely needed to replace the once(ctx)
function, was to be able to await a child symbol's first frame, after it has run its controller logic & exported its data. This meant I probably needed a mount
& a mounted
event or something on every symbol.
The problem is a pretty unique one, as I couldn't just use events, since the controllers would run every time the frame was entered; I'd need a function that accepted a callback, which would either be registered internally to run on mount/mounted, or be disregarded. Even just naming such a function made me draw a blank. Eventually though, I ended up with useMount
& useMounted
, which would run all registered callbacks when the component mounts, & run all registered callbacks when the components have finished running its mount
callbacks, respectfully.
A big snag however, was when to trigger these callbacks. Initially I just tried to call them from each other, but there it came a bunch of random buggery that transcended my intelligence. Eventually I'd find out it was because actions for the first frame were run in createjs.MovieClip.prototype._renderFirstFrame()
during their initial construction, & thus the mount
callbacks, & subsequently mounted
callbacks would be run early, before their children's, for some reason. However at the time, it drove me insane.
After a bunch of digging though, I realised that CreateJS actually exposed functions for me to create my own tweens, which would then allow me to declare when to execute my own arbitrary code at specified points of the timeline, programmatically. A little fighting with concurrent modification woes inside CreateJS's handling of action execution later, I ended up with a system which would call the extensions' pseudo-constructor after the first frame is rendered, & set up tweens at the first & last position of the tween list to fire mount
& mounted
respectfully.
All this would finally enable me to easily pass data between symbols, without buggy jank (before this I had a setTimeout
for a 100ms waiting to access the exported data).
final:
reflection:
Overall, the experience was pretty okay. Whilst there were a tonne of challenges along the way, they were a pretty fun challenges. At many points though it did feel like Adobe Animate might not have been the most suitable tool for this, having lacking the ability to change instance properties like XD or Figma, nor did it seem like the most modern tool, with many features like 9-tile slicing & font embedding not supported in Canvas-based projects... actually yeah that's it. Whilst I would very much definitely preferred to use After Effects & Lottie for this, or maybe even the bleeding edge Rive, the experience, the skills, & the deep understanding of the innards of Adobe Animate was extremely insightful.
Besides the technical stuff mentioned above, I feel like the it did make me better at designing interfaces. This is because it forced a separation between design & implementation. Granted, that was because it was such a pain in the a** to implement things, but as a person who hyperfixates & falls victim to scope creep a tonne, the extra resistance, whilst driving down productivity overall, did help with discipline. Such is a soft-skill I did not expect, not think I'd want to learn, but hey, I wouldn't have done it any different.
At the end of it all, whilst I probably won't ever touch this program ever again even if it could save my new-born's life, the knowledge picked up will be in use in the future. As they say, maybe the real treasure was the friends technical knowledge & marketable soft-skills we made along the way.
Comments