Play this playlist for the intro, the run-down on what we’ll learn and do in Episode 2, the show and tell, the interview, and the seven part lab.
Ben gives a quick run-down on what we’ll learn and build today, the technologies we’ll use, who we’ll be talking to and what they built.
Tools and technology we’ll use today:
Ben shows off our special guest’s component they built and maintain called “Shader Doodle” before sitting down to chat.
Ben chats with special guest Merl. We talk about creative coding on the web, motivations and inspiration behind Shader Doodle, how and if being a musician influences creating coding, and more.
We start our “Space Doodle” app with a bit of front-end tooling setup. For this project, we’ll be using Typescript and delve into using some web-dev server plugins.
To begin, we start with installing 3 packages
- Lit - we used this in Episode 1 and we’ll be using it again to help with Web Component dev
- @web-dev/server - Also used in Episode 1 and we’ll be using it again to serve our dev environment
- Typescript - Adds types to our JS variables and functions, but also is one way to use decorators in Lit
Next, we’ll set up some tasks in our package.json. New to us will be the Typescript compilation task, and the task to transpile and watch our TS files. The serve task that launches our page for development has been covered in Episode 1 and adds TS transpilation to the mix. TS and serving are done with an ampersand (&) so they both execute in parallel. TS files will be watched and when changes happen will be transpiled to JS. And these JS files that were transpiled will force the page to be reloaded. We’ll also need a simple
tsconfig.jsonfile to kick us off with some light and relaxed settings for Typescript.
Now we’re ready to create our app. We’ll be creating our index.html file which, like Episode 1 will be styled and sized to the full page and doesn’t scroll.
We’ll also add the
doodle-appcomponent/tag/element to our page body, and include a script link to our “Space Doodle” app entry point found at
In our application entrypoint, doodle.ts (which is Typescript), we’ll create a mostly empty class. This class will be our
doodle-appweb component which uses Lit by extending it. Inside this class will be the Lit
rendercall which renders nothing yet (by way of an empty html tagged template).
To define the Web Component, we’ll be using our first “decorator”.
Next up, we’ll demonstrate adding style to our component. Unlike Episode 1, we won’t be using CSS in JS, or rather we won’t LOOK like we are. We’ll start by creating a
doodle.cssfile and adding a simple
:hostrule to make our page red.
We’ll import this CSS in the same way, however it won’t quite work yet. We’ll need to create a
web-dev-server.config.jsfile to start editing our web-dev server config. Here we’ll allow the CSS MIME type to be treated as JS, as well as using the
rollup-plugin-lit-cssRollup plugin to wrap our CSS inside of Lit-ready JS, so we can import it into our component.
Lastly, to make Typescript happy, we need to create a global rule to give any CSS we import a proper type definition.
With all that, we have a full page application with simply a red background.
It’s time to add our UI. We’ll be adding (mostly) non-functional UI as a first step of building our application. We will NOT be paying attention to organization, so we’ll be overloading our one single component with all of our markup. For a real application this would be less than ideal - it’s better to split things up more granularly as smaller and less complicated components, but thats not what we’ll be focusing on today, so I’m allowing this project to get a bit messy.
We start by adding Spectrum Web Components to our package.json. For Episode 2, I’m using dark mode in Spectrum just because in Episode 1 we used light mode.
The application will be divided into two sections on the left and right. On the left, there will be a canvas area where editable text overlays the
shader-doodlecomponent. Below that there will be some controls for setting the text color and recording a GIF of the canvas.
On the right, we have some dropdowns to allow us to load different sample shaders, and different textures (if applicable). Also, one nuance of
shader-doodleis additional configuration to run “ShaderToy” shaders, so there is a switch to turn that off and on.
Below that is an accordion menu that will contain text editing capabilities for the vertex and fragment shaders for the shader set. And lastly, a simple button to reload/refresh the shader after a user has made edits.
But of course, none of this is wired up!
Now it’s time to pop in our remaining components. Of COURSE we need Shader Doodle but, we’ll need a code editor as well. I found something called
lit-codewhich is a PrismJS + Lit based code editor.
We’ll add those packages to our package.json. Note that for Shader Doodle we’re pulling the experimental alpha. We should also import PrismJS so it’s default languages like JS can have some nice syntax coloring in
lit-code. It might make more sense to set the language to HLSL (shader language), but it’s a bit of a hassle to get working, and I’ve found that it looks virtually identical to JS in practice (at least as far as the color styling goes).
Starting slowly by experimenting and adding shaders manually to our Shader Doodle HTML markup, we’ll get a bit more organized and use the separate
shaders.tsfile to hold and export a set of sample shaders. We’ll do the same with the
lit-codecomponent. As a middle step, however, we’ll store the entire shader script tag in a variable and use Lit’s
unsafeHTMLto render it, just to show this particular escape hatch exists when we need it.
Lastly, we can add some dark style CSS vars to our ‘lit-code’ element to match the dark mode we already have for the overall web app with Spectrum Web Component.
The code editors and Shader Doodle will be a bit more difficult to hook up, but we can at least get the easy stuff out of the way.
Starting with the color picker, we’ll add a
@propertydecorator. This is a reactive property, and is something we explored in Episode #1. This time however, we can use a simple decorator instead of the wordy syntax in a pure JS project.
By hooking up the input event to the color picker, setting the current
colorattribute on each to this new
textColorproperty, the color slider and area are now affected by user input. To finish up, we can add this CSS property as an inline style on the editable text element.
We can do similar with the actual editable text content. For our purposes, however, a reactive property isn’t needed here. The only place this editable text will appear is here on this
div. And then in a later step, we’ll use this text to render a GIF of our shader + text overlay. We just need to update this
textproperty when our field updates.
So after creating the property (non-reactive), we’ll add an input event on this
divto simply keep the property updated as the user edits it.
Also easy, we can wire up our sliders that control how many frames we’d like to record for our GIF and time between snapshots. For this, however, we won’t use the
@propertydecorator for these properties. Instead, lets use the more appropriate
@statedecorator. This is new in Lit 2 and meant for internal properties that we don’t care to expose outside of the component. Incidentally, it would have been more correct to use them for the colors and text. These reactive properties will now update the label below the sliders to indicate what the timing of the recording will be.
To finish up this left side, let’s just fire off an alert as a placeholder for the function to record our GIF. So we’ll create this placeholder function that uses the
@clicklistener on the “Record and Save GIF” button.
Lastly, we’ll get the pickers/comboboxes working on the right side. These control the “shader” and optional texture used for the shader. The menu items for the shader picker will be driven from the list of shaders found in
shaders.tswhile the textures will just be an array of images in our assets folder with the addition of a web cam and the option to not have a texture at all.
Given that Shader Doodle is a bit abnormal of a component (I’ll discuss why in the next step), we won’t actually take action on loading the shader quite yet.
But wait! Even after we’ve wired this up, the picker menu is having some issues displaying when clicking to open it up. We’re back to the issue in Episode #1, where we get a
process not definederror. Again, this is due to an overlay management library trying to query if we’re using Node.js or in a browser. We fixed this with a hack before, but now let’s fix it properly.
web-dev-server.config.js, we’ll remove
nodeResolveobject. This object is much like the
lit-cssplugin we’re using, but
nodeResolveis so important and central to how
@web/dev-serverworks, it’s a top level configuration option. Typically, you’ll just want it turned on, so
trueis what you’d set this to. However, to give it more of a configuration, we can set this to an object. We’ll do that, and use the
exportConditionsproperty to set this in production mode. Doing this injects the
processobject and allows the internal 3rd party library to know that what we’re doing is inside the browser.
So here’s the thing with Shader Doodle. It’s pretty awesome, but doesn’t seem designed to keep switching shaders on the same component instance. It seems designed to be setup once in code and then run. Our SpaceDoodle app probably falls outside the normal usecase!
That’s OK, we can work with this! Lit is designed to make minimal changes and not tear things down. This means that when an attribute or your component’s slot changes, that’s all that’s going to change. Your component as a DOM element will not get torn down and re-rendered. Your constructor won’t get called again and it won’t get removed and re-added to the DOM.
But in this case, this is actually the behavior we want! At least for this tiny section of our UI. We want to remove Shader Doodle from the DOM so that it gets torn down, and then add it back with a new shader such that it gets a fresh start. To do this, we’ll fall back to the very behavior we use Lit to escape from: appending directly to the DOM.
So, we’re going to manually add Shader Doodle to
<div id=“shader-doodle-container”></div>. To do this, in our
shaders.tsfile we’ll be adding a
createShaderHTMLfunction which constructs a tag containing the correct vertex and fragment script tags, as well as the appropriate texture custom element for the shader. But, we’re going admittedly low-tech here. We’re creating this tag through string concatenation and then with the
shaderUpdatemethod, we set the
innerHTMLof the Shader Doodle container to this string. But just prior tohat we set the
innerHTMLto an empty string. This effectively removes the element from the DOM, and forces it to recreate itself in its entirety with the new HTML string.
In this same
shaderUpdatemethod, we’ll update the code editors as well by using the
setCodemethod of these
lit-codecomponents. We’ll also want the code editor components to update the shader. To do this we’ll want to listen for the
@updateevent. We’ll create a brand new custom shader object with the new code by cloning the current shader object.
Of course to update all of these elements, we need references to them. And for this, we can use the
Lastly we’ll use Lit’s
firstUpdatedmethod to call this new
shaderUpdatemethod against the first shader in the shader list.
Let’s finally render a GIF from Shader Doodle. There are some technical challenges here, but first, let’s start with the UI.
We’re going to add another reactive property called
recording. Even though it will be used internally in practice and can be decorated as
@state, we’re going to fall back to
@property. The reason is that we’d like to hide our recording settings when recording, but also show a progress bar when we do.
So, first, we’ll import the Spectrum Web Components
sp-progress-barand add it to our component markup. A
@statedecorated reactive property called
frameRecordingwill also be added to our class. This property will indicate which frame we are currently recording, and we’ll use
framesToRecordto calculate progress.
Because recording happens for a limited time, and so will this state of our UI, we can set this new
recordingproperty to true or false manually to show either state. We also set it up to reflect so we can use the attribute in our dev tools to flip this property on or off and test the transition of the UI back and forth before we actually wire up the record function.
The outcome here is that we’ll be adding some CSS rules to show or hide elements depending on this
And now the fun part, getting our GIF recorder to work. This is going to be a bit tricky, but we can show off some more
@web/dev-serverplugins. Which, again, are actually Rollup plugins - so we have a huge library to choose from.
As we install and import
GIF.js, we quickly see that it won’t work as is. First off, it’s a Coffeescript based library, which even the bundled version doesn’t quite work as an import. So we’ll reach into it’s source folder, skip the Coffeescript and go straight to the JS files that originated as a Flash/Actionscript 3 library and was converted to JS over time.
Unfortunately even THESE JS files use CommonJS which don’t work as an ES Import. So we’ll fix this problem by installing and using a Rollup CommonJS plugin. This plugin will transform the
requirecalls to be compatible with ES modules and allow us to use the library.
After this gets added to
@web/dev-server, we find that there is an additional problem. Our browser starts erroring out because some variables aren’t defined before use. This problem is the result of this older library not conforming to strict-mode JS. ES module imports actually do enforce strict mode, so there’s no way around this problem…or is there?
And with that, we can import our GIF encoder and set up the function and timer to capture frames, saving them as a GIF at the end of the recording period.
In Episode #1, we never really finished our project to package it up for deployment somewhere. Rollup is a great choice for us, especially because we’re already using Rollup in
@web/dev-serverfor node module resolution and our various plugins.
So, in this last step, we’ll install Rollup and create a build task in our
We’ll start by
npm install-ing a few packages. First, Rollup, and then some more plugins like
@rollup/plugin-node-resolveto do the same module resolving work as our server,
@web/rollup-plugin-htmlto manage our HTML page and linked script, as well as
rollup-plugin-cleanto include sourcemaps and clean our build folder before builds.
Once installed, we can create a Rollup config file and set it up in much the same way, using largely the same plugins and options as our
Lastly is adding our build task and running it. We can now run our bundled file with associate HTML file right in a browser on any server, ready to be put online!
Season 1 Episode 2
Posted on:May 10, 2022 at 03:00 PM