Skip to content

Commit

Permalink
Added more content to the SVG heatmap page
Browse files Browse the repository at this point in the history
  • Loading branch information
bbag committed Feb 29, 2024
1 parent e80fe26 commit 9becff7
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 30 deletions.
133 changes: 110 additions & 23 deletions src/content/experiments/svg-heatmaps/Visualizer.astro
Original file line number Diff line number Diff line change
@@ -1,42 +1,129 @@
---
import { Image } from 'astro:assets'
import { Code } from 'astro:components'
import Xray from './Xray.jpg'
const codeTheme = 'material-theme-palenight'
const colors = [
{ r: 0.165, g: 0.239, b: 0.361 },
{ r: 0.094, g: 0.353, b: 0.647 },
{ r: 0.192, g: 0.667, b: 0.31 },
{ r: 0.949, g: 0.882, b: 0.129 },
{ r: 0.984, g: 0.027, b: 0.078 }
]
function calcBgColor(color: { r: number, g: number, b: number }) {
return `${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}`
}
const code = `<feFuncR type="table" tableValues="${colors.map(color => color.r).join(' ')}" />
<feFuncG type="table" tableValues="${colors.map(color => color.g).join(' ')}" />
<feFuncB type="table" tableValues="${colors.map(color => color.b).join(' ')}" />
`
---

<div>
<p>Original gradient:</p>
<svg viewBox="0 0 200 10">
<rect x="0" y="0" width="200" height="10" />
</svg>
<p class="yolo">Filtered gradient:</p>
<svg viewBox="0 0 200 10">
<div class="gradient-preview">
<div class="color-stops">
{colors.map(color => (
<span
class="color-stop"
style={`--bg: rgb(${calcBgColor(color)})`}
></span>
))}
</div>

<svg viewBox="0 0 200 25">
<filter id="heatmap-filter" color-interpolation-filters="sRGB">
<!-- Crank up the exposure just a tad -->
<feComponentTransfer>
<feFuncR type="table" tableValues="0 0.3 0.6 0.9 1" />
<feFuncG type="table" tableValues="0 0.3 0.6 0.9 1" />
<feFuncB type="table" tableValues="0 0.3 0.6 0.9 1" />
</feComponentTransfer>
<!-- Turn black-white gradient into blue-green-yellow-red gradient -->
<feComponentTransfer>
<feFuncR type="table" tableValues="0.165 0.094 0.192 0.949 0.984" />
{/* <feFuncR type="table" tableValues="0.165 0.094 0.192 0.949 0.984" />
<feFuncG type="table" tableValues="0.239 0.353 0.667 0.882 0.027" />
<feFuncB type="table" tableValues="0.361 0.647 0.31 0.129 0.078" />
<!-- <feFuncR type="table" tableValues="0.043 0.094 0.192 0.949 0.984" />
<feFuncG type="table" tableValues="0.047 0.353 0.667 0.882 0.027" />
<feFuncB type="table" tableValues="0.055 0.647 0.31 0.129 0.078" /> -->
<feFuncB type="table" tableValues="0.361 0.647 0.31 0.129 0.078" /> */}
<feFuncR type="table" tableValues={colors.map(color => color.r).join(' ')} />
<feFuncG type="table" tableValues={colors.map(color => color.g).join(' ')} />
<feFuncB type="table" tableValues={colors.map(color => color.b).join(' ')} />
</feComponentTransfer>
</filter>
<rect x="0" y="0" width="200" height="10" filter="url(#heatmap-filter)" />
<use href="#svgRect" filter="url(#heatmap-filter)" />
<use href="#svgRect" y="15" />
</svg>
</div>

<style>
<Code theme={codeTheme} code={code} lang="html" />

<figure>
<div style="display: grid; gap: 2rem; grid-template-columns: 1fr 1fr; margin: 1rem 0;">
<Image src={Xray} alt="X-ray" style="border-radius: 0.5rem;" />
<Image src={Xray} alt="X-ray" style="border-radius: 0.5rem; filter: url(#heatmap-filter);" />
</div>
<figcaption>Photo credit to <a href="https://www.pexels.com/@pixabay/" target="_blank" rel="noreferrer nofollow noopener">Pixabay</a> on Pexels.</figcaption>
</figure>

<!-- <p>To show it in action, we’ll start with a simple black and white gradient and apply it to a rectangle in the SVG:</p>
<Code theme={codeTheme} code={`<linearGradient id="gradient">
<stop stop-color="black" offset="0%" />
<stop stop-color="white" offset="100%" />
</linearGradient>
<rect width="200" height="10" fill="url(#gradient)" />`} lang="html" />
<svg viewBox="0 0 200 10">
<filter id="basic-filter" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="table" tableValues="1 0" />
<feFuncG type="table" tableValues="0 0" />
<feFuncB type="table" tableValues="0 1" />
</feComponentTransfer>
</filter>
<filter id="heatmap-filter" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="table" tableValues="0.165 0.094 0.192 0.949 0.984" />
<feFuncG type="table" tableValues="0.239 0.353 0.667 0.882 0.027" />
<feFuncB type="table" tableValues="0.361 0.647 0.31 0.129 0.078" />
</feComponentTransfer>
</filter>
<linearGradient id="bw-gradient">
<stop stop-color="black" offset="0%" />
<stop stop-color="white" offset="100%" />
</linearGradient>
<rect id="svgRect" x="0" y="0" width="200" height="10" fill="url(#bw-gradient)" />
</svg> -->

<style lang="scss">
svg {
margin-bottom: 2rem;
}

.yolo {
color: blue;
.gradient-preview {
padding: 0 0.625rem;
}

.color-stops {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}

.color-stop {
position: relative;

&,
&::after {
height: 1.25rem;
}

&::after {
background-color: var(--bg, #FFF);
border-radius: 1rem 1rem 0 1rem;
border: 2px solid #FFF;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0.125rem 0.375rem rgba(0, 20, 40, 0.375);
content: '';
display: block;
left: 0;
position: absolute;
top: 0;
transform: translateX(-50%) rotate(45deg);
width: 1.25rem;
}
}
</style>

Expand Down
86 changes: 79 additions & 7 deletions src/content/experiments/svg-heatmaps/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ link: https://codepen.io/bbagg/pen/yLwWXvx
---

import Visualizer from './Visualizer.astro'
import { Image } from 'astro:assets'
import Trees from './trees.jpg'

SVGs (and especially their filters) are really, *really* cool. I also tend to get injured doing Brazilian jiu-jitsu, *a lot*.

So I figured why not combine these two for a cool little code experiment.

## The magic of `<feComponentTransfer>`

When I learned about the `<feComponentTransfer>` filter and how it basically applies a gradient map to an image, the idea popped into my head to try a “heatmap” effect to see where my most common (and most severe) injuries were located. Best case, I learn what to strengthen up to prevent more injuries; worst case, I know what to watch out for in the future. 😅

The `<feComponentTransfer>` filter works like this:

```html
<svg>
<!-- 1: Define your filter -->
<!-- 1. Define your filter -->
<filter id="heatmap">
<!-- 2: Add the feComponentTransfer effect -->
<!-- 2. Add the feComponentTransfer effect -->
<feComponentTransfer>
<!-- Add a function for each channel (red, green, blue, alpha) -->
<feFuncR type="table" tableValues="0 1" />
Expand All @@ -36,12 +40,80 @@ On each transfer function element (e.g. `<feFuncR>`) we set the `type="table"` a

Any values in between them also get interpolated (with `0` being the min and `1` being the max).

So for example, `<feFuncR type="table" tableValues="0 1 0"></feFuncR>` means “for black areas of the image, have no red color whatsoever, for halfway-bright areas of the image have maximum red, and for white areas of the image have no red again.” It'd look something like this:
So for example, `<feFuncR type="table" tableValues="0 1 0"></feFuncR>` means “for black areas of the image, have no red color whatsoever, for halfway-bright areas of the image have maximum red, and for white areas of the image have no red again.”

<p>To show it in action, we’ll start with a simple black and white gradient and apply it to a rectangle in the SVG:</p>
```html
<linearGradient id="gradient">
<stop stop-color="black" offset="0%" />
<stop stop-color="white" offset="100%" />
</linearGradient>
<rect width="200" height="10" fill="url(#gradient)" />
```

Result:

<svg viewBox="0 0 200 10" style="margin-bottom: 2rem;">
<linearGradient id="bw-gradient">
<stop stop-color="black" offset="0%" />
<stop stop-color="white" offset="100%" />
</linearGradient>
<rect id="svgRect" x="0" y="0" width="200" height="10" fill="url(#bw-gradient)" />
</svg>

Then we’ll add a filter to the rectangle which basically says “the black end of the spectrum should only be blue, and the white end of the spectrum should only be yellow (i.e. max red and green values):”

```html
<!-- 1. Define your filter -->
<filter id="basic-filter" color-interpolation-filters="sRGB">
<feComponentTransfer>
<!-- 2. Have 0% red at black and 100% red at white -->
<feFuncR type="table" tableValues="0 1" />
<!-- 3. Have 0% green at black and 100% green at white -->
<feFuncG type="table" tableValues="0 1" />
<!-- 4. Have 100% blue at black and 0% blue at white -->
<feFuncB type="table" tableValues="1 0" />
</feComponentTransfer>
</filter>
<!-- 5. Apply the filter to the gradient rectangle -->
<rect x="0" y="0" width="200" height="10"
fill="url(#gradient)"
filter="url(#basic-filter)"
/>
```

New result:

<svg viewBox="0 0 200 10" style="margin-bottom: 2rem;">
<filter id="basic-filter" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="table" tableValues="0 1" />
<feFuncG type="table" tableValues="0 1" />
<feFuncB type="table" tableValues="1 0" />
</feComponentTransfer>
</filter>
<use href="#svgRect" filter="url(#basic-filter)" />
</svg>

You can apply that filter to an image too, which is extra neat:

<figure>
<div style="display: grid; gap: 2rem; grid-template-columns: 1fr 1fr; margin-bottom: 1rem;">
<Image src={Trees} alt="Trees" style="border-radius: 0.5rem;" />
<Image src={Trees} alt="Trees" style="border-radius: 0.5rem; filter: url(#basic-filter);" />
</div>
<figcaption>Photo credit to <a href="https://www.pexels.com/@jplenio/" target="_blank" rel="noreferrer nofollow noopener">Johannes Plenio</a> on Pexels.</figcaption>
</figure>


## Adding multiple color stops

Now comes the fun part: you can add as many stops as you like in between the starting and ending `tableValues` values for each channel, and crank those values up and down anyway you like.

(Will show three gradients here: one with the filter itself, one with an image, and one with the filter applied to the image)
Here’s a fun playground — add/remove colors below and click the color picker to customize each one, then notice how the values in the code update:

(Next: embed an interactive widget for toying with the table values to show how they affect the final filter)
<Visualizer />

(Then: will talk about the process of building the final SVG and the CodePen demo itself... stay tuned!)
## Building the final heatmap SVG

<Visualizer />
(Coming up next: will talk about the process of building the final SVG and the CodePen demo itself... stay tuned!)
Binary file added src/content/experiments/svg-heatmaps/trees.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/content/experiments/svg-heatmaps/xray.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/styles/_Reset.scss
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ figure {
margin: 0 0 1rem;
}

figcaption {
font-size: 0.875em;
font-style: italic;
opacity: 0.75;
text-align: center;
}

img {
border-style: none;
height: auto;
Expand Down

0 comments on commit 9becff7

Please sign in to comment.