Skip to content

packages/core/src/lib/transitions/bifrost.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { bifrostKernel } from './wasm/bifrost-kernel.js';
2 import { opacity } from '../../filters/native.js';
3 import { defineTransitionEffect, type PibblTransitionEffect } from './fade.js';
4 import { captureTransition, clamp, finite, regionSnapshot, type TransitionRegion } from './capture.js';
5 
6 /**
7  * Color and motion controls for a bifrost visibility transition.
8  *
9  * @see {@link bifrost}
10  */
11 export interface PibblBifrostOptions {
12   /** Logical region over which the effect is evaluated. See {@link TransitionRegion}. */
13   readonly region: TransitionRegion;
14   /** Entry location or direction used by the transition. See {@link PibblBifrostOptions}. */
15   readonly entry?: Readonly<{
16     /**
17      * Horizontal coordinate or displacement in the containing coordinate system. See
18      * {@link PibblBifrostOptions}.
19      */
20     x: number;
21     /**
22      * Vertical coordinate or displacement in the containing coordinate system. See
23      * {@link PibblBifrostOptions}.
24      */
25     y: number;
26   }>;
27   /** Brightness control for the transition effect. See {@link PibblBifrostOptions}. */
28   readonly brightness?: number;
29   /** Hue control for the transition's emitted color. See {@link PibblBifrostOptions}. */
30   readonly hue?: number;
31   /** Amount of stretching applied by the transition. See {@link PibblBifrostOptions}. */
32   readonly stretch?: number;
33   /** Intensity of the electrical halo around the core. See {@link PibblBifrostOptions}. */
34   readonly glow?: number;
35   /**
36    * Seed used for deterministic sampling or simulation initialization. See
37    * {@link PibblBifrostOptions}.
38    */
39   readonly seed?: number;
40 }
41 /**
42  * A prismatic beam stretches a white-hot captured subject toward its entry point.
43  *
44  * @param options - Bifrost appearance and animation settings. See {@link PibblBifrostOptions}.
45  * @returns A bifrost transition effect. See {@link PibblTransitionEffect}.
46  *
47  * @see {@link PibblBifrostOptions}
48  * @see {@link PibblTransitionEffect}
49  */
50 export function bifrost(options: PibblBifrostOptions): PibblTransitionEffect {
51   const r = regionSnapshot(options.region), ax = r.x + r.width / 2, ay = r.y + r.height;
52   const ex = finite(options.entry?.x ?? ax, 'entry.x'), ey = finite(options.entry?.y ?? r.y - r.height * 2, 'entry.y');
53   const brightness = finite(options.brightness ?? 1, 'brightness', 0), hue = finite(options.hue ?? 0, 'hue');
54   const stretchAmount = finite(options.stretch ?? 7, 'stretch', 0), aura = finite(options.glow ?? 1, 'glow', 0);
55   const samplePatches = bifrostKernel(finite(options.seed ?? 0, 'seed'));
56   const distance = Math.hypot(ex - ax, ey - ay), ux = distance ? (ex - ax) / distance : 0, uy = distance ? (ey - ay) / distance : -1;
57   return defineTransitionEffect((q, phase, frame) => {
58     if (q === 1) return [];
59     if (q === 0) return [opacity({ amount: 0 })];
60     return [captureTransition(({ context: c, windows, drawSource, drawTint }) => {
61       const power = Math.pow(Math.sin(Math.PI * q), .65) * brightness * Math.pow(clamp((1 - q) / .32), 2);
62       const color = (h: number, a: number, light = 80) => `hsla(${h + hue},100%,${light}%,${clamp(a)})`;
63       const glow = (x: number, y: number, rx: number, ry: number, a: number) => {
64         c.save(); c.translate(x, y); c.scale(rx, ry);
65         const gradient = c.createRadialGradient(0, 0, 0, 0, 0, 1); gradient.addColorStop(0, `rgba(255,255,255,${clamp(a)})`); gradient.addColorStop(.25, color(230, a * .5)); gradient.addColorStop(1, color(230, 0));
66         c.fillStyle = gradient; c.fillRect(-1, -1, 2, 2); c.restore();
67       };
68       // Seeded, feathered clouds stretched along the transit axis. Unequal
69       // lengths, widths, hues and velocities avoid a fixed rainbow comb.
70       c.save(); c.globalCompositeOperation = 'lighter';
71       const dx = ax - ex, dy = ay - ey;
72       const angle = Math.atan2(dy, dx), seconds = frame.elapsed / 1000;
73       const flow = phase === 'enter' ? 1 : -1;
74       // End the beam at the impact plane, as the original ribbons did.
75       c.translate(ex, ey); c.rotate(angle);
76       c.beginPath(); c.rect(-Math.max(1, distance), -r.width * 2, Math.max(1, distance) * 2, r.width * 4); c.clip();
77       c.rotate(-angle); c.translate(-ex, -ey);
78       const patches = samplePatches(seconds, flow, power, r.width, distance, ex, ey, dx, dy);
79       for (let i = 0; i < 56; i++) {
80         const offset = i * 6;
81         const length = patches[offset + 2]!, width = patches[offset + 3]!, h = patches[offset + 4]!, alpha = patches[offset + 5]!;
82         c.save(); c.translate(patches[offset]!, patches[offset + 1]!);
83         c.rotate(angle); c.scale(length, width);
84         const patch = c.createRadialGradient(0, 0, 0, 0, 0, 1);
85         patch.addColorStop(0, color(h, alpha, 78));
86         patch.addColorStop(.25, color(h + 12, alpha * .8, 72));
87         patch.addColorStop(.6, color(h - 8, alpha * .3, 68));
88         patch.addColorStop(1, color(h, 0, 68));
89         c.fillStyle = patch; c.fillRect(-1, -1, 2, 2); c.restore();
90       }
91       c.restore();
92       const softness = 1 - clamp(q / .56), stretch = stretchAmount * softness * softness;
93       const travel = r.height * .7 * Math.pow(1 - clamp(q / .5), 2);
94       c.save(); c.translate(ax + ux * travel, ay + uy * travel);
95       c.transform(1 + stretch * ux * ux, stretch * ux * uy, stretch * ux * uy, 1 + stretch * uy * uy, 0, 0); c.translate(-ax, -ay);
96       c.globalAlpha = clamp(q / .075);
97       if (softness > 0) {
98         c.save(); c.globalAlpha *= clamp(brightness * .7); c.filter = `blur(${(22 + softness * 20) * aura * windows.density}px)`; drawTint('#e1e9ff'); c.restore();
99         c.filter = `blur(${(4 + softness * 10) * windows.density}px)`; drawTint('#fff');
100       } else {
101         drawSource(); c.globalAlpha *= clamp(clamp((1 - q) / .28) * brightness * 1.5); drawTint('#fff');
102       }
103       c.restore(); c.globalCompositeOperation = 'lighter';
104       const impact = Math.exp(-Math.pow((q - .56) / .085, 2));
105       glow(ax, r.y + r.height / 2, r.width, r.height * .8, impact * brightness * aura);
106       glow(ax, ay, r.width * .9, 24, power * aura);
107     })];
108   });
109 }
110 

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.