blotter

Documentation

What follows are the main classes an average developer will encounter when working with Blotter. Many classes and utilities are left out of this documentation, as they are more internal to the codebase rather than part of the interface one would need to use Blotter as it comes. If you would like to see Blotter laid bare, please view the source.

Everything documented here is a named export of the blotter.ts package. The ready-made effects live under the blotter.ts/materials subpath.

import { Blotter, Material, ShaderMaterial, Text, shaders } from "blotter.ts";
import { ChannelSplitMaterial } from "blotter.ts/materials";

Blotter

Blotter is the orchestrator. It packs all of its texts into one atlas, renders them through the material's shader in a single draw call, and hands each text its own output canvas. Whenever you want to apply a material to texts on your page, you will instantiate a new object of the Blotter class.

  • construction

    const blotter = new Blotter(material, options);

    Create a new instance of Blotter where material is an instance of any Material and options is an optional object with any of the following optional parameters. The constructor throws when the device does not support WebGL; check isWebGLSupported() first if you need a fallback.

    • texts options.texts

      An array of Text objects, or optionally a single Text object. Any texts given to an instance of Blotter will be drawn according to the instance's material.

    • ratio options.ratio

      The backing store size in relation to the canvas element, or otherwise known as the devicePixelRatio. This property tells Blotter what pixel ratio to draw texts for in relation to the screen, and will affect the clarity of rendered texts on retina devices. Blotter determines this property by default to ensure the most appropriate pixel ratio for your user's device, so it's unlikely you'll need to set this yourself.

    • autobuild options.autobuild

      Tells the instance of Blotter whether or not to immediately build and configure itself using the supplied Material instance and optional parameters such as supplied Text objects. By default, Blotter will assume this to be true. If you intend to add additional texts or change the material prior to use, set this to false and later call blotter.update() to trigger a fresh build.

    • autostart options.autostart

      Tells the instance of Blotter whether or not to immediately begin rendering and updating universal uniforms. By default, Blotter will assume this to be true.

    • autoplay options.autoplay

      Tells the instance of Blotter whether or not to immediately begin the render loops for all RenderScope objects created for each of its texts. By default, Blotter will assume this to be true. Each draw will happen on requestAnimationFrame.

    Await blotter.ready (or listen for the ready event) to perform work after the instance has finished building itself.

    const blotter = new Blotter(material, { texts: [text] });
    blotter.forText(text)?.appendTo(document.body);
    await blotter.ready;
  • ready blotter.ready

    A Promise that resolves with the instance after its first successful build. This is the primary way to wait for a Blotter; the ready event still fires for the same moment.

  • material blotter.material

    A reference to the Material instance being used to render your texts. Assigning a new material (or calling setMaterial) after your Blotter object's initialization requires that your instance be rebuilt by calling blotter.update().

  • texts blotter.texts

    A read-only array of the Text objects being rendered by your instance of Blotter. Use addTexts and removeTexts to change it.

  • ratio blotter.ratio

    The pixel ratio the instance renders at. Read-only after construction.

  • imageData blotter.imageData

    The ImageData of the whole back buffer from the most recent frame, or undefined before the first render. Each RenderScope copies its own rectangle out of this.

  • update blotter.update()

    Rebuild the atlas and shader from the current texts and material. Returns a Promise that resolves when the rebuild has settled. Concurrent calls coalesce: one build runs at a time, with at most one trailing rebuild queued. Text, property and material changes call this for you; you only need it after setMaterial, addTexts, removeTexts, or when autobuild is false.

    The instance emits ready on the first completed build and update on every later one.

  • start blotter.start()

    Tells the instance of Blotter to begin rendering and updating universal uniforms. At the time of each draw on requestAnimationFrame the instance will emit a render event.

  • stop blotter.stop()

    Tells the instance of Blotter to stop rendering and updating universal uniforms.

  • teardown blotter.teardown()

    Disposes the instance's render target and removes every listener it registered on its texts and material. Call stop() first, then teardown(), when you are done with an instance; the shared WebGL context stays alive for other instances.

  • setMaterial blotter.setMaterial(material)

    Sets the Material instance being used to render your texts. In order for this change to go into effect you'll have to rebuild your Blotter instance by calling blotter.update().

  • addText blotter.addText(text)

    Alias method for blotter.addTexts(text), see below.

  • addTexts blotter.addTexts(texts)

    Adds one or more Text objects to be rendered by your instance of Blotter. In order for this change to go into effect you'll have to rebuild your Blotter instance by calling blotter.update().

  • removeText blotter.removeText(text)

    Alias method for blotter.removeTexts(text), see below.

  • removeTexts blotter.removeTexts(texts)

    Removes one or more Text objects from being rendered by your instance of Blotter. In order for this change to go into effect you'll have to rebuild your Blotter instance by calling blotter.update().

  • forText blotter.forText(text)

    Returns the RenderScope for the given Text object, or undefined if the text is not known to this instance.

  • boundsForText blotter.boundsForText(text)

    Returns an object containing the width (w), height (h), x-offset (x), and y-offset (y) of the given Text object in the back buffer the Blotter instance uses to render the material's effect collectively for all texts in the instance. These values, especially the x-offset and y-offset, will likely be of little use to you.

  • on blotter.on(event, listener)

    Subscribes to one of the instance's events and returns a function that unsubscribes. Every emitter in Blotter (Material, Text, RenderScope, UniformInterface) has the same on, off, and once shape.

    const off = blotter.on("render", () => {
      // called after every frame
    });
    off(); // unsubscribe
  • events "ready" | "update" | "render"

    ready fires once after the first build, update after each later rebuild, and render after every frame.

Material

Blotter Materials describe how your texts will be rendered and provide the interface through which you can control uniforms to introduce variety in the characteristics of any effect.

As the Material class is the base class for all Blotter Materials, using this class on its own will simply render your texts according to their font and style properties. See the documentation for specific Materials for further details on their individual usages, and custom materials below for writing your own.

  • construction

    const material = new Material({ mainImage, uniforms });

    Create a new instance of Material. Both options are optional.

    • mainImage options.mainImage

      A Shadertoy-style GLSL fragment body. When omitted, the material draws the text unchanged.

    • uniforms options.uniforms

      A UniformMap of the uniforms your mainImage reads, each with a type and an initial value.

  • mainImage material.mainImage

    For every Blotter Material, there is an underlying GLSL fragment shader that describes how the individual Material's effect will be rendered. The material.mainImage property returns the shader's GLSL in string form. Assigning a falsy value restores the default passthrough shader. Setting this property after initialization requires that the shader be rebuilt by calling material.update().

    The shader must define mainImage and sample the text with textTexture(uv) rather than texture2D:

    void mainImage( out vec4 mainImage, in vec2 fragCoord ) {
        // fragCoord is in pixels; uResolution is the size of this text.
        vec2 uv = fragCoord / uResolution;
        mainImage = textTexture(uv);
    }
  • uniforms material.uniforms

    The fundamental interface for all effects in Blotter. Reading this property gives you a map of UniformInterface objects, each having a type and a live value. To manipulate any effect, simply set the value of the uniform you wish to change; the running shader picks it up on the next frame.

    Assigning a whole UniformMap replaces every uniform. Because new uniform names need new shader declarations, call material.update() afterwards.

    material.uniforms = {
      uAmount: { type: "1f", value: 0.05 },
    };
    material.update();

    Listed below are the four uniform types available in Blotter.

    • "1f"

      For "1f" type uniforms, values should be set using floating point values: material.uniforms.myUniform1.value = 0.5;

    • "2f"

      For "2f" type uniforms, values should be set using an array of two floating point values: material.uniforms.myUniform2.value = [0.5, 0.5];

    • "3f"

      For "3f" type uniforms, values should be set using an array of three floating point values: material.uniforms.myUniform3.value = [0.5, 0.5, 0.5];

    • "4f"

      For "4f" type uniforms, values should be set using an array of four floating point values: material.uniforms.myUniform4.value = [0.5, 0.5, 0.5, 0.5];

    As stated, every Blotter Material will provide its own uniforms unique to the characteristics of its own effect. However, there are a handful of uniforms on which all Blotter Materials rely. These are always present, and always overwrite a same-named uniform of your own.

    • uResolution

      The resolution of an individual text within the mapping material being rendered by your material. Type "2f". You should never set the value of this uniform yourself.

    • uGlobalTime

      The global time in seconds. Type "1f". You should never set the value of this uniform yourself.

    • uTimeDelta

      The render time in seconds. Type "1f". You should never set the value of this uniform yourself.

    • uBlendColor

      The base color against which all blending should occur within an effect. Type "4f". The default value for this uniform is white, or [1.0, 1.0, 1.0, 1.0], where each index in the array represents an R, G, B, and A value respectively in a 0.0 to 1.0 range.

      This uniform is important for effects that sample the area around your texts for blending purposes, such as for the RGB splitting that occurs in the ChannelSplitMaterial, and you should set it to match the RGBA color that will be the background for any of your texts.

    • uPixelRatio

      The pixel ratio of the user's device. Type "1f". You should never set the value of this uniform yourself.

  • update material.update()

    Tells every Blotter instance using this material that it changed structurally, for example after assigning a new mainImage or a new uniforms map, so they rebuild their shader. Plain value writes on a uniform do not need this.

  • events "update" | "update:uniform"

    update fires after material.update(); update:uniform fires with the uniform's name whenever one of its values changes.

ShaderMaterial

A convenience subclass of Material for a one-off fragment shader, so the shader source comes first and nothing has to be subclassed.

  • construction

    const material = new ShaderMaterial(mainImage, { uniforms });

    Equivalent to new Material({ mainImage, uniforms }). Everything documented for Material applies.

Text

For each string of text you wish to render with a given Material, you should create an instance of Text.

  • construction

    const text = new Text(value, properties);

    Create a new instance of Text where value is a string of text1 and properties is an optional styles object for applying styles to your texts prior to them being rendered with your Material. The styles object takes any of the following optional parameters.

    • family properties.family

      A string representing the font family to be applied to the text. The default value is "sans-serif".

    • size properties.size

      A number representing the size of the rendered text in pixels. The default value is 12.

    • leading properties.leading

      The leading or line-height of the rendered text: a unitless multiplier, or a string in "px" or "%". The default value is 1.5. You should try to keep this number above 1.0, as Blotter bases text positions within the text atlas on computed text heights, and leading values that are lower than the text size they correspond with can cause texts to overflow into adjacent canvases.

    • fill properties.fill

      A string representing the color of the rendered text. The default value is "#000".

    • style properties.style

      A string representing the style of the rendered text, such as "normal" or "italic". The default value is "normal".

    • weight properties.weight

      A number or string representing the weight of the rendered text. The default value is 400.

    • padding properties.padding

      A number representing the padding around the rendered text. The default value is 0.

    • paddingTop properties.paddingTop

      A number representing the padding above the rendered text. The default value is 0.

    • paddingRight properties.paddingRight

      A number representing the padding to the right of the rendered text. The default value is 0.

    • paddingBottom properties.paddingBottom

      A number representing the padding below the rendered text. The default value is 0.

    • paddingLeft properties.paddingLeft

      A number representing the padding to the left of the rendered text. The default value is 0.

  • id text.id

    A unique, read-only identifier for the instance.

  • value text.value

    The string of text for the instance. Assigning a new value after initialization rebuilds every Blotter instance rendering this text automatically.

  • properties text.properties

    The styles object representing the style properties being applied to your string of text. Reading it returns the full, defaulted set. Assigning a partial object replaces the properties (merged over the defaults, not over the previous values) and rebuilds automatically.

    text.value = "Goodbye";        // rebuilds automatically
    text.properties = { size: 90 }; // ditto (merged over the defaults)
  • update text.update()

    Notifies observers that this text changed. value and properties writes call it automatically; call it directly after bulk edits.

  • events "update"

    Fires whenever the text's value or properties change.

RenderScope

Blotter RenderScopes are the primary interface for interacting with your texts on an individual basis. When you render one or more texts inside an instance of Blotter, the instance will create the scopes for you automatically, and you can use them to perform a number of critical actions, such as placing your rendered texts within DOM elements and setting uniform values specific to individual texts while rendering texts collectively in a single material.

  • obtainment

    const scope = blotter.forText(text); // RenderScope | undefined

    You will never create RenderScope objects directly. Instead, you should obtain them from your instance of blotter by calling blotter.forText(text) where text is an instance of Text known to your blotter object.

  • text scope.text

    The instance of Text for the scope.

  • domElement scope.domElement

    The DOM element for the given scope. This will return a HiDPI canvas with a 2d context containing an up-to-date rendering of your text. You should place this element where you want the text for the scope to appear on the page. You may access the element directly through this property, or append it to another DOM element using the convenience method, scope.appendTo(domElement).

  • material scope.material

    The primary interface for interacting with the Material being rendered by your Blotter instance in a manner specific to an individual text. This is critically important for updating uniforms in a non-global way. For example, if you are rendering several texts using a single Material, and want to have one of those texts update its visual appearance on a mouseover event without changing the other texts, you would want to access the uniforms for the Material through the scope, and not on the Material's instance directly.

    material.uniforms.uOffset.value = 0.1; // every text
    
    const scope = blotter.forText(text);
    if (scope) {
      scope.material.uniforms.uOffset.value = 0.2; // this text only
    }

    Per-text uniforms only exist after the first build, so read them after await blotter.ready.

  • playing scope.playing

    A boolean value indicating whether or not the scope will actively be rendered by the Blotter instance. On initialization, this value is set to the value of the Blotter instance's autoplay property.

  • timeDelta scope.timeDelta

    The scope's render time in seconds.

  • frameCount scope.frameCount

    The number of times the scope has been rendered.

  • play scope.play()

    Tells the scope to begin or resume being rendered by the instance of Blotter.

  • pause scope.pause()

    Tells the scope to pause being rendered by the instance of Blotter.

  • appendTo scope.appendTo(element)

    A convenience method for appending the scope's domElement to the page. Returns the scope, so it can be chained. Pointer events are only wired up once the canvas has been appended this way.

  • events "ready" | "update" | "render" | "update:uniform" | "mousedown" | "mouseup" | "mousemove" | "mouseenter" | "mouseleave"

    ready and update mirror the Blotter events for this text; render receives the frame count. The pointer events receive the position normalised to the canvas, as { x, y } from 0 to 1, with y growing downwards as in the DOM.

    const scope = blotter.forText(text);
    if (scope) {
      scope.on("mouseenter", ({ x, y }) => {
        scope.material.uniforms.uDodgePosition.value = [x, 1 - y];
      });
    }

Uniforms

Uniforms are declared as plain descriptors and exposed as live interfaces. The descriptor types are exported for TypeScript users.

type UniformType = "1f" | "2f" | "3f" | "4f";
type Vec2 = [number, number];
type Vec3 = [number, number, number];
type Vec4 = [number, number, number, number];

type UniformDescriptor =
  | { type: "1f"; value: number }
  | { type: "2f"; value: Vec2 }
  | { type: "3f"; value: Vec3 }
  | { type: "4f"; value: Vec4 };

type UniformMap = Record<string, UniformDescriptor>;
  • UniformInterface material.uniforms.uName

    The live handle for one uniform, found on material.uniforms and scope.material.uniforms.

    • type uniform.type

      The UniformType. Read-only.

    • value uniform.value

      The current value. Writes are validated against the type (a wrong shape is logged and ignored) and emit update, which is how the change reaches the running shader.

    • toDescriptor uniform.toDescriptor()

      Returns a plain { type, value } UniformDescriptor.

shaders

GLSL helper snippets are exported as strings from the shaders namespace, ready to be interpolated into a mainImage: blending, blinnPhongSpecular, easing, gamma, inf, lineMath, map, noise, noise2d, noise3d, noise4d, pi, and random. The blending helpers (normalBlend and friends) are always available to every material.

import { Material, shaders } from "blotter.ts";

const material = new Material({
  mainImage: /* glsl */ `
    ${shaders.noise3d}

    void mainImage( out vec4 mainImage, in vec2 fragCoord ) {
      vec2 uv = fragCoord / uResolution;
      uv.y += snoise(vec3(uv * 4.0, uGlobalTime)) * uAmount;
      mainImage = textTexture(uv);
    }
  `,
  uniforms: {
    uAmount: { type: "1f", value: 0.05 },
  },
});

Utilities

  • isWebGLSupported isWebGLSupported()

    Returns true when the browser can create a WebGL context. The Blotter constructor throws when it cannot, so check this first when you want to fall back to plain text.

  • pixelRatio pixelRatio()

    The device pixel ratio, or 1 outside a browser. This is what Blotter uses when no ratio option is given.

  • filterTexts filterTexts(texts)

    Coerces a Text, an array of texts, or an array-like into Text[], warning on and dropping anything that is not a Text.

    import { filterTexts, isWebGLSupported, pixelRatio } from "blotter.ts";
    
    if (isWebGLSupported()) {
      const blotter = new Blotter(material, { ratio: pixelRatio() });
    }

Custom materials

A material is a Shadertoy-style mainImage fragment function. Use textTexture(uv) instead of texture2D to sample the text, read your own uniforms by name, and interpolate any helpers you need from shaders. Every material also receives uResolution, uGlobalTime, uTimeDelta, uBlendColor, and uPixelRatio.

import { Material, shaders } from "blotter.ts";

const material = new Material({
  mainImage: /* glsl */ `
    ${shaders.noise3d}

    void mainImage( out vec4 mainImage, in vec2 fragCoord ) {
      vec2 uv = fragCoord / uResolution;
      uv.y += snoise(vec3(uv * 4.0, uGlobalTime)) * uAmount;
      mainImage = textTexture(uv);
    }
  `,
  uniforms: {
    uAmount: { type: "1f", value: 0.05 },
  },
});

Or subclass Material, which is how the bundled effects in blotter.ts/materials are written:

import { Material, shaders } from "blotter.ts";

const mainImage = /* glsl */ `
  ${shaders.noise3d}

  void mainImage( out vec4 mainImage, in vec2 fragCoord ) {
    vec2 uv = fragCoord / uResolution;
    uv.y += snoise(vec3(uv * 4.0, uGlobalTime)) * uAmount;
    mainImage = textTexture(uv);
  }
`;

export class WobbleMaterial extends Material {
  constructor() {
    super({ mainImage, uniforms: { uAmount: { type: "1f", value: 0.05 } } });
  }
}
source code