Over Pesach there was some debate during Hallel over the time signature of the beginning of a melody to מה אשיב/Ma Ashiv. Some people do the beginning in 3/4 time, and some do it in 4/4.
I had to find out who was right (though I was pretty sure it was 4/4 because that’s consistent with the rest of the song), so did a bit of digging. Here’s what I was able to find.
It was composed by Aviezer Wolfson and first recorded by Leibele Haschel. I couldn’t find the year or full recording, but FAU has a 45 second sample available. There seems to have been a rerelease of that album in 2007, so here are other 30 second samples (jump to 30 seconds in, because it’s track 2 in one concatenated file). In terms of the exact year, it must have been after 1978 because the liner notes say (if I’m correctly making out extremely blurry text) that the arranger, Moshe Laufer, was named composer of the year (by…someone) in 1978.
The songs seems to have had its breakout moment when Morechai Ben David recorded it in 1987 on a compilation album with Avraham Fried, The Piamentas, and Dov Levine. It’s not available on YouTube or Apple Music, but it is on Spotify.
MBD has rerecorded it at least twice since, once in 2002 (Kumzits), and one other time (on an album that contains such titles as “Beethoven In Birdland”, “The Malach Piano Concerto In C”, and “The Bach Suite #2 In B Minor”).
All of these versions were in 4/4.
I did find one recording that starts in 3/4, which is an instrumental/classical guitar arrangement played by a guy who also happens to be a small plane pilot.
With Bear 2 seemingly around the corner and the beta being great (though still very much a beta), I decided it was time to move from Simplenote.
Exporting from Simplenote gives you two things, neither of which is directly usable by Bear:
A file for each note with correct creation and modification dates and tags appended at the end as not-hashtags
A JSON file containing each note and its metadata (UUID, tags, creation date, and modification date)
What I needed for Bear was a file for each note with correct creation and modification dates and tags at the end as hashtags. So I wrote a quick Python script that reads in the JSON file and — with some help from SetFile — outputs a file in the format I needed.
from subprocess import run import os import json from datetime import datetime
# path to input JSON in_path = os.path.join(os.environ['HOME'],'Downloads/notes/source/notes.json')
# path to output directory. it must exist. out_path = os.path.join(os.environ['HOME'],'Downloads/for-bear')
# read the data withopen(in_path)as f: note_data = json.load(f)
datetime_format ='%m/%d/%Y %H:%M:%S.%f' defcreate_note_file(datum): # the note text content = datum['content']
# if there are tags, append them as space-separated hashtags at the end of the text if'tags'in datum: tags_string ='\n\n' tags_string +=' '.join(['#'+ tag for tag in datum['tags']]) content += tags_string
# convert times to local timezone (Simplenote is in UTC) and format for SetFile creation_date = datetime.fromisoformat(datum['creationDate']).astimezone() creation_date_str = creation_date.strftime(datetime_format)
# write out the file outfile_path = os.path.join(out_path, datum['id']+'.md') withopen(outfile_path,'w+')as outfile: outfile.write(content)
# set the creation and modification dates run(['SetFile','-d', creation_date_str, outfile_path]) run(['SetFile','-m', modification_date_str, outfile_path])
# iterate through the notes for note in note_data['activeNotes']: create_note_file(note)
Note: Using datetime.fromisoformat() with the date format Simplenote uses requires Python 3.11+.
After running the script, import them into Bear with these settings:
☑️ Keep original creation and modification date
☑️ Use first line as title (txt, md, textbundle)
🔲 Use filename as title (txt, md, textbundle)
🔲 Escape involuntary tags
Possible improvements
It would be nice to \ escape all incidental hashtags, but it wouldn’t be trivial. For example, you shouldn’t escape # followed by a non-space character inside a Markdown `#code block`.
If you click on the search box and don't see a cursor, press the down arrow on your keyboard.
I had wanted to do something like this for a couple weeks, but I didn’t know how to get text input into CSS without JS. Then I read Terence Eden’s post on encrypting a static page with no JS, where I was horrified to learn that you can set a <style> element to display: block. Yikes.
The trick, then, was to visually hide all but the blank query-goes-here line.
body style{ display: block; width: 18rem; height: 2.8rem; white-space: pre; font-size: 2rem; line-height: 1; border: 4px solid white; padding: 4px; overflow: hidden;/* the rest of the CSS should be hidden */ }
/* hide the first line as best as possible */ body style:first-line{ font-size: 0; }
This is kind of quite fragile and finicky.
The cursor insertion point has to be at the right place. Even when clicking to give it focus, it’s not always right. (This seems to be worst in Firefox. If you don’t see a cursor, press the down arrow to move from the invisible first line to the correct blank/query line.)
If someone selects too much, deletes too much, or arrows down, the CSS both becomes invalid and you’ll see software gore.
The [attr*=] selector doesn’t have quotes around the query string, which means searches starting with numbers aren’t tokenized as strings. But it does mean that an “empty” search is deliberately an invalid selector, so it shows all results.
Accessibility? No 😬
So, uh, do not use it in production. But it’s still kinda neat I guess?
I love D3, but much of it was designed for SVG. When drawing a data visualization with SVG, you need to tell it exactly where and how big each item needs to be. That’s why D3 has layout utilities like scaleBand.bandwidth and stack(), and why you need to explicitly handle resizing. And when using D3 axis, it directly draws its own SVG.
But you don’t have to use SVG. You can take advantage of CSS’s grid and flex layouts to create many types of visualizations. I recently made an SVG-free bar chart that uses CSS to lay itself out as its container is resized and as data is added and removed — all without calculating any sizes using JavaScript.
The axes and axis labels should take up only as much space as they need, while the area for the graph itself takes up the rest of the place. That means giving axis and axis label rows and columns a dimension of auto, and letting the graph area take up the rest of the space with 1fr.
.graph{ display: grid; grid-template-columns: auto auto 1fr; grid-template-rows: 1fr auto auto; grid-template-areas: "y-axis-label y-axis graph-area" ". . x-axis" ". . x-axis-label"; }
(As an aside, I normally don’t reach for grid-template-areas, preferring to use grid-area: <start-row> / <start-col> / <end-row> / <end-col>, but in this case I think it’s nicely illustrative.)
We can now assign each of the graph elements to its respective grid area.
Note that I gave the graph-area an aspect ratio of 5:4. Since, as basically a block element, the inline size (a.k.a. width in left-to-right and right-to-left languages) of the .graph element is determined by its container, we can set the aspect ratio to ensure that it has a nicely proportional block size (a.k.a. height). The exact value is a matter of taste, but 5:4 works for me. Depending on your needs, you might size the container differently.
Axes
Now that we have our skeleton, let’s quickly take care of the axis labels before moving on to the axes themselves. I’m going to make them bold and align them center, and also add a bit of margin between the labels and the axes.
Now for the axes themselves, I’m going to make an important assumption: all ticks are evenly spaced. That means that what I’m about to do won’t work if you want to show certain specific ticks or are using a nonlinear (e.g., log) axis scale.
With that stated, let’s add some ticks to our markup. Later on, we’ll have D3 take care of this, but let’s do it manually for now so we can get them laid out and styled.
Because this is a bar graph, meaning that the x values are categorical and the y values are continuous, we’re going to treat the x and y axes somewhat differently. But we know they’ll have some things in common, so we can set those things up once.
To begin with, each axis is going to be a flex layout, but they get different flex directions. Similarly, both will have the same border, but they’re oriented differently, so the border will be along different edges, so all we can do is define a custom property to be assigned later.
The ticks themselves will also have many things in common, like font size, tick size, and tick color. We’ll also put a small gap between the tick label and the tick line.
But there’s a problem! The flex layout is taking into account the height of the tick labels themselves. To fix it, we have to give the tick a height of 0. That way, from a layout perspective a tick can be placed exactly where it’s supposed to be, but the tick content will happily overflow its 0-height container and look fine.
.graph .y-axis .tick{ height: 0; }
The x axis is similar, but being categorical, it’s typical to not have the bars go edge to edge like space-between; rather, they get laid out with space-around.
One other thing with the x axis is that when there’s a lot of data (or the graph is small) the tick labels can overlap. That isn’t always the case, but we can add an option to let someone turn the labels vertical by setting data-orientation="vertical" on the axis element.
Thinking a bit about the width and horizontal position of each data point, when there’s only a small number of data points, they should use the same space-around justification as the x axis labels (the data had better line up with the labels!), but should only grow to be so wide when there aren’t that many of them (we don’t want bars the width of the graph). But when there is a lot of data, the bars need to get narrower so more of them can fit.
To do this, we can have a grid create as many columns as there are data points, letting them get arbitrarily (well, 1 pixel) narrow, but only allowed to grow to 25% of the graph area. (Because there are no other columns, the remaining width and the total width are the same. That’s why .25fr is 25% of the total width.)
Remember from the markup above that each .datum has two children: a .value, where the text value goes (though this is optional), and a .bar. The value sits on top of the bar, so let’s use a column flex layout for the .datums (data?). Notice that each grid column in .graph-area is the full height of the graph area. That means that each .datum will also be the full height of the graph area. So, we can use justify-content: end to send the bar and value label to the bottom.
I also added a bit of inline margin here. This is what makes sure there’s space between the bars even when there are a lot of them. I used margin instead of a grid gap for a couple reasons. First, by keeping the spacing as part of each .datum rather than part of the layout we don’t have to compensate for it in the axis; everything stays nicely lined up. Second, we can use a percent value, which means that as you add more bars, the space gets narrower. This uses space more efficiently and feels more natural to me.
For the bar itself, you probably noticed in the .datum markup that its height is set on each bar. Beyond that, the bars get two additional styles. A color, of course (although that could also be set based on the data), and, importantly, flex-shrink: 0. We need that because if a bar gets tall enough that the value text hits the top of the graph area, by default the browser will squish the bar so the text fits. We don’t want that because the height represents a specific numeric value.
The value itself is pretty simple: center it with the datum. I also gave it a z index so that if the bars get close enough, and the value gets wide enough, the value will be on top of the adjacent bars. That said, if you find yourself in this situation, I’d recommend hiding the value label except when a bar is hovered over.
Did someone say D3? Ah yes, that was me a long time ago.
People think of D3 as a data visualization library, but that’s not quite right. It’s a library for binding data to DOM elements and manipulating those elements based on the data — and it happens to have a ton of utilities to help with visualization.
The class
So let’s build a JavaScript class that will build the DOM structure described above based on data.
To do this, we need a bit of D3. Not all of it, but we can import just the bits we need at the top. You’ll see how these all get used soon.
import{ select, selectAll }from"d3-selection"; import{ scaleLinear }from"d3-scale"; import{ max }from"d3-array";
I like the pattern of passing a container element and an object of optional options into a class constructor, then doing everything else from inside the class. Here’s our basic class definition:
Continuing in the constructor, the options object will handle axis labels and orientation, with some logical defaults. If an axis label is omitted, it’s set to an empty string. This is great, because if you don’t want a label, there simply won’t be one. This is a nice advantage over SVG: since the browser automatically sizes the axis label containers based on their content, if that content is "" it essentially collapses out of the layout. Compare this to SVG, where the idiomatic D3 way of leaving room for things like axis labels is manually adding some space to a margin = { top, right, bottom, left } object.
Then, our constructor builds the DOM. It gives the container a class of graph, creates the DOM tree, and applies the three options we just set up. It also assigns the three elements whose content will be dynamically built — xAxis, yAxis, and graphArea — to instance fields.
Note that with this approach, you can’t change the options later. We could easily change this by applying the options in the update() method I’ll describe below instead of here in the constructor.
Remember that the height of each bar is determined by a percent. The last thing the constructor does is create the only D3 scale we need: a scale that will map the values of our data to 0–100%.
this.y =scaleLinear().range([0,100]);
Next, let’s get some data. Our class will provide a getter and setter for the data.
The getter is simple enough: just return the data itself. The setter sets the data, of course, then does a couple more things. It sets the domain of our y scale, and it calls the (yet-to-be-defined) update() method.
There are two important things about setting the scale domain that I want to call out. First, rather than using d3-array’s extent(), it goes from 0 to max because you should make good bar charts. Second, notice the call to nice() at the end. This is because we don’t necessarily want the top of the y axis to be the exact maximum value of our data; we want it to be a nice round number. That’s exactly what nice() does for us.
Finally, we get to the heart of any D3 application: the update() method. But first, let’s state what the data has to look like. It’ll be an array of objects, and each object has (at least) a category and value field.
If you’re not used to thinking with joins that might be a little confusing. Basically, this selects all elements matching .tick in .x-axis. (To start, there should be none.) It then binds the data to them with the data() method. The second parameter to data() is a key so that it knows which data point is which should the data update later on. It then uses the join() method, which is a handy D3 shorthand for treating the enter and update selections the same. What that means is that for each row of data we’ll end up with a <div> with the class tick and text equal to the category — exactly what we want for the x axis.
The y axis is similar, but the data isn’t the data we’re trying to graph; we want a list of ticks. For that, we’ll rely on the D3 scale’s ticks() method. It takes an optional argument for the number of ticks, so we could be clever and set it based on the height of the container, but we can just stick with the default, which is 10. So this
this.y.ticks()
returns a list of 10 nice, evenly spaced ticks across the y scale’s domain. From there, we can have it add those ticks to the DOM the same way we did for the x axis:
Finally — finally! — we can actually graph our data. Because each .datum element has two children, it’s going to be a little different than how we did the x ticks. Rather than using the join() shorthand, we need to handle the enter and update selections separately. Specifically, when a new data point shows up (the enter selection), we need to create the .datum and its two children, .value and .bar. Then, for all data points, whether new or not (a merged enter and update selection), we want to update the value and bar height.
CSS has gotten to the point where things that, a few years ago, I would have used SVG and JavaScript for I can now do in just HTML and CSS. Being able to rely on the browser for layout, plus :has(), make things so much more flexible and responsive.
The entire sign is sized based on the font size. aspect-ratio, combined with fr and em units, means that you set the font size, and the whole rest of the sign — its overall aspect ratio and the ratio between the colored bars and the rest of the sign — is proportionally correct.
The train line color stripes are automatically laid out by a grid layout. Being able to add and remove stripes and have the rest of them adjust is very handy.
As many others have noted, the :has() selector lets you easily hook up checkboxes and radio buttons to other parts of the DOM. It’s new, so it’s neat :)
Most of this isn’t exactly new, but when it all comes together it can be quite powerful.
The sound podcast Twenty Thousand Hertz asked people to share their memories about old audio formats. I doubt they’ll use mine on the show, but here’s what I shared.
Growing up in my dad’s recording studio in the 1990s I was exposed to a variety of audio formats. There was 24-track 2-inch tape, and 1/4” reel-to-reel tape that radio stations wanted commercials on until nearly the turn of the century[1] when they switched to DAT.
But the most unique format was the DTRS format used by the Tascam DA-88. It used Hi8 video cassettes to record 8 tracks of digital audio. My dad linked 3 of them together for 24 tracks. As a kid, those machines were cool because when you turned them on they were programmed to marquee the word TASCAM across the 8 LED meters, and that was just fun to watch.
I remember my dad using a razor blade to splice in leader even after he was editing and mixing in Pro Tools. ↩︎
With Audio Hijack 4, Rouge Amoeba added the ability to manually edit connections. I’ve been an occasional Audio Hijack user since 2004[1], but I knew right away that this had the potential to bring it into daily use.
As I wrote a couple years ago, I had been using a DAW, Reaper, to route and process my everyday call audio, but that seemed heavy, and definitely not what it was designed for.
Instead of Reaper, I set up Audio Hijack. As with before, I’m using BlackHole to create two virtual audio devices: a 2-channel device I use as the call app’s mic, and a 16-channel device I use as the call app’s speaker. Then, Audio Hijack looks like this:
It looks like a lot, but talking through it from top to bottom, left to right:
Call audio comes into the block labeled “Call far side” from the device called BlackHole 16ch because the call app is using it as its speaker.
Far side audio runs through a volume control (that I pretty much always keep at 100%), then directly on to my USB audio interface so I can hear it (the block labeled Monitoring)
My actual, physical microphone is plugged into my USB interface’s input, which shows up here as the input block labeled Mic.
The mic is a mono device on input 1 the USB interface, so there’s a Channels block to duplicate input 1 so it comes out on both channels.
As part of proper gain staging, I want to make sure that the gain knob of the interface is set correctly, so I stuck in a PFL meter block. I leave plenty of headroom and compress it later (more on that below), but want to make sure it’s high enough, too.
Then there are several blocks Audio Units plugins to process my mic audio:
First up is AUHipass (labeled HPF), which I use to get rid of low-frequency energy that’s really just rumble and other noise. I set it to roll off below about 50 Hz.
For reasons I haven’t figured out, I sometimes have hum in my signal, so I keep RX 8 De-hum around. I don’t love how it sounds, though, so unless the hum is particularly bad, I usually keep it off.
The RX 8 Voice De-noiser is pretty magical. HVAC noise? Outside noise? Nah. I turn on adaptive mode and leave the rest set to the defaults.
The final block of the processing chain in a compressor. I use MCompressor because it’s free. The exact settings depend on mic placement (sometimes I have the mic in the frame of my video because it sounds better, but sometimes I move it out of the frame because it looks nicer), so I have to adjust the threshold. I usually stick to a 4:1 ratio and turn on make-up gain (“Maximize to 0 dB”). Because of that latter setting, I also turn on the limiter.
Speaking of levels, next up is the AFL meter block. It should hopefully be hotter than the input meter, and more even. Still not clipping, though.
Next I have a couple of switches:
The one labeled “Self 🎧” toggles whether or not I want to hear myself in my headphones. (Usually I do, but that’s just personal preference.) When it’s on, it allows audio to travel from the mic processing chain on to the Monitoring block.
The one labeled “Mute 🎙️” toggles whether my mic is sent on to the call, which is the BlackHole 2ch output device.
That’s it for mic routing, but there’s one more thing in the Audio Hijack session. Sharing application audio with a call can be tricky and unreliable (“Can y’all hear this?”), so I keep a block around that I can turn on to capture application audio and send it to both my ears and the call. If I can hear it, they can hear it. Plus, I didn’t have to un-share and re-share my screen to make it happen.
One other thing I’ve started doing is using a global shortcut to mute myself. Usually when I’m talking in a meeting, the call app isn’t in the foreground, and I like to be able to quickly toggle it because (let’s be honest), with a mic in my face, I worry about mouth sounds. (Unless you enable Original Sound in Zoom or a similar feature in other apps, this probably isn’t a huge concern, but still….)
There are a gazillion of these little apps out there. They all seem just a little scammy. I’d like to use Mic Drop, but it doesn’t support muting only a single device at a time. I ultimately settled on MuteKey. It’s not perfect: I need to quit and relaunch it after the computer sleeps and/or it’s unplugged from the selected device, and sometimes after quitting while muted it turns down the input level, so I need to use SoundSource or Audio MIDI Setup to turn it back up. But it gets the job done. I use the shortcut ⌃⇧Space because I can easily use it one handed with either hand — very handy for pointing with a mouse while talking on a call.
One of the reasons I like being able to hear myself in my headphones is that when the USB interface is muted with this app, I can’t hear myself either. So if I can’t hear myself, neither can the call, and if I can hear myself, the call can hear me, too.
So there’s the latest on my call audio setup. Let me know if you have any questions or suggestions on how to sound even better!
In college I set Audio Hijack to automatically start and record the Real Audio (remember that‽) stream of Morning Edition on Michigan Radio from 05:00–07:00, then put it on my iPod to listen to throughout the day. Yep, I invented podcasting. ↩︎
I finally read that Why the Super Rich are Inevitable piece that’s been going around. It reminds me a lot of something I’ve thought about for years:
The economy is like the early universe shortly after the Big Bang. You might think of matter as being uniformly distributed, but in reality small, random perturbations meant that some regions of space ended up with more matter than others. Over billions of years, the effect of gravity is that some places will have collected all the matter from surrounding areas.
The detailed, all-sky picture of the infant universe created from nine years of WMAP data. The image reveals 13.77 billion year old temperature fluctuations (shown as color differences) that correspond to the seeds that grew to become the galaxies. The signal from our galaxy was subtracted using the multi-frequency data. This image shows a temperature range of ± 200 microKelvin.
The natural result of the Big Bang is that a relatively few areas of space will be extremely densely filled with matter (e.g., stars), while most of the universe will be almost entirely empty (e.g., intergalactic space).
As the yard sale model demonstrates, wealth distribution in a market economy is similar, with the rich getting richer by drawing capital from the less wealthy people around them until there are just a handful of oligarchs and a lot of poor people.
A few weeks ago I talked about the bottom of the header that gets wavy when you scroll. Now I’m going to talk about the main navigation links that “pluck” when you hover over them.
These underlines are examples of standing waves, waves with nodes and antinodes (peaks) that are fixed in space. For example, here is a standing wave with a period of 2 seconds.
Lucas Vieira, Public domain, via Wikimedia Commons
The only thing that changes is the amplitude of the wave, so I started out by creating a JavaScript object that generates an SVG path of a sine wave. You initially configure it with the number of half-periods you want, then, using a simple setter, it updates the amplitude every time you change the amp property.
One of the great things about GSAP is that it can tween any property of an object. In this case, I needed to tween amplitude values. The initial “pulling” of the line (when hovered) was easy: I picked an easing that looked natural and used GSAP to tween to the maximum amplitude value.
pull(){ gsap.killTweensOf(this);
// gsap is going to tween the amplitude of this Plucky object gsap.to(this,{amp:this.maxAmp,ease:"power2.out",duration:this.pullDuration }); }
Now when you pull() the underline, it animates from whatever the current amplitude is to the configured maximum amplitude (I’m using 10px).
The release and subsequent decay was trickier because I wanted it to dampen and decay (what’s technically known as “jiggle-jiggle”) in a physically natural way. To do this, I needed an easing curve based on a damped sinusoid, as described in the Wikipedia Damping article:
So let’s talk about easing functions. An easing function, like those from cubic-bezier.com or the GSAP ease visualizer, describes how a value changes over time, with both inputs and outputs to that function normalized to the range 0–1. As time progresses from 0 to 1, the value progresses from its start value (denoted 0) to a target value (denoted 1).
In this case, the goal is to tween from an amplitude of maxAmp (say, 10) to an amplitude of 0 (a flat line). The trick here is that overshooting the “end” of the ease (at which the easing function outputs 1 and our amplitude is 0) will result in a value that’s past the target of the ease. For us, that’s a negative amplitude. Negative amplitude means amplitude in the other direction, so what had been above the 0 line is now below. That’s exactly what we want to happen, as you can see in the standing wave animation at the top of this post.
I wanted to use the damped cosine above — or something like it — as a GSAP easing function. At this point, a normal person would have splurged on CustomWiggle, but that’s not what I did. Instead, it meant doing a few things: altering the function so its output value starts at 0 and ends at 1, setting a parameter that makes it settle down at 1 at time 1, setting a parameter for how many times it should jiggle before settling down, and transforming it into an SVG path string so GSAP can accept it as a custom easing function.
Looking at the plot above, we can see that it starts at y=1 and settles at y=0. Multiplying by -1 will flip it so it starts at y=−1 and ends at y=0. Adding 1 to the whole thing will make it start at y=0 and end at y=1. So now our function is
y(t)=−e−tcos(2πt)+1.
Next we need to parameterize it. There are two parameters we care about: b, the damping coefficient, which determines how quickly the envelope approaches its asymptote[2]; and f, the frequency, which determines the number of jiggles before it settles down[3].
y(t)=−e−btcos(2πft)+1
For us, how to set f is a matter of aesthetics, but for simplicity it’s nice for b to be set so that when time t=1, it has just about settled down. (GSAP’s CustomEase will normalize any input to 0–1, but it was easier for me to think about if I did it this way.) Playing around with it in a graphing calculator a bit told me that b=5 gives the desired result. I then picked f=8 because I liked how the result looked when used as an easing function.
So our final easing function becomes:
y(t)=−e−5tcos(2π8t)+1
The last step in turning this function into a custom easing curve was converting it into an SVG path string. I used more or less the same approach as I did for the wavy header: I sampled the function and ran the resulting values through d3-shape’s line() generator.
I probably over-engineered how I sampled the function, especially in how I came up with the stopping condition. I tried to be clever and only sample at/near[4] peaks and zero-crossings and to stop when peaks were no higher than 0.01. It works, though it makes it a bit hard to read.
calcDampCurve(){ // b: dampening coefficient. higher numbers dampen more quickly. // fixing it at 5 has it reaching ~.01 by t = 1 // adjusting the decayFreq and releaseDuration allows for full flexibility const b =5; constdecayFn=t=>-1*(Math.E**(-b * t))* Math.cos(2* Math.PI*this.decayFreq * t)+1;
let sampling =true; let stopNextZero =false; const samples =[]; letT=0; while(sampling){ const t =T/sampleRate; const y =decayFn(t); samples.push([t,-y]);
if(T% samplesPerWavelength/2===0){// near a local extreme if(Math.abs(y -1)<.01){ stopNextZero =true; } } if(stopNextZero &&(T% samplesPerWavelength ===2||T% samplesPerWavelength ===6)){// at a zero crossing sampling =false; } else{ T+=1; } } // use d3-shape to turn the points into a path string for gsap returnline().curve(curveNatural)(samples); }
We can drop the resulting SVG string into the GSAP Custom Ease Visualizer to check it, and it works!
Finally, we can apply this easing function to a GSAP tween when “releasing” the underline:
As with before, there were a few other things to take care of before calling it done, like respecting someone’s prefers-reduced-motion setting. In terms of progressive enhancement, I should point out that I made sure that the default link underline isn’t removed until the relevant JavaScript loads so that there’s always an appropriate affordance.
So there you go! That’s how I did my plucky nav links.
And as with before, be sure to check out the whole thing on CodePen.
I later found that the d3-shape curveNatural generates an essentially identical curve. ↩︎
This kind of corresponds to CustomWiggle’s amplitudeEase. ↩︎
This kind of corresponds to CustomWiggle’s timingEase. ↩︎
The peaks and 0 crossings aren’t exactly where they’d be if it were a pure (co)sine wave because changing the amplitude envelope creates higher harmonics. See Fourier for more details 😅 ↩︎
Last week was new computer day at work. As I was looking between the new machine and the old one, I was thinking about what makes a computer feel like mine. There are settings, little utilities, and how I arrange things that make it feel like home.
I’ve been shockingly consistent over the years. Here’s a screenshot from 2005.
And here’s my personal machine today (with the Dock visible for the photo op).
(I promise I’ve changed my desktop background a few times, but Hurricane Isabel from the ISS is in my regular rotation.)
Make things small by cranking up the scaled resolution. On a laptop that means the smallest Apple offers — or smaller. On my 13” personal machine I used a hack to enable 1920 × 1200 HiDPI. I don’t go full-native on my 27” external 4K display, but I do use the second-from-highest, 3360 × 1890.
Colors: I set the system accent color to gray (aka Graphite) but keep the highlight color blue.
Clock settings: Day of week, no date, 24-hour clock, show the seconds.
Take basically everything out of the Dock (all I have there permanently is an editor to drag files to), turn off showing recent apps, and turn on auto-hiding. I also make it smaller, using the second-from-smallest tick when resizing while holding ⌥. But yes, I keep my Dock at the bottom.
Non-default gestures and hot corners:
ExposéMission Control: 4 fingers up
App windows: 4 fingers down and top left corner
Move between spaces/full-screen apps: 4 fingers side-to-side
Set up a keyboard shortcut (⌃⇧⬅) for Notification Center. (I didn’t have a Magic Trackpad for a while, so wanted a quick way to access it. Now it’s habit.)
Revert a couple of recent design changes via accessibility settings: turn on persistent proxy icons and Reduce Transparency.
Finder settings:
Turn on the status and path bars
Have new windows default to my documents folder (inside ~/Documents/)
Set search scope to the current window
Show all file extensions
Put the path item in the toolbar (though I usually end up ⌘-clicking the titlebar)
Windows default to list view (though I’m constantly switching between ⌘2 list and ⌘3 columns)
The menu bar: after the clock, I start out right to left with battery, wifi, volume, MenuMeters[1] average CPU graph, MenuMeters network activity graph, and Day One. Everything else is hidden by Bartender (with a handful of show-for-updates exceptions).
Install Alfred[2] and set it to the “macOS” theme. The ⇧⌥⎵ muscle memory for the launcher and ⌘⌥J for clipboard history are deeply ingrained.
Keyboard layout to Dvorak. (What can I say, I switched 20 years ago.)
And rounding out (pun intended) the I Hate Change category is Displaperture, which I use to round the menu bar on non-notched displays.
I also have iStat Menus, but I’ve been using MenuMeters since ~2004 and honestly I think it just feels more Mac-like and at home in the menu bar. ↩︎