inspired by 2024 MSI
This repo provides an easy way to "convert" HTML-elements into signed distance fields. By converting elements into sdf's you can easily achieve a metaball like effect.
In your HTML-Document create a div with id meta-container, where the canvas will live ...
<div id="meta-container"></div>
... and include Three.js and the meta-logic.js file.
<script type="importmap">
{
"imports": {
"three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.164.1/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@<version>/examples/jsm/"
}
}
</script>
<script type="module" src="src/meta-logic.js"></script>
In your script you can then simply import the MetaCanvas class and create the canvas.
import { MetaCanvas, ColorRamp, ColorStop } from "./meta-logic.js";
const metaCanvas = new MetaCanvas({});
You can mark the HTML-elements that you want to include in the sdf with classes. Currently there are four supported classes:
- A circle that appears wherever the element is placed in the document. If the width and height of the element do not equal the width gets used as the diameter of the circle with the sdf-circle being tugged into the top left corner of the element. e.g:
<div class="meta-circle"></div>
- A rectangle that takes up the same space as the bounding-box of its respective HTML-element. e.g:
<div class="meta-box"></div>
- Similar to meta-box, but with rounded corners, the radius of which can be adjusted with the HTML-attribute r. e.g:
<div class="meta-box-rounded" r="15"></div>
- The main attraction. SDF-text! Currently only lowercase letters are supported, that render in a custom font, that was specifically designed with no sharp corners. The original font file can be found in ./src/fonts/metaball_font.ttf (The font file includes all of ASCII). e.g:
<div class="meta-text">my sdf text</div>
In order to debug your element, you can attach the class meta-debugb which shows where the objects should appear. e.g:
<div class="meta-text meta-debugb">my sdf text</div>
To get started you have to import the needed classes:
import { MetaCanvas, ColorRamp, ColorStop } from "./meta-logic.js";
ColorRamp and ColorStop are only needed if you want to change the colors. Then you can simply create a new MetaCanvas and you should see the result.
const metaCanvas = new MetaCanvas({});
You can pass following optional parameters into the constructor, with the default values being:
{ smoothRadius = 15,
gain = 0,
contrast = 0.1,
steps = 5,
colorRamp = ColorRamp.toEquallySpaced([new THREE.Color("black"), new THREE.Color("white")]) }
All these parameters can also be changed with their respective setters. e.g:
metaCanvas.setSmoothRadius(40);
metaCanvas.setGain(.2);
metaCanvas.setContrast(2);
metaCanvas.setSteps(500);
metaCanvas.setColorRamp(new ColorRamp([new ColorStop(new THREE.Color("rgb(0, 0, 255)"), 0), new ColorStop(new THREE.Color("rgb(0, 255, 0)"), 1)]));
Normally all elements get updated on resize and scroll. If your element changes whenever the mouse moves, you can attach the updateMouse Attribute to update the element also on mousemove. e.g:
<div class="meta-text" updateMouse="">my sdf text</div>
Additionally, you can also update manually by calling the update function of the canvas.
metaCanvas.update();
- Controls the radius of the metaball-effect (the flowing into another of elements).
- gain can be used to "shift" the boundary up or down, making the elements bigger or smaller and contrast changes the width of the boundary, where you can see the colors of the chosen ColorRamp. The color of a pixel get calculated according to this formula:
colorRamp(sdf(pixelPosition) * contrast + gain);
-
Controls the number of discrete steps inside the boundary.
-
Controlls the Color of the output. A color ramp is a List of ColorStops. A Color Stop is a color with a value between 0 and 1 that controlls its position in the ramp. In this case the color is a THREE.Color which can be initialised in many ways. The colors are interpolated linearly. The ColorRamp class provides a static function to generate a ColorRamp with equally spaced colors from a list of colors. Otherwise, a list can be passed into the Constructor or colors added with the function append. e.g:
const c = ColorRamp.toEquallySpaced([new THREE.Color("black"), new THREE.Color(0, 1, 0), new THREE.Color("white")]);
c = new ColorRamp([new ColorStop(new THREE.Color("rgb(0, 0, 255)"), 0), new ColorStop(new THREE.Color("rgb(0, 255, 0)"), 0.7)]);
c.append(new ColorStop(new THREE.Color("0xff0006"), 1)));
To play around with all the parameters, go to this site.
The Class creates a Three.js canvas with a single plane covering the whole screen. This plane has a ShaderMaterial that can be programmed completely by the user. On Setup the Fragment shader is generated procedually according to the meta-objects in the scene. The result is a single function that contains the SDF of the scene. This sdf is then called in every fragment and the resulting color calculated according to the previously stated formula. 4x MSAA was used to smooth out the edges. The position, scale, edge-radius, ... of all the elements and the global parameters of the MetaCanvas are passed as Uniforms and updated when changed or on events like scroll, resize or mousemove. By doing this the expensive step of building and compiling the fragment shader has to be done only in the beginning and not every time something changes.