Reading List
The most recent articles from a list of feeds I subscribe to.
JavaScript is 25 Years Old
This is the original JavaScript announcement on this day 25 years ago.
Coinbase Continues to be Trash
Coinbase published this blog post yesterday. It has this summary at the top:
Tl;dr: The New York Times is planning to publish a negative story about Coinbase at some point in the next few days online, and it will appear in print on Sunday. Given that this story may be read by your friends, family and professional contacts, we wanted to give everyone a heads-up and provide some important context.
That doesn’t sound suspicious at all considering the drama they’ve been involved in recently. π
Redesigning the Website
I haven’t posted in a little while.
I’m mostly done with the next article I was supposed to post, which is about getting WebRTC working for video chats through the website. But before I could post it, I decided to redesign the whole site.
I’m partway through it right now, so I don’t have anything to show yet. I’ll write a series of posts about the redesign once it launches, but I’ll talk a little about the effort right now.
Backend
I use Go on the server for this project. I’ve been using Node (Javascript) on the backend for years whenever I did web projects, but I made speed/efficiency the highest priority for everything on this server, so I choose Go for my own code and Go projects for the other stuff I’m using (analytics, code repo, IRC server, etc).
But I wrote the first line of Go code back in January when I started the project (before I even knew all the stuff I wanted to add) and have been rapidly adding features since. I’m at 155 releases/deployments since January, and the site is on version 6.6.2 (I try to stick to semver even for this, so major.minor.patch releases).
At this point, the code is a complete mess.
Issues
Bad project architecture
Here’s a breakdown of the lines of code1 for this web project:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Language Files Lines Blanks Comments Code Complexity βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ HTML 99 5590 686 1 4903 0 JSON 42 10927 0 0 10927 0 Go 34 9565 1275 19 8271 1417 SVG 33 61037 8 28 61001 0 CSS 20 8096 1321 346 6429 0 SQL 18 435 35 0 400 0 JavaScript 11 7598 818 766 6014 1284 Plain Text 2 58 9 0 49 0 Makefile 1 27 7 0 20 0 Markdown 1 7 3 0 4 0 gitignore 1 71 17 21 33 0 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Total 262 103411 4179 1181 98051 2701 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Estimated Cost to Develop $3,331,347 Estimated Schedule Effort 21.727494 months Estimated People Required 13.621541 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Processed 14319092 bytes, 14.319 megabytes (SI) βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Most of the code is in one big Go package called “handlers”. Out of the 9,565 lines of Go, 9,487 of them are in the handlers package. π
Go Templates
Not only are the Go files a mess, but there are 99 html files in the project. 98 of them are in one templates directory. ππ
And I didn’t really know Go template best practices at the time so there is NO code reuse or partials or anything like that.2
Frontend
Like with the backend, I’m not using any frameworks or libraries on the frontend. Just raw CSS/Javascript (with a minor goal of having every single feature besides video chat work with Javascript disabled).
That part isn’t such a problem on the frontend, but the general design I’m using is.
When I started this in January, the only thing I knew for sure was that I was going to post articles and pictures. I hadn’t made the decision to do freelance development yet, I had no plans for user accounts, web tools, video chat, or anything else.
The design you’re seeing right now has been updated a lot this year, but the foundations are the same: A couple links at the top of the page, an ineffective list of “More Links” on the side of the home page, and a mostly one column layout for everything else.
But this design has existed through the introduction of the following features:
- Comments (and comment administration)
- Video chat (with chat requests from users)
- Liked posts
- Utils/Reference pages
- Web/IRC chat
- Freelancing services
- Notes/Link shortener service (these are just for me)
- User accounts and user management
- Financial contributions
Clearly a new design is needed.
Other Frontend Issues
- I really like the monochrome theme. It works well in both light and dark modes. But it’s time for some color and theme flexibility.
- I also like the animations on the site, but they aren’t good enough. This is a good opportunity to make them even better.
- I think the site is relatively accessible (especially since I’m not relying on Javascript for things), but I need some better HTML structure and semantic tag usage to make this even better.
Progress
As of this writing, I’ve completely rewritten the backend (the page routes and queries and everything are the same, so functionality should HOPEFULLY be exactly the same) to be clearer, more organized, and more efficient.
Now I’m working through the frontend. I’m in the middle of rewriting pretty much every template files. And I basically deleted all of the CSS so I can start over.
Here are the lines of code right now:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Language Files Lines Blanks Comments Code Complexity βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Go 120 11828 1662 264 9902 1370 HTML 104 5611 693 1 4917 0 JSON 42 10927 0 0 10927 0 SVG 33 61037 8 28 61001 0 CSS 19 5965 918 217 4830 0 SQL 19 547 39 1 507 0 JavaScript 11 7598 818 766 6014 1284 Plain Text 2 58 9 0 49 0 Makefile 1 27 7 0 20 0 Markdown 1 7 3 0 4 0 gitignore 1 71 17 21 33 0 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Total 353 103676 4174 1298 98204 2654 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Estimated Cost to Develop $3,336,806 Estimated Schedule Effort 21.741015 months Estimated People Required 13.635374 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Processed 14321306 bytes, 14.321 megabytes (SI) βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Some interesting things I’ve noticed so far:
- Estimated cost to develop/people required3 hasn’t changed much (which makes sense considering the project size/effort is the same).
- Go code files went from 34 to 120(!!!), but the actual lines of code only increased by like 1,631 (8,271 to 9,902). This is good!! This means I’m using Go packages way more effectively.
- HTML files increased because I started adding partials and splitting up some of the files that were too big. But I didn’t finish actually using those partials so the actual lines of HTML haven’t changed much. I expect there to be a lot less HTML code when I’m done.
- Some third party HTML/CSS/Javascript is included in this count (the IRC web client I’m using). Even with those included, you can still see the effect of me deleting all my CSS to start over. Total lines of CSS went from 6,429 to 4,830.
That’s it for now. I’ll write another breakdown closer to release. I’m gonna dive back in now and get this done.
-
I used github.com/boyter/scc for this. ↩︎
-
Honestly, my Go template usage was disgusting. I wasn’t using any template functions at all, just passing blobs of string data. I have copied/pasted HTML all over the place. I wasn’t even caching the parsed templates. Ugh. ↩︎
-
I don’t know how this is calculated. I’m gonna read more about it after I publish this. It’s interesting though now that I’ve been freelancing and tracking (and charging for) my time, I’ve been thinking a lot about the many hours I put into this project and how much it actually costs. It’s nice to get a rough estimate. ↩︎
Switching from Gmail to Fastmail
I finally started my last “de-googling” step: switching from Gmail to using my own domain (dwayne.xyz) with a paid mail provider. I decided to go with Fastmail.
Why “De-Google”?
As you can probably tell from my other posts, I’m really not a fan of the tech industry these days. I have a problem with the level of surveillance capitalism we’re dealing with and how easy it is to passively hand over a lot of data to these companies without even realizing it.
Google is in the news a lot over the years for being one of the biggest examples of it (along with Facebook obviously). If you want to check out a documentary that explains some of the problem, check out The Social Dilemma on Netflix1.
I also just really don’t like the idea of depending on these companies for services that have a huge effect on my life that can disappear for any reason. I recently came across an article about getting locked out of your Google account, and it mentions a Google employee whose husband was locked out of his account and they were still unable to resolve it.2
So over the past few (5? 10??) years, I’ve been slowly making moves to reduce my own reliance on some of the more problematic companies. It’s why I made a push to self host stuff on this server when possible.
Google Services I Already Dropped
Basics - I’ve already been in the Apple ecosystem for a long time3, so it was already easy to use the Apple alternatives to Google stuff (photos, contacts, notes, calendar, file storage, etc). Of course, I have my criticisms of Apple, but I believe they have less of an incentive to participate in surveillance for money since they chose to stick with a decent business strategy: charging consumers for the products they want.
Chrome - I don’t even install Chrome anymore. I use Safari, then Firefox, then Brave for development4 and general usage.
Search - I’ve already set all my browser search defaults to DuckDuckGo a while back, so I don’t start my searches with Google Search. But DuckDuckGo is definitely not as good at technical/code searches for me. I end up putting !g in my search queries to have it send me over to Google like… a lot. Eventually I’ll find a search engine that works a little better.
RSS/News - People bring up Google Reader a lot when talking about RSS. I used to use it before switching to Twitter for my RSS replacement a while back. But at some point after that I realized relying on Twitter was also a terrible idea, so I started using RSS again, but through Feedly. Then even more recently I installed a self hosted instance of Miniflux to replace that. (See what articles I’m reading here.)
Maps - Apple Maps really isn’t that bad. (Disclaimer: I worked on the Apple Maps team for a year.)
Password Management and 2FA - I don’t use any browsers for saving my credentials. I don’t use Google Authenticator either. I use 1Password for all of it, including OTP.
YouTube - Yeah I don’t know what to do about this one. I have serious fucking issues with the things they choose to either remove or leave up on the website. I believe YouTube is responsible pushing a lot of people towards very hateful/harmful/racist content and normalizing a lot of terrible shit. There is also like… decades of content there that you’ll never see anywhere else. This one just bothers me.
Analytics - I would never use Google Analytics (I use a self hosted instance of Fathom for analytics) and I don’t use any kind of Google (or Facebook) libraries/services for the website.
Google Meet - This isn’t really specific to Google (since Zoom really took off), but part of the reason I’m building WebRTC video chat into this website is so I don’t have to depend on or trust either Google Meet or Zoom to video chat and/or share my screen with any of my contacts.
Gmail to Fastmail
All of that said, I’ve still been using a Gmail account since around 2007. That’s 13 years of my email being scanned for keywords and my correspondence flowing in and out of the servers of a data collection company.
I already knew I had to get off it eventually. But considering how long I’ve used it, I figured switching would be too much of a hassle.
But after reading some of the stories of people losing their Google accounts, and thinking about next steps with my business and this website, I figured now was the time to go through it and get rid of my last Google dependency.
Benefits
I can finally use this domain (dwayne.xyz) for my email addresses and set up some aliases and organize my communication a little more. Also, if I need to switch off of Fastmail at any point, I can just change DNS settings and I don’t have to go through the switching process again.
Also, theoretically, paying for the service means I’m not the product and my messages aren’t being scanned to serve me ads5. And I can actually expect real support when I need it.
Downsides
I read some reviews of the popular paid email providers and there’s usually a discussion about privacy and the jurisdiction the company/servers reside in. Fastmail is an Australian company with servers in New York and Amsterdam. And they don’t offer end to end encryption like some of the others (ProtonMail usually comes up).
That’s significant to some. It’s acceptable for me right now for a few reasons.
- I’m a US citizen and I live in NYC. My web and database servers are in a datacenter here in the city. Using an email provider with servers in New York and connections to the US government doesn’t really change my situation at all.
- I already don’t really consider email secure6, so encryption isn’t a big part of the decision for me.
- I mentioned being able to switch providers by changing my DNS settings. It feels like I can do a little experimentation with them right now.
Steps
So here’s the order I did things in:
- Signed up for Fastmail and specified using this domain (dwayne.xyz) from the start. When I logged in I went to the Domain settings and followed the instructions to edit my DNS settings at DigitalOcean.
- Created a few aliases including one that’s specifically for emails that I forward from Gmail. (I created contact@dwayne.xyz and support@dwayne.xyz for this website, and other aliases for other things.)
- Turned on email forwarding in Gmail. I set it to forward to the alias I mentioned in the last step.
- Ran the “Import from Google” process inside Fastmail (82,889 messages imported!).
- Removed the Google account from all my devices and added the Fastmail/dwayne.xyz account.
- Started the eventual process of changing the email addresses at all the sites I use to the new one. 1Password makes this not so bad. And since I have email forwarding on there’s no rush. Whenever I notice the old email address being used somewhere I’ll change it.
So now with this setup:
- I still have all my emails, labels, and contacts.
- I won’t miss any emails that are sent to the old address.
- I can turn off emails from the old address from either side by either turning off forwarding from inside Gmail or rejecting emails going to the forwarding alias from inside Fastmail.
- I can switch from Fastmail to another provider without switching email addresses again (after all the account setup, data import, and configuration stuff of course).
- I already changed a lot of my accounts to use the new address. All email coming into the new address avoids Gmail and scanning.
- If Google decides they don’t like my account anymore and disables it, it’ll be a slight inconvenience (I’ll just have to update my accounts faster) instead of a major life issue.
Have you been thinking about dropping Gmail? It might not be as hard as you think. For me, signing up for the account, configuring everything, and doing the data import all took maybe 2 or 3 hours. I’m still in the process of updating my accounts, but changing email addresses every once in a while is easy enough.
Overall this was all easier and quicker than I thought it would be and I really do feel more at ease about my digital life than I used to. Definitely worth it (so far).
-
There were a lot of complaints about it. I thought it was pretty good for a documentary. ↩︎
-
My first smartphone was a Motorola Q, which was running Windows Mobile. I really liked it a lot! My next one was a Motorola Droid, which was running Android 2.something. ↩︎
-
I was actually just thinking recently that cross browser development has felt a lot easier lately with some consolidation around browser engines and good implementations of Flexbox/CSS Grid. Anybody feel the same way? ↩︎
-
That’s their promise at least. I have to put my trust in somebody… but it’s a little easier to do when I’m paying. ↩︎
-
I was looking into end to end encrypted email solutions, and it really just seems like a huge hassle for everyone involved. ProtonMail doesn’t support POP/IMAP without a separate app that handles encryption/decryption. And PGP is notorious for being difficult to use consistently with your contacts. ↩︎
Getting Audio Visualizations working with Web Audio API
Web Audio API
I’ve been working on getting WebRTC video chat working here on the website for a few weeks now. I finally got to the point where both text, video chat, and screen sharing all work really well, but somewhere in the back of my mind I kept thinking about complaints about “Zoom fatigue” during the pandemic:
Zoom fatigue, Hall argues now, is real. βZoom is exhausting and lonely because you have to be so much more attentive and so much more aware of whatβs going on than you do on phone calls.β If you havenβt turned off your own camera, you are also watching yourself speak, which can be arousing and disconcerting. The blips, delays and cut off sentences also create confusion. Much more exploration needs to be done, but he says, βmaybe this isnβt the solution to our problems that we thought it might have been.β Phone calls, by comparison, are less demanding. βYou can be in your own space. You can take a walk, make dinner,β Hall says.
It’s kind of an interesting thing to have on your mind while spending weeks writing/debugging/testing video chat code.
So I decided to add an audio-only mode. And if I was gonna do that, I had to show something cool in place of the video. So I figured I would try to add audio visualizations when one or both of the users didn’t have video on. Using the relatively recent1 Web Audio API seemed like the right way to go.
Here’s what I came up with:
Creating and hooking up an AnalyserNode
To create audio visualizations, the first thing you’ll need is an AnalyserNode, which you can get from the createAnalyser method of a BaseAudioContext. You can get both of these things pretty easily2 like this:
1const audioContext = new window.AudioContext(); 2const analyser = audioContext.createAnalyser();
Next, create a MediaStreamAudioSourceNode from an existing data stream (I use either the local or remote data streams from either getUserMedia or from the ‘track’ event of RTCPeerConnection respectively) using AudioContext.createMediaStreamSource. Then you can connect that audio source to the analyser object like this:
1const audioSource = this.audioContext.createMediaStreamSource(stream); 2audioSource.connect(analyser);
Using requestAnimationFrame
window.requestAnimationFrame is nice. Call it, passing in your drawing function, and then inside that function call requestAnimationFrame again. Get yourself a nice little recursive loop going that’s automatically timed properly by the browser.
In my situation, there will either be 0, 1, or 2 visualizations running, since either side can choose either video chat, audio-only (…except during screen sharing), or just text chat. So I have one loop that draws both. It looks like this:
1const drawAudioVisualizations = () => { 2 audioCancel = window.requestAnimationFrame(drawAudioVisualizations); 3 localAudioVisualization.draw(); 4 remoteAudioVisualization.draw(); 5};
I created the class for those visualization objects, and they handle whether or not to draw. They each contain the analyser, source, and context objects for their visualization.
Then when I detect that loop doesn’t have to run anymore, I can cancel it using that audioCancel value:
1window.cancelAnimationFrame(audioCancel); 2audioCancel = 0;
Configuring the Analyser
Like in the example you’ll see a lot if you look at the MDN documentation for this stuff, I provide options for two audio visualizations: frequency bars and a sine wave. Here’s how I configure the analyser for each type:
1switch (this.type) { 2 case 'frequencybars': 3 this.analyser.minDecibels = -90; 4 this.analyser.maxDecibels = -10; 5 this.analyser.smoothingTimeConstant = 0.85; 6 this.analyser.fftSize = 256; 7 this.bufferLength = this.analyser.frequencyBinCount; 8 this.dataArray = new Uint8Array(this.bufferLength); 9 break; 10 default: 11 this.analyser.minDecibels = -90; 12 this.analyser.maxDecibels = -10; 13 this.analyser.smoothingTimeConstant = 0.9; 14 this.analyser.fftSize = 1024; 15 this.bufferLength = this.analyser.fftSize; 16 this.dataArray = new Uint8Array(this.bufferLength); 17 break; 18}
I’ve adjusted these numbers a lot, and I’m gonna keep doing it. A note about fftSize and frequencyBinCount: frequencyBinCount is set right after you set fftSize and it’s usually just half the fftSize value. These values are about the amount of data you want to receive from the main analyser functions I’m about to talk about next. As you can see, they directly control the size of the data array that you’ll use to store the audio data on each draw call.
Using the Analyser
On each draw call, depending on the type of visualization, call either getByteFrequencyData or getByteTimeDomainData with the array that was created above, and it’ll be filled with data. Then you run a simple loop over each element and start drawing. Here’s my sine wave code:
1this.analyser.getByteTimeDomainData(this.dataArray); 2this.ctx.lineWidth = 2; 3this.ctx.strokeStyle = audioSecondaryStroke; 4 5this.ctx.beginPath(); 6 7let v, y; 8for (let i = 0; i < this.bufferLength; i++) { 9 v = this.dataArray[i] / 128.0; 10 y = v * height / 2; 11 12 if (i === 0) { 13 this.ctx.moveTo(x, y); 14 } else { 15 this.ctx.lineTo(x, y); 16 } 17 18 x += width * 1.0 / this.bufferLength; 19} 20 21this.ctx.lineTo(width, height / 2); 22this.ctx.stroke();
The fill and stroke colors are dynamic based on the website color scheme.
Good ol' Safari
So I did all of this stuff I just talked about, but for days I could not get this to work in Safari. Not because of errors or anything, but because both getByteFrequencyData and getByteTimeDomainData just filled the array with 0s every time. No matter what I did. I was able to get the audio data in Firefox just fine.
So at first, I figured it just didn’t work at all in Safari and I would just have to wait until Apple fixed it. But then I came across this sample audio project and noticed it worked just fine in Safari.
So I studied the code for an hour trying to understand what was different about my code and theirs. I made a lot of changes to my code to make it more like what they were doing. One of the big differences is that they’re connecting the audio source to different audio distortion nodes to actually change the audio. I just want to create a visualization so I wasn’t using any of those objects.
Audio Distortion Effects
The BaseAudioContext has a few methods you can use to create audio distortion objects.
WaveShaperNode: UseBaseAudioContext.createWaveShaperto create a non-linear distortion. You can use a custom function to change the audio data.GainNode: UseBaseAudioContext.createGainto control the overall gain (volume) of the audio.BiquadFilterNode: UseBaseAudioContext.createBiquadFilterto apply some common audio effects.ConvolverNode: UseBaseAudioContext.createConvolverto apply reverb effects to audio.
Each one of these objects has a connect function where you pass another context, output, or filter. Each one has a certain number of inputs and outputs. Here’s an example from that sample project of connecting all of them:
1source = audioCtx.createMediaStreamSource(stream); 2source.connect(distortion); 3distortion.connect(biquadFilter); 4biquadFilter.connect(gainNode); 5convolver.connect(gainNode); 6gainNode.connect(analyser); 7analyser.connect(audioCtx.destination);
Note: Don’t connect to your audio context destination if you’re just trying to create a visualization for a call. The user will hear themselves talking.
Anyway, I tried adding these things to my code to see if that would get it working in Safari, but I had no luck.
Figuring out the Safari issue
I was starting to get real frustrated trying to figure this out. I was gonna let it go when I thought Safari was just broken (because it usually is), but since I knew it could work in Safari, I couldn’t leave it alone.
Eventually I downloaded the actual HTML and Javascript files from that sample and started removing shit from their code, running it locally and seeing if it worked. Which it did. So now I’m editing my own code, and their code, to get them to be pretty much the same. Which I did. And still theirs worked and mine didn’t.
Next I just started desperately logging every single object at different points in my code to figure out what the fuck was going on. Then I noticed something.

The state is “suspended”? Why? I don’t know. I did the same log in the sample code (that I had downloaded and was running on my machine) and it was “running”.
This is the code that fixes it:
1this.audioSource = this.audioContext.createMediaStreamSource(this.stream); 2this.audioSource.connect(this.analyser); 3this.audioContext.resume(); // Why??????
Calling resume changes the state and then everything works. To this day I still don’t know why the sample code didn’t need that line. Update: Dom wrote a comment below that helped me figure this out. In the sample code, the AudioContext and AudioSource objects were created and connected to the analyser directly in response to a click event. In mine, the AudioContext is created on page load, and the AudioSource (and connect call) happens in response to an event from a RTCPeerConnection object. I’m not finding a straight answer about it so far, but Safari might only start the context in a running state in response to user interaction.
Drawing the image and supporting light/dark modes
Like everything else on my site, all of this must support different color schemes (and screen sizes, and mobile devices). That was surprisingly difficult when trying to draw an SVG on the canvas.
I’m using FontAwesome for all my icons on the site. I wanted to use one of them for these visualizations. The FontAwesome files are all SVGs (which is great), but I didn’t know how to draw the image in different colors in Javascript. The way I decided to do this was to load the SVG file into a Javascript Image object, then draw that onto the canvas each draw call.
That worked, but it only drew it black even after changing the fill and stroke colors. So after some web searching I read about someone deciding to draw out an image on an offscreen canvas, reading all the image data, and manually rewriting the image data for each pixel if the alpha channel is greater than 0. Then the actual visualization code can just copy the image from the offscreen canvas onto the real one.
So that’s what I did. But of course there was a browser specific issue. But not from Safari!!!!!
It turns out that loading a SVG file into an Image object (offscreen) doesn’t actually populate the width and height attributes of that object in Firefox. It does in Safari, which is what I tested this with3. I actually need the width and height to do the canvas drawing operations.
So as a workaround, I try to load the SVG, and if the object has no width, I load a png file I made from the SVG using Pixelmator. Here’s the code for loading the image and drawing it to a canvas:
1audioImage.onload = () => { 2 if (!audioImage.width) { 3 audioImage.src = '/static/images/microphone.png'; 4 return; 5 } 6 7 audioCanvas.width = audioImage.width; 8 audioCanvas.height = audioImage.height; 9 10 const ctx = audioCanvas.getContext('2d'); 11 ctx.drawImage(audioImage, 0, 0); 12 13 const svgData = ctx.getImageData(0, 0, audioImage.width, audioImage.height); 14 const data = svgData.data; 15 for (let i = 0; i < data.length; i += 4) { 16 if (data[i + 3] !== 0) { 17 data[i] = parseInt(audioStroke.substring(1, 3), 16); 18 data[i + 1] = parseInt(audioStroke.substring(3, 5), 16); 19 data[i + 2] = parseInt(audioStroke.substring(5, 7), 16); 20 } 21 } 22 23 ctx.putImageData(svgData, 0, 0); 24}; 25 26audioImage.src = '/static/images/microphone.svg';
In this case, I know the audioStroke value is always in the format #000000, so I just parse the colors and write them to the array.
High Resolution Canvas Drawing
If you’ve done any canvas element drawing (especially when you have both high and low DPI monitors) you know by default it looks pretty low resolution. Any canvas drawing I do takes window.devicePixelRatio into account.
The idea is to adjust the canvas “real” width to factor in the screen pixel ratio, then CSS resize it back down to the original size. So on a high resolution screen (like in any Macbook), window.devicePixelRatio will be 2, so you’ll resize the canvas to be twice the width and height, and then CSS size it down to what you wanted.
This is the same concept as creating 2x images when Retina screens first came out so they can be sized down and look sharp af.
Here’s what that code looks like for me:
1const dpr = window.devicePixelRatio || 1; 2this.canvasRect = this.canvas.getBoundingClientRect(); 3 4this.canvas.width = this.canvasRect.width * dpr; 5this.canvas.height = this.canvasRect.height * dpr; 6this.ctx = this.canvas.getContext('2d'); 7this.ctx.scale(dpr, dpr); 8 9this.canvas.style.width = this.canvasRect.width + 'px'; 10this.canvas.style.height = this.canvasRect.height + 'px';
I store the canvasRect so I can use the width and height for all the other drawing calculations.
Wrapping Up
I really like the way this eventually turned out. There were a few times where I figured it would just be completely broken in some browsers, and a brief moment where I thought I would have to give up on my goal to have everything on the site react to color scheme switches, but I actually did everything I wanted to.
Now I just have to keep messing around with those AnalyserNode values until I get something that looks perfect.4
-
It looks like the early Mozilla version of this API has been around since 2010, but Apple’s been working on this official Web Audio API standard a lot recently. See release 115 (current as of the date I’m writing this article) of their Safari Technology Preview release notes. ↩︎
-
I’ve been using this “adapter.js” shim from Google to smooth over browser differences with WebRTC objects, and it’s also helpful with Web Audio API. Some browsers still have
AudioContextprefixed aswebkitAudioContextso if you’re not using something like adapter.js you’ll have to donew (window.AudioContext || window.webkitAudioContext)(). ↩︎ -
It’s funny how after all this time fighting with and complaining about Safari issues (of which there are many) I still develop with Safari. In this case, a lot of the reason is because Firefox runs my fans when I do WebRTC testing. ↩︎
-
lol. There is no “perfect” with computers. The work never ends. I’ll be messing with all of this code until it’s completely replaced. ↩︎