Showing preview only (288K chars total). Download the full file or copy to clipboard to get everything.
Repository: g-harel/blobs
Branch: master
Commit: 9f4506913d4b
Files: 41
Total size: 274.3 KB
Directory structure:
gitextract_u04v749x/
├── .github/
│ └── workflows/
│ └── push.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── CNAME
├── LICENSE
├── README.legacy.md
├── README.md
├── demo/
│ ├── content.ts
│ ├── example.ts
│ ├── index.html
│ └── internal/
│ ├── canvas.ts
│ ├── debug.ts
│ └── layout.ts
├── examples/
│ └── corner-expand.html
├── index.html
├── internal/
│ ├── animate/
│ │ ├── frames.ts
│ │ ├── interpolate.ts
│ │ ├── prepare.ts
│ │ ├── state.ts
│ │ ├── testing/
│ │ │ ├── index.html
│ │ │ └── script.ts
│ │ └── timing.ts
│ ├── check.ts
│ ├── gen.ts
│ ├── rand.ts
│ ├── render/
│ │ ├── canvas.ts
│ │ ├── svg.test.ts
│ │ └── svg.ts
│ ├── types.ts
│ └── util.ts
├── package.json
├── public/
│ ├── __snapshots__/
│ │ └── legacy.test.ts.snap
│ ├── animate.test.ts
│ ├── animate.ts
│ ├── blobs.test.ts
│ ├── blobs.ts
│ ├── legacy.test.ts
│ └── legacy.ts
├── rollup.config.mjs
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/push.yml
================================================
on: push
name: on-push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '18.x'
- run: yarn
- run: yarn run build
- run: yarn run test
================================================
FILE: .gitignore
================================================
coverage/
node_modules/
~*
*.js
*.js.map
*.d.ts
.cache
dist
!rollup.config.js
docs/*.js
docs/*.svg
docs/*.css
================================================
FILE: .npmignore
================================================
*
!README.md
!LICENSE
!package.json
!**/*.js
!**/*.js.map
!**/index.d.ts
================================================
FILE: CHANGELOG.md
================================================
# 2.3.0
- Add `CanvasCustomKeyframe` to `v2/animate`
- Add `wigglePreset` to `v2/animate`
# 2.2.1
- Add support for custom point-based keyframes
- Add option to set custom timestamp provider
- Add module support, thank you to #4 and #7
- Export `Animation` and `TimestampProvider` types from `v2/animate`
# 2.2.0
- Remove added points from end keyframe after interpolation completes.
- Add play/pause/playPause API for animations.
# 2.1.0
- Improved type checks on user-provided data
- Added `"blobs/v2/animate"`
- Animate between arbitrary blob keyframes
- Separate import to keep main bundle small
- New demo website with animated blob transitions
- Supports only canvas rendering
# 2.0.1
- Fix typo in code example of README
# 2.0.0
- **BREAKING** Editable SVG element creation function has moved to
`blobs.xml(tagName)`.
- Added `"blobs/v2"`
- 30% smaller compressed size
- Supports canvas rendering
- Supports raw SVG path rendering
# 1.1.0
- Add support for editable output
# 1.0.5
- Fix assets in README on npmjs.com
# 1.0.4
- Use snapshot tests to verify consistency
- Ignore unnecessary files in npm tarball
- Output sourcemap file
- Add project logo
- README content updates
# 1.0.3
- Add link to demo page in the README
# 1.0.2
- Make transpiled output minified
- Minor changes to the README
# 1.0.1
- Remove accidental dependency
- Minor changes to the README
# 1.0.0
- Initial release
================================================
FILE: CNAME
================================================
blobs.dev
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) Gabriel Harel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.legacy.md
================================================
The legacy API exists to preserve compatibility for users importing the package
using a `script` tag. Because [unpkg.com](https://unpkg.com) serves the latest
version of the package if no version is specified, I can't break backwards
compatibility, even with a major release. This API also preserves a few features
that could potentially still be useful to some users (guide rendering and
editable svg).
---
## Install
```ts
// $ npm install blobs
const blobs = require("blobs");
```
```html
<script src="https://unpkg.com/blobs"></script>
```
## Usage
```typescript
const svg = blobs(options);
```



_Options are **not**
[sanitized](https://en.wikipedia.org/wiki/HTML_sanitization). Never trust raw
user-submitted values in the options._
## Options
#### Required
| Name | Type | Description |
| ------------ | -------- | -------------------------------------------- |
| `size` | `number` | Bounding box dimensions (in pixels) |
| `complexity` | `number` | Blob complexity (number of points) |
| `contrast` | `number` | Blob contrast (randomness of point position) |
#### Optional
| Name | Type | Default | Description |
| -------------- | ---------- | ---------- | ------------------------------------- |
| `color` | `string?` | `"none"` | Fill color |
| `stroke` | `object?` | `...` | Stroke options |
| `stroke.color` | `string` | `"none"` | Stroke color |
| `stroke.width` | `number` | `0` | Stroke width (in pixels) |
| `seed` | `string?` | _`random`_ | Value to seed random number generator |
| `guides` | `boolean?` | `false` | Render points, handles and stroke |
_Either `stroke` or `color` must be defined._
_Guides will use stroke color and width if defined. Otherwise, they default to
`black` stroke with width of `1`._
##### Example Options Object
```typescript
const options = {
size: 600,
complexity: 0.2,
contrast: 0.4,
color: "#ec576b",
stroke: {
width: 0,
color: "black",
},
guides: false,
seed: "1234",
};
```
## Advanced
If you need to edit the output svg for your use case, blobs also allows for
_editable_ output.
```typescript
const editableSvg = blobs.editable(options);
```
The output of this function is a data structure that represents a nested svg
document. This structure can be changed and rendered to a string using its
`render` function.
```typescript
editableSvg.attributes.width = 1000;
const svg = editableSvg.render();
```
New elements can be added anywhere in the hierarchy.
```typescript
const xmlChild = blobs.xml("path");
xmlChild.attributes.stroke = "red";
// ...
editableSvg.children.push(xmlChild);
```
================================================
FILE: README.md
================================================
<p align="center">
<a href="https://github.com/g-harel/blobs/blob/master/README.legacy.md"><b>Legacy documentation</b></a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/blobs"><!--
--><img src="https://img.shields.io/npm/v/blobs.svg"><!--
--></a>
<a href="https://github.com/g-harel/blobs/actions?query=workflow%3Aon-push"><!--
--><img src="https://img.shields.io/github/actions/workflow/status/g-harel/blobs/push.yml?event=on-push"><!--
--></a>
</p>
<p align="center">
<a href="https://blobs.dev">
<img src="./assets/logo.svg?sanitize=true">
</a>
</p>
## Install
```bash
$ npm install blobs
```
```ts
import * as blobs2 from "blobs/v2";
```
```ts
import * as blobs2Animate from "blobs/v2/animate";
```
<p align="center">
OR
</p>
```html
<script src="https://unpkg.com/blobs/v2"></script>
```
```html
<script src="https://unpkg.com/blobs/v2/animate"></script>
```
## SVG Path
```js
const svgPath = blobs2.svgPath({
seed: Math.random(),
extraPoints: 8,
randomness: 4,
size: 256,
});
doSomething(svgPath);
```
## SVG
```js
const svgString = blobs2.svg(
{
seed: Math.random(),
extraPoints: 8,
randomness: 4,
size: 256,
},
{
fill: "white", // 🚨 NOT SANITIZED
stroke: "black", // 🚨 NOT SANITIZED
strokeWidth: 4,
},
);
container.innerHTML = svgString;
```
## Canvas
```js
const path = blobs2.canvasPath(
{
seed: Math.random(),
extraPoints: 16,
randomness: 2,
size: 128,
},
{
offsetX: 16,
offsetY: 32,
},
);
ctx.stroke(path);
```
## Canvas Animation
```js
const ctx = /* ... */;
const animation = blobs2Animate.canvasPath();
// Set up rendering loop.
const renderAnimation = () => {
ctx.clearRect(0, 0, width, height);
ctx.fill(animation.renderFrame());
requestAnimationFrame(renderAnimation);
};
requestAnimationFrame(renderAnimation);
// Transition to new blob on canvas click.
ctx.canvas.onclick = () => {
animation.transition({
duration: 4000,
timingFunction: "ease",
callback: loopAnimation,
blobOptions: {...},
});
};
```
## Canvas Wiggle
```js
const ctx = /* ... */;
const animation = blobs2Animate.canvasPath();
// Set up rendering loop.
const renderAnimation = () => {
ctx.clearRect(0, 0, width, height);
ctx.fill(animation.renderFrame());
requestAnimationFrame(renderAnimation);
};
requestAnimationFrame(renderAnimation);
// Begin wiggle animation.
blobs2Animate.wigglePreset(
animation
/* blobOptions= */ {...},
/* canvasOptions= */ {},
/* wiggleOptions= */ {speed: 2},
)
```
## Complete API
### `"blobs/v2"`
```ts
export interface BlobOptions {
// A given seed will always produce the same blob.
// Use `Math.random()` for pseudorandom behavior.
seed: string | number;
// Actual number of points will be `3 + extraPoints`.
extraPoints: number;
// Increases the amount of variation in point position.
randomness: number;
// Size of the bounding box.
size: number;
}
export interface CanvasOptions {
// Coordinates of top-left corner of the blob.
offsetX?: number;
offsetY?: number;
}
export const canvasPath: (blobOptions: BlobOptions, canvasOptions?: CanvasOptions) => Path2D;
export interface SvgOptions {
fill?: string; // Default: "#ec576b".
stroke?: string; // Default: "none".
strokeWidth?: number; // Default: 0.
}
export const svg: (blobOptions: BlobOptions, svgOptions?: SvgOptions) => string;
export const svgPath: (blobOptions: BlobOptions) => string;
```
### `"blobs/v2/animate"`
```ts
interface Keyframe {
// Duration of the keyframe animation in milliseconds.
duration: number;
// Delay before animation begins in milliseconds.
// Default: 0.
delay?: number;
// Controls the speed of the animation over time.
// Default: "linear".
timingFunction?:
| "linear"
| "easeEnd"
| "easeStart"
| "ease"
| "elasticEnd0"
| "elasticEnd1"
| "elasticEnd2"
| "elasticEnd3";
// Called after keyframe end-state is reached or passed.
// Called exactly once when the keyframe end-state is rendered.
// Not called if the keyframe is preempted by a new transition.
callback?: () => void;
// Standard options, refer to "blobs/v2" documentation.
canvasOptions?: {
offsetX?: number;
offsetY?: number;
};
}
export interface CanvasKeyframe extends Keyframe {
// Standard options, refer to "blobs/v2" documentation.
blobOptions: {
seed: number | string;
randomness: number;
extraPoints: number;
size: number;
};
}
export interface CanvasCustomKeyframe extends Keyframe {
// List of point coordinates that produce a single, closed shape.
points: Point[];
}
export interface Animation {
// Renders the current state of the animation.
renderFrame: () => Path2D;
// Renders the current state of the animation as points.
renderPoints: () => Point[];
// Immediately begin animating through the given keyframes.
// Non-rendered keyframes from previous transitions are cancelled.
transition: (...keyframes: (CanvasKeyframe | CanvasCustomKeyframe)[]) => void;
// Resume a paused animation. Has no effect if already playing.
play: () => void;
// Pause a playing animation. Has no effect if already paused.
pause: () => void;
// Toggle between playing and pausing the animation.
playPause: () => void;
}
// Function that returns the current timestamp. This value will be used for all
// duration/delay values and will be used to interpolate between keyframes. It
// must produce values increasing in size.
// Default: `Date.now`.
export interface TimestampProvider {
(): number;
}
export const canvasPath: (timestampProvider?: TimestampProvider) => Animation;
export interface WiggleOptions {
// Speed of the wiggle movement. Higher is faster.
speed: number;
// Length of the transition from the current state to the wiggle blob.
// Default: 0
initialTransition?: number;
}
// Preset animation that produces natural-looking random movement.
// The wiggle animation will continue indefinitely until the next transition.
export const wigglePreset = (
animation: Animation,
blobOptions: BlobOptions,
canvasOptions: CanvasOptions,
wiggleOptions: WiggleOptions,
)
```
## License
[MIT](./LICENSE)
================================================
FILE: demo/content.ts
================================================
import {addCanvas, addTitle, colors, sizes} from "./internal/layout";
import {
calcBouncePercentage,
drawClosed,
drawHandles,
drawLine,
drawOpen,
drawPoint,
forceStyles,
point,
tempStyles,
} from "./internal/canvas";
import {
coordPoint,
deg,
distance,
expandHandle,
forPoints,
mapPoints,
mod,
shift,
split,
splitLine,
} from "../internal/util";
import {timingFunctions} from "../internal/animate/timing";
import {Coord, Point} from "../internal/types";
import {rand} from "../internal/rand";
import {genFromOptions, smoothBlob} from "../internal/gen";
import {BlobOptions} from "../public/blobs";
import {interpolateBetween, interpolateBetweenSmooth} from "../internal/animate/interpolate";
import {divide} from "../internal/animate/prepare";
import {statefulAnimationGenerator} from "../internal/animate/state";
import {CanvasKeyframe, canvasPath, wigglePreset} from "../public/animate";
const makePoly = (pointCount: number, radius: number, center: Coord): Point[] => {
const angle = (2 * Math.PI) / pointCount;
const points: Point[] = [];
const nullHandle = {angle: 0, length: 0};
for (let i = 0; i < pointCount; i++) {
const coord = expandHandle(center, {angle: i * angle, length: radius});
points.push({...coord, handleIn: nullHandle, handleOut: nullHandle});
}
return points;
};
const centeredBlob = (options: BlobOptions, center: Coord): Point[] => {
return mapPoints(genFromOptions(options), ({curr}) => {
curr.x += center.x - options.size / 2;
curr.y += center.y - options.size / 2;
return curr;
});
};
const calcFullDetails = (percentage: number, a: Point, b: Point) => {
const a0: Coord = a;
const a1 = expandHandle(a, a.handleOut);
const a2 = expandHandle(b, b.handleIn);
const a3: Coord = b;
const b0 = splitLine(percentage, a0, a1);
const b1 = splitLine(percentage, a1, a2);
const b2 = splitLine(percentage, a2, a3);
const c0 = splitLine(percentage, b0, b1);
const c1 = splitLine(percentage, b1, b2);
const d0 = splitLine(percentage, c0, c1);
return {a0, a1, a2, a3, b0, b1, b2, c0, c1, d0};
};
addTitle(4, "Vector graphics");
addCanvas(
1.3,
// Pixelated circle.
(ctx, width, height) => {
const center: Coord = {x: width * 0.5, y: height * 0.5};
const gridSize = width * 0.01;
const gridCountX = width / gridSize;
const gridCountY = height / gridSize;
// https://www.desmos.com/calculator/psohl602g5
const radius = width * 0.3;
const falloff = width * 0.0015;
const thickness = width * 0.01;
for (let x = 0; x < gridCountX; x++) {
for (let y = 0; y < gridCountY; y++) {
const curr = {
x: x * gridSize + gridSize / 2,
y: y * gridSize + gridSize / 2,
};
const d = distance(curr, center);
const opacity = Math.max(
0,
Math.min(1, Math.abs(thickness / (d - radius)) - falloff),
);
tempStyles(
ctx,
() => {
ctx.globalAlpha = opacity;
ctx.fillStyle = colors.highlight;
},
() => ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize),
);
}
}
return `Raster image formats encode images as a finite number of pixel values. They
therefore have a maximum scale which depends on the display.`;
},
// Smooth circle.
(ctx, width, height) => {
const pt = width * 0.01;
const shapeSize = width * 0.6;
const cx = width * 0.5;
const cy = height * 0.5;
tempStyles(
ctx,
() => {
ctx.lineWidth = pt;
ctx.strokeStyle = colors.highlight;
},
() => {
ctx.beginPath();
ctx.arc(cx, cy, shapeSize / 2, 0, 2 * Math.PI);
ctx.stroke();
},
);
return `By contrast vector formats are defined by formulas and can scale infinitely. They
are well suited for artwork with sharp lines and are used for font glyphs.`;
},
);
addCanvas(
1.3,
(ctx, width, height, animate) => {
const startPeriod = (1 + Math.E) * 1000;
const endPeriod = (1 + Math.PI) * 1000;
animate((frameTime) => {
const startPercentage = calcBouncePercentage(
startPeriod,
timingFunctions.ease,
frameTime,
);
const startLengthPercentage = calcBouncePercentage(
startPeriod * 0.8,
timingFunctions.ease,
frameTime,
);
const startAngle = split(startPercentage, -45, +45);
const startLength = width * 0.1 + width * 0.2 * startLengthPercentage;
const start = point(width * 0.2, height * 0.5, 0, 0, startAngle, startLength);
const endPercentage = calcBouncePercentage(endPeriod, timingFunctions.ease, frameTime);
const endLengthPercentage = calcBouncePercentage(
endPeriod * 0.8,
timingFunctions.ease,
frameTime,
);
const endAngle = split(endPercentage, 135, 225);
const endLength = width * 0.1 + width * 0.2 * endLengthPercentage;
const end = point(width * 0.8, height * 0.5, endAngle, endLength, 0, 0);
drawOpen(ctx, start, end, true);
});
return `Vector-based image formats often support Bezier curves. A cubic bezier curve is defined
by four coordinates: the start/end points and corresponding "handle" points. Visually, these
handles define the direction and "momentum" of the line. The curve is tangent to the handle
at either of the points.`;
},
(ctx, width, height, animate) => {
const angleRange = 20;
const lengthRange = 40;
const period = 5000;
const r = rand("blobs");
const ra = r();
const rb = r();
const rc = r();
const rd = r();
const wobbleHandle = (
frameTime: number,
period: number,
p: Point,
locked: boolean,
): Point => {
const angleIn =
deg(p.handleIn.angle) +
angleRange *
(0.5 - calcBouncePercentage(period * 1.1, timingFunctions.ease, frameTime));
const lengthIn =
p.handleIn.length +
lengthRange *
(0.5 - calcBouncePercentage(period * 0.9, timingFunctions.ease, frameTime));
const angleOut =
deg(p.handleOut.angle) +
angleRange *
(0.5 - calcBouncePercentage(period * 0.9, timingFunctions.ease, frameTime));
const lengthOut =
p.handleOut.length +
lengthRange *
(0.5 - calcBouncePercentage(period * 1.1, timingFunctions.ease, frameTime));
return point(p.x, p.y, angleIn, lengthIn, locked ? angleIn + 180 : angleOut, lengthOut);
};
animate((frameTime) => {
const a = wobbleHandle(
frameTime,
period / 2 + (ra * period) / 2,
point(width * 0.5, height * 0.3, 210, 100, -30, 100),
false,
);
const b = wobbleHandle(
frameTime,
period / 2 + (rb * period) / 2,
point(width * 0.8, height * 0.5, -90, 100, 90, 100),
true,
);
const c = wobbleHandle(
frameTime,
period / 2 + (rc * period) / 2,
point(width * 0.5, height * 0.9, -30, 75, -150, 75),
false,
);
const d = wobbleHandle(
frameTime,
period / 2 + (rd * period) / 2,
point(width * 0.2, height * 0.5, 90, 100, -90, 100),
true,
);
drawClosed(ctx, [a, b, c, d], true);
});
return `Chaining curves together creates closed shapes. When the in/out handles of a point
form a line, the transition is smooth, and the curve is tangent to the line.`;
},
);
addCanvas(2, (ctx, width, height, animate) => {
const period = Math.PI * Math.E * 1000;
const start = point(width * 0.3, height * 0.8, 0, 0, -105, width * 0.32);
const end = point(width * 0.7, height * 0.8, -75, width * 0.25, 0, 0);
animate((frameTime) => {
const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);
const d = calcFullDetails(percentage, start, end);
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
},
() => {
drawLine(ctx, d.a0, d.a1, 1);
drawLine(ctx, d.a1, d.a2, 1);
drawLine(ctx, d.a2, d.a3, 1);
drawLine(ctx, d.b0, d.b1, 1);
drawLine(ctx, d.b1, d.b2, 1);
drawLine(ctx, d.c0, d.c1, 1);
drawPoint(ctx, d.a0, 1.3, "a0");
drawPoint(ctx, d.a1, 1.3, "a1");
drawPoint(ctx, d.a2, 1.3, "a2");
drawPoint(ctx, d.a3, 1.3, "a3");
drawPoint(ctx, d.b0, 1.3, "b0");
drawPoint(ctx, d.b1, 1.3, "b1");
drawPoint(ctx, d.b2, 1.3, "b2");
drawPoint(ctx, d.c0, 1.3, "c0");
drawPoint(ctx, d.c1, 1.3, "c1");
drawPoint(ctx, d.d0, 1.3, "d0");
},
);
tempStyles(
ctx,
() => (ctx.fillStyle = colors.highlight),
() => drawPoint(ctx, d.d0, 3),
);
drawOpen(ctx, start, end, false);
});
return `Curves are rendered using the four input points (ends + handles). By connecting
points a0-a3 with a line and then splitting each line by the same percentage, we've reduced
the number of points by one. Repeating the same process with the new set of points until
there is only one point remaining (d0) produces a single point on the line. Repeating this
calculation for many different percentage values will produce a curve.
<br><br>
<i>Note there is no constant relationship between the
percentage that "drew" the point and the arc lengths before/after it. Uniform motion along
the curve can only be approximated.`;
});
addTitle(4, "Making a blob");
addCanvas(
1.3,
(ctx, width, height, animate) => {
const center: Coord = {x: width * 0.5, y: height * 0.5};
const radius = width * 0.3;
const minPoints = 3;
const extraPoints = 6;
const pointDurationMs = 2000;
animate((frameTime) => {
const points =
minPoints + extraPoints + (extraPoints / 2) * Math.sin(frameTime / pointDurationMs);
const shape = makePoly(points, radius, center);
// Draw lines from center to each point..
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
},
() => {
drawPoint(ctx, center, 2);
forPoints(shape, ({curr}) => {
drawLine(ctx, center, curr, 1, 2);
});
},
);
drawClosed(ctx, shape, false);
});
return `Points are first distributed evenly around the center. At this stage the points
technically have handles, but since they have a length of zero, they have no effect on
the shape and it looks like a polygon.`;
},
(ctx, width, height, animate) => {
const period = Math.PI * 1500;
const center: Coord = {x: width * 0.5, y: height * 0.5};
const radius = width * 0.3;
const points = 5;
const randSeed = Math.random();
const randStrength = 0.5;
const shape = makePoly(points, radius, center);
animate((frameTime) => {
const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);
const rgen = rand(randSeed + Math.floor(frameTime / period) + "");
// Draw original shape.
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
},
() => {
drawPoint(ctx, center, 2);
forPoints(shape, ({curr, next}) => {
drawLine(ctx, curr, next(), 1, 2);
});
},
);
// Draw randomly shifted shape.
const shiftedShape = shape.map(
(p): Point => {
const randOffset = percentage * (randStrength * rgen() - randStrength / 2);
return coordPoint(splitLine(randOffset, p, center));
},
);
drawClosed(ctx, shiftedShape, true);
});
return `Points are then randomly moved further or closer to the center. Using a seeded
random number generator allows repeatable "randomness" whenever the blob is generated
at a different time or place.`;
},
);
addCanvas(
1.3,
(ctx, width, height, animate) => {
const options: BlobOptions = {
extraPoints: 2,
randomness: 6,
seed: "random",
size: width * 0.7,
};
const center: Coord = {x: width * 0.5, y: height * 0.5};
const interval = 2000;
const blob = centeredBlob(options, center);
const handles = mapPoints(blob, ({curr: p}) => {
p.handleIn.length = 150;
p.handleOut.length = 150;
return p;
});
const polyBlob = blob.map(coordPoint);
const pointCount = polyBlob.length;
animate((frameTime) => {
const activeIndex = Math.floor(frameTime / interval) % pointCount;
const opacity = Math.abs(Math.sin((frameTime * Math.PI) / interval));
tempStyles(
ctx,
() => {
ctx.strokeStyle = colors.secondary;
ctx.globalAlpha = opacity;
},
() => {
forPoints(polyBlob, ({prev, next, index}) => {
if (index !== activeIndex) return;
drawLine(ctx, prev(), next(), 1, 2);
});
forPoints(handles, ({curr, index}) => {
if (index !== activeIndex) return;
drawHandles(ctx, curr, 1);
});
},
);
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
},
() => {
drawPoint(ctx, center, 2);
},
);
drawClosed(ctx, polyBlob, false);
});
return `The angle of the handles for each point is parallel with the imaginary line
stretching between its neighbors. Even when they have length zero, the angle of the
handles can still be calculated.`;
},
(ctx, width, height, animate) => {
const period = Math.PI * 1500;
const options: BlobOptions = {
extraPoints: 2,
randomness: 6,
seed: "random",
size: width * 0.7,
};
const center: Coord = {x: width * 0.5, y: height * 0.5};
const blob = centeredBlob(options, center);
animate((frameTime) => {
const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);
// Draw original blob.
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
},
() => {
drawPoint(ctx, center, 2);
forPoints(blob, ({curr, next}) => {
drawLine(ctx, curr, next(), 1, 2);
});
},
);
// Draw animated blob.
const animatedBlob = mapPoints(blob, ({curr}) => {
curr.handleIn.length *= percentage;
curr.handleOut.length *= percentage;
return curr;
});
drawClosed(ctx, animatedBlob, true);
});
return `The blob is then made smooth by extending the handles. The exact length
depends on the distance between the given point and it's next neighbor. This value is
multiplied by a ratio that would roughly produce a circle if the points had not been
randomly moved.`;
},
);
addTitle(4, "Interpolating between blobs");
addCanvas(2, (ctx, width, height, animate) => {
const period = Math.PI * 1000;
const center: Coord = {x: width * 0.5, y: height * 0.5};
const fadeSpeed = 10;
const fadeLead = 0.05;
const fadeFloor = 0.2;
const blobA = centeredBlob(
{
extraPoints: 3,
randomness: 6,
seed: "12345",
size: height * 0.8,
},
center,
);
const blobB = centeredBlob(
{
extraPoints: 3,
randomness: 6,
seed: "abc",
size: height * 0.8,
},
center,
);
animate((frameTime) => {
const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);
const shiftedFrameTime = frameTime + period * fadeLead;
const shiftedPercentage = calcBouncePercentage(
period,
timingFunctions.ease,
shiftedFrameTime,
);
const shiftedPeriodPercentage = mod(shiftedFrameTime, period) / period;
forceStyles(ctx, () => {
const {pt} = sizes();
ctx.fillStyle = "transparent";
ctx.lineWidth = pt;
ctx.strokeStyle = colors.secondary;
ctx.setLineDash([2 * pt]);
if (shiftedPeriodPercentage > 0.5) {
ctx.globalAlpha = fadeFloor + fadeSpeed * (1 - shiftedPercentage);
drawClosed(ctx, blobA, false);
ctx.globalAlpha = fadeFloor;
drawClosed(ctx, blobB, false);
} else {
ctx.globalAlpha = fadeFloor + fadeSpeed * shiftedPercentage;
drawClosed(ctx, blobB, false);
ctx.globalAlpha = fadeFloor;
drawClosed(ctx, blobA, false);
}
});
drawClosed(ctx, interpolateBetween(percentage, blobA, blobB), true);
});
return `The simplest way to interpolate between blobs would be to move points 0-N from their
position in the start blob to their position in the end blob. The problem with this approach
is that it doesn't allow for all blob to map to all blobs. Specifically it would only be
possible to animate between blobs that have the same number of points. This means something
more generic is required.`;
});
addCanvas(
1.3,
(ctx, width, height, animate) => {
const center: Coord = {x: width * 0.5, y: height * 0.5};
const maxExtraPoints = 7;
const period = maxExtraPoints * Math.PI * 300;
const {pt} = sizes();
const blob = centeredBlob(
{
extraPoints: 0,
randomness: 6,
seed: "flip",
size: height * 0.9,
},
center,
);
animate((frameTime) => {
const percentage = mod(frameTime, period) / period;
const extraPoints = Math.floor(percentage * (maxExtraPoints + 1));
drawClosed(ctx, divide(extraPoints + blob.length, blob), true);
forPoints(blob, ({curr}) => {
ctx.beginPath();
ctx.arc(curr.x, curr.y, pt * 6, 0, 2 * Math.PI);
tempStyles(
ctx,
() => {
ctx.strokeStyle = colors.secondary;
ctx.lineWidth = pt;
},
() => {
ctx.stroke();
},
);
});
});
return `The first step to prepare animation is to make the number of points between the
start and end shapes equal. This is done by adding points to the shape with least points
until they are both equal.
<br><br>
For best animation quality it is important that these points are as evenly distributed
as possible all around the shape so this is not a recursive algorithm.`;
},
(ctx, width, height, animate) => {
const period = Math.PI ** Math.E * 1000;
const start = point(width * 0.1, height * 0.6, 0, 0, -45, width * 0.5);
const end = point(width * 0.9, height * 0.6, 160, width * 0.3, 0, 0);
animate((frameTime) => {
const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);
const d = calcFullDetails(percentage, start, end);
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
},
() => {
drawLine(ctx, d.a0, d.a1, 1);
drawLine(ctx, d.a1, d.a2, 1, 2);
drawLine(ctx, d.a2, d.a3, 1);
drawLine(ctx, d.b0, d.b1, 1, 2);
drawLine(ctx, d.b1, d.b2, 1, 2);
drawPoint(ctx, d.a0, 1.3, "a0");
drawPoint(ctx, d.a1, 1.3, "a1");
drawPoint(ctx, d.a2, 1.3, "a2");
drawPoint(ctx, d.a3, 1.3, "a3");
drawPoint(ctx, d.b1, 1.3, "b1");
},
);
forceStyles(ctx, () => {
const {pt} = sizes();
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
ctx.lineWidth = pt;
drawOpen(ctx, start, end, false);
});
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.highlight;
ctx.strokeStyle = colors.highlight;
},
() => {
drawLine(ctx, d.c0, d.c1, 1);
drawLine(ctx, d.a0, d.b0, 1);
drawLine(ctx, d.a3, d.b2, 1);
drawPoint(ctx, d.b0, 1.3, "b0");
drawPoint(ctx, d.b2, 1.3, "b2");
drawPoint(ctx, d.c0, 1.3, "c0");
drawPoint(ctx, d.c1, 1.3, "c1");
},
);
tempStyles(
ctx,
() => (ctx.fillStyle = colors.highlight),
() => drawPoint(ctx, d.d0, 1.3, "d0"),
);
});
return `It is only possible to reliably <i>add</i> points to a blob because attempting to
remove points without modifying the shape is almost never possible and is expensive to
compute.
<br><br>
Adding a point is done using the line-drawing geometry. In this example "d0" is the new
point with its handles being "c0" and "c1". The original points get new handles "b0" and
"b2"`;
},
);
addCanvas(
1.3,
(ctx, width, height, animate) => {
const period = (Math.E / Math.PI) * 1000;
const center: Coord = {x: width * 0.5, y: height * 0.5};
const blob = centeredBlob(
{
extraPoints: 3,
randomness: 6,
seed: "shift",
size: height * 0.9,
},
center,
);
const shiftedBlob = shift(1, blob);
let prev = 0;
let count = 0;
animate((frameTime) => {
const animationTime = mod(frameTime, period);
const percentage = timingFunctions.ease(mod(animationTime, period) / period);
// Count animation loops.
if (percentage < prev) count++;
prev = percentage;
// Draw lines points are travelling.
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
},
() => {
drawPoint(ctx, center, 2);
forPoints(blob, ({curr, next}) => {
drawLine(ctx, curr, next(), 1, 2);
});
},
);
// Pause in-place every other animation loop.
if (count % 2 === 0) {
drawClosed(ctx, interpolateBetweenSmooth(2, percentage, blob, shiftedBlob), true);
} else {
drawClosed(ctx, blob, true);
}
});
return `Once both shapes have the same amount of points, an ordering of points which reduces
the total amount of distance traveled by the points during the transition needs to be
selected. Because the shapes are closed, points can be shifted by any amount without
visually affecting the shape.`;
},
(ctx, width, height, animate) => {
const period = Math.PI * Math.E * 1000;
const center: Coord = {x: width * 0.5, y: height * 0.5};
const blob = centeredBlob(
{
extraPoints: 3,
randomness: 6,
seed: "flip",
size: height * 0.9,
},
center,
);
const reversedBlob = mapPoints(blob, ({curr}) => {
const temp = curr.handleIn;
curr.handleIn = curr.handleOut;
curr.handleOut = temp;
return curr;
});
reversedBlob.reverse();
animate((frameTime) => {
const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);
forceStyles(ctx, () => {
const {pt} = sizes();
ctx.fillStyle = "transparent";
ctx.lineWidth = pt;
ctx.strokeStyle = colors.secondary;
ctx.setLineDash([2 * pt]);
drawClosed(ctx, blob, false);
});
drawClosed(ctx, interpolateBetweenSmooth(2, percentage, blob, reversedBlob), true);
});
return `Points can also be reversed without visually affecting the shape. Then, again can
be shifted all around. Although reversed ordering doesn't change the shape, it has a
dramatic effect on the animation as it makes the loop flip over itself.
<br><br>
In total there are 2 * num_points different orderings of the
points that can work for transition purposes.`;
},
);
addCanvas(
1.3,
(ctx, width, height) => {
// Only animate in the most recent painter call.
const animationID = Math.random();
const wasReplaced = () => (ctx.canvas as any).animationID !== animationID;
const period = Math.PI * 1000;
const center: Coord = {x: width * 0.5, y: height * 0.5};
const size = Math.min(width, height) * 0.8;
const canvasBlobGenerator = (keyframe: CanvasKeyframe): Point[] => {
return mapPoints(genFromOptions(keyframe.blobOptions), ({curr}) => {
curr.x += center.x - size / 2;
curr.y += center.y - size / 2;
return curr;
});
};
const animation = statefulAnimationGenerator(
canvasBlobGenerator,
(points: Point[]) => drawClosed(ctx, points, true),
() => {},
)(Date.now);
const renderFrame = () => {
if (wasReplaced()) return;
ctx.clearRect(0, 0, width, height);
animation.renderFrame();
requestAnimationFrame(renderFrame);
};
requestAnimationFrame(renderFrame);
const loopAnimation = (): void => {
if (wasReplaced()) return;
animation.transition(genFrame());
};
let frameCount = -1;
const genFrame = (overrides: Partial<CanvasKeyframe> = {}): CanvasKeyframe => {
frameCount++;
return {
duration: period,
timingFunction: "ease",
callback: loopAnimation,
blobOptions: {
extraPoints: Math.max(0, mod(frameCount, 4) - 1),
randomness: 4,
seed: Math.random(),
size,
},
...overrides,
};
};
animation.transition(genFrame({duration: 0}));
ctx.canvas.onclick = () => {
if (wasReplaced()) return;
animation.playPause();
};
(ctx.canvas as any).animationID = animationID;
return `The added points can be removed at the end of a transition when the target shape has
been reached. However, if the animation is interrupted during interpolation there is no
opportunity to clean up the extra points.`;
},
(ctx, width, height, animate) => {
const center: Coord = {x: width * 0.5, y: height * 0.5};
const size = Math.min(width, height) * 0.8;
const drawStar = (rays: number, od: number, id: number): Point[] => {
const pointCount = 2 * rays;
const angle = (Math.PI * 2) / pointCount;
const points: Point[] = [];
for (let i = 0; i < pointCount; i++) {
const pointX = Math.sin(i * angle);
const pointY = Math.cos(i * angle);
const distanceMultiplier = (i % 2 === 0 ? od : id) / 2;
points.push({
x: center.x + pointX * distanceMultiplier,
y: center.y + pointY * distanceMultiplier,
handleIn: {angle: 0, length: 0},
handleOut: {angle: 0, length: 0},
});
}
return points;
};
const drawPolygon = (sides: number, od: number): Point[] => {
const angle = (Math.PI * 2) / sides;
const points: Point[] = [];
for (let i = 0; i < sides; i++) {
const pointX = Math.sin(i * angle);
const pointY = Math.cos(i * angle);
const distanceMultiplier = od / 2;
points.push({
x: center.x + pointX * distanceMultiplier,
y: center.y + pointY * distanceMultiplier,
handleIn: {angle: 0, length: 0},
handleOut: {angle: 0, length: 0},
});
}
return points;
};
const shapes = [
drawStar(8, size, size * 0.7),
smoothBlob(drawPolygon(3, size)),
smoothBlob(drawStar(10, size, size * 0.9)),
drawPolygon(4, size),
smoothBlob(drawStar(3, size, size * 0.6)),
];
const animation = canvasPath();
const genFrame = (index: number) => () => {
animation.transition({
points: shapes[index % shapes.length],
duration: 3000,
delay: 1000,
timingFunction: "ease",
callback: genFrame(index + 1),
});
};
animation.transition({
points: shapes[0],
duration: 0,
callback: genFrame(1),
});
animate(() => {
drawClosed(ctx, animation.renderPoints(), true);
});
return `Putting all these pieces together, the blob transition library can also be used to
tween between non-blob shapes. The more detail a shape has, the more unconvincing the
animation will look. In these cases, manually creating in-between frames can be a
helpful tool.`;
},
);
addTitle(4, "Gooeyness");
addCanvas(
1.3,
(ctx, width, height, animate) => {
const size = Math.min(width, height) * 0.8;
const center: Coord = {x: (width - size) * 0.5, y: (height - size) * 0.5};
const animation = canvasPath();
const genFrame = (duration: number) => {
animation.transition({
duration: duration,
blobOptions: {
extraPoints: 2,
randomness: 3,
seed: Math.random(),
size,
},
callback: () => genFrame(3000),
timingFunction: "ease",
canvasOptions: {offsetX: center.x, offsetY: center.y},
});
};
genFrame(0);
animate(() => {
drawClosed(ctx, animation.renderPoints(), true);
});
return `This library uses the keyframe model to define animations. This is a flexible
approach, but it does not lend itself well to the kind of gooey blob shapes invite.
<br><br>
When looking at this animation, you may be able to notice the rhythm of the
keyframes where the points start moving and stop moving at the same time.`;
},
(ctx, width, height, animate) => {
const size = Math.min(width, height) * 0.8;
const center: Coord = {x: width * 0.5, y: height * 0.5};
const animation = canvasPath();
wigglePreset(
animation,
{
extraPoints: 2,
randomness: 3,
seed: Math.random(),
size,
},
{
offsetX: center.x - size / 2,
offsetY: center.y - size / 2,
},
{
speed: 2,
},
);
animate(() => {
drawClosed(ctx, animation.renderPoints(), true);
});
return `In addition to the keyframe API, there is now also pre-built preset which produces a
gooey animation without much effort and much prettier results.
<br><br>
This approach uses a noise field instead of random numbers to move individual points
around continuously and independently. Repeated calls to a noise-field-powered random
number generator will produce self-similar results.`;
},
);
================================================
FILE: demo/example.ts
================================================
import {CanvasKeyframe, canvasPath, wigglePreset} from "../public/animate";
import {drawHandles, drawPoint} from "./internal/canvas";
import {isDebug} from "./internal/debug";
import {colors} from "./internal/layout";
// Fetch reference to example container.
const exampleContainer = document.querySelector(".example")!;
const canvas = document.createElement("canvas")!;
exampleContainer.appendChild(canvas);
let size = 0;
const resize = () => {
// Set blob size relative to window, but limit to 600.
const rawSize = Math.min(600, Math.min(window.innerWidth - 64, window.innerHeight / 2));
canvas.style.width = `${rawSize}px`;
canvas.style.height = `${rawSize}px`;
// Scale resolution to take into account device pixel ratio.
size = rawSize * (window.devicePixelRatio || 1);
canvas.width = size;
canvas.height = size;
};
// Set blob color and set context to erase intersection of content.
const ctx = canvas.getContext("2d")!;
// Create animation and draw its frames in `requestAnimationFrame` callbacks.
const animation = canvasPath();
const renderFrame = () => {
ctx.clearRect(0, 0, size, size);
ctx.fillStyle = colors.highlight;
ctx.strokeStyle = colors.highlight;
if (isDebug()) {
const points = animation.renderPoints();
for (const point of points) {
drawPoint(ctx, point, 2);
drawHandles(ctx, point, 1);
}
}
ctx.fill(animation.renderFrame());
requestAnimationFrame(renderFrame);
};
requestAnimationFrame(renderFrame);
// Extra points that increases when blob gets clicked.
let extraPoints = 0;
const genWiggle = (transition: number) => {
wigglePreset(
animation,
{
extraPoints: 3 + extraPoints,
randomness: 1.5,
seed: Math.random(),
size,
},
{},
{speed: 2, initialTransition: transition},
);
};
// Generate a keyframe with overridable default values.
const genFrame = (overrides: any = {}): CanvasKeyframe => {
const blobOptions = {
extraPoints: 3 + extraPoints,
randomness: 4,
seed: Math.random(),
size,
...overrides.blobOptions,
};
return {
duration: 4000,
timingFunction: "ease",
callback: loopAnimation,
...overrides,
blobOptions,
};
};
// Callback for every frame which starts transition to a new frame.
const loopAnimation = (): void => {
extraPoints = 0;
genWiggle(5000);
};
// Quickly animate to a new frame when canvas is clicked.
canvas.onclick = () => {
extraPoints++;
animation.transition(
genFrame({
duration: 400,
timingFunction: "elasticEnd0",
blobOptions: {extraPoints},
}),
);
};
// Immediately show a new frame.
window.addEventListener("load", () => {
resize();
genWiggle(0);
});
// Make blob a circle while window is being resized.
window.addEventListener("resize", () => {
resize();
const tempSize = (size * 6) / 7;
animation.transition(
genFrame({
duration: 100,
timingFunction: "easeEnd",
blobOptions: {
extraPoints: 0,
randomness: 0,
seed: "",
size: tempSize,
},
canvasOptions: {
offsetX: (size - tempSize) / 2,
offsetY: (size - tempSize) / 2,
},
}),
);
});
================================================
FILE: demo/index.html
================================================
<html>
<head>
<link rel="shortcut icon" href="https://blobs.dev/assets/favicon.ico?v=3ewlwLn2WO" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
html {
font-size: calc(0.1vw + 1.2rem);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
overflow-x: hidden;
margin: 0;
}
header {
align-items: center;
display: flex;
flex-direction: column;
height: 16vh;
padding: 5vh 0 0;
user-select: none;
-moz-user-select: none;
}
header img {
height: 16vh;
max-height: 6rem;
}
header nav a,
.how-it-works {
color: #aaa;
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
padding: 0.5rem;
text-decoration: none;
text-transform: uppercase;
}
main {
align-items: center;
display: flex;
flex-direction: column;
}
.example {
align-items: center;
display: flex;
height: 50vh;
padding: 5vh 2rem;
}
.how-it-works {
cursor: pointer;
height: 10vh;
margin-top: 5vh;
transform: rotate(-2deg) translateY(-1px);
user-select: none;
}
.how-it-works.hidden {
display: none;
}
.container {
align-items: center;
display: flex;
flex-direction: column;
padding: 0 1rem 20vh;
}
.container:not(.open) {
display: none;
}
.container .title {
color: #aaa;
font-weight: 700;
margin: 1rem 0 0.5rem;
max-width: 1000px;
text-transform: uppercase;
user-select: none;
width: 100%;
}
.container .section {
border: 1px solid #eee;
border-radius: 0.5rem;
display: flex;
margin: 1rem 0;
max-width: 1000px;
width: 100%;
}
.container .section .number {
color: #ccc;
font-size: 0.6rem;
font-weight: 100;
height: 0;
position: relative;
text-decoration: none;
transform: translate(1rem, 0.5rem);
user-select: none;
width: 0;
}
.container .section .text {
box-sizing: border-box;
padding: 2rem 3rem 1.5rem;
}
.container .section .cell {
flex-grow: 1;
}
.container .section .cell canvas {
width: 100%;
}
.container .section .cell .label {
color: #555;
font-size: 0.6rem;
padding: 0 1rem 1rem;
}
</style>
</head>
<body>
<header>
<img src="../assets/logo.svg" />
<nav>
<a
href="https://github.com/g-harel/blobs"
style="transform: rotate(1deg) translateY(3px);"
>GITHUB</a
>
<a
href="https://npmjs.com/package/blobs"
style="transform: rotate(-2deg) translateY(-1px);"
>NPM</a
>
<a
href="mailto:gabrielj.harel@gmail.com"
style="transform: rotate(4deg) translateY(1px);"
>CONTACT</a
>
</nav>
</header>
<main>
<div class="example"></div>
<div class="how-it-works">How it works</div>
<div class="container"></div>
</main>
<script src="./example.ts"></script>
<script src="./content.ts"></script>
</body>
</html>
================================================
FILE: demo/internal/canvas.ts
================================================
import {TimingFunc} from "../../internal/animate/timing";
import {Coord, Point} from "../../internal/types";
import {expandHandle, forPoints, mod, rad} from "../../internal/util";
import {isDebug} from "../internal/debug";
import {colors, sizes} from "../internal/layout";
export const forceStyles = (ctx: CanvasRenderingContext2D, fn: () => void) => {
if (!(ctx as any).forcedStyles) (ctx as any).forcedStyles = 0;
(ctx as any).forcedStyles++;
ctx.save();
fn();
ctx.restore();
(ctx as any).forcedStyles--;
};
export const tempStyles = (ctx: CanvasRenderingContext2D, style: () => void, fn: () => void) => {
if ((ctx as any).forcedStyles > 0) {
fn();
} else {
ctx.save();
style();
fn();
ctx.restore();
}
};
export const rotateAround = (
options: {
ctx: CanvasRenderingContext2D;
angle: number;
cx: number;
cy: number;
},
fn: () => void,
) => {
tempStyles(
options.ctx,
() => {
options.ctx.translate(options.cx, options.cy);
options.ctx.rotate(options.angle);
},
() => {
if (isDebug()) {
tempStyles(
options.ctx,
() => (options.ctx.fillStyle = colors.debug),
() => {
options.ctx.fillRect(0, -4, 1, 8);
options.ctx.fillRect(-32, 0, 64, 1);
},
);
}
fn();
},
);
};
export const point = (
x: number,
y: number,
ia: number,
il: number,
oa: number,
ol: number,
): Point => {
return {
x: x,
y: y,
handleIn: {angle: rad(ia), length: il},
handleOut: {angle: rad(oa), length: ol},
};
};
export const drawPoint = (
ctx: CanvasRenderingContext2D,
coord: Coord,
size: number,
label?: string,
) => {
const radius = sizes().pt * size;
const pointPath = new Path2D();
pointPath.arc(coord.x, coord.y, radius, 0, 2 * Math.PI);
ctx.fill(pointPath);
if (label) {
tempStyles(
ctx,
() => (ctx.font = `${6 * radius}px monospace`),
() => ctx.fillText(label, coord.x + 2 * radius, coord.y - radius),
);
}
};
export const drawLine = (
ctx: CanvasRenderingContext2D,
a: Coord,
b: Coord,
size: number,
dash?: number,
) => {
tempStyles(
ctx,
() => {
const width = sizes().pt * size;
if (dash) ctx.setLineDash([dash * width]);
},
() => {
const width = sizes().pt * size;
const linePath = new Path2D();
linePath.moveTo(a.x, a.y);
linePath.lineTo(b.x, b.y);
ctx.lineWidth = width;
ctx.stroke(linePath);
},
);
};
export const drawClosed = (ctx: CanvasRenderingContext2D, points: Point[], handles?: boolean) => {
forPoints(points, ({curr, next}) => {
drawOpen(ctx, curr, next(), handles);
});
};
export const drawDebugClosed = (ctx: CanvasRenderingContext2D, points: Point[], size: number) => {
forPoints(points, ({curr, next: nextFn}) => {
drawHandles(ctx, curr, size);
const next = nextFn();
const currHandle = expandHandle(curr, curr.handleIn);
const nextHandle = expandHandle(curr, curr.handleOut);
const curve = new Path2D();
curve.moveTo(curr.x, curr.y);
curve.bezierCurveTo(currHandle.x, currHandle.y, nextHandle.x, nextHandle.y, next.x, next.y);
ctx.lineWidth = sizes().pt * size * 2;
ctx.stroke(curve);
drawPoint(ctx, curr, size * 1.1);
});
};
export const drawHandles = (ctx: CanvasRenderingContext2D, point: Point, size: number) => {
const inHandle = expandHandle(point, point.handleIn);
const outHandle = expandHandle(point, point.handleOut);
drawLine(ctx, point, inHandle, size);
drawLine(ctx, point, outHandle, size, 2);
drawPoint(ctx, inHandle, size * 1.4);
drawPoint(ctx, outHandle, size * 1.4);
};
export const drawOpen = (
ctx: CanvasRenderingContext2D,
start: Point,
end: Point,
handles?: boolean,
) => {
const width = sizes().width;
const startHandle = expandHandle(start, start.handleOut);
const endHandle = expandHandle(end, end.handleIn);
// Draw handles.
if (handles) {
tempStyles(
ctx,
() => {
ctx.fillStyle = colors.secondary;
ctx.strokeStyle = colors.secondary;
},
() => {
drawLine(ctx, start, startHandle, 1);
drawLine(ctx, end, endHandle, 1, 2);
drawPoint(ctx, startHandle, 1.4);
drawPoint(ctx, endHandle, 1.4);
},
);
}
// Draw curve.
tempStyles(
ctx,
() => {
const lineWidth = width * 0.003;
ctx.lineWidth = lineWidth;
},
() => {
const curve = new Path2D();
curve.moveTo(start.x, start.y);
curve.bezierCurveTo(
startHandle.x,
startHandle.y,
endHandle.x,
endHandle.y,
end.x,
end.y,
);
tempStyles(
ctx,
() => (ctx.strokeStyle = colors.highlight),
() => ctx.stroke(curve),
);
tempStyles(
ctx,
() => (ctx.fillStyle = colors.highlight),
() => {
drawPoint(ctx, start, 2);
drawPoint(ctx, end, 2);
},
);
},
);
};
export const calcBouncePercentage = (period: number, timingFunc: TimingFunc, frameTime: number) => {
const halfPeriod = period / 2;
const animationTime = mod(frameTime, period);
if (animationTime <= halfPeriod) {
return timingFunc(animationTime / halfPeriod);
} else {
return timingFunc(1 - (animationTime - halfPeriod) / halfPeriod);
}
};
================================================
FILE: demo/internal/debug.ts
================================================
// If debug is initially set to false it will not be toggleable.
let debug = window.location.search.includes("debug") && location.hostname === "localhost";
export const isDebug = () => debug;
const debugListeners: ((debug: boolean) => void)[] = [];
export const onDebugStateChange = (fn: (debug: boolean) => void) => {
debugListeners.push(fn);
fn(debug);
};
if (debug && document.body) {
const toggleButton = document.createElement("button");
toggleButton.innerHTML = "debug";
toggleButton.style.padding = "2rem";
toggleButton.style.position = "fixed";
toggleButton.style.top = "0";
toggleButton.onclick = () => {
debug = !debug;
for (const listener of debugListeners) {
listener(debug);
}
};
document.body.prepend(toggleButton);
}
================================================
FILE: demo/internal/layout.ts
================================================
import {tempStyles} from "./canvas";
import {isDebug, onDebugStateChange} from "./debug";
export const colors = {
debug: "green",
highlight: "#ec576b",
secondary: "#555",
};
interface Cell {
aspectRatio: number;
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
painter: CellPainter;
animationID: number;
}
export interface CellPainter {
(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
animate: (painter: AnimationPainter) => void,
): string | void;
}
export interface AnimationPainter {
(timestamp: number): void;
}
// Global cell state.
const cells = ((window as any).cells as Cell[][]) || [];
((window as any).cells as Cell[][]) = cells;
const containerElement = document.querySelector(".container");
if (!containerElement) throw "missing container";
const howItWorksElement = document.querySelector(".how-it-works");
if (!howItWorksElement) throw "missing container";
let animating = false;
const reveal = () => {
containerElement.classList.add("open");
howItWorksElement.classList.add("hidden");
animating = true;
redraw();
};
howItWorksElement.addEventListener("click", reveal);
if (document.location.hash || isDebug()) setTimeout(reveal);
export const sizes = (): {width: number; pt: number} => {
const sectionStyle = window.getComputedStyle(
(containerElement.lastChild as any) || document.body,
);
const sectionWidth = Number(sectionStyle.getPropertyValue("width").slice(0, -2));
const width = sectionWidth * window.devicePixelRatio;
return {width, pt: width * 0.002};
};
const createSection = (): HTMLElement => {
const numberLabel = ("000" + cells.length).substr(-3);
const sectionElement = document.createElement("div");
sectionElement.classList.add("section");
sectionElement.setAttribute("id", numberLabel);
containerElement.appendChild(sectionElement);
const numberElement = document.createElement("a");
numberElement.classList.add("number");
numberElement.setAttribute("href", "#" + numberLabel);
numberElement.appendChild(document.createTextNode(numberLabel));
sectionElement.appendChild(numberElement);
return sectionElement;
};
// Adds a section of text to the bottom of the layout.
export const addTitle = (heading: number, text: string) => {
const wrapperElement = document.createElement(`h${heading}`);
wrapperElement.classList.add("title");
containerElement.appendChild(wrapperElement);
const textWrapperElement = document.createElement("div");
textWrapperElement.classList.add("text");
wrapperElement.appendChild(textWrapperElement);
text = text.replace("\n", " ").replace(/\s+/g, " ").trim();
const textElement = document.createTextNode(text);
textWrapperElement.appendChild(textElement);
};
const handleIntersection = (entries: any) => {
entries.map((entry: any) => {
entry.target.setAttribute("data-visible", entry.isIntersecting);
});
};
// Adds a row of cells to the bottom of the layout.
export const addCanvas = (aspectRatio: number, ...painters: CellPainter[]) => {
const sectionElement = createSection();
if (painters.length == 0) {
painters = [() => {}];
}
const cellRow: Cell[] = [];
for (const painter of painters) {
const cellElement = document.createElement("div");
cellElement.classList.add("cell");
sectionElement.appendChild(cellElement);
const canvas = document.createElement("canvas");
cellElement.appendChild(canvas);
const labelElement = document.createElement("div");
labelElement.classList.add("label");
cellElement.appendChild(labelElement);
const ctx = canvas.getContext("2d");
if (!ctx) throw "missing canvas context";
const cell = {aspectRatio, canvas, ctx, painter, animationID: -1};
cellRow.push(cell);
new IntersectionObserver(handleIntersection, {
threshold: 0.1,
}).observe(canvas);
}
cells.push(cellRow);
redraw();
};
// Lazily redraw canvas cells to match window resolution.
let redrawTimeout: undefined | number = undefined;
const redraw = () => {
window.clearTimeout(redrawTimeout);
redrawTimeout = window.setTimeout(() => {
for (const cellRow of cells) {
const cellWidth = sizes().width / cellRow.length;
for (const cell of cellRow) {
const cellHeight = cellWidth / cell.aspectRatio;
// Resize canvas;
cell.canvas.width = cellWidth;
cell.canvas.height = cellHeight;
// Draw canvas debug info.
const drawDebug = () => {
if (isDebug()) {
tempStyles(
cell.ctx,
() => (cell.ctx.strokeStyle = colors.debug),
() => cell.ctx.strokeRect(0, 0, cellWidth, cellHeight - 1),
);
}
};
drawDebug();
// Keep track of paused state.
let pausedAt = 0;
let pauseOffset = 0;
cell.canvas.onclick = () => {
if (pausedAt === 0) {
pausedAt = Date.now();
} else {
pauseOffset += Date.now() - pausedAt;
pausedAt = 0;
}
};
// Cell-specific callback for providing an animation painter.
const animate = (painter: AnimationPainter) => {
if (!animating) return;
const animationID = Math.random();
const startTime = Date.now();
cell.animationID = animationID;
const drawFrame = () => {
// Stop animating if cell is redrawn.
if (cell.animationID !== animationID) return;
const visible = cell.canvas.getAttribute("data-visible") === "true";
if (pausedAt === 0 && visible) {
const frameTime = Date.now() - startTime - pauseOffset;
cell.ctx.clearRect(0, 0, cellWidth, cellHeight);
drawDebug();
if (isDebug()) {
tempStyles(
cell.ctx,
() => (cell.ctx.fillStyle = colors.debug),
() => cell.ctx.fillText(String(frameTime), 10, 15),
);
}
painter(frameTime);
}
requestAnimationFrame(drawFrame);
};
drawFrame();
};
// Redraw canvas contents and replace label if changed.
const label = cell.painter(cell.ctx, cellWidth, cellHeight, animate);
if (label) {
const cellElement = cell.canvas.parentElement;
if (cellElement) {
cellElement.style.width = `${100 / cellRow.length}%`;
const labelElement = cellElement.querySelector(".label");
if (labelElement && labelElement.innerHTML !== label) {
labelElement.innerHTML = "";
labelElement.innerHTML = label;
}
}
}
}
}
}, 100);
};
window.addEventListener("load", redraw);
window.addEventListener("resize", redraw);
onDebugStateChange(redraw);
================================================
FILE: examples/corner-expand.html
================================================
<html>
<head>
<title>Expand demo</title>
<script src="https://unpkg.com/blobs/v2/animate"></script>
<style>
body {
background-color: #ffffff;
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
#container {
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 2px 10px 0 rgb(0 0 0 / 20%);
cursor: pointer;
display: flex;
flex-direction: column;
position: absolute;
top: 35vh;
left: 15vw;
min-height: 20%;
width: 400px;
}
canvas {
border: 1px solid #ec576b;
box-sizing: border-box;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: -1;
}
</style>
</head>
<body>
<div id="container"></div>
<canvas></canvas>
<script>
const canvas = document.querySelector("canvas");
const container = document.getElementById("container");
const ctx = canvas.getContext("2d");
const animation = blobs2Animate.canvasPath();
const width = canvas.clientWidth;
const height = canvas.clientHeight;
canvas.width = width;
canvas.height = height;
const renderAnimation = () => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "#ec576b";
ctx.fill(animation.renderFrame());
requestAnimationFrame(renderAnimation);
};
requestAnimationFrame(renderAnimation);
const size = Math.min(width, height) * 1.2;
const defaultOptions = () => ({
blobOptions: {
seed: Math.random(),
extraPoints: 36,
randomness: 0.7,
size,
},
canvasOptions: {
offsetX: -size / 2.2,
offsetY: -size / 2.2,
},
});
// Keyframe loop.
const loopAnimation = () => {
animation.transition({
duration: 4000,
timingFunction: "ease",
callback: loopAnimation,
...defaultOptions(),
});
};
// Initial frame.
animation.transition({
duration: 0, // Render immediately.
callback: loopAnimation,
...defaultOptions(),
});
container.onclick = () => {
const options = defaultOptions();
options.blobOptions.size = Math.max(width, height) * 1.6;
options.blobOptions.randomness = 1.4;
options.canvasOptions.offsetX = -size / 2;
options.canvasOptions.offsetY = -size / 2;
animation.transition({
duration: 2000,
timingFunction: "elasticEnd0",
...options,
});
};
</script>
</body>
</html>
================================================
FILE: index.html
================================================
<html><head><link rel="icon shortcut" href="https://blobs.dev/assets/favicon.ico?v=3ewlwLn2WO"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>html{font-size:calc(.1vw + 1.2rem)}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;overflow-x:hidden;margin:0}header{align-items:center;display:flex;flex-direction:column;height:16vh;padding:5vh 0 0;user-select:none;-moz-user-select:none}header img{height:16vh;max-height:6rem}.how-it-works,header nav a{color:#aaa;display:inline-block;font-size:.7rem;font-weight:700;padding:.5rem;text-decoration:none;text-transform:uppercase}main{flex-direction:column}.example,main{align-items:center;display:flex}.example{height:50vh;padding:5vh 2rem}.how-it-works{cursor:pointer;height:10vh;margin-top:5vh;transform:rotate(-2deg) translateY(-1px);user-select:none}.how-it-works.hidden{display:none}.container{align-items:center;display:flex;flex-direction:column;padding:0 1rem 20vh}.container:not(.open){display:none}.container .title{color:#aaa;font-weight:700;margin:1rem 0 .5rem;max-width:1000px;text-transform:uppercase;user-select:none;width:100%}.container .section{border:1px solid #eee;border-radius:.5rem;display:flex;margin:1rem 0;max-width:1000px;width:100%}.container .section .number{color:#ccc;font-size:.6rem;font-weight:100;height:0;position:relative;text-decoration:none;transform:translate(1rem,.5rem);user-select:none;width:0}.container .section .text{box-sizing:border-box;padding:2rem 3rem 1.5rem}.container .section .cell{flex-grow:1}.container .section .cell canvas{width:100%}.container .section .cell .label{color:#555;font-size:.6rem;padding:0 1rem 1rem}</style></head><body> <header> <img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTAwMHB4IiBoZWlnaHQ9IjQwMHB4IiB2aWV3Qm94PSIwIDAgMTAwMCA0MDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMDAgNDAwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNFQzU3NkIiIGQ9Ik00NjMuMjk1LDY1LjIxM2MtMjUuNywwLTQ0LjIsMTAuOS01NS40LDMyLjVjLTExLjMsMjEuNjAxLTE0LjEsNTEuNjAxLTguNSw4OS41DQoJYzYuMTAxLDQxLDE3LjksNzEuNzk4LDM1LjQsOTIuNGMxNy41LDIwLjY5NywzOS44OTgsMzAsNjcuMTk5LDI4LjVjMjYuNC0xLjUsNDUuMTAyLTEzLjIwMSw1Ni4xMDItMzQuODAzDQoJYzExLjEwMS0yMS42OTcsMTMuNjk5LTUxLjI5OSw3Ljc5OS04OWMtNS43OTktMzgtMTcuNS02Ny4xLTM1LjEtODcuOEM1MTMuMTk1LDc1LjgxNCw0OTAuNjk1LDY1LjIxMyw0NjMuMjk1LDY1LjIxM3oNCgkgTTUxMS43OTUsMjQwLjkxNGMtMy42OTksOS42OTktMTAuMzAxLDE0LjgwMS0xOS45LDE1LjE5OWMtOC44OTksMC4zOTgtMTYuMzk5LTQtMjIuNS0xMy4zOThjLTYuMS05LjQtMTEuMy0yOC40LTE1LjYtNTcuMjAxDQoJYy02LjctNDQuNy0wLjgwMS02Ny40LDE3LjY5OS02Ny42MDJjMTkuNy0wLjE5OCwzMi44MDIsMjIuMzAyLDM5LjYwMSw2N0M1MTUuMTk1LDIxMi42MTMsNTE1LjQ5NCwyMzEuMjE1LDUxMS43OTUsMjQwLjkxNHoiLz4NCjxwYXRoIGZpbGw9IiNFQzU3NkIiIGQ9Ik0zNzQuMjk1LDI1Ni4yMTVjLTQuMzAyLDAuMzk4LTguNjAyLDAuNzk5LTEyLjksMS4xOTdjLTcuOCwwLjcwMS0xNC44LTQuNzk5LTE1Ljg5OS0xMi41DQoJYy03LjUtNTEuMjk5LTE1LjEwMS0xMDIuNzk5LTIyLjYwMS0xNTQuNWMtMS4xOTktNy44LTguMy0xMy4zLTE2LjEtMTIuNWMtNy4zMDIsMC43LTE0LjcsMS41LTIyLjEwMywyLjM5OQ0KCWMtOC4zLDEtMTQuMSw4LjYwMS0xMi44OTksMTYuODAxYzEwLjEsNjkuODk5LDIwLjMsMTM5LjQwMSwzMC4zOTksMjA4LjYwMmMxLjEwMyw4LDguNTAxLDEzLjUsMTYuNTAxLDEyLjM5OA0KCWMyMC42OTktMi44OTgsNDEuMy01LjMwMSw2Mi03LjMwMWM4LjMtMC44MDEsMTQuMy04LjUsMTMuMTk5LTE2LjljLTEuMTk5LTguMzk4LTIuNS0xNi43OTktMy42OTktMjUuMTk3DQoJQzM4OC45OTUsMjYxLjAxNCwzODIuMDk1LDI1NS41MTQsMzc0LjI5NSwyNTYuMjE1eiIvPg0KPHBhdGggZmlsbD0iI0VDNTc2QiIgZD0iTTgzMi4zOTUsMjE5LjcxNWMtNi42OTgsMS44OTgtMTQuNSwyLjI5OS0yMy42LDEuMjk5Yy05LjYwMi0xLjEwMi0xNy42MDIsNy4zMDEtMTYsMTYuODAxDQoJYzAuODk4LDUuNiwxLjg5OCwxMS4xLDIuODAxLDE2LjZjMS4xMDEsNi41LDYuMzk4LDExLjYwMiwxMywxMmMxMS4xOTksMC42OTksMjIuMTk5LTAuNSwzMi44OTgtMy44MDENCgljMjAuOS02LjM5OCwzNy4xOTktMTguMjk5LDQ4LjktMzQuNWMxMS42OTgtMTYuMTk5LDE2LTMyLjg5OCwxMy4xLTQ5LjdjLTIuNi0xNC4zLTEwLTI0Ljg5OC0yMi4zOTgtMzIuNQ0KCWMtMTIuMzk5LTcuNi0yNC4yMDEtMTMuMTk4LTM1LjM5OS0xNi42OThjLTE4LjgwMi02LTI4Ljg5OC0xMy44OTktMzAuMzk4LTIyLjVjLTAuODk5LTQuODk5LDAuMjk5LTkuMiwzLjUtMTIuOA0KCWMzLjEwMS0zLjYsNy44OTgtNi4yLDE0LjE5Ny03LjdjNS42MDItMS4zLDEyLjMwMy0xLjcsMjAuMzAzLTEuMmM5LjM5OCwwLjYwMSwxNi44OTgtNy42LDE1LjI5OS0xNi44OTkNCgljLTAuNzk5LTQuOC0xLjY5OC05LjUtMi41LTE0LjNjLTEuMTk4LTYuOS03LjEwMi0xMS45LTE0LTEyLjEwMWMtMTEtMC4zLTIxLjY5OCwwLjctMzIuMjk5LDIuOWMtMTkuNSw0LjEtMzQuMzk5LDEyLjMtNDQuODAzLDI1LjcNCgljLTEwLjM5OCwxMy4zOTktMTQuMSwyOS4xLTExLjEsNDcuM2MyLjIsMTMuMyw4LjIsMjQuOCwxNy44OTksMzQuMTAyYzkuODAxLDkuMywyNC41LDE4LjMsNDQuMzAxLDI2DQoJYzguMTk5LDMuMTk4LDE0LjEwMSw2LDE3LjY5OSw4LjY5OGMzLjYsMi43LDUuNjk5LDYsNi4zOTgsMTBjMC44OTksNS4yMDItMC4xOTksOS45MDEtMy4zOTgsMTQuMjAyDQoJQzg0My41OTYsMjE0LjgxNCw4MzguNzk1LDIxNy45MTQsODMyLjM5NSwyMTkuNzE1eiIvPg0KPHBhdGggZmlsbD0iI0VDNTc2QiIgZD0iTTczMy45OTQsMTI1LjExNGMtMi43OTktMTcuMzk4LTExLjc5OS0zMC45LTI2Ljg5OC00MS4xMDFjLTE1LjE5OC0xMC4xOTktMzcuODk5LTE0LjUtNjguMTk4LTEzLjUNCgljLTguMzAyLDAuMi0xNi43MDEsMC40LTI1LDAuNjAxYy0xNC4zMDIsMC4zLTI1LjEwMSwxMy4xLTIyLjksMjcuM2M5LDU4LjcsMTguMTAyLDExNywyNy4xMDIsMTc0LjgwMQ0KCWMxLjg5OCwxMi4yOTksMTMuMTAxLDIwLjg5OCwyNS41LDE5Ljc5OWMxNC44OTgtMS40LDI5Ljg5OC0yLjksNDQuOC00Ljc5OWMyMi4zMDEtMi43MDEsMzkuMzk5LTExLjIwMSw1MS4yLTI0LjcwMQ0KCWMxMS44LTEzLjUsMTYuMTk5LTI5LjQsMTMuMTk5LTQ3LjVjLTMuMzAzLTIwLjUtMTQuNjAyLTMzLjUtMzMuODAzLTQwLjFjLTIuNzk5LTAuODk4LTMuNS00LjUtMS4zOTgtNi41DQoJQzczMS4yOTUsMTU2LjYxNCw3MzYuNjk1LDE0MS44MTQsNzMzLjk5NCwxMjUuMTE0eiBNNjUxLjU5NiwxNTYuODE0Yy0yLjEwMi0xMy4xMDEtNC4xMDItMjYuMi02LjIwMS0zOS4zMDENCgljLTAuMzk5LTIuMzk5LDEuNS00LjYwMiw0LTQuNjAyYzkuNzAxLDAuMTAyLDE3LjEwMSwxLjUsMjIuMzAyLDQuMmM2LDMuMiw5LjY5OCw5LjEwMiwxMS4xMDIsMTcuODk5DQoJYzIuMTk3LDEzLjg5OC02LjcwMSwyMi41LTI2LjcwMSwyNS4xMDFDNjUzLjg5NSwxNjAuNDE0LDY1MS44OTUsMTU4LjkxNCw2NTEuNTk2LDE1Ni44MTR6IE02OTQuMTk1LDIzOC45MTQNCgljLTUuMzAyLDMuOS0xMy4zOTgsNi42OTktMjQuMzAyLDguMzAxYy0yLjEsMC4zMDEtNC4xOTgtMS4xMDItNC41LTMuMzAxYy0yLTEzLjEtNC4xLTI2LjE5OS02LjE5OC0zOS4zMDENCgljLTAuMzk4LTIuMjk5LDEuMjk5LTQuMzk5LDMuNjAyLTQuNmMyMy0xLjgsMzUuNzk5LDQuNSwzOC4xOTcsMTkuODk5QzcwMi4zOTUsMjI4LjIxNSw3MDAuMDk2LDIzNC41MTQsNjk0LjE5NSwyMzguOTE0eiIvPg0KPHBhdGggZmlsbD0iI0VDNTc2QiIgZD0iTTE1NS45OTUsMzU2LjkxNGMxMi44OTktNC42LDI1LjgtOC42OTksMzguNjk5LTEyLjVjMjIuMzAxLTYuNSwzOS40LTE3LDUxLjItMzIuNg0KCWMxMS44LTE1LjUsMTYuMi0zMy4zMDMsMTMuMi01My45Yy0zLjMtMjIuNS0xNC4xLTM3LjMwMS0zMi41LTQzLjM5OGMtMy41LTEuMTAyLTQuNy01LjQtMi4zLTguMjAxYzEzLjMtMTUuNSwxOC42LTMyLDE1Ljg5OS01MC41OTkNCgljLTIuOC0xOS41LTExLjgtMzQuMzk5LTI2Ljg5OS00NC4yYy0xNS4xMDEtOS42OTktMzcuOC0xMS4zLTY4LjEwMS0xLjhjLTExLjEsMy41LTIyLjE5OSw3LjMtMzMuMywxMS41DQoJYy0xMSw0LjEwMS0xNy43LDE1LjMtMTYsMjYuODk4YzkuMTAxLDYzLjIwMSwxOC4zLDEyNi4yMDEsMjcuNCwxODlDMTI1LjQ5NSwzNTIuNTE0LDE0MS4yOTUsMzYyLjExMywxNTUuOTk1LDM1Ni45MTR6DQoJIE0yMDcuMjk1LDI2NS42MTNjMS4zOTksOS4yMDEtMC45LDE2LjYwMi02LjgsMjIuMzAxYy00LjksNC44MDEtMTIuMzAxLDguOS0yMi4yLDEyLjY5OWMtMywxLjIwMS02LjQtMC43OTktNi45LTQuMTAyDQoJYy0yLTEzLjY5Ny00LTI3LjM5OC02LTQxLjEwMmMtMC4zOTktMi41LDEuMi00Ljg5OCwzLjYwMS01LjY5N0MxOTIuMDk1LDI0Mi43MTUsMjA0Ljg5NSwyNDguNjEzLDIwNy4yOTUsMjY1LjYxM3ogTTE1NS41OTUsMTU0LjgxNA0KCWM5LjctMi40LDE3LjItMi40LDIyLjQtMC40YzYsMi4zMDIsOS42OTksOC40LDExLjEsMTguMTAyYzIuMiwxNC44MDEtNiwyNS4zMDEtMjQuNCwzMi44OTljLTMuMSwxLjMwMS02LjYtMC44MDEtNy4xLTQuMQ0KCWMtMi0xMy41LTMuOS0yNy4xLTUuOS00MC43MDFDMTUxLjI5NSwxNTguMDEzLDE1Mi45OTUsMTU1LjQxNCwxNTUuNTk1LDE1NC44MTR6Ii8+DQo8L3N2Zz4NCg=="> <nav> <a href="https://github.com/g-harel/blobs" style="transform:rotate(1deg) translateY(3px);">GITHUB</a> <a href="https://npmjs.com/package/blobs" style="transform:rotate(-2deg) translateY(-1px);">NPM</a> <a href="mailto:gabrielj.harel@gmail.com" style="transform:rotate(4deg) translateY(1px);">CONTACT</a> </nav> </header> <main> <div class="example"></div> <div class="how-it-works">How it works</div> <div class="container"></div> </main> <script>parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c<t.length;c++)try{f(t[c])}catch(e){i||(i=e)}if(t.length){var l=f(t[t.length-1]);"object"==typeof exports&&"undefined"!=typeof module?module.exports=l:"function"==typeof define&&define.amd?define(function(){return l}):n&&(this[n]=l)}if(parcelRequire=f,i)throw i;return f}({"NSCe":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.splitLine=exports.split=exports.smooth=exports.shift=exports.reverse=exports.rad=exports.mod=exports.mapPoints=exports.length=exports.insertCount=exports.insertAt=exports.forPoints=exports.expandHandle=exports.distance=exports.deg=exports.copyPoint=exports.coordPoint=exports.coordEqual=exports.angleOf=exports.angle=void 0;var n=function(){return(n=Object.assign||function(n){for(var t,r=1,e=arguments.length;r<e;r++)for(var o in t=arguments[r])Object.prototype.hasOwnProperty.call(t,o)&&(n[o]=t[o]);return n}).apply(this,arguments)},t=function(n,t,r){if(r||2===arguments.length)for(var e,o=0,a=t.length;o<a;o++)!e&&o in t||(e||(e=Array.prototype.slice.call(t,0,o)),e[o]=t[o]);return n.concat(e||Array.prototype.slice.call(t))},r=function(t){return{x:t.x,y:t.y,handleIn:n({},t.handleIn),handleOut:n({},t.handleOut)}};exports.copyPoint=r;var e=function(t){return n(n({},t),{handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}})};exports.coordPoint=e;var o=function(n,t){for(var e=function(e){var o=function(t){return r(n[v(t,n.length)])};t({curr:r(n[e]),index:e,sibling:o,prev:function(){return o(e-1)},next:function(){return o(e+1)}})},o=0;o<n.length;o++)e(o)};exports.forPoints=o;var a=function(n,t){var r=[];return o(n,function(n){r.push(t(n))}),r};exports.mapPoints=a;var u=function(n,t){return n.x===t.x&&n.y===t.y};exports.coordEqual=u;var s=function(n,t){var r=t.x-n.x,e=-t.y+n.y,o=Math.atan2(e,r);return o<0?Math.abs(o):2*Math.PI-o};exports.angleOf=s;var l=function(n,t){return{x:n.x+t.length*Math.cos(t.angle),y:n.y+t.length*Math.sin(t.angle)}};exports.expandHandle=l;var i=function(n,t){return{angle:s(n,t),length:Math.sqrt(Math.pow(t.x-n.x,2)+Math.pow(t.y-n.y,2))}},x=function(n,t){var r=l(n,n.handleOut),e=l(t,t.handleIn);return(M(n,t)+M(r,e)+n.handleOut.length+t.handleIn.length)/2};exports.length=x;var p=function(n){return a(n,function(t){var r=t.index,e=(0,t.sibling)(n.length-r-1);return e.handleIn.angle+=Math.PI,e.handleOut.angle+=Math.PI,e})};exports.reverse=p;var h=function(n,t){return a(t,function(t){var r=t.index;return(0,t.sibling)(r+n)})};exports.shift=h;var c=function(n,t,e){var o=r(t);o.handleOut.length*=n;var a=r(e);a.handleIn.length*=1-n;var u=l(t,t.handleOut),s=l(e,e.handleIn),x=l(o,o.handleOut),p=l(a,a.handleIn),h=P(n,u,s),c=P(n,x,h),f=P(1-n,p,h),d=P(n,c,f);return[o,{x:d.x,y:d.y,handleIn:i(d,c),handleOut:i(d,f)},a]};exports.insertAt=c;var f=function n(r,e,o){if(r<2)return[e,o];var a=c(1/r,e,o),u=a[0],s=a[1],l=a[2];return 2===r?[u,s,l]:t([u],n(r-1,s,l),!0)};exports.insertCount=f;var d=function(n,t){return a(n,function(n){var r=n.curr,e=n.next,o=n.prev,a=s(o(),e());return{x:r.x,y:r.y,handleIn:{angle:a+Math.PI,length:t*M(r,o())},handleOut:{angle:a,length:t*M(r,e())}}})};exports.smooth=d;var v=function(n,t){return(n%t+t)%t};exports.mod=v;var g=function(n){return n/360*2*Math.PI};exports.rad=g;var y=function(n){return n/Math.PI*1/2*360};exports.deg=y;var M=function(n,t){return Math.sqrt(Math.pow(n.x-t.x,2)+Math.pow(n.y-t.y,2))};exports.distance=M;var I=function(n,t){return y(Math.atan2(t.y-n.y,t.x-n.x))};exports.angle=I;var O=function(n,t,r){return t+n*(r-t)};exports.split=O;var P=function(n,t,r){return{x:O(n,t.x,r.x),y:O(n,t.y,r.y)}};exports.splitLine=P;
},{}],"5PF2":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.renderPath2D=exports.drawInfo=exports.drawClosed=exports.clear=void 0;var e=require("../util"),t=2,n=20,r=function(e){e.clearRect(0,0,e.canvas.width,e.canvas.height)};exports.clear=r;var o=function(e,t,r,o){e.fillText("".concat(r,": ").concat(o),n,(t+1)*n)};exports.drawInfo=o;var a=function(e,t,n,r){var o=e.strokeStyle;e.beginPath(),e.moveTo(t.x,t.y),e.lineTo(n.x,n.y),e.strokeStyle=r,e.stroke(),e.strokeStyle=o},l=function(e,n,r){var o=e.fillStyle;e.beginPath(),e.arc(n.x,n.y,t,0,2*Math.PI),e.fillStyle=r,e.fill(),e.fillStyle=o},i=function(t,n,r){if(r.length<2)throw new Error("not enough points");n&&(0,e.forPoints)(r,function(n){var r=n.curr,o=(0,n.next)(),i=(0,e.expandHandle)(r,r.handleOut),c=(0,e.expandHandle)(o,o.handleIn);l(t,r,""),a(t,r,i,"#ccc"),a(t,o,c,"#b6b")}),t.stroke(c(r))};exports.drawClosed=i;var c=function(t){var n=new Path2D;return t.length<1?n:(n.moveTo(t[0].x,t[0].y),(0,e.forPoints)(t,function(t){var r=t.curr,o=(0,t.next)(),a=(0,e.expandHandle)(r,r.handleOut),l=(0,e.expandHandle)(o,o.handleIn);n.bezierCurveTo(a.x,a.y,l.x,l.y,o.x,o.y)}),n)};exports.renderPath2D=c;
},{"../util":"NSCe"}],"/uvX":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.buildPermutationTable=y,exports.createNoise2D=f,exports.createNoise3D=u,exports.createNoise4D=p;const t=.5*(Math.sqrt(3)-1),e=(3-Math.sqrt(3))/6,n=1/3,r=1/6,o=(Math.sqrt(5)-1)/4,a=(5-Math.sqrt(5))/20,s=t=>0|Math.floor(t),l=new Float64Array([1,1,-1,1,1,-1,-1,-1,1,0,-1,0,1,0,-1,0,0,1,0,-1,0,1,0,-1]),c=new Float64Array([1,1,0,-1,1,0,1,-1,0,-1,-1,0,1,0,1,-1,0,1,1,0,-1,-1,0,-1,0,1,1,0,-1,1,0,1,-1,0,-1,-1]),i=new Float64Array([0,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,1,0,1,1,1,0,1,-1,1,0,-1,1,1,0,-1,-1,-1,0,1,1,-1,0,1,-1,-1,0,-1,1,-1,0,-1,-1,1,1,0,1,1,1,0,-1,1,-1,0,1,1,-1,0,-1,-1,1,0,1,-1,1,0,-1,-1,-1,0,1,-1,-1,0,-1,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,0]);function f(n=Math.random){const r=y(n),o=new Float64Array(r).map(t=>l[t%12*2]),a=new Float64Array(r).map(t=>l[t%12*2+1]);return function(n,l){let c=0,i=0,f=0;const u=(n+l)*t,p=s(n+u),y=s(l+u),m=(p+y)*e,w=n-(p-m),A=l-(y-m);let F,M;w>A?(F=1,M=0):(F=0,M=1);const h=w-F+e,d=A-M+e,x=w-1+2*e,q=A-1+2*e,b=255&p,D=255&y;let N=.5-w*w-A*A;if(N>=0){const t=b+r[D];c=(N*=N)*N*(o[t]*w+a[t]*A)}let P=.5-h*h-d*d;if(P>=0){const t=b+F+r[D+M];i=(P*=P)*P*(o[t]*h+a[t]*d)}let _=.5-x*x-q*q;if(_>=0){const t=b+1+r[D+1];f=(_*=_)*_*(o[t]*x+a[t]*q)}return 70*(c+i+f)}}function u(t=Math.random){const e=y(t),o=new Float64Array(e).map(t=>c[t%12*3]),a=new Float64Array(e).map(t=>c[t%12*3+1]),l=new Float64Array(e).map(t=>c[t%12*3+2]);return function(t,c,i){let f,u,p,y;const m=(t+c+i)*n,w=s(t+m),A=s(c+m),F=s(i+m),M=(w+A+F)*r,h=t-(w-M),d=c-(A-M),x=i-(F-M);let q,b,D,N,P,_;h>=d?d>=x?(q=1,b=0,D=0,N=1,P=1,_=0):h>=x?(q=1,b=0,D=0,N=1,P=0,_=1):(q=0,b=0,D=1,N=1,P=0,_=1):d<x?(q=0,b=0,D=1,N=0,P=1,_=1):h<x?(q=0,b=1,D=0,N=0,P=1,_=1):(q=0,b=1,D=0,N=1,P=1,_=0);const j=h-q+r,v=d-b+r,O=x-D+r,T=h-N+2*r,U=d-P+2*r,g=x-_+2*r,k=h-1+3*r,z=d-1+3*r,B=x-1+3*r,C=255&w,E=255&A,G=255&F;let H=.6-h*h-d*d-x*x;if(H<0)f=0;else{const t=C+e[E+e[G]];f=(H*=H)*H*(o[t]*h+a[t]*d+l[t]*x)}let I=.6-j*j-v*v-O*O;if(I<0)u=0;else{const t=C+q+e[E+b+e[G+D]];u=(I*=I)*I*(o[t]*j+a[t]*v+l[t]*O)}let J=.6-T*T-U*U-g*g;if(J<0)p=0;else{const t=C+N+e[E+P+e[G+_]];p=(J*=J)*J*(o[t]*T+a[t]*U+l[t]*g)}let K=.6-k*k-z*z-B*B;if(K<0)y=0;else{const t=C+1+e[E+1+e[G+1]];y=(K*=K)*K*(o[t]*k+a[t]*z+l[t]*B)}return 32*(f+u+p+y)}}function p(t=Math.random){const e=y(t),n=new Float64Array(e).map(t=>i[t%32*4]),r=new Float64Array(e).map(t=>i[t%32*4+1]),l=new Float64Array(e).map(t=>i[t%32*4+2]),c=new Float64Array(e).map(t=>i[t%32*4+3]);return function(t,i,f,u){let p,y,m,w,A;const F=(t+i+f+u)*o,M=s(t+F),h=s(i+F),d=s(f+F),x=s(u+F),q=(M+h+d+x)*a,b=t-(M-q),D=i-(h-q),N=f-(d-q),P=u-(x-q);let _=0,j=0,v=0,O=0;b>D?_++:j++,b>N?_++:v++,b>P?_++:O++,D>N?j++:v++,D>P?j++:O++,N>P?v++:O++;const T=_>=3?1:0,U=j>=3?1:0,g=v>=3?1:0,k=O>=3?1:0,z=_>=2?1:0,B=j>=2?1:0,C=v>=2?1:0,E=O>=2?1:0,G=_>=1?1:0,H=j>=1?1:0,I=v>=1?1:0,J=O>=1?1:0,K=b-T+a,L=D-U+a,Q=N-g+a,R=P-k+a,S=b-z+2*a,V=D-B+2*a,W=N-C+2*a,X=P-E+2*a,Y=b-G+3*a,Z=D-H+3*a,$=N-I+3*a,tt=P-J+3*a,et=b-1+4*a,nt=D-1+4*a,rt=N-1+4*a,ot=P-1+4*a,at=255&M,st=255&h,lt=255&d,ct=255&x;let it=.6-b*b-D*D-N*N-P*P;if(it<0)p=0;else{const t=at+e[st+e[lt+e[ct]]];p=(it*=it)*it*(n[t]*b+r[t]*D+l[t]*N+c[t]*P)}let ft=.6-K*K-L*L-Q*Q-R*R;if(ft<0)y=0;else{const t=at+T+e[st+U+e[lt+g+e[ct+k]]];y=(ft*=ft)*ft*(n[t]*K+r[t]*L+l[t]*Q+c[t]*R)}let ut=.6-S*S-V*V-W*W-X*X;if(ut<0)m=0;else{const t=at+z+e[st+B+e[lt+C+e[ct+E]]];m=(ut*=ut)*ut*(n[t]*S+r[t]*V+l[t]*W+c[t]*X)}let pt=.6-Y*Y-Z*Z-$*$-tt*tt;if(pt<0)w=0;else{const t=at+G+e[st+H+e[lt+I+e[ct+J]]];w=(pt*=pt)*pt*(n[t]*Y+r[t]*Z+l[t]*$+c[t]*tt)}let yt=.6-et*et-nt*nt-rt*rt-ot*ot;if(yt<0)A=0;else{const t=at+1+e[st+1+e[lt+1+e[ct+1]]];A=(yt*=yt)*yt*(n[t]*et+r[t]*nt+l[t]*rt+c[t]*ot)}return 27*(p+y+m+w+A)}}function y(t){const e=new Uint8Array(512);for(let n=0;n<256;n++)e[n]=n;for(let n=0;n<255;n++){const r=n+~~(t()*(256-n)),o=e[n];e[n]=e[r],e[r]=o}for(let n=256;n<512;n++)e[n]=e[n-256];return e}
},{}],"BWRk":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.rand=exports.noise=void 0;var r=require("simplex-noise"),e=function(r){var e,n,t,o,u=function(r){for(var e=2166136261,n=0;n<r.length;n++)e=Math.imul(e^r.charCodeAt(n),16777619);return function(){return e+=e<<13,e^=e>>>7,e+=e<<3,e^=e>>>17,(e+=e<<5)>>>0}}(r);return e=u(),n=u(),t=u(),o=u(),function(){var r=(e>>>=0)+(n>>>=0)|0;return e=n^n>>>9,n=(t>>>=0)+(t<<3)|0,t=(t=t<<21|t>>>11)+(r=r+(o=1+(o>>>=0)|0)|0)|0,(r>>>0)/4294967296}};exports.rand=e;var n=function(n){var t=(0,r.createNoise2D)(e(n));return function(r,e){return t(r,e)}};exports.noise=n;
},{"simplex-noise":"/uvX"}],"BJ3L":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.smoothBlob=exports.genFromOptions=exports.genBlobygon=exports.genBlob=void 0;var n=require("../internal/rand"),e=require("../internal/util"),t=require("./util"),r=function(n){var e=2*Math.PI/n.length,r=4/3*Math.tan(e/4)/Math.sin(e/2)/2;return(0,t.smooth)(n,r)};exports.smoothBlob=r;var o=function(n,e){for(var t=2*Math.PI/n,r=[],o=0;o<n;o++){var a=e(o),s=Math.sin(o*t),i=Math.cos(o*t);r.push({x:.5+s*a,y:.5+i*a,handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}})}return r};exports.genBlobygon=o;var a=function(n,e){return r(o(n,e))};exports.genBlob=a;var s=function(t,r){var o=r||(0,n.rand)(String(t.seed)),s=1/(1+t.randomness/10),i=a(3+t.extraPoints,function(n){return(s+o(n)*(1-s))/2}),u=t.size;return(0,e.mapPoints)(i,function(n){var e=n.curr;return e.x*=u,e.y*=u,e.handleIn.length*=u,e.handleOut.length*=u,e})};exports.genFromOptions=s;
},{"../internal/rand":"BWRk","../internal/util":"NSCe","./util":"NSCe"}],"SjCR":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.timingFunctions=void 0;var t=function(t){return t},n=function(t){return 1-Math.pow(t-1,2)},e=function(t){return 1-n(1-t)},i=function(t){return.5+.5*Math.sin(Math.PI*(t+1.5))},r=function(t){return function(n){return Math.pow(2,-10*n)*Math.sin((n-t/4)*(2*Math.PI)/t)+1}},a={linear:t,easeEnd:n,easeStart:e,ease:i,elasticEnd0:r(1),elasticEnd1:r(.64),elasticEnd2:r(.32),elasticEnd3:r(.16)};exports.timingFunctions=a;var s=a;
},{}],"F/j+":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.prepare=exports.divide=void 0;var n=require("../util"),e=function(e,r){var t=e.length,a=1/0,o=0,l=[],u=function(r){for(var u=0;u<t;u++){for(var i=0,h=0;h<t&&!((i+=Math.pow(100*(0,n.distance)(e[h],r[(0,n.mod)(h+u,t)]),2))>a);h++);i<=a&&(a=i,o=u,l=r)}};return u(r),u((0,n.reverse)(r)),(0,n.shift)(o,l)},r=function(e,r){if(r.length<3)throw new Error("not enough points");if(e<r.length)throw new Error("cannot remove points");if(e===r.length)return r.slice();var t=[];(0,n.forPoints)(r,function(e){var r=e.curr,a=e.next;t.push((0,n.length)(r,a()))});for(var a=o(t,e-r.length),l=[],u=0;u<r.length;u++){var i=l[l.length-1]||r[u],h=r[(0,n.mod)(u+1,r.length)];l.pop(),l.push.apply(l,(0,n.insertCount)(a[u],i,h))}var d=l.pop();return l[0]=Object.assign({},l[0],{handleIn:d.handleIn}),l};exports.divide=r;var t=function(e,r){return(0,n.mapPoints)(e,function(e){var t=e.index,a=e.curr,o=e.prev,l=e.next;return 0===a.handleIn.length&&(0,n.coordEqual)(o(),a)&&(a.handleIn.angle=r[t].handleIn.angle),0===a.handleOut.length&&(0,n.coordEqual)(l(),a)&&(a.handleOut.angle=r[t].handleOut.angle),a})},a=function(e){return(0,n.mapPoints)(e,function(e){var r=e.curr,t=e.prev,a=e.next,o=(0,n.angleOf)(t(),a());return 0===r.handleIn.length&&(r.handleIn.angle=o+Math.PI),0===r.handleOut.length&&(r.handleOut.angle=o),r})},o=function(n,e){for(var r=n.map(function(){return 1}),t=n.slice(),a=0;a<e;a++){for(var o=0,l=1;l<t.length;l++)t[l]>t[o]?o=l:t[l]===t[o]&&n[l]>n[o]&&(o=l);r[o]++,t[o]=n[o]/r[o]}return r},l=function(n,o,l){var u=l.divideRatio*Math.max(n.length,o.length),i=r(u,n),h=r(u,o),d=e(i,h);return[l.rawAngles?i:t(a(i),d),l.rawAngles?d:t(a(d),i)]};exports.prepare=l;
},{"../util":"NSCe"}],"/Sl0":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.interpolateBetweenSmooth=exports.interpolateBetween=void 0;var e=require("../util"),n=function(){return(n=Object.assign||function(e){for(var n,t=1,a=arguments.length;t<a;t++)for(var l in n=arguments[t])Object.prototype.hasOwnProperty.call(n,l)&&(e[l]=n[l]);return e}).apply(this,arguments)},t=function(n,t,a){var l=2*Math.PI,h=(0,e.mod)(t,l),r=(0,e.mod)(a,l);return Math.abs(h-r)>Math.PI&&(h<r?h+=l:r+=l),(0,e.split)(n,h,r)},a=function(a,l,h){if(l.length!==h.length)throw new Error("must have equal number of points");for(var r=Math.min(1,Math.max(0,a)),o=[],u=0;u<l.length;u++)o.push(n(n({},(0,e.splitLine)(a,l[u],h[u])),{handleIn:{angle:t(a,l[u].handleIn.angle,h[u].handleIn.angle),length:(0,e.split)(r,l[u].handleIn.length,h[u].handleIn.length)},handleOut:{angle:t(a,l[u].handleOut.angle,h[u].handleOut.angle),length:(0,e.split)(r,l[u].handleOut.length,h[u].handleOut.length)}}));return o};exports.interpolateBetween=a;var l=function(n,l,h,r){n*=Math.min(1,Math.min(Math.abs(0-l),Math.abs(1-l)));var o=a(l,h,r),u=(0,e.smooth)(o,Math.sqrt(n+.25)/3);return(0,e.mapPoints)(o,function(a){var l=a.index,h=a.curr,r=u[l];return h.handleIn.angle=t(n,h.handleIn.angle,r.handleIn.angle),h.handleIn.length=(0,e.split)(n,h.handleIn.length,r.handleIn.length),h.handleOut.angle=t(n,h.handleOut.angle,r.handleOut.angle),h.handleOut.length=(0,e.split)(n,h.handleOut.length,r.handleOut.length),h})};exports.interpolateBetweenSmooth=l;
},{"../util":"NSCe"}],"bUxv":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.transitionFrames=exports.renderFramesAt=void 0;var t=require("./timing"),i=require("./prepare"),e=require("./interpolate"),n=function(){return String(Math.random()).substr(2)},r=function(t){var n,r,a,s=t.renderCache,m=t.currentFrames;if(0===m.length)return{renderCache:s,lastFrameId:null,points:[]};if(1===m.length){var o=m[0];return{renderCache:s,lastFrameId:o.id,points:o.initialPoints}}for(var d=m[0],l=m[1],p=2;p<m.length&&!(l.timestamp>t.timestamp);p++)d=m[p-1],l=m[p];var u=l===m[m.length-1];if(l.timestamp<t.timestamp&&u)return{renderCache:s,lastFrameId:l.id,points:l.initialPoints};var h=null===(r=s[d.id])||void 0===r?void 0:r.preparedStartPoints,F=null===(a=s[l.id])||void 0===a?void 0:a.preparedEndPoints;h&&F||(h=(n=(0,i.prepare)(d.initialPoints,l.initialPoints,{rawAngles:!1,divideRatio:1}))[0],F=n[1],s[d.id]=s[d.id]||{},s[d.id].preparedStartPoints=h,s[l.id]=s[l.id]||{},s[l.id].preparedEndPoints=F);var c=(t.timestamp-d.timestamp)/(l.timestamp-d.timestamp),g=Math.max(0,Math.min(1,c)),v=l.timingFunction(g);return{renderCache:s,lastFrameId:1===g?l.id:d.id,points:(0,e.interpolateBetween)(v,h,F)}};exports.renderFramesAt=r;var a=function(i){var e=[];if(0===i.newFrames.length)return{newFrames:e};var a=r(i);if(null===a.lastFrameId){for(var s=i.shapeGenerator(i.newFrames[0]),m={x:0,y:0,handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}},o=0,d=s;o<d.length;o++){var l=d[o];m.x+=l.x/s.length,m.y+=l.y/s.length}a.points=[m,m,m]}e.push({id:n(),initialPoints:a.points,timestamp:i.timestamp,timingFunction:t.timingFunctions.linear,transitionSourceFrameIndex:-1,isSynthetic:!0});for(var p=0,u=0;u<i.newFrames.length;u++){var h=i.newFrames[u];if(h.delay){p+=h.delay;var F=e[e.length-1];e.push({id:n(),initialPoints:F.initialPoints,timestamp:i.timestamp+p,timingFunction:t.timingFunctions.linear,transitionSourceFrameIndex:u-1,isSynthetic:!0})}p+=h.duration,e.push({id:n(),initialPoints:i.shapeGenerator(h),timestamp:i.timestamp+p,timingFunction:t.timingFunctions[h.timingFunction||"linear"],transitionSourceFrameIndex:u,isSynthetic:!1})}return{newFrames:e}};exports.transitionFrames=a;
},{"./timing":"SjCR","./prepare":"F/j+","./interpolate":"/Sl0"}],"+LE9":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.statefulAnimationGenerator=void 0;var e=require("./frames"),r=function(r,t,n){return function(a){var i=[],o={},s={},u=0,c=0,m=function(){return a()-c},f=function(){return 0!==u},d=function(){f()&&(c+=m()-u,u=0)},l=function(){f()||(u=m())},F=function(){var r=(0,e.renderFramesAt)({renderCache:o,timestamp:f()?u:m(),currentFrames:i});return o=r.renderCache,r.lastFrameId&&s[r.lastFrameId]&&(setTimeout(s[r.lastFrameId]),delete s[r.lastFrameId]),r.points};return{renderFrame:function(){return t(F())},renderPoints:F,transition:function(){for(var t=[],a=0;a<arguments.length;a++)t[a]=arguments[a];for(var u=0;u<t.length;u++)n(t[u],u);var c=(0,e.transitionFrames)({renderCache:o,timestamp:m(),currentFrames:i,newFrames:t,shapeGenerator:r});i=c.newFrames,s={},o={};for(var f=0,d=i;f<d.length;f++){var l=d[f];if(!l.isSynthetic){var F=t[l.transitionSourceFrameIndex].callback;F&&(s[l.id]=F)}}},play:d,pause:l,playPause:function(){f()?d():l()}}}};exports.statefulAnimationGenerator=r;
},{"./frames":"bUxv"}],"Aed7":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.checkSvgOptions=exports.checkPoints=exports.checkKeyframeOptions=exports.checkCanvasOptions=exports.checkBlobOptions=void 0;var n=require("./animate/timing");function t(n){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}var e=function(n,e,o){var i=t(e);if("number"===i&&isNaN(e)&&(i="NaN"),"object"===i&&null===e&&(i="null"),!o.includes(i))throw'"'.concat(n,'" should have type "').concat(o.join("|"),'" but was "').concat(i,'".')},o=function(t){e("keyframe",t,["object"]);var o=t.delay,i=t.duration,r=t.timingFunction,s=t.callback;if(e("delay",o,["number","undefined"]),o&&o<0)throw'delay is invalid "'.concat(o,'".');if(e("duration",i,["number"]),i&&i<0)throw'duration is invalid "'.concat(i,'".');if(e("timingFunction",r,["string","undefined"]),r&&!n.timingFunctions[r])throw'".timingFunction" is not recognized "'.concat(r,'".');e("callback",s,["function","undefined"])};exports.checkKeyframeOptions=o;var i=function(n){e("blobOptions",n,["object"]);var t=n.seed,o=n.extraPoints,i=n.randomness,r=n.size;if(e("blobOptions.seed",t,["string","number"]),e("blobOptions.extraPoints",o,["number"]),o<0)throw'blobOptions.extraPoints is invalid "'.concat(o,'".');if(e("blobOptions.randomness",i,["number"]),i<0)throw'blobOptions.randomness is invalid "'.concat(i,'".');if(e("blobOptions.size",r,["number"]),r<0)throw'blobOptions.size is invalid "'.concat(r,'".')};exports.checkBlobOptions=i;var r=function(n){if(e("canvasOptions",n,["object","undefined"]),n){var t=n.offsetX,o=n.offsetY;e("canvasOptions.offsetX",t,["number","undefined"]),e("canvasOptions.offsetY",o,["number","undefined"])}};exports.checkCanvasOptions=r;var s=function(n){if(e("svgOptions",n,["object","undefined"]),n){var t=n.fill,o=n.stroke,i=n.strokeWidth;e("svgOptions.fill",t,["string","undefined"]),e("svgOptions.stroke",o,["string","undefined"]),e("svgOptions.strokeWidth",i,["number","undefined"])}};exports.checkSvgOptions=s;var a=function(n){if(!Array.isArray(n))throw'points should be an array but was "'.concat(t(n),'".');if(n.length<3)throw'expected more than two points but received "'.concat(n.length,'".');for(var o=0,i=n;o<i.length;o++){var r=i[o];e("point.x",r.x,["number"]),e("point.y",r.y,["number"]),e("point.handleIn",r.handleIn,["object"]),e("point.handleIn.angle",r.handleIn.angle,["number"]),e("point.handleIn.length",r.handleIn.length,["number"]),e("point.handleOut",r.handleOut,["object"]),e("point.handleOut.angle",r.handleOut.angle,["number"]),e("point.handleOut.length",r.handleOut.length,["number"])}};exports.checkPoints=a;
},{"./animate/timing":"SjCR"}],"+HZB":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.wigglePreset=exports.canvasPath=void 0;var n=require("../internal/render/canvas"),e=require("../internal/gen"),t=require("../internal/util"),i=require("../internal/animate/state"),r=require("../internal/check"),a=require("../internal/rand"),o=require("../internal/animate/interpolate"),s=require("../internal/animate/prepare"),c=function(n){var i;return i="points"in n?n.points:(0,e.genFromOptions)(n.blobOptions),(0,t.mapPoints)(i,function(e){var t,i,r=e.curr;return r.x+=(null===(t=null==n?void 0:n.canvasOptions)||void 0===t?void 0:t.offsetX)||0,r.y+=(null===(i=null==n?void 0:n.canvasOptions)||void 0===i?void 0:i.offsetY)||0,r})},l=function(n,e){try{if("points"in n)return(0,r.checkPoints)(n.points);(0,r.checkBlobOptions)(n.blobOptions),(0,r.checkCanvasOptions)(n.canvasOptions),(0,r.checkKeyframeOptions)(n)}catch(t){throw"(blobs2): keyframe ".concat(e,": ").concat(t)}},u=function(e){var t=Date.now;if(void 0!==e){var r=0;t=function(){var n=e();if(n<r)throw"timestamp provider generated decreasing value: ".concat(r," then ").concat(n,".");return r=n,n}}return(0,i.statefulAnimationGenerator)(c,n.renderPath2D,l)(t)};exports.canvasPath=u;var p=function(n,t,i,r){var c=.01*r.speed,l=(0,a.noise)(String(t.seed)),u=Math.min((r.initialTransition||0)/160),p=n.renderPoints(),v=0;!function r(){v++;var a=(0,e.genFromOptions)(t,function(n){return l(c*v,n)});if(v<u){var d=(0,s.prepare)(p,a,{rawAngles:!0,divideRatio:1}),f=d[0],m=d[1],h=Math.min(1,2/(u-v)),g=(0,o.interpolateBetween)(h,f,m);p=g,n.transition({duration:160,delay:0,timingFunction:"linear",canvasOptions:i,points:g,callback:r})}else n.transition({duration:160,delay:0,timingFunction:"linear",canvasOptions:i,points:a,callback:r})}()};exports.wigglePreset=p;
},{"../internal/render/canvas":"5PF2","../internal/gen":"BJ3L","../internal/util":"NSCe","../internal/animate/state":"+LE9","../internal/check":"Aed7","../internal/rand":"BWRk","../internal/animate/interpolate":"/Sl0","../internal/animate/prepare":"F/j+"}],"q9J3":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.onDebugStateChange=exports.isDebug=void 0;var e=window.location.search.includes("debug")&&"localhost"===location.hostname,t=function(){return e};exports.isDebug=t;var o=[],n=function(t){o.push(t),t(e)};if(exports.onDebugStateChange=n,e&&document.body){var r=document.createElement("button");r.innerHTML="debug",r.style.padding="2rem",r.style.position="fixed",r.style.top="0",r.onclick=function(){e=!e;for(var t=0,n=o;t<n.length;t++){(0,n[t])(e)}},document.body.prepend(r)}
},{}],"rSMP":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.sizes=exports.colors=exports.addTitle=exports.addCanvas=void 0;var e=require("./canvas"),t=require("./debug"),n={debug:"green",highlight:"#ec576b",secondary:"#555"};exports.colors=n;var a=window.cells||[];window.cells=a;var i=document.querySelector(".container");if(!i)throw"missing container";var r=document.querySelector(".how-it-works");if(!r)throw"missing container";var o=!1,c=function(){i.classList.add("open"),r.classList.add("hidden"),o=!0,h()};r.addEventListener("click",c),(document.location.hash||(0,t.isDebug)())&&setTimeout(c);var d=function(){var e=window.getComputedStyle(i.lastChild||document.body),t=Number(e.getPropertyValue("width").slice(0,-2))*window.devicePixelRatio;return{width:t,pt:.002*t}};exports.sizes=d;var s=function(){var e=("000"+a.length).substr(-3),t=document.createElement("div");t.classList.add("section"),t.setAttribute("id",e),i.appendChild(t);var n=document.createElement("a");return n.classList.add("number"),n.setAttribute("href","#"+e),n.appendChild(document.createTextNode(e)),t.appendChild(n),t},l=function(e,t){var n=document.createElement("h".concat(e));n.classList.add("title"),i.appendChild(n);var a=document.createElement("div");a.classList.add("text"),n.appendChild(a),t=t.replace("\n"," ").replace(/\s+/g," ").trim();var r=document.createTextNode(t);a.appendChild(r)};exports.addTitle=l;var u=function(e){e.map(function(e){e.target.setAttribute("data-visible",e.isIntersecting)})},v=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var i=s();0==t.length&&(t=[function(){}]);for(var r=[],o=0,c=t;o<c.length;o++){var d=c[o],l=document.createElement("div");l.classList.add("cell"),i.appendChild(l);var v=document.createElement("canvas");l.appendChild(v);var p=document.createElement("div");p.classList.add("label"),l.appendChild(p);var m=v.getContext("2d");if(!m)throw"missing canvas context";var f={aspectRatio:e,canvas:v,ctx:m,painter:d,animationID:-1};r.push(f),new IntersectionObserver(u,{threshold:.1}).observe(v)}a.push(r),h()};exports.addCanvas=v;var p=void 0,h=function(){window.clearTimeout(p),p=window.setTimeout(function(){for(var i=function(a){for(var i=d().width/a.length,r=function(r){var c=i/r.aspectRatio;r.canvas.width=i,r.canvas.height=c;var d=function(){(0,t.isDebug)()&&(0,e.tempStyles)(r.ctx,function(){return r.ctx.strokeStyle=n.debug},function(){return r.ctx.strokeRect(0,0,i,c-1)})};d();var s=0,l=0;r.canvas.onclick=function(){0===s?s=Date.now():(l+=Date.now()-s,s=0)};var u=r.painter(r.ctx,i,c,function(a){if(o){var u=Math.random(),v=Date.now();r.animationID=u,function o(){if(r.animationID===u){var p="true"===r.canvas.getAttribute("data-visible");if(0===s&&p){var h=Date.now()-v-l;r.ctx.clearRect(0,0,i,c),d(),(0,t.isDebug)()&&(0,e.tempStyles)(r.ctx,function(){return r.ctx.fillStyle=n.debug},function(){return r.ctx.fillText(String(h),10,15)}),a(h)}requestAnimationFrame(o)}}()}});if(u){var v=r.canvas.parentElement;if(v){v.style.width="".concat(100/a.length,"%");var p=v.querySelector(".label");p&&p.innerHTML!==u&&(p.innerHTML="",p.innerHTML=u)}}},c=0,s=a;c<s.length;c++){r(s[c])}},r=0,c=a;r<c.length;r++){i(c[r])}},100)};window.addEventListener("load",h),window.addEventListener("resize",h),(0,t.onDebugStateChange)(h);
},{"./canvas":"PBVq","./debug":"q9J3"}],"PBVq":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.tempStyles=exports.rotateAround=exports.point=exports.forceStyles=exports.drawPoint=exports.drawOpen=exports.drawLine=exports.drawHandles=exports.drawDebugClosed=exports.drawClosed=exports.calcBouncePercentage=void 0;var e=require("../../internal/util"),t=require("../internal/debug"),n=require("../internal/layout"),r=function(e,t){e.forcedStyles||(e.forcedStyles=0),e.forcedStyles++,e.save(),t(),e.restore(),e.forcedStyles--};exports.forceStyles=r;var o=function(e,t,n){e.forcedStyles>0?n():(e.save(),t(),n(),e.restore())};exports.tempStyles=o;var a=function(e,r){o(e.ctx,function(){e.ctx.translate(e.cx,e.cy),e.ctx.rotate(e.angle)},function(){(0,t.isDebug)()&&o(e.ctx,function(){return e.ctx.fillStyle=n.colors.debug},function(){e.ctx.fillRect(0,-4,1,8),e.ctx.fillRect(-32,0,64,1)}),r()})};exports.rotateAround=a;var i=function(t,n,r,o,a,i){return{x:t,y:n,handleIn:{angle:(0,e.rad)(r),length:o},handleOut:{angle:(0,e.rad)(a),length:i}}};exports.point=i;var l=function(e,t,r,a){var i=(0,n.sizes)().pt*r,l=new Path2D;l.arc(t.x,t.y,i,0,2*Math.PI),e.fill(l),a&&o(e,function(){return e.font="".concat(6*i,"px monospace")},function(){return e.fillText(a,t.x+2*i,t.y-i)})};exports.drawPoint=l;var s=function(e,t,r,a,i){o(e,function(){var t=(0,n.sizes)().pt*a;i&&e.setLineDash([i*t])},function(){var o=(0,n.sizes)().pt*a,i=new Path2D;i.moveTo(t.x,t.y),i.lineTo(r.x,r.y),e.lineWidth=o,e.stroke(i)})};exports.drawLine=s;var c=function(t,n,r){(0,e.forPoints)(n,function(e){var n=e.curr,o=e.next;x(t,n,o(),r)})};exports.drawClosed=c;var u=function(t,r,o){(0,e.forPoints)(r,function(r){var a=r.curr,i=r.next;d(t,a,o);var s=i(),c=(0,e.expandHandle)(a,a.handleIn),u=(0,e.expandHandle)(a,a.handleOut),x=new Path2D;x.moveTo(a.x,a.y),x.bezierCurveTo(c.x,c.y,u.x,u.y,s.x,s.y),t.lineWidth=(0,n.sizes)().pt*o*2,t.stroke(x),l(t,a,1.1*o)})};exports.drawDebugClosed=u;var d=function(t,n,r){var o=(0,e.expandHandle)(n,n.handleIn),a=(0,e.expandHandle)(n,n.handleOut);s(t,n,o,r),s(t,n,a,r,2),l(t,o,1.4*r),l(t,a,1.4*r)};exports.drawHandles=d;var x=function(t,r,a,i){var c=(0,n.sizes)().width,u=(0,e.expandHandle)(r,r.handleOut),d=(0,e.expandHandle)(a,a.handleIn);i&&o(t,function(){t.fillStyle=n.colors.secondary,t.strokeStyle=n.colors.secondary},function(){s(t,r,u,1),s(t,a,d,1,2),l(t,u,1.4),l(t,d,1.4)}),o(t,function(){var e=.003*c;t.lineWidth=e},function(){var e=new Path2D;e.moveTo(r.x,r.y),e.bezierCurveTo(u.x,u.y,d.x,d.y,a.x,a.y),o(t,function(){return t.strokeStyle=n.colors.highlight},function(){return t.stroke(e)}),o(t,function(){return t.fillStyle=n.colors.highlight},function(){l(t,r,2),l(t,a,2)})})};exports.drawOpen=x;var f=function(t,n,r){var o=t/2,a=(0,e.mod)(r,t);return n(a<=o?a/o:1-(a-o)/o)};exports.calcBouncePercentage=f;
},{"../../internal/util":"NSCe","../internal/debug":"q9J3","../internal/layout":"rSMP"}],"0UHT":[function(require,module,exports) {
"use strict";var n=require("../public/animate"),e=require("./internal/canvas"),t=require("./internal/debug"),i=require("./internal/layout"),r=function(){return(r=Object.assign||function(n){for(var e,t=1,i=arguments.length;t<i;t++)for(var r in e=arguments[t])Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}).apply(this,arguments)},a=document.querySelector(".example"),o=document.createElement("canvas");a.appendChild(o);var s=0,c=function(){var n=Math.min(600,Math.min(window.innerWidth-64,window.innerHeight/2));o.style.width="".concat(n,"px"),o.style.height="".concat(n,"px"),s=n*(window.devicePixelRatio||1),o.width=s,o.height=s},d=o.getContext("2d"),l=(0,n.canvasPath)(),u=function n(){if(d.clearRect(0,0,s,s),d.fillStyle=i.colors.highlight,d.strokeStyle=i.colors.highlight,(0,t.isDebug)())for(var r=0,a=l.renderPoints();r<a.length;r++){var o=a[r];(0,e.drawPoint)(d,o,2),(0,e.drawHandles)(d,o,1)}d.fill(l.renderFrame()),requestAnimationFrame(n)};requestAnimationFrame(u);var h=0,f=function(e){(0,n.wigglePreset)(l,{extraPoints:3+h,randomness:1.5,seed:Math.random(),size:s},{},{speed:2,initialTransition:e})},m=function(n){void 0===n&&(n={});var e=r({extraPoints:3+h,randomness:4,seed:Math.random(),size:s},n.blobOptions);return r(r({duration:4e3,timingFunction:"ease",callback:g},n),{blobOptions:e})},g=function(){h=0,f(5e3)};o.onclick=function(){h++,l.transition(m({duration:400,timingFunction:"elasticEnd0",blobOptions:{extraPoints:h}}))},window.addEventListener("load",function(){c(),f(0)}),window.addEventListener("resize",function(){c();var n=6*s/7;l.transition(m({duration:100,timingFunction:"easeEnd",blobOptions:{extraPoints:0,randomness:0,seed:"",size:n},canvasOptions:{offsetX:(s-n)/2,offsetY:(s-n)/2}}))});
},{"../public/animate":"+HZB","./internal/canvas":"PBVq","./internal/debug":"q9J3","./internal/layout":"rSMP"}]},{},["0UHT"], null)
//# sourceMappingURL=/example.255e5482.js.map</script> <script>parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c<t.length;c++)try{f(t[c])}catch(e){i||(i=e)}if(t.length){var l=f(t[t.length-1]);"object"==typeof exports&&"undefined"!=typeof module?module.exports=l:"function"==typeof define&&define.amd?define(function(){return l}):n&&(this[n]=l)}if(parcelRequire=f,i)throw i;return f}({"NSCe":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.splitLine=exports.split=exports.smooth=exports.shift=exports.reverse=exports.rad=exports.mod=exports.mapPoints=exports.length=exports.insertCount=exports.insertAt=exports.forPoints=exports.expandHandle=exports.distance=exports.deg=exports.copyPoint=exports.coordPoint=exports.coordEqual=exports.angleOf=exports.angle=void 0;var n=function(){return(n=Object.assign||function(n){for(var t,r=1,e=arguments.length;r<e;r++)for(var o in t=arguments[r])Object.prototype.hasOwnProperty.call(t,o)&&(n[o]=t[o]);return n}).apply(this,arguments)},t=function(n,t,r){if(r||2===arguments.length)for(var e,o=0,a=t.length;o<a;o++)!e&&o in t||(e||(e=Array.prototype.slice.call(t,0,o)),e[o]=t[o]);return n.concat(e||Array.prototype.slice.call(t))},r=function(t){return{x:t.x,y:t.y,handleIn:n({},t.handleIn),handleOut:n({},t.handleOut)}};exports.copyPoint=r;var e=function(t){return n(n({},t),{handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}})};exports.coordPoint=e;var o=function(n,t){for(var e=function(e){var o=function(t){return r(n[v(t,n.length)])};t({curr:r(n[e]),index:e,sibling:o,prev:function(){return o(e-1)},next:function(){return o(e+1)}})},o=0;o<n.length;o++)e(o)};exports.forPoints=o;var a=function(n,t){var r=[];return o(n,function(n){r.push(t(n))}),r};exports.mapPoints=a;var u=function(n,t){return n.x===t.x&&n.y===t.y};exports.coordEqual=u;var s=function(n,t){var r=t.x-n.x,e=-t.y+n.y,o=Math.atan2(e,r);return o<0?Math.abs(o):2*Math.PI-o};exports.angleOf=s;var l=function(n,t){return{x:n.x+t.length*Math.cos(t.angle),y:n.y+t.length*Math.sin(t.angle)}};exports.expandHandle=l;var i=function(n,t){return{angle:s(n,t),length:Math.sqrt(Math.pow(t.x-n.x,2)+Math.pow(t.y-n.y,2))}},x=function(n,t){var r=l(n,n.handleOut),e=l(t,t.handleIn);return(M(n,t)+M(r,e)+n.handleOut.length+t.handleIn.length)/2};exports.length=x;var p=function(n){return a(n,function(t){var r=t.index,e=(0,t.sibling)(n.length-r-1);return e.handleIn.angle+=Math.PI,e.handleOut.angle+=Math.PI,e})};exports.reverse=p;var h=function(n,t){return a(t,function(t){var r=t.index;return(0,t.sibling)(r+n)})};exports.shift=h;var c=function(n,t,e){var o=r(t);o.handleOut.length*=n;var a=r(e);a.handleIn.length*=1-n;var u=l(t,t.handleOut),s=l(e,e.handleIn),x=l(o,o.handleOut),p=l(a,a.handleIn),h=P(n,u,s),c=P(n,x,h),f=P(1-n,p,h),d=P(n,c,f);return[o,{x:d.x,y:d.y,handleIn:i(d,c),handleOut:i(d,f)},a]};exports.insertAt=c;var f=function n(r,e,o){if(r<2)return[e,o];var a=c(1/r,e,o),u=a[0],s=a[1],l=a[2];return 2===r?[u,s,l]:t([u],n(r-1,s,l),!0)};exports.insertCount=f;var d=function(n,t){return a(n,function(n){var r=n.curr,e=n.next,o=n.prev,a=s(o(),e());return{x:r.x,y:r.y,handleIn:{angle:a+Math.PI,length:t*M(r,o())},handleOut:{angle:a,length:t*M(r,e())}}})};exports.smooth=d;var v=function(n,t){return(n%t+t)%t};exports.mod=v;var g=function(n){return n/360*2*Math.PI};exports.rad=g;var y=function(n){return n/Math.PI*1/2*360};exports.deg=y;var M=function(n,t){return Math.sqrt(Math.pow(n.x-t.x,2)+Math.pow(n.y-t.y,2))};exports.distance=M;var I=function(n,t){return y(Math.atan2(t.y-n.y,t.x-n.x))};exports.angle=I;var O=function(n,t,r){return t+n*(r-t)};exports.split=O;var P=function(n,t,r){return{x:O(n,t.x,r.x),y:O(n,t.y,r.y)}};exports.splitLine=P;
},{}],"q9J3":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.onDebugStateChange=exports.isDebug=void 0;var e=window.location.search.includes("debug")&&"localhost"===location.hostname,t=function(){return e};exports.isDebug=t;var o=[],n=function(t){o.push(t),t(e)};if(exports.onDebugStateChange=n,e&&document.body){var r=document.createElement("button");r.innerHTML="debug",r.style.padding="2rem",r.style.position="fixed",r.style.top="0",r.onclick=function(){e=!e;for(var t=0,n=o;t<n.length;t++){(0,n[t])(e)}},document.body.prepend(r)}
},{}],"PBVq":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.tempStyles=exports.rotateAround=exports.point=exports.forceStyles=exports.drawPoint=exports.drawOpen=exports.drawLine=exports.drawHandles=exports.drawDebugClosed=exports.drawClosed=exports.calcBouncePercentage=void 0;var e=require("../../internal/util"),t=require("../internal/debug"),n=require("../internal/layout"),r=function(e,t){e.forcedStyles||(e.forcedStyles=0),e.forcedStyles++,e.save(),t(),e.restore(),e.forcedStyles--};exports.forceStyles=r;var o=function(e,t,n){e.forcedStyles>0?n():(e.save(),t(),n(),e.restore())};exports.tempStyles=o;var a=function(e,r){o(e.ctx,function(){e.ctx.translate(e.cx,e.cy),e.ctx.rotate(e.angle)},function(){(0,t.isDebug)()&&o(e.ctx,function(){return e.ctx.fillStyle=n.colors.debug},function(){e.ctx.fillRect(0,-4,1,8),e.ctx.fillRect(-32,0,64,1)}),r()})};exports.rotateAround=a;var i=function(t,n,r,o,a,i){return{x:t,y:n,handleIn:{angle:(0,e.rad)(r),length:o},handleOut:{angle:(0,e.rad)(a),length:i}}};exports.point=i;var l=function(e,t,r,a){var i=(0,n.sizes)().pt*r,l=new Path2D;l.arc(t.x,t.y,i,0,2*Math.PI),e.fill(l),a&&o(e,function(){return e.font="".concat(6*i,"px monospace")},function(){return e.fillText(a,t.x+2*i,t.y-i)})};exports.drawPoint=l;var s=function(e,t,r,a,i){o(e,function(){var t=(0,n.sizes)().pt*a;i&&e.setLineDash([i*t])},function(){var o=(0,n.sizes)().pt*a,i=new Path2D;i.moveTo(t.x,t.y),i.lineTo(r.x,r.y),e.lineWidth=o,e.stroke(i)})};exports.drawLine=s;var c=function(t,n,r){(0,e.forPoints)(n,function(e){var n=e.curr,o=e.next;x(t,n,o(),r)})};exports.drawClosed=c;var u=function(t,r,o){(0,e.forPoints)(r,function(r){var a=r.curr,i=r.next;d(t,a,o);var s=i(),c=(0,e.expandHandle)(a,a.handleIn),u=(0,e.expandHandle)(a,a.handleOut),x=new Path2D;x.moveTo(a.x,a.y),x.bezierCurveTo(c.x,c.y,u.x,u.y,s.x,s.y),t.lineWidth=(0,n.sizes)().pt*o*2,t.stroke(x),l(t,a,1.1*o)})};exports.drawDebugClosed=u;var d=function(t,n,r){var o=(0,e.expandHandle)(n,n.handleIn),a=(0,e.expandHandle)(n,n.handleOut);s(t,n,o,r),s(t,n,a,r,2),l(t,o,1.4*r),l(t,a,1.4*r)};exports.drawHandles=d;var x=function(t,r,a,i){var c=(0,n.sizes)().width,u=(0,e.expandHandle)(r,r.handleOut),d=(0,e.expandHandle)(a,a.handleIn);i&&o(t,function(){t.fillStyle=n.colors.secondary,t.strokeStyle=n.colors.secondary},function(){s(t,r,u,1),s(t,a,d,1,2),l(t,u,1.4),l(t,d,1.4)}),o(t,function(){var e=.003*c;t.lineWidth=e},function(){var e=new Path2D;e.moveTo(r.x,r.y),e.bezierCurveTo(u.x,u.y,d.x,d.y,a.x,a.y),o(t,function(){return t.strokeStyle=n.colors.highlight},function(){return t.stroke(e)}),o(t,function(){return t.fillStyle=n.colors.highlight},function(){l(t,r,2),l(t,a,2)})})};exports.drawOpen=x;var f=function(t,n,r){var o=t/2,a=(0,e.mod)(r,t);return n(a<=o?a/o:1-(a-o)/o)};exports.calcBouncePercentage=f;
},{"../../internal/util":"NSCe","../internal/debug":"q9J3","../internal/layout":"rSMP"}],"rSMP":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.sizes=exports.colors=exports.addTitle=exports.addCanvas=void 0;var e=require("./canvas"),t=require("./debug"),n={debug:"green",highlight:"#ec576b",secondary:"#555"};exports.colors=n;var a=window.cells||[];window.cells=a;var i=document.querySelector(".container");if(!i)throw"missing container";var r=document.querySelector(".how-it-works");if(!r)throw"missing container";var o=!1,c=function(){i.classList.add("open"),r.classList.add("hidden"),o=!0,h()};r.addEventListener("click",c),(document.location.hash||(0,t.isDebug)())&&setTimeout(c);var d=function(){var e=window.getComputedStyle(i.lastChild||document.body),t=Number(e.getPropertyValue("width").slice(0,-2))*window.devicePixelRatio;return{width:t,pt:.002*t}};exports.sizes=d;var s=function(){var e=("000"+a.length).substr(-3),t=document.createElement("div");t.classList.add("section"),t.setAttribute("id",e),i.appendChild(t);var n=document.createElement("a");return n.classList.add("number"),n.setAttribute("href","#"+e),n.appendChild(document.createTextNode(e)),t.appendChild(n),t},l=function(e,t){var n=document.createElement("h".concat(e));n.classList.add("title"),i.appendChild(n);var a=document.createElement("div");a.classList.add("text"),n.appendChild(a),t=t.replace("\n"," ").replace(/\s+/g," ").trim();var r=document.createTextNode(t);a.appendChild(r)};exports.addTitle=l;var u=function(e){e.map(function(e){e.target.setAttribute("data-visible",e.isIntersecting)})},v=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var i=s();0==t.length&&(t=[function(){}]);for(var r=[],o=0,c=t;o<c.length;o++){var d=c[o],l=document.createElement("div");l.classList.add("cell"),i.appendChild(l);var v=document.createElement("canvas");l.appendChild(v);var p=document.createElement("div");p.classList.add("label"),l.appendChild(p);var m=v.getContext("2d");if(!m)throw"missing canvas context";var f={aspectRatio:e,canvas:v,ctx:m,painter:d,animationID:-1};r.push(f),new IntersectionObserver(u,{threshold:.1}).observe(v)}a.push(r),h()};exports.addCanvas=v;var p=void 0,h=function(){window.clearTimeout(p),p=window.setTimeout(function(){for(var i=function(a){for(var i=d().width/a.length,r=function(r){var c=i/r.aspectRatio;r.canvas.width=i,r.canvas.height=c;var d=function(){(0,t.isDebug)()&&(0,e.tempStyles)(r.ctx,function(){return r.ctx.strokeStyle=n.debug},function(){return r.ctx.strokeRect(0,0,i,c-1)})};d();var s=0,l=0;r.canvas.onclick=function(){0===s?s=Date.now():(l+=Date.now()-s,s=0)};var u=r.painter(r.ctx,i,c,function(a){if(o){var u=Math.random(),v=Date.now();r.animationID=u,function o(){if(r.animationID===u){var p="true"===r.canvas.getAttribute("data-visible");if(0===s&&p){var h=Date.now()-v-l;r.ctx.clearRect(0,0,i,c),d(),(0,t.isDebug)()&&(0,e.tempStyles)(r.ctx,function(){return r.ctx.fillStyle=n.debug},function(){return r.ctx.fillText(String(h),10,15)}),a(h)}requestAnimationFrame(o)}}()}});if(u){var v=r.canvas.parentElement;if(v){v.style.width="".concat(100/a.length,"%");var p=v.querySelector(".label");p&&p.innerHTML!==u&&(p.innerHTML="",p.innerHTML=u)}}},c=0,s=a;c<s.length;c++){r(s[c])}},r=0,c=a;r<c.length;r++){i(c[r])}},100)};window.addEventListener("load",h),window.addEventListener("resize",h),(0,t.onDebugStateChange)(h);
},{"./canvas":"PBVq","./debug":"q9J3"}],"SjCR":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.timingFunctions=void 0;var t=function(t){return t},n=function(t){return 1-Math.pow(t-1,2)},e=function(t){return 1-n(1-t)},i=function(t){return.5+.5*Math.sin(Math.PI*(t+1.5))},r=function(t){return function(n){return Math.pow(2,-10*n)*Math.sin((n-t/4)*(2*Math.PI)/t)+1}},a={linear:t,easeEnd:n,easeStart:e,ease:i,elasticEnd0:r(1),elasticEnd1:r(.64),elasticEnd2:r(.32),elasticEnd3:r(.16)};exports.timingFunctions=a;var s=a;
},{}],"/uvX":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.buildPermutationTable=y,exports.createNoise2D=f,exports.createNoise3D=u,exports.createNoise4D=p;const t=.5*(Math.sqrt(3)-1),e=(3-Math.sqrt(3))/6,n=1/3,r=1/6,o=(Math.sqrt(5)-1)/4,a=(5-Math.sqrt(5))/20,s=t=>0|Math.floor(t),l=new Float64Array([1,1,-1,1,1,-1,-1,-1,1,0,-1,0,1,0,-1,0,0,1,0,-1,0,1,0,-1]),c=new Float64Array([1,1,0,-1,1,0,1,-1,0,-1,-1,0,1,0,1,-1,0,1,1,0,-1,-1,0,-1,0,1,1,0,-1,1,0,1,-1,0,-1,-1]),i=new Float64Array([0,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,1,0,1,1,1,0,1,-1,1,0,-1,1,1,0,-1,-1,-1,0,1,1,-1,0,1,-1,-1,0,-1,1,-1,0,-1,-1,1,1,0,1,1,1,0,-1,1,-1,0,1,1,-1,0,-1,-1,1,0,1,-1,1,0,-1,-1,-1,0,1,-1,-1,0,-1,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,0]);function f(n=Math.random){const r=y(n),o=new Float64Array(r).map(t=>l[t%12*2]),a=new Float64Array(r).map(t=>l[t%12*2+1]);return function(n,l){let c=0,i=0,f=0;const u=(n+l)*t,p=s(n+u),y=s(l+u),m=(p+y)*e,w=n-(p-m),A=l-(y-m);let F,M;w>A?(F=1,M=0):(F=0,M=1);const h=w-F+e,d=A-M+e,x=w-1+2*e,q=A-1+2*e,b=255&p,D=255&y;let N=.5-w*w-A*A;if(N>=0){const t=b+r[D];c=(N*=N)*N*(o[t]*w+a[t]*A)}let P=.5-h*h-d*d;if(P>=0){const t=b+F+r[D+M];i=(P*=P)*P*(o[t]*h+a[t]*d)}let _=.5-x*x-q*q;if(_>=0){const t=b+1+r[D+1];f=(_*=_)*_*(o[t]*x+a[t]*q)}return 70*(c+i+f)}}function u(t=Math.random){const e=y(t),o=new Float64Array(e).map(t=>c[t%12*3]),a=new Float64Array(e).map(t=>c[t%12*3+1]),l=new Float64Array(e).map(t=>c[t%12*3+2]);return function(t,c,i){let f,u,p,y;const m=(t+c+i)*n,w=s(t+m),A=s(c+m),F=s(i+m),M=(w+A+F)*r,h=t-(w-M),d=c-(A-M),x=i-(F-M);let q,b,D,N,P,_;h>=d?d>=x?(q=1,b=0,D=0,N=1,P=1,_=0):h>=x?(q=1,b=0,D=0,N=1,P=0,_=1):(q=0,b=0,D=1,N=1,P=0,_=1):d<x?(q=0,b=0,D=1,N=0,P=1,_=1):h<x?(q=0,b=1,D=0,N=0,P=1,_=1):(q=0,b=1,D=0,N=1,P=1,_=0);const j=h-q+r,v=d-b+r,O=x-D+r,T=h-N+2*r,U=d-P+2*r,g=x-_+2*r,k=h-1+3*r,z=d-1+3*r,B=x-1+3*r,C=255&w,E=255&A,G=255&F;let H=.6-h*h-d*d-x*x;if(H<0)f=0;else{const t=C+e[E+e[G]];f=(H*=H)*H*(o[t]*h+a[t]*d+l[t]*x)}let I=.6-j*j-v*v-O*O;if(I<0)u=0;else{const t=C+q+e[E+b+e[G+D]];u=(I*=I)*I*(o[t]*j+a[t]*v+l[t]*O)}let J=.6-T*T-U*U-g*g;if(J<0)p=0;else{const t=C+N+e[E+P+e[G+_]];p=(J*=J)*J*(o[t]*T+a[t]*U+l[t]*g)}let K=.6-k*k-z*z-B*B;if(K<0)y=0;else{const t=C+1+e[E+1+e[G+1]];y=(K*=K)*K*(o[t]*k+a[t]*z+l[t]*B)}return 32*(f+u+p+y)}}function p(t=Math.random){const e=y(t),n=new Float64Array(e).map(t=>i[t%32*4]),r=new Float64Array(e).map(t=>i[t%32*4+1]),l=new Float64Array(e).map(t=>i[t%32*4+2]),c=new Float64Array(e).map(t=>i[t%32*4+3]);return function(t,i,f,u){let p,y,m,w,A;const F=(t+i+f+u)*o,M=s(t+F),h=s(i+F),d=s(f+F),x=s(u+F),q=(M+h+d+x)*a,b=t-(M-q),D=i-(h-q),N=f-(d-q),P=u-(x-q);let _=0,j=0,v=0,O=0;b>D?_++:j++,b>N?_++:v++,b>P?_++:O++,D>N?j++:v++,D>P?j++:O++,N>P?v++:O++;const T=_>=3?1:0,U=j>=3?1:0,g=v>=3?1:0,k=O>=3?1:0,z=_>=2?1:0,B=j>=2?1:0,C=v>=2?1:0,E=O>=2?1:0,G=_>=1?1:0,H=j>=1?1:0,I=v>=1?1:0,J=O>=1?1:0,K=b-T+a,L=D-U+a,Q=N-g+a,R=P-k+a,S=b-z+2*a,V=D-B+2*a,W=N-C+2*a,X=P-E+2*a,Y=b-G+3*a,Z=D-H+3*a,$=N-I+3*a,tt=P-J+3*a,et=b-1+4*a,nt=D-1+4*a,rt=N-1+4*a,ot=P-1+4*a,at=255&M,st=255&h,lt=255&d,ct=255&x;let it=.6-b*b-D*D-N*N-P*P;if(it<0)p=0;else{const t=at+e[st+e[lt+e[ct]]];p=(it*=it)*it*(n[t]*b+r[t]*D+l[t]*N+c[t]*P)}let ft=.6-K*K-L*L-Q*Q-R*R;if(ft<0)y=0;else{const t=at+T+e[st+U+e[lt+g+e[ct+k]]];y=(ft*=ft)*ft*(n[t]*K+r[t]*L+l[t]*Q+c[t]*R)}let ut=.6-S*S-V*V-W*W-X*X;if(ut<0)m=0;else{const t=at+z+e[st+B+e[lt+C+e[ct+E]]];m=(ut*=ut)*ut*(n[t]*S+r[t]*V+l[t]*W+c[t]*X)}let pt=.6-Y*Y-Z*Z-$*$-tt*tt;if(pt<0)w=0;else{const t=at+G+e[st+H+e[lt+I+e[ct+J]]];w=(pt*=pt)*pt*(n[t]*Y+r[t]*Z+l[t]*$+c[t]*tt)}let yt=.6-et*et-nt*nt-rt*rt-ot*ot;if(yt<0)A=0;else{const t=at+1+e[st+1+e[lt+1+e[ct+1]]];A=(yt*=yt)*yt*(n[t]*et+r[t]*nt+l[t]*rt+c[t]*ot)}return 27*(p+y+m+w+A)}}function y(t){const e=new Uint8Array(512);for(let n=0;n<256;n++)e[n]=n;for(let n=0;n<255;n++){const r=n+~~(t()*(256-n)),o=e[n];e[n]=e[r],e[r]=o}for(let n=256;n<512;n++)e[n]=e[n-256];return e}
},{}],"BWRk":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.rand=exports.noise=void 0;var r=require("simplex-noise"),e=function(r){var e,n,t,o,u=function(r){for(var e=2166136261,n=0;n<r.length;n++)e=Math.imul(e^r.charCodeAt(n),16777619);return function(){return e+=e<<13,e^=e>>>7,e+=e<<3,e^=e>>>17,(e+=e<<5)>>>0}}(r);return e=u(),n=u(),t=u(),o=u(),function(){var r=(e>>>=0)+(n>>>=0)|0;return e=n^n>>>9,n=(t>>>=0)+(t<<3)|0,t=(t=t<<21|t>>>11)+(r=r+(o=1+(o>>>=0)|0)|0)|0,(r>>>0)/4294967296}};exports.rand=e;var n=function(n){var t=(0,r.createNoise2D)(e(n));return function(r,e){return t(r,e)}};exports.noise=n;
},{"simplex-noise":"/uvX"}],"BJ3L":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.smoothBlob=exports.genFromOptions=exports.genBlobygon=exports.genBlob=void 0;var n=require("../internal/rand"),e=require("../internal/util"),t=require("./util"),r=function(n){var e=2*Math.PI/n.length,r=4/3*Math.tan(e/4)/Math.sin(e/2)/2;return(0,t.smooth)(n,r)};exports.smoothBlob=r;var o=function(n,e){for(var t=2*Math.PI/n,r=[],o=0;o<n;o++){var a=e(o),s=Math.sin(o*t),i=Math.cos(o*t);r.push({x:.5+s*a,y:.5+i*a,handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}})}return r};exports.genBlobygon=o;var a=function(n,e){return r(o(n,e))};exports.genBlob=a;var s=function(t,r){var o=r||(0,n.rand)(String(t.seed)),s=1/(1+t.randomness/10),i=a(3+t.extraPoints,function(n){return(s+o(n)*(1-s))/2}),u=t.size;return(0,e.mapPoints)(i,function(n){var e=n.curr;return e.x*=u,e.y*=u,e.handleIn.length*=u,e.handleOut.length*=u,e})};exports.genFromOptions=s;
},{"../internal/rand":"BWRk","../internal/util":"NSCe","./util":"NSCe"}],"/Sl0":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.interpolateBetweenSmooth=exports.interpolateBetween=void 0;var e=require("../util"),n=function(){return(n=Object.assign||function(e){for(var n,t=1,a=arguments.length;t<a;t++)for(var l in n=arguments[t])Object.prototype.hasOwnProperty.call(n,l)&&(e[l]=n[l]);return e}).apply(this,arguments)},t=function(n,t,a){var l=2*Math.PI,h=(0,e.mod)(t,l),r=(0,e.mod)(a,l);return Math.abs(h-r)>Math.PI&&(h<r?h+=l:r+=l),(0,e.split)(n,h,r)},a=function(a,l,h){if(l.length!==h.length)throw new Error("must have equal number of points");for(var r=Math.min(1,Math.max(0,a)),o=[],u=0;u<l.length;u++)o.push(n(n({},(0,e.splitLine)(a,l[u],h[u])),{handleIn:{angle:t(a,l[u].handleIn.angle,h[u].handleIn.angle),length:(0,e.split)(r,l[u].handleIn.length,h[u].handleIn.length)},handleOut:{angle:t(a,l[u].handleOut.angle,h[u].handleOut.angle),length:(0,e.split)(r,l[u].handleOut.length,h[u].handleOut.length)}}));return o};exports.interpolateBetween=a;var l=function(n,l,h,r){n*=Math.min(1,Math.min(Math.abs(0-l),Math.abs(1-l)));var o=a(l,h,r),u=(0,e.smooth)(o,Math.sqrt(n+.25)/3);return(0,e.mapPoints)(o,function(a){var l=a.index,h=a.curr,r=u[l];return h.handleIn.angle=t(n,h.handleIn.angle,r.handleIn.angle),h.handleIn.length=(0,e.split)(n,h.handleIn.length,r.handleIn.length),h.handleOut.angle=t(n,h.handleOut.angle,r.handleOut.angle),h.handleOut.length=(0,e.split)(n,h.handleOut.length,r.handleOut.length),h})};exports.interpolateBetweenSmooth=l;
},{"../util":"NSCe"}],"F/j+":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.prepare=exports.divide=void 0;var n=require("../util"),e=function(e,r){var t=e.length,a=1/0,o=0,l=[],u=function(r){for(var u=0;u<t;u++){for(var i=0,h=0;h<t&&!((i+=Math.pow(100*(0,n.distance)(e[h],r[(0,n.mod)(h+u,t)]),2))>a);h++);i<=a&&(a=i,o=u,l=r)}};return u(r),u((0,n.reverse)(r)),(0,n.shift)(o,l)},r=function(e,r){if(r.length<3)throw new Error("not enough points");if(e<r.length)throw new Error("cannot remove points");if(e===r.length)return r.slice();var t=[];(0,n.forPoints)(r,function(e){var r=e.curr,a=e.next;t.push((0,n.length)(r,a()))});for(var a=o(t,e-r.length),l=[],u=0;u<r.length;u++){var i=l[l.length-1]||r[u],h=r[(0,n.mod)(u+1,r.length)];l.pop(),l.push.apply(l,(0,n.insertCount)(a[u],i,h))}var d=l.pop();return l[0]=Object.assign({},l[0],{handleIn:d.handleIn}),l};exports.divide=r;var t=function(e,r){return(0,n.mapPoints)(e,function(e){var t=e.index,a=e.curr,o=e.prev,l=e.next;return 0===a.handleIn.length&&(0,n.coordEqual)(o(),a)&&(a.handleIn.angle=r[t].handleIn.angle),0===a.handleOut.length&&(0,n.coordEqual)(l(),a)&&(a.handleOut.angle=r[t].handleOut.angle),a})},a=function(e){return(0,n.mapPoints)(e,function(e){var r=e.curr,t=e.prev,a=e.next,o=(0,n.angleOf)(t(),a());return 0===r.handleIn.length&&(r.handleIn.angle=o+Math.PI),0===r.handleOut.length&&(r.handleOut.angle=o),r})},o=function(n,e){for(var r=n.map(function(){return 1}),t=n.slice(),a=0;a<e;a++){for(var o=0,l=1;l<t.length;l++)t[l]>t[o]?o=l:t[l]===t[o]&&n[l]>n[o]&&(o=l);r[o]++,t[o]=n[o]/r[o]}return r},l=function(n,o,l){var u=l.divideRatio*Math.max(n.length,o.length),i=r(u,n),h=r(u,o),d=e(i,h);return[l.rawAngles?i:t(a(i),d),l.rawAngles?d:t(a(d),i)]};exports.prepare=l;
},{"../util":"NSCe"}],"bUxv":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.transitionFrames=exports.renderFramesAt=void 0;var t=require("./timing"),i=require("./prepare"),e=require("./interpolate"),n=function(){return String(Math.random()).substr(2)},r=function(t){var n,r,a,s=t.renderCache,m=t.currentFrames;if(0===m.length)return{renderCache:s,lastFrameId:null,points:[]};if(1===m.length){var o=m[0];return{renderCache:s,lastFrameId:o.id,points:o.initialPoints}}for(var d=m[0],l=m[1],p=2;p<m.length&&!(l.timestamp>t.timestamp);p++)d=m[p-1],l=m[p];var u=l===m[m.length-1];if(l.timestamp<t.timestamp&&u)return{renderCache:s,lastFrameId:l.id,points:l.initialPoints};var h=null===(r=s[d.id])||void 0===r?void 0:r.preparedStartPoints,F=null===(a=s[l.id])||void 0===a?void 0:a.preparedEndPoints;h&&F||(h=(n=(0,i.prepare)(d.initialPoints,l.initialPoints,{rawAngles:!1,divideRatio:1}))[0],F=n[1],s[d.id]=s[d.id]||{},s[d.id].preparedStartPoints=h,s[l.id]=s[l.id]||{},s[l.id].preparedEndPoints=F);var c=(t.timestamp-d.timestamp)/(l.timestamp-d.timestamp),g=Math.max(0,Math.min(1,c)),v=l.timingFunction(g);return{renderCache:s,lastFrameId:1===g?l.id:d.id,points:(0,e.interpolateBetween)(v,h,F)}};exports.renderFramesAt=r;var a=function(i){var e=[];if(0===i.newFrames.length)return{newFrames:e};var a=r(i);if(null===a.lastFrameId){for(var s=i.shapeGenerator(i.newFrames[0]),m={x:0,y:0,handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}},o=0,d=s;o<d.length;o++){var l=d[o];m.x+=l.x/s.length,m.y+=l.y/s.length}a.points=[m,m,m]}e.push({id:n(),initialPoints:a.points,timestamp:i.timestamp,timingFunction:t.timingFunctions.linear,transitionSourceFrameIndex:-1,isSynthetic:!0});for(var p=0,u=0;u<i.newFrames.length;u++){var h=i.newFrames[u];if(h.delay){p+=h.delay;var F=e[e.length-1];e.push({id:n(),initialPoints:F.initialPoints,timestamp:i.timestamp+p,timingFunction:t.timingFunctions.linear,transitionSourceFrameIndex:u-1,isSynthetic:!0})}p+=h.duration,e.push({id:n(),initialPoints:i.shapeGenerator(h),timestamp:i.timestamp+p,timingFunction:t.timingFunctions[h.timingFunction||"linear"],transitionSourceFrameIndex:u,isSynthetic:!1})}return{newFrames:e}};exports.transitionFrames=a;
},{"./timing":"SjCR","./prepare":"F/j+","./interpolate":"/Sl0"}],"+LE9":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.statefulAnimationGenerator=void 0;var e=require("./frames"),r=function(r,t,n){return function(a){var i=[],o={},s={},u=0,c=0,m=function(){return a()-c},f=function(){return 0!==u},d=function(){f()&&(c+=m()-u,u=0)},l=function(){f()||(u=m())},F=function(){var r=(0,e.renderFramesAt)({renderCache:o,timestamp:f()?u:m(),currentFrames:i});return o=r.renderCache,r.lastFrameId&&s[r.lastFrameId]&&(setTimeout(s[r.lastFrameId]),delete s[r.lastFrameId]),r.points};return{renderFrame:function(){return t(F())},renderPoints:F,transition:function(){for(var t=[],a=0;a<arguments.length;a++)t[a]=arguments[a];for(var u=0;u<t.length;u++)n(t[u],u);var c=(0,e.transitionFrames)({renderCache:o,timestamp:m(),currentFrames:i,newFrames:t,shapeGenerator:r});i=c.newFrames,s={},o={};for(var f=0,d=i;f<d.length;f++){var l=d[f];if(!l.isSynthetic){var F=t[l.transitionSourceFrameIndex].callback;F&&(s[l.id]=F)}}},play:d,pause:l,playPause:function(){f()?d():l()}}}};exports.statefulAnimationGenerator=r;
},{"./frames":"bUxv"}],"5PF2":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.renderPath2D=exports.drawInfo=exports.drawClosed=exports.clear=void 0;var e=require("../util"),t=2,n=20,r=function(e){e.clearRect(0,0,e.canvas.width,e.canvas.height)};exports.clear=r;var o=function(e,t,r,o){e.fillText("".concat(r,": ").concat(o),n,(t+1)*n)};exports.drawInfo=o;var a=function(e,t,n,r){var o=e.strokeStyle;e.beginPath(),e.moveTo(t.x,t.y),e.lineTo(n.x,n.y),e.strokeStyle=r,e.stroke(),e.strokeStyle=o},l=function(e,n,r){var o=e.fillStyle;e.beginPath(),e.arc(n.x,n.y,t,0,2*Math.PI),e.fillStyle=r,e.fill(),e.fillStyle=o},i=function(t,n,r){if(r.length<2)throw new Error("not enough points");n&&(0,e.forPoints)(r,function(n){var r=n.curr,o=(0,n.next)(),i=(0,e.expandHandle)(r,r.handleOut),c=(0,e.expandHandle)(o,o.handleIn);l(t,r,""),a(t,r,i,"#ccc"),a(t,o,c,"#b6b")}),t.stroke(c(r))};exports.drawClosed=i;var c=function(t){var n=new Path2D;return t.length<1?n:(n.moveTo(t[0].x,t[0].y),(0,e.forPoints)(t,function(t){var r=t.curr,o=(0,t.next)(),a=(0,e.expandHandle)(r,r.handleOut),l=(0,e.expandHandle)(o,o.handleIn);n.bezierCurveTo(a.x,a.y,l.x,l.y,o.x,o.y)}),n)};exports.renderPath2D=c;
},{"../util":"NSCe"}],"Aed7":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.checkSvgOptions=exports.checkPoints=exports.checkKeyframeOptions=exports.checkCanvasOptions=exports.checkBlobOptions=void 0;var n=require("./animate/timing");function t(n){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}var e=function(n,e,o){var i=t(e);if("number"===i&&isNaN(e)&&(i="NaN"),"object"===i&&null===e&&(i="null"),!o.includes(i))throw'"'.concat(n,'" should have type "').concat(o.join("|"),'" but was "').concat(i,'".')},o=function(t){e("keyframe",t,["object"]);var o=t.delay,i=t.duration,r=t.timingFunction,s=t.callback;if(e("delay",o,["number","undefined"]),o&&o<0)throw'delay is invalid "'.concat(o,'".');if(e("duration",i,["number"]),i&&i<0)throw'duration is invalid "'.concat(i,'".');if(e("timingFunction",r,["string","undefined"]),r&&!n.timingFunctions[r])throw'".timingFunction" is not recognized "'.concat(r,'".');e("callback",s,["function","undefined"])};exports.checkKeyframeOptions=o;var i=function(n){e("blobOptions",n,["object"]);var t=n.seed,o=n.extraPoints,i=n.randomness,r=n.size;if(e("blobOptions.seed",t,["string","number"]),e("blobOptions.extraPoints",o,["number"]),o<0)throw'blobOptions.extraPoints is invalid "'.concat(o,'".');if(e("blobOptions.randomness",i,["number"]),i<0)throw'blobOptions.randomness is invalid "'.concat(i,'".');if(e("blobOptions.size",r,["number"]),r<0)throw'blobOptions.size is invalid "'.concat(r,'".')};exports.checkBlobOptions=i;var r=function(n){if(e("canvasOptions",n,["object","undefined"]),n){var t=n.offsetX,o=n.offsetY;e("canvasOptions.offsetX",t,["number","undefined"]),e("canvasOptions.offsetY",o,["number","undefined"])}};exports.checkCanvasOptions=r;var s=function(n){if(e("svgOptions",n,["object","undefined"]),n){var t=n.fill,o=n.stroke,i=n.strokeWidth;e("svgOptions.fill",t,["string","undefined"]),e("svgOptions.stroke",o,["string","undefined"]),e("svgOptions.strokeWidth",i,["number","undefined"])}};exports.checkSvgOptions=s;var a=function(n){if(!Array.isArray(n))throw'points should be an array but was "'.concat(t(n),'".');if(n.length<3)throw'expected more than two points but received "'.concat(n.length,'".');for(var o=0,i=n;o<i.length;o++){var r=i[o];e("point.x",r.x,["number"]),e("point.y",r.y,["number"]),e("point.handleIn",r.handleIn,["object"]),e("point.handleIn.angle",r.handleIn.angle,["number"]),e("point.handleIn.length",r.handleIn.length,["number"]),e("point.handleOut",r.handleOut,["object"]),e("point.handleOut.angle",r.handleOut.angle,["number"]),e("point.handleOut.length",r.handleOut.length,["number"])}};exports.checkPoints=a;
},{"./animate/timing":"SjCR"}],"+HZB":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.wigglePreset=exports.canvasPath=void 0;var n=require("../internal/render/canvas"),e=require("../internal/gen"),t=require("../internal/util"),i=require("../internal/animate/state"),r=require("../internal/check"),a=require("../internal/rand"),o=require("../internal/animate/interpolate"),s=require("../internal/animate/prepare"),c=function(n){var i;return i="points"in n?n.points:(0,e.genFromOptions)(n.blobOptions),(0,t.mapPoints)(i,function(e){var t,i,r=e.curr;return r.x+=(null===(t=null==n?void 0:n.canvasOptions)||void 0===t?void 0:t.offsetX)||0,r.y+=(null===(i=null==n?void 0:n.canvasOptions)||void 0===i?void 0:i.offsetY)||0,r})},l=function(n,e){try{if("points"in n)return(0,r.checkPoints)(n.points);(0,r.checkBlobOptions)(n.blobOptions),(0,r.checkCanvasOptions)(n.canvasOptions),(0,r.checkKeyframeOptions)(n)}catch(t){throw"(blobs2): keyframe ".concat(e,": ").concat(t)}},u=function(e){var t=Date.now;if(void 0!==e){var r=0;t=function(){var n=e();if(n<r)throw"timestamp provider generated decreasing value: ".concat(r," then ").concat(n,".");return r=n,n}}return(0,i.statefulAnimationGenerator)(c,n.renderPath2D,l)(t)};exports.canvasPath=u;var p=function(n,t,i,r){var c=.01*r.speed,l=(0,a.noise)(String(t.seed)),u=Math.min((r.initialTransition||0)/160),p=n.renderPoints(),v=0;!function r(){v++;var a=(0,e.genFromOptions)(t,function(n){return l(c*v,n)});if(v<u){var d=(0,s.prepare)(p,a,{rawAngles:!0,divideRatio:1}),f=d[0],m=d[1],h=Math.min(1,2/(u-v)),g=(0,o.interpolateBetween)(h,f,m);p=g,n.transition({duration:160,delay:0,timingFunction:"linear",canvasOptions:i,points:g,callback:r})}else n.transition({duration:160,delay:0,timingFunction:"linear",canvasOptions:i,points:a,callback:r})}()};exports.wigglePreset=p;
},{"../internal/render/canvas":"5PF2","../internal/gen":"BJ3L","../internal/util":"NSCe","../internal/animate/state":"+LE9","../internal/check":"Aed7","../internal/rand":"BWRk","../internal/animate/interpolate":"/Sl0","../internal/animate/prepare":"F/j+"}],"hNRT":[function(require,module,exports) {
"use strict";var e=require("./internal/layout"),n=require("./internal/canvas"),t=require("../internal/util"),a=require("../internal/animate/timing"),o=require("../internal/rand"),i=require("../internal/gen"),r=require("../internal/animate/interpolate"),s=require("../internal/animate/prepare"),l=require("../internal/animate/state"),c=require("../public/animate"),d=function(){return(d=Object.assign||function(e){for(var n,t=1,a=arguments.length;t<a;t++)for(var o in n=arguments[t])Object.prototype.hasOwnProperty.call(n,o)&&(e[o]=n[o]);return e}).apply(this,arguments)},h=function(e,n,a){for(var o=2*Math.PI/e,i=[],r={angle:0,length:0},s=0;s<e;s++){var l=(0,t.expandHandle)(a,{angle:s*o,length:n});i.push(d(d({},l),{handleIn:r,handleOut:r}))}return i},u=function(e,n){return(0,t.mapPoints)((0,i.genFromOptions)(e),function(t){var a=t.curr;return a.x+=n.x-e.size/2,a.y+=n.y-e.size/2,a})},f=function(e,n,a){var o=n,i=(0,t.expandHandle)(n,n.handleOut),r=(0,t.expandHandle)(a,a.handleIn),s=a,l=(0,t.splitLine)(e,o,i),c=(0,t.splitLine)(e,i,r),d=(0,t.splitLine)(e,r,s),h=(0,t.splitLine)(e,l,c),u=(0,t.splitLine)(e,c,d);return{a0:o,a1:i,a2:r,a3:s,b0:l,b1:c,b2:d,c0:h,c1:u,d0:(0,t.splitLine)(e,h,u)}};(0,e.addTitle)(4,"Vector graphics"),(0,e.addCanvas)(1.3,function(a,o,i){for(var r={x:.5*o,y:.5*i},s=.01*o,l=o/s,c=i/s,d=.3*o,h=.0015*o,u=.01*o,f=function(o){for(var i=function(i){var l={x:o*s+s/2,y:i*s+s/2},c=(0,t.distance)(l,r),f=Math.max(0,Math.min(1,Math.abs(u/(c-d))-h));(0,n.tempStyles)(a,function(){a.globalAlpha=f,a.fillStyle=e.colors.highlight},function(){return a.fillRect(o*s,i*s,s,s)})},l=0;l<c;l++)i(l)},p=0;p<l;p++)f(p);return"Raster image formats encode images as a finite number of pixel values. They\n therefore have a maximum scale which depends on the display."},function(t,a,o){var i=.01*a,r=.6*a,s=.5*a,l=.5*o;return(0,n.tempStyles)(t,function(){t.lineWidth=i,t.strokeStyle=e.colors.highlight},function(){t.beginPath(),t.arc(s,l,r/2,0,2*Math.PI),t.stroke()}),"By contrast vector formats are defined by formulas and can scale infinitely. They\n are well suited for artwork with sharp lines and are used for font glyphs."}),(0,e.addCanvas)(1.3,function(e,o,i,r){var s=1e3*(1+Math.E),l=1e3*(1+Math.PI);return r(function(r){var c=(0,n.calcBouncePercentage)(s,a.timingFunctions.ease,r),d=(0,n.calcBouncePercentage)(.8*s,a.timingFunctions.ease,r),h=(0,t.split)(c,-45,45),u=.1*o+.2*o*d,f=(0,n.point)(.2*o,.5*i,0,0,h,u),p=(0,n.calcBouncePercentage)(l,a.timingFunctions.ease,r),m=(0,n.calcBouncePercentage)(.8*l,a.timingFunctions.ease,r),b=(0,t.split)(p,135,225),g=.1*o+.2*o*m,y=(0,n.point)(.8*o,.5*i,b,g,0,0);(0,n.drawOpen)(e,f,y,!0)}),'Vector-based image formats often support Bezier curves. A cubic bezier curve is defined\n by four coordinates: the start/end points and corresponding "handle" points. Visually, these\n handles define the direction and "momentum" of the line. The curve is tangent to the handle\n at either of the points.'},function(e,i,r,s){var l=(0,o.rand)("blobs"),c=l(),d=l(),h=l(),u=l(),f=function(e,o,i,r){var s=(0,t.deg)(i.handleIn.angle)+20*(.5-(0,n.calcBouncePercentage)(1.1*o,a.timingFunctions.ease,e)),l=i.handleIn.length+40*(.5-(0,n.calcBouncePercentage)(.9*o,a.timingFunctions.ease,e)),c=(0,t.deg)(i.handleOut.angle)+20*(.5-(0,n.calcBouncePercentage)(.9*o,a.timingFunctions.ease,e)),d=i.handleOut.length+40*(.5-(0,n.calcBouncePercentage)(1.1*o,a.timingFunctions.ease,e));return(0,n.point)(i.x,i.y,s,l,r?s+180:c,d)};return s(function(t){var a=f(t,2500+5e3*c/2,(0,n.point)(.5*i,.3*r,210,100,-30,100),!1),o=f(t,2500+5e3*d/2,(0,n.point)(.8*i,.5*r,-90,100,90,100),!0),s=f(t,2500+5e3*h/2,(0,n.point)(.5*i,.9*r,-30,75,-150,75),!1),l=f(t,2500+5e3*u/2,(0,n.point)(.2*i,.5*r,90,100,-90,100),!0);(0,n.drawClosed)(e,[a,o,s,l],!0)}),"Chaining curves together creates closed shapes. When the in/out handles of a point\n form a line, the transition is smooth, and the curve is tangent to the line."}),(0,e.addCanvas)(2,function(t,o,i,r){var s=Math.PI*Math.E*1e3,l=(0,n.point)(.3*o,.8*i,0,0,-105,.32*o),c=(0,n.point)(.7*o,.8*i,-75,.25*o,0,0);return r(function(o){var i=(0,n.calcBouncePercentage)(s,a.timingFunctions.ease,o),r=f(i,l,c);(0,n.tempStyles)(t,function(){t.fillStyle=e.colors.secondary,t.strokeStyle=e.colors.secondary},function(){(0,n.drawLine)(t,r.a0,r.a1,1),(0,n.drawLine)(t,r.a1,r.a2,1),(0,n.drawLine)(t,r.a2,r.a3,1),(0,n.drawLine)(t,r.b0,r.b1,1),(0,n.drawLine)(t,r.b1,r.b2,1),(0,n.drawLine)(t,r.c0,r.c1,1),(0,n.drawPoint)(t,r.a0,1.3,"a0"),(0,n.drawPoint)(t,r.a1,1.3,"a1"),(0,n.drawPoint)(t,r.a2,1.3,"a2"),(0,n.drawPoint)(t,r.a3,1.3,"a3"),(0,n.drawPoint)(t,r.b0,1.3,"b0"),(0,n.drawPoint)(t,r.b1,1.3,"b1"),(0,n.drawPoint)(t,r.b2,1.3,"b2"),(0,n.drawPoint)(t,r.c0,1.3,"c0"),(0,n.drawPoint)(t,r.c1,1.3,"c1"),(0,n.drawPoint)(t,r.d0,1.3,"d0")}),(0,n.tempStyles)(t,function(){return t.fillStyle=e.colors.highlight},function(){return(0,n.drawPoint)(t,r.d0,3)}),(0,n.drawOpen)(t,l,c,!1)}),'Curves are rendered using the four input points (ends + handles). By connecting\n points a0-a3 with a line and then splitting each line by the same percentage, we\'ve reduced\n the number of points by one. Repeating the same process with the new set of points until\n there is only one point remaining (d0) produces a single point on the line. Repeating this\n calculation for many different percentage values will produce a curve.\n <br><br>\n <i>Note there is no constant relationship between the\n percentage that "drew" the point and the arc lengths before/after it. Uniform motion along\n the curve can only be approximated.'}),(0,e.addTitle)(4,"Making a blob"),(0,e.addCanvas)(1.3,function(a,o,i,r){var s={x:.5*o,y:.5*i},l=.3*o;return r(function(o){var i=9+3*Math.sin(o/2e3),r=h(i,l,s);(0,n.tempStyles)(a,function(){a.fillStyle=e.colors.secondary,a.strokeStyle=e.colors.secondary},function(){(0,n.drawPoint)(a,s,2),(0,t.forPoints)(r,function(e){var t=e.curr;(0,n.drawLine)(a,s,t,1,2)})}),(0,n.drawClosed)(a,r,!1)}),"Points are first distributed evenly around the center. At this stage the points\n technically have handles, but since they have a length of zero, they have no effect on\n the shape and it looks like a polygon."},function(i,r,s,l){var c=1500*Math.PI,d={x:.5*r,y:.5*s},u=.3*r,f=Math.random(),p=h(5,u,d);return l(function(r){var s=(0,n.calcBouncePercentage)(c,a.timingFunctions.ease,r),l=(0,o.rand)(f+Math.floor(r/c)+"");(0,n.tempStyles)(i,function(){i.fillStyle=e.colors.secondary,i.strokeStyle=e.colors.secondary},function(){(0,n.drawPoint)(i,d,2),(0,t.forPoints)(p,function(e){var t=e.curr,a=e.next;(0,n.drawLine)(i,t,a(),1,2)})});var h=p.map(function(e){var n=s*(.5*l()-.25);return(0,t.coordPoint)((0,t.splitLine)(n,e,d))});(0,n.drawClosed)(i,h,!0)}),'Points are then randomly moved further or closer to the center. Using a seeded\n random number generator allows repeatable "randomness" whenever the blob is generated\n at a different time or place.'}),(0,e.addCanvas)(1.3,function(a,o,i,r){var s={x:.5*o,y:.5*i},l=u({extraPoints:2,randomness:6,seed:"random",size:.7*o},s),c=(0,t.mapPoints)(l,function(e){var n=e.curr;return n.handleIn.length=150,n.handleOut.length=150,n}),d=l.map(t.coordPoint),h=d.length;return r(function(o){var i=Math.floor(o/2e3)%h,r=Math.abs(Math.sin(o*Math.PI/2e3));(0,n.tempStyles)(a,function(){a.strokeStyle=e.colors.secondary,a.globalAlpha=r},function(){(0,t.forPoints)(d,function(e){var t=e.prev,o=e.next;e.index===i&&(0,n.drawLine)(a,t(),o(),1,2)}),(0,t.forPoints)(c,function(e){var t=e.curr;e.index===i&&(0,n.drawHandles)(a,t,1)})}),(0,n.tempStyles)(a,function(){a.fillStyle=e.colors.secondary},function(){(0,n.drawPoint)(a,s,2)}),(0,n.drawClosed)(a,d,!1)}),"The angle of the handles for each point is parallel with the imaginary line\n stretching between its neighbors. Even when they have length zero, the angle of the\n handles can still be calculated."},function(o,i,r,s){var l=1500*Math.PI,c={x:.5*i,y:.5*r},d=u({extraPoints:2,randomness:6,seed:"random",size:.7*i},c);return s(function(i){var r=(0,n.calcBouncePercentage)(l,a.timingFunctions.ease,i);(0,n.tempStyles)(o,function(){o.fillStyle=e.colors.secondary,o.strokeStyle=e.colors.secondary},function(){(0,n.drawPoint)(o,c,2),(0,t.forPoints)(d,function(e){var t=e.curr,a=e.next;(0,n.drawLine)(o,t,a(),1,2)})});var s=(0,t.mapPoints)(d,function(e){var n=e.curr;return n.handleIn.length*=r,n.handleOut.length*=r,n});(0,n.drawClosed)(o,s,!0)}),"The blob is then made smooth by extending the handles. The exact length\n depends on the distance between the given point and it's next neighbor. This value is\n multiplied by a ratio that would roughly produce a circle if the points had not been\n randomly moved."}),(0,e.addTitle)(4,"Interpolating between blobs"),(0,e.addCanvas)(2,function(o,i,s,l){var c=1e3*Math.PI,d={x:.5*i,y:.5*s},h=u({extraPoints:3,randomness:6,seed:"12345",size:.8*s},d),f=u({extraPoints:3,randomness:6,seed:"abc",size:.8*s},d);return l(function(i){var s=(0,n.calcBouncePercentage)(c,a.timingFunctions.ease,i),l=i+.05*c,d=(0,n.calcBouncePercentage)(c,a.timingFunctions.ease,l),u=(0,t.mod)(l,c)/c;(0,n.forceStyles)(o,function(){var t=(0,e.sizes)().pt;o.fillStyle="transparent",o.lineWidth=t,o.strokeStyle=e.colors.secondary,o.setLineDash([2*t]),u>.5?(o.globalAlpha=.2+10*(1-d),(0,n.drawClosed)(o,h,!1),o.globalAlpha=.2,(0,n.drawClosed)(o,f,!1)):(o.globalAlpha=.2+10*d,(0,n.drawClosed)(o,f,!1),o.globalAlpha=.2,(0,n.drawClosed)(o,h,!1))}),(0,n.drawClosed)(o,(0,r.interpolateBetween)(s,h,f),!0)}),"The simplest way to interpolate between blobs would be to move points 0-N from their\n position in the start blob to their position in the end blob. The problem with this approach\n is that it doesn't allow for all blob to map to all blobs. Specifically it would only be\n possible to animate between blobs that have the same number of points. This means something\n more generic is required."}),(0,e.addCanvas)(1.3,function(a,o,i,r){var l={x:.5*o,y:.5*i},c=7*Math.PI*300,d=(0,e.sizes)().pt,h=u({extraPoints:0,randomness:6,seed:"flip",size:.9*i},l);return r(function(o){var i=(0,t.mod)(o,c)/c,r=Math.floor(8*i);(0,n.drawClosed)(a,(0,s.divide)(r+h.length,h),!0),(0,t.forPoints)(h,function(t){var o=t.curr;a.beginPath(),a.arc(o.x,o.y,6*d,0,2*Math.PI),(0,n.tempStyles)(a,function(){a.strokeStyle=e.colors.secondary,a.lineWidth=d},function(){a.stroke()})})}),"The first step to prepare animation is to make the number of points between the\n start and end shapes equal. This is done by adding points to the shape with least points\n until they are both equal.\n <br><br>\n For best animation quality it is important that these points are as evenly distributed\n as possible all around the shape so this is not a recursive algorithm."},function(t,o,i,r){var s=1e3*Math.pow(Math.PI,Math.E),l=(0,n.point)(.1*o,.6*i,0,0,-45,.5*o),c=(0,n.point)(.9*o,.6*i,160,.3*o,0,0);return r(function(o){var i=(0,n.calcBouncePercentage)(s,a.timingFunctions.ease,o),r=f(i,l,c);(0,n.tempStyles)(t,function(){t.fillStyle=e.colors.secondary,t.strokeStyle=e.colors.secondary},function(){(0,n.drawLine)(t,r.a0,r.a1,1),(0,n.drawLine)(t,r.a1,r.a2,1,2),(0,n.drawLine)(t,r.a2,r.a3,1),(0,n.drawLine)(t,r.b0,r.b1,1,2),(0,n.drawLine)(t,r.b1,r.b2,1,2),(0,n.drawPoint)(t,r.a0,1.3,"a0"),(0,n.drawPoint)(t,r.a1,1.3,"a1"),(0,n.drawPoint)(t,r.a2,1.3,"a2"),(0,n.drawPoint)(t,r.a3,1.3,"a3"),(0,n.drawPoint)(t,r.b1,1.3,"b1")}),(0,n.forceStyles)(t,function(){var a=(0,e.sizes)().pt;t.fillStyle=e.colors.secondary,t.strokeStyle=e.colors.secondary,t.lineWidth=a,(0,n.drawOpen)(t,l,c,!1)}),(0,n.tempStyles)(t,function(){t.fillStyle=e.colors.highlight,t.strokeStyle=e.colors.highlight},function(){(0,n.drawLine)(t,r.c0,r.c1,1),(0,n.drawLine)(t,r.a0,r.b0,1),(0,n.drawLine)(t,r.a3,r.b2,1),(0,n.drawPoint)(t,r.b0,1.3,"b0"),(0,n.drawPoint)(t,r.b2,1.3,"b2"),(0,n.drawPoint)(t,r.c0,1.3,"c0"),(0,n.drawPoint)(t,r.c1,1.3,"c1")}),(0,n.tempStyles)(t,function(){return t.fillStyle=e.colors.highlight},function(){return(0,n.drawPoint)(t,r.d0,1.3,"d0")})}),'It is only possible to reliably <i>add</i> points to a blob because attempting to\n remove points without modifying the shape is almost never possible and is expensive to\n compute.\n <br><br>\n Adding a point is done using the line-drawing geometry. In this example "d0" is the new\n point with its handles being "c0" and "c1". The original points get new handles "b0" and\n "b2"'}),(0,e.addCanvas)(1.3,function(o,i,s,l){var c=Math.E/Math.PI*1e3,d={x:.5*i,y:.5*s},h=u({extraPoints:3,randomness:6,seed:"shift",size:.9*s},d),f=(0,t.shift)(1,h),p=0,m=0;return l(function(i){var s=(0,t.mod)(i,c),l=a.timingFunctions.ease((0,t.mod)(s,c)/c);l<p&&m++,p=l,(0,n.tempStyles)(o,function(){o.fillStyle=e.colors.secondary,o.strokeStyle=e.colors.secondary},function(){(0,n.drawPoint)(o,d,2),(0,t.forPoints)(h,function(e){var t=e.curr,a=e.next;(0,n.drawLine)(o,t,a(),1,2)})}),m%2==0?(0,n.drawClosed)(o,(0,r.interpolateBetweenSmooth)(2,l,h,f),!0):(0,n.drawClosed)(o,h,!0)}),"Once both shapes have the same amount of points, an ordering of points which reduces\n the total amount of distance traveled by the points during the transition needs to be\n selected. Because the shapes are closed, points can be shifted by any amount without\n visually affecting the shape."},function(o,i,s,l){var c=Math.PI*Math.E*1e3,d=u({extraPoints:3,randomness:6,seed:"flip",size:.9*s},{x:.5*i,y:.5*s}),h=(0,t.mapPoints)(d,function(e){var n=e.curr,t=n.handleIn;return n.handleIn=n.handleOut,n.handleOut=t,n});return h.reverse(),l(function(t){var i=(0,n.calcBouncePercentage)(c,a.timingFunctions.ease,t);(0,n.forceStyles)(o,function(){var t=(0,e.sizes)().pt;o.fillStyle="transparent",o.lineWidth=t,o.strokeStyle=e.colors.secondary,o.setLineDash([2*t]),(0,n.drawClosed)(o,d,!1)}),(0,n.drawClosed)(o,(0,r.interpolateBetweenSmooth)(2,i,d,h),!0)}),"Points can also be reversed without visually affecting the shape. Then, again can\n be shifted all around. Although reversed ordering doesn't change the shape, it has a\n dramatic effect on the animation as it makes the loop flip over itself.\n <br><br>\n In total there are 2 * num_points different orderings of the\n points that can work for transition purposes."}),(0,e.addCanvas)(1.3,function(e,a,o){var r=Math.random(),s=function(){return e.canvas.animationID!==r},c=1e3*Math.PI,h=.5*a,u=.5*o,f=.8*Math.min(a,o),p=(0,l.statefulAnimationGenerator)(function(e){return(0,t.mapPoints)((0,i.genFromOptions)(e.blobOptions),function(e){var n=e.curr;return n.x+=h-f/2,n.y+=u-f/2,n})},function(t){return(0,n.drawClosed)(e,t,!0)},function(){})(Date.now);requestAnimationFrame(function n(){s()||(e.clearRect(0,0,a,o),p.renderFrame(),requestAnimationFrame(n))});var m=function(){s()||p.transition(g())},b=-1,g=function(e){return void 0===e&&(e={}),b++,d({duration:c,timingFunction:"ease",callback:m,blobOptions:{extraPoints:Math.max(0,(0,t.mod)(b,4)-1),randomness:4,seed:Math.random(),size:f}},e)};return p.transition(g({duration:0})),e.canvas.onclick=function(){s()||p.playPause()},e.canvas.animationID=r,"The added points can be removed at the end of a transition when the target shape has\n been reached. However, if the animation is interrupted during interpolation there is no\n opportunity to clean up the extra points."},function(e,t,a,o){var r=.5*t,s=.5*a,l=.8*Math.min(t,a),d=function(e,n,t){for(var a=2*e,o=2*Math.PI/a,i=[],l=0;l<a;l++){var c=Math.sin(l*o),d=Math.cos(l*o),h=(l%2==0?n:t)/2;i.push({x:r+c*h,y:s+d*h,handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}})}return i},h=function(e,n){for(var t=2*Math.PI/e,a=[],o=0;o<e;o++){var i=Math.sin(o*t),l=Math.cos(o*t),c=n/2;a.push({x:r+i*c,y:s+l*c,handleIn:{angle:0,length:0},handleOut:{angle:0,length:0}})}return a},u=[d(8,l,.7*l),(0,i.smoothBlob)(h(3,l)),(0,i.smoothBlob)(d(10,l,.9*l)),h(4,l),(0,i.smoothBlob)(d(3,l,.6*l))],f=(0,c.canvasPath)();return f.transition({points:u[0],duration:0,callback:function e(n){return function(){f.transition({points:u[n%u.length],duration:3e3,delay:1e3,timingFunction:"ease",callback:e(n+1)})}}(1)}),o(function(){(0,n.drawClosed)(e,f.renderPoints(),!0)}),"Putting all these pieces together, the blob transition library can also be used to\n tween between non-blob shapes. The more detail a shape has, the more unconvincing the\n animation will look. In these cases, manually creating in-between frames can be a\n helpful tool."}),(0,e.addTitle)(4,"Gooeyness"),(0,e.addCanvas)(1.3,function(e,t,a,o){var i=.8*Math.min(t,a),r={x:.5*(t-i),y:.5*(a-i)},s=(0,c.canvasPath)();return function e(n){s.transition({duration:n,blobOptions:{extraPoints:2,randomness:3,seed:Math.random(),size:i},callback:function(){return e(3e3)},timingFunction:"ease",canvasOptions:{offsetX:r.x,offsetY:r.y}})}(0),o(function(){(0,n.drawClosed)(e,s.renderPoints(),!0)}),"This library uses the keyframe model to define animations. This is a flexible\n approach, but it does not lend itself well to the kind of gooey blob shapes invite.\n <br><br>\n When looking at this animation, you may be able to notice the rhythm of the\n keyframes where the points start moving and stop moving at the same time."},function(e,t,a,o){var i=.8*Math.min(t,a),r=.5*t,s=.5*a,l=(0,c.canvasPath)();return(0,c.wigglePreset)(l,{extraPoints:2,randomness:3,seed:Math.random(),size:i},{offsetX:r-i/2,offsetY:s-i/2},{speed:2}),o(function(){(0,n.drawClosed)(e,l.renderPoints(),!0)}),"In addition to the keyframe API, there is now also pre-built preset which produces a\n gooey animation without much effort and much prettier results.\n <br><br>\n This approach uses a noise field instead of random numbers to move individual points\n around continuously and independently. Repeated calls to a noise-field-powered random\n number generator will produce self-similar results."});
},{"./internal/layout":"rSMP","./internal/canvas":"PBVq","../internal/util":"NSCe","../internal/animate/timing":"SjCR","../internal/rand":"BWRk","../internal/gen":"BJ3L","../internal/animate/interpolate":"/Sl0","../internal/animate/prepare":"F/j+","../internal/animate/state":"+LE9","../public/animate":"+HZB"}]},{},["hNRT"], null)
//# sourceMappingURL=/content.a90eb1ee.js.map</script> </body></html>
================================================
FILE: internal/animate/frames.ts
================================================
import {TimingFunc, timingFunctions} from "./timing";
import {Point} from "../types";
import {prepare} from "./prepare";
import {interpolateBetween} from "./interpolate";
export interface Keyframe {
delay?: number;
duration: number;
timingFunction?: keyof typeof timingFunctions;
}
export interface InternalKeyframe {
id: string;
timestamp: number;
timingFunction: TimingFunc;
initialPoints: Point[];
transitionSourceFrameIndex: number;
// Synthetic keyframes are generated to represent the current state when
// a new transition is begun.
isSynthetic: boolean;
}
export interface RenderCache {
[frameId: string]: {
preparedEndPoints?: Point[];
preparedStartPoints?: Point[];
};
}
export interface RenderInput {
currentFrames: InternalKeyframe[];
timestamp: number;
renderCache: RenderCache;
}
export interface RenderOutput {
points: Point[];
lastFrameId: string | null;
renderCache: RenderCache;
}
export interface TransitionInput<T extends Keyframe> extends RenderInput {
newFrames: T[];
shapeGenerator: (keyframe: T) => Point[];
}
export interface TransitionOutput {
newFrames: InternalKeyframe[];
}
const genId = (): string => {
return String(Math.random()).substr(2);
};
export const renderFramesAt = (input: RenderInput): RenderOutput => {
const {renderCache, currentFrames} = input;
if (currentFrames.length === 0) {
return {renderCache, lastFrameId: null, points: []};
}
// Animation freezes at the final shape if there are no more keyframes.
if (currentFrames.length === 1) {
const first = currentFrames[0];
return {renderCache, lastFrameId: first.id, points: first.initialPoints};
}
// Find the start/end keyframes according to the timestamp.
let startKeyframe = currentFrames[0];
let endKeyframe = currentFrames[1];
for (let i = 2; i < currentFrames.length; i++) {
if (endKeyframe.timestamp > input.timestamp) break;
startKeyframe = currentFrames[i - 1];
endKeyframe = currentFrames[i];
}
// Return original end shape when past the end of the animation.
const endKeyframeIsLast = endKeyframe === currentFrames[currentFrames.length - 1];
const animationIsPastEndKeyframe = endKeyframe.timestamp < input.timestamp;
if (animationIsPastEndKeyframe && endKeyframeIsLast) {
return {
renderCache,
lastFrameId: endKeyframe.id,
points: endKeyframe.initialPoints,
};
}
// Use and cache prepared points for current interpolation.
let preparedStartPoints: Point[] | undefined =
renderCache[startKeyframe.id]?.preparedStartPoints;
let preparedEndPoints: Point[] | undefined = renderCache[endKeyframe.id]?.preparedEndPoints;
if (!preparedStartPoints || !preparedEndPoints) {
[preparedStartPoints, preparedEndPoints] = prepare(
startKeyframe.initialPoints,
endKeyframe.initialPoints,
{rawAngles: false, divideRatio: 1},
);
renderCache[startKeyframe.id] = renderCache[startKeyframe.id] || {};
renderCache[startKeyframe.id].preparedStartPoints = preparedStartPoints;
renderCache[endKeyframe.id] = renderCache[endKeyframe.id] || {};
renderCache[endKeyframe.id].preparedEndPoints = preparedEndPoints;
}
// Calculate progress between frames as a fraction.
const progress =
(input.timestamp - startKeyframe.timestamp) /
(endKeyframe.timestamp - startKeyframe.timestamp);
// Keep progress within expected range (ex. division by 0).
const clampedProgress = Math.max(0, Math.min(1, progress));
// Apply timing function of end frame.
const adjustedProgress = endKeyframe.timingFunction(clampedProgress);
return {
renderCache,
lastFrameId: clampedProgress === 1 ? endKeyframe.id : startKeyframe.id,
points: interpolateBetween(adjustedProgress, preparedStartPoints, preparedEndPoints),
};
};
export const transitionFrames = <T extends Keyframe>(
input: TransitionInput<T>,
): TransitionOutput => {
// Erase all old frames.
const newInternalFrames: InternalKeyframe[] = [];
// Reset animation when given no keyframes.
if (input.newFrames.length === 0) {
return {newFrames: newInternalFrames};
}
// Add current state as initial frame.
const currentState = renderFramesAt(input);
if (currentState.lastFrameId === null) {
// If there is currently no shape being rendered, use a point in the
// center of the next frame as the initial point.
const firstShape = input.shapeGenerator(input.newFrames[0]);
let firstShapeCenterPoint: Point = {
x: 0,
y: 0,
handleIn: {angle: 0, length: 0},
handleOut: {angle: 0, length: 0},
};
for (const point of firstShape) {
firstShapeCenterPoint.x += point.x / firstShape.length;
firstShapeCenterPoint.y += point.y / firstShape.length;
}
currentState.points = [firstShapeCenterPoint, firstShapeCenterPoint, firstShapeCenterPoint];
}
newInternalFrames.push({
id: genId(),
initialPoints: currentState.points,
timestamp: input.timestamp,
timingFunction: timingFunctions.linear,
transitionSourceFrameIndex: -1,
isSynthetic: true,
});
// Generate and add new frames.
let totalOffset = 0;
for (let i = 0; i < input.newFrames.length; i++) {
const keyframe = input.newFrames[i];
// Copy previous frame when current one has a delay.
if (keyframe.delay) {
totalOffset += keyframe.delay;
const prevFrame = newInternalFrames[newInternalFrames.length - 1];
newInternalFrames.push({
id: genId(),
initialPoints: prevFrame.initialPoints,
timestamp: input.timestamp + totalOffset,
timingFunction: timingFunctions.linear,
transitionSourceFrameIndex: i - 1,
isSynthetic: true,
});
}
totalOffset += keyframe.duration;
newInternalFrames.push({
id: genId(),
initialPoints: input.shapeGenerator(keyframe),
timestamp: input.timestamp + totalOffset,
timingFunction: timingFunctions[keyframe.timingFunction || "linear"],
transitionSourceFrameIndex: i,
isSynthetic: false,
});
}
return {newFrames: newInternalFrames};
};
================================================
FILE: internal/animate/interpolate.ts
================================================
import {Point} from "../types";
import {mapPoints, mod, smooth, split, splitLine} from "../util";
// Interpolates between angles a and b. Angles are normalized to avoid unnecessary rotation.
// Direction is chosen to produce the smallest possible movement.
const interpolateAngle = (percentage: number, a: number, b: number): number => {
const tau = Math.PI * 2;
let aNorm = mod(a, tau);
let bNorm = mod(b, tau);
if (Math.abs(aNorm - bNorm) > Math.PI) {
if (aNorm < bNorm) {
aNorm += tau;
} else {
bNorm += tau;
}
}
return split(percentage, aNorm, bNorm);
};
// Interpolates linearly between a and b. Can only interpolate between point lists that have the
// same number of points. Easing effects can be applied to the percentage given to this function.
// Percentages outside the 0-1 range are supported.
export const interpolateBetween = (percentage: number, a: Point[], b: Point[]): Point[] => {
if (a.length !== b.length) {
throw new Error("must have equal number of points");
}
// Clamped range for use in values that could look incorrect otherwise.
// ex. Handles that invert if their value goes negative (creates loops at corners).
const clamped = Math.min(1, Math.max(0, percentage));
const points: Point[] = [];
for (let i = 0; i < a.length; i++) {
points.push({
...splitLine(percentage, a[i], b[i]),
handleIn: {
angle: interpolateAngle(percentage, a[i].handleIn.angle, b[i].handleIn.angle),
length: split(clamped, a[i].handleIn.length, b[i].handleIn.length),
},
handleOut: {
angle: interpolateAngle(percentage, a[i].handleOut.angle, b[i].handleOut.angle),
length: split(clamped, a[i].handleOut.length, b[i].handleOut.length),
},
});
}
return points;
};
// Interpolates between a and b while applying a smoothing effect. Smoothing effect's strength is
// relative to how far away the percentage is from either 0 or 1. It is strongest in the middle of
// the animation (percentage = 0.5) or when bounds are exceeded (percentage = 1.8).
export const interpolateBetweenSmooth = (
strength: number,
percentage: number,
a: Point[],
b: Point[],
): Point[] => {
strength *= Math.min(1, Math.min(Math.abs(0 - percentage), Math.abs(1 - percentage)));
const interpolated = interpolateBetween(percentage, a, b);
const smoothed = smooth(interpolated, Math.sqrt(strength + 0.25) / 3);
return mapPoints(interpolated, ({index, curr}) => {
const sp = smoothed[index];
curr.handleIn.angle = interpolateAngle(strength, curr.handleIn.angle, sp.handleIn.angle);
curr.handleIn.length = split(strength, curr.handleIn.length, sp.handleIn.length);
curr.handleOut.angle = interpolateAngle(strength, curr.handleOut.angle, sp.handleOut.angle);
curr.handleOut.length = split(strength, curr.handleOut.length, sp.handleOut.length);
return curr;
});
};
================================================
FILE: internal/animate/prepare.ts
================================================
import {
angleOf,
coordEqual,
distance,
forPoints,
insertCount,
length,
mapPoints,
mod,
reverse,
shift,
} from "../util";
import {Point} from "../types";
// Iterate through point ordering possibilities to find an option with the least
// distance between points. Also reverse the list to try and optimize.
const optimizeOrder = (a: Point[], b: Point[]): Point[] => {
const count = a.length;
let minTotal = Infinity;
let minOffset = 0;
let minOffsetBase: Point[] = [];
const setMinOffset = (points: Point[]) => {
for (let i = 0; i < count; i++) {
let total = 0;
for (let j = 0; j < count; j++) {
total += (100 * distance(a[j], points[mod(j + i, count)])) ** 2;
if (total > minTotal) break;
}
if (total <= minTotal) {
minTotal = total;
minOffset = i;
minOffsetBase = points;
}
}
};
setMinOffset(b);
setMinOffset(reverse(b));
return shift(minOffset, minOffsetBase);
};
// Modify the input shape to be the exact same path visually, but with
// additional points so that the total number of points is "count".
export const divide = (count: number, points: Point[]): Point[] => {
if (points.length < 3) throw new Error("not enough points");
if (count < points.length) throw new Error("cannot remove points");
if (count === points.length) return points.slice();
const lengths: number[] = [];
forPoints(points, ({curr, next}) => {
lengths.push(length(curr, next()));
});
const divisors = divideLengths(lengths, count - points.length);
const out: Point[] = [];
for (let i = 0; i < points.length; i++) {
const curr: Point = out[out.length - 1] || points[i];
const next = points[mod(i + 1, points.length)];
out.pop();
out.push(...insertCount(divisors[i], curr, next));
}
// Remove redundant last point to produce closed shape, but use its incoming \
// handle for the first point.
const last = out.pop();
out[0] = Object.assign({}, out[0], {handleIn: last!.handleIn});
return out;
};
// If point has no handle and is on top of the point before or after it, use the
// angle of the fixer shape's point at the same index. This is especially useful
// when all the points of the initial shape are concentrated on the same
// coordinates and "expand" into the target shape.
const fixAnglesWith = (fixee: Point[], fixer: Point[]): Point[] => {
return mapPoints(fixee, ({index, curr, prev, next}) => {
if (curr.handleIn.length === 0 && coordEqual(prev(), curr)) {
curr.handleIn.angle = fixer[index].handleIn.angle;
}
if (curr.handleOut.length === 0 && coordEqual(next(), curr)) {
curr.handleOut.angle = fixer[index].handleOut.angle;
}
return curr;
});
};
// If point has no handle, use angle between before and after points.
const fixAnglesSelf = (points: Point[]): Point[] => {
return mapPoints(points, ({curr, prev, next}) => {
const angle = angleOf(prev(), next());
if (curr.handleIn.length === 0) {
curr.handleIn.angle = angle + Math.PI;
}
if (curr.handleOut.length === 0) {
curr.handleOut.angle = angle;
}
return curr;
});
};
// Split the input lengths into smaller segments to add the target amount of
// lengths while minimizing the standard deviation of the list of lengths.
const divideLengths = (lengths: number[], add: number): number[] => {
const divisors = lengths.map(() => 1);
const sizes = lengths.slice();
for (let i = 0; i < add; i++) {
let maxSizeIndex = 0;
for (let j = 1; j < sizes.length; j++) {
if (sizes[j] > sizes[maxSizeIndex]) {
maxSizeIndex = j;
continue;
}
if (sizes[j] === sizes[maxSizeIndex]) {
if (lengths[j] > lengths[maxSizeIndex]) {
maxSizeIndex = j;
}
}
}
divisors[maxSizeIndex]++;
sizes[maxSizeIndex] = lengths[maxSizeIndex] / divisors[maxSizeIndex];
}
return divisors;
};
export const prepare = (
a: Point[],
b: Point[],
options: {rawAngles: boolean; divideRatio: number},
): [Point[], Point[]] => {
const pointCount = options.divideRatio * Math.max(a.length, b.length);
const aNorm = divide(pointCount, a);
const bNorm = divide(pointCount, b);
const bOpt = optimizeOrder(aNorm, bNorm);
return [
options.rawAngles ? aNorm : fixAnglesWith(fixAnglesSelf(aNorm), bOpt),
options.rawAngles ? bOpt : fixAnglesWith(fixAnglesSelf(bOpt), aNorm),
];
};
================================================
FILE: internal/animate/state.ts
================================================
import {Point} from "../types";
import {InternalKeyframe, Keyframe, RenderCache, renderFramesAt, transitionFrames} from "./frames";
interface CallbackKeyframe extends Keyframe {
callback?: () => void;
}
interface FrameCallbackStore {
[frameId: string]: () => void;
}
export const statefulAnimationGenerator = <K extends CallbackKeyframe, T>(
generator: (keyframe: K) => Point[],
renderer: (points: Point[]) => T,
checker: (keyframe: K, index: number) => void,
) => (timestampProvider: () => number) => {
let internalFrames: InternalKeyframe[] = [];
let renderCache: RenderCache = {};
let frameCallbackStore: FrameCallbackStore = {};
// Keep track of paused state.
let pausedAt = 0;
let pauseOffset = 0;
const getAnimationTimestamp = () => timestampProvider() - pauseOffset;
const isPaused = () => pausedAt !== 0;
const play = () => {
if (!isPaused()) return;
pauseOffset += getAnimationTimestamp() - pausedAt;
pausedAt = 0;
};
const pause = () => {
if (isPaused()) return;
pausedAt = getAnimationTimestamp();
};
const playPause = () => {
if (isPaused()) {
play();
} else {
pause();
}
};
const renderPoints = (): Point[] => {
const renderOutput = renderFramesAt({
renderCache: renderCache,
timestamp: isPaused() ? pausedAt : getAnimationTimestamp(),
currentFrames: internalFrames,
});
// Update render cache with returned value.
renderCache = renderOutput.renderCache;
// Invoke callback if defined and the first time the frame is reached.
if (renderOutput.lastFrameId && frameCallbackStore[renderOutput.lastFrameId]) {
setTimeout(frameCallbackStore[renderOutput.lastFrameId]);
delete frameCallbackStore[renderOutput.lastFrameId];
}
return renderOutput.points;
};
const renderFrame = (): T => {
return renderer(renderPoints());
};
const transition = (...keyframes: K[]) => {
// Make sure frame info is valid.
for (let i = 0; i < keyframes.length; i++) {
checker(keyframes[i], i);
}
const transitionOutput = transitionFrames<K>({
renderCache: renderCache,
timestamp: getAnimationTimestamp(),
currentFrames: internalFrames,
newFrames: keyframes,
shapeGenerator: generator,
});
// Reset internal state..
internalFrames = transitionOutput.newFrames;
frameCallbackStore = {};
renderCache = {};
// Populate callback store using returned frame ids.
for (const newFrame of internalFrames) {
if (newFrame.isSynthetic) continue;
const {callback} = keyframes[newFrame.transitionSourceFrameIndex];
if (callback) frameCallbackStore[newFrame.id] = callback;
}
};
return {renderFrame, renderPoints, transition, play, pause, playPause};
};
================================================
FILE: internal/animate/testing/index.html
================================================
<html>
<head>
<style>
body {
align-items: center;
display: flex;
height: 100vh;
justify-content: center;
margin: 0;
width: 100vw;
}
body * {
border: 1px solid #eee;
}
.buttons {
display: flex;
flex-direction: column;
}
#interact {
margin-top: 20px;
padding: 10px;
}
</style>
</head>
<body>
<div class="buttons">
<button id="toggle">TOGGLE<br />DEBUG</button>
<button id="interact">INTERACT</button>
</div>
<script src="./script.ts"></script>
</body>
</html>
================================================
FILE: internal/animate/testing/script.ts
================================================
import {interpolateBetweenSmooth} from "../interpolate";
import {divide, prepare} from "../prepare";
import {Coord, Point} from "../../types";
import {forPoints, insertAt, insertCount, length, mapPoints, mod, rad} from "../../util";
import {clear, drawClosed, drawInfo} from "../../render/canvas";
import {genBlob, genFromOptions} from "../../gen";
import {rand} from "../../rand";
import * as blobs2 from "../../../public/blobs";
import * as blobs2Animate from "../../../public/animate";
let animationSpeed = 2;
let animationStart = 0.3;
let debug = true;
let size = 1300;
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.height = size;
canvas.width = size;
const temp = canvas.getContext("2d");
if (temp === null) throw new Error("context is null");
const ctx = temp;
const toggle = document.getElementById("toggle");
if (toggle === null) throw new Error("no toggle");
toggle.onclick = () => (debug = !debug);
const interact = document.getElementById("interact") as any;
if (toggle === null) throw new Error("no interact");
const addInteraction = (newOnclick: () => void) => {
const oldOnclick = interact.onclick || (() => 0);
interact.onclick = () => {
oldOnclick();
newOnclick();
};
};
const point = (x: number, y: number, ia: number, il: number, oa: number, ol: number): Point => {
return {
x: x * size,
y: y * size,
handleIn: {angle: rad(ia), length: il * size},
handleOut: {angle: rad(oa), length: ol * size},
};
};
const testSplitAt = (percentage: number) => {
let points: Point[] = [
point(0.15, 0.15, 135, 0.1, 315, 0.2),
point(0.85, 0.15, 225, 0.1, 45, 0.2),
point(0.85, 0.85, 315, 0.1, 135, 0.2),
point(0.15, 0.85, 45, 0.1, 225, 0.2),
];
const count = points.length;
const stop = 2 * count - 1;
for (let i = 0; i < count; i++) {
const double = i * 2;
const next = mod(double + 1, stop);
points.splice(double, 2, ...insertAt(percentage, points[double], points[next]));
}
points.splice(0, 1);
let sum = 0;
forPoints(points, ({curr, next}) => {
sum += length(curr, next());
});
drawInfo(ctx, 1, "split at lengths sum", sum);
drawClosed(ctx, debug, points);
};
const testSplitBy = () => {
const count = 10;
for (let i = 0; i < count; i++) {
drawClosed(
ctx,
debug,
insertCount(
i + 1,
point(0.15, 0.2 + i * 0.06, 30, 0.04, -30, 0.04),
point(0.25, 0.2 + i * 0.06, 135, 0.04, 225, 0.04),
),
);
}
};
const testDividePoints = () => {
const count = 10;
for (let i = 0; i < count; i++) {
drawClosed(
ctx,
debug,
divide(i + 3, [
point(0.3, 0.2 + i * 0.05, -10, 0.04, -45, 0.02),
point(0.35, 0.2 + i * 0.05 - 0.02, 180, 0.02, 0, 0.02),
point(0.4, 0.2 + i * 0.05, -135, 0.02, 170, 0.04),
]),
);
}
};
const testInterpolateBetween = (percentage: number) => {
const a = [
point(0.3, 0.72, 135, 0.05, -45, 0.05),
point(0.4, 0.72, -135, 0.05, 45, 0.05),
point(0.4, 0.82, -45, 0.05, 135, 0.05),
point(0.3, 0.82, 45, 0.05, 225, 0.05),
];
const b = [
point(0.35, 0.72, 180, 0, 0, 0),
point(0.4, 0.77, -90, 0, 90, 0),
point(0.35, 0.82, 360 * 10, 0, 180, 0),
point(0.3, 0.77, 90, 0, -90, 0),
];
drawClosed(ctx, debug, loopBetween(percentage, a, b));
};
const testPrepPointsA = (percentage: number) => {
const a = blob("a", 6, 0.15, {x: 0.45, y: 0.1});
const b = blob("b", 10, 0.15, {x: 0.45, y: 0.1});
drawClosed(
ctx,
debug,
loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),
);
};
const testPrepPointsB = (percentage: number) => {
const a = blob("a", 8, 0.15, {x: 0.45, y: 0.25});
const b: Point[] = [
point(0.45, 0.25, 0, 0, 0, 0),
point(0.6, 0.25, 0, 0, 0, 0),
point(0.6, 0.4, 0, 0, 0, 0),
point(0.45, 0.4, 0, 0, 0, 0),
];
drawClosed(
ctx,
debug,
loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),
);
};
const testPrepPointsC = (percentage: number) => {
const a = blob("c", 8, 0.15, {x: 0.45, y: 0.45});
const b: Point[] = [
point(0.5, 0.45, 0, 0, 0, 0),
point(0.55, 0.45, 0, 0, 0, 0),
point(0.55, 0.5, 0, 0, 0, 0),
point(0.6, 0.5, 0, 0, 0, 0),
point(0.6, 0.55, 0, 0, 0, 0),
point(0.55, 0.55, 0, 0, 0, 0),
point(0.55, 0.6, 0, 0, 0, 0),
point(0.5, 0.6, 0, 0, 0, 0),
point(0.5, 0.55, 0, 0, 0, 0),
point(0.45, 0.55, 0, 0, 0, 0),
point(0.45, 0.5, 0, 0, 0, 0),
point(0.5, 0.5, 0, 0, 0, 0),
];
drawClosed(
ctx,
debug,
loopBetween(percentage, ...prepare(b, a, {rawAngles: false, divideRatio: 1})),
);
};
const testPrepPointsD = (percentage: number) => {
const a = blob("d", 8, 0.15, {x: 0.45, y: 0.65});
const b: Point[] = [
point(0.525, 0.725, 0, 0, 0, 0),
point(0.525, 0.725, 0, 0, 0, 0),
point(0.525, 0.725, 0, 0, 0, 0),
];
drawClosed(
ctx,
debug,
loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),
);
};
const testPrepLetters = (percentage: number) => {
const a: Point[] = [
point(0.65, 0.2, 0, 0, 0, 0),
point(0.85, 0.2, 0, 0, 0, 0),
point(0.85, 0.25, 0, 0, 0, 0),
point(0.7, 0.25, 0, 0, 0, 0),
point(0.7, 0.4, 0, 0, 0, 0),
point(0.8, 0.4, 0, 0, 0, 0),
point(0.8, 0.35, 0, 0, 0, 0),
point(0.75, 0.35, 0, 0, 0, 0),
point(0.75, 0.3, 0, 0, 0, 0),
point(0.85, 0.3, 0, 0, 0, 0),
point(0.85, 0.45, 0, 0, 0, 0),
point(0.65, 0.45, 0, 0, 0, 0),
];
const b: Point[] = blob("", 8, 0.25, {x: 0.65, y: 0.2});
drawClosed(
ctx,
debug,
loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),
);
};
const testGen = () => {
const cellSideCount = 16;
const cellSize = size / cellSideCount;
ctx.save();
ctx.strokeStyle = "#fafafa";
ctx.fillStyle = "#f1f1f1";
for (let i = 0; i < cellSideCount; i++) {
for (let j = 0; j < cellSideCount; j++) {
ctx.strokeRect(i * cellSize, j * cellSize, cellSize, cellSize);
ctx.fill(
blobs2.canvasPath(
{
extraPoints: j,
randomness: i,
seed: i + j - i * j,
size: cellSize,
},
{
offsetX: i * cellSize,
offsetY: j * cellSize,
},
),
);
}
}
ctx.restore();
};
const blob = (seed: string, count: number, scale: number, offset: Coord): Point[] => {
const rgen = rand(seed);
const points = genBlob(count, () => 0.3 + 0.2 * rgen());
return mapPoints(points, ({curr}) => {
curr.x *= scale * size;
curr.y *= scale * size;
curr.x += offset.x * size;
curr.y += offset.y * size;
curr.handleIn.length *= scale * size;
curr.handleOut.length *= scale * size;
return curr;
});
};
const loopBetween = (percentage: number, a: Point[], b: Point[]): Point[] => {
// Draw before/after shapes + point path.
ctx.save();
ctx.strokeStyle = "#ffaaaa";
drawClosed(ctx, false, a);
ctx.strokeStyle = "#aaaaff";
drawClosed(ctx, false, b);
ctx.strokeStyle = "#33ff33";
for (let i = 0; i < a.length; i++) {
ctx.beginPath();
ctx.moveTo(a[i].x, a[i].y);
ctx.lineTo(b[i].x, b[i].y);
ctx.stroke();
}
ctx.restore();
if (percentage < 0.5) {
return interpolateBetweenSmooth(1, 2 * percentage, a, b);
} else {
return interpolateBetweenSmooth(1, -2 * percentage + 2, a, b);
}
};
const genBlobAnimation = (
speed: number,
offset: number,
timing: blobs2Animate.CanvasKeyframe["timingFunction"],
timeWarp: number,
) => {
const animation = blobs2Animate.canvasPath(() => Date.now() * timeWarp);
const loopAnimation = () => {
animation.transition(
{
duration: speed,
delay: speed,
timingFunction: "ease",
blobOptions: {
extraPoints: 3,
randomness: 4,
seed: Math.random(),
size: 200,
},
canvasOptions: {
offsetX: offset,
},
},
{
duration: speed,
timingFunction: "ease",
blobOptions: {
extraPoints: 3,
randomness: 4,
seed: Math.random(),
size: 200,
},
canvasOptions: {
offsetX: offset,
},
},
{
duration: speed,
delay: speed,
timingFunction: "ease",
blobOptions: {
extraPoints: 3,
randomness: 4,
seed: Math.random(),
size: 200,
},
canvasOptions: {
offsetX: offset,
},
},
{
duration: speed,
callback: loopAnimation,
timingFunction: "ease",
blobOptions: {
extraPoints: 39,
randomness: 2,
seed: Math.random(),
size: 200,
},
canvasOptions: {
offsetX: offset,
},
},
);
};
animation.transition({
duration: 0,
callback: loopAnimation,
blobOptions: {
extraPoints: 1,
randomness: 0,
seed: 0,
size: 200,
},
canvasOptions: {
offsetX: offset,
},
});
addInteraction(() => {
animation.transition({
duration: speed,
callback: loopAnimation,
timingFunction: timing,
blobOptions: {
extraPoints: 30,
randomness: 8,
seed: Math.random(),
size: 180,
},
canvasOptions: {
offsetX: 10 + offset,
offsetY: 10,
},
});
});
return animation;
};
const genCustomAnimation = (speed: number, offset: number) => {
const noHandles = {
handleIn: {angle: 0, length: 0},
handleOut: {angle: 0, length: 0},
};
const animation = blobs2Animate.canvasPath();
const loopAnimation = (immediate: boolean = false) => {
const size = 200;
animation.transition(
{
duration: immediate ? 0 : speed,
delay: 100,
timingFunction: "elasticEnd0",
blobOptions: {
extraPoints: 3,
randomness: 4,
seed: Math.random(),
size: size,
},
canvasOptions: {offsetX: offset, offsetY: 220},
},
{
duration: speed,
delay: 100,
timingFunction: "elasticEnd0",
points: [
{x: 0, y: 0, ...noHandles},
{x: 0, y: size, ...noHandles},
{x: size, y: size, ...noHandles},
{x: size, y: 0, ...noHandles},
],
canvasOptions: {offsetX: offset, offsetY: 220},
callback: loopAnimation,
},
);
};
loopAnimation(true);
addInteraction(() => animation.playPause());
return animation;
};
const wigglePresetBad = (
animation: blobs2Animate.Animation,
config: {
blobOptions: blobs2.BlobOptions;
period: number;
delay?: number;
timingFunction?: blobs2Animate.CanvasKeyframe["timingFunction"];
canvasOptions?: {
offsetX?: number;
offsetY?: number;
};
},
) => {
const targetBlob: Point[] = genFromOptions(config.blobOptions);
const numberOfPoints = 3 + config.blobOptions.extraPoints;
const mutatesPerPeriod = 1 * numberOfPoints;
const mutateInterval = config.period / mutatesPerPeriod;
const mutateRatio = 1 / mutatesPerPeriod;
console.log(
"mutatesPerPeriod",
mutatesPerPeriod,
"mutateInterval",
mutateInterval,
"mutateRatio",
mutateRatio,
"config",
JSON.stringify(config),
);
const loopAnimation = () => {
const newBlob = genFromOptions(Object.assign(config.blobOptions, {seed: Math.random()}));
for (let i = 0; i < newBlob.length; i++) {
if (Math.random() < mutateRatio) {
targetBlob[i] = newBlob[i];
}
}
animation.transition({
duration: config.period,
timingFunction: config.timingFunction,
canvasOptions: config.canvasOptions,
points: targetBlob,
});
};
animation.transition({
duration: 0,
delay: config.delay || 0,
timingFunction: config.timingFunction,
canvasOptions: config.canvasOptions,
points: genFromOptions(config.blobOptions),
callback: () => setInterval(loopAnimation, mutateInterval),
});
addInteraction(() => animation.playPause());
};
const genBadWiggle = (period: number, offset: number) => {
const animation = blobs2Animate.canvasPath();
wigglePresetBad(animation, {
blobOptions: {
extraPoints: 1,
randomness: 4,
seed: Math.random(),
size: 200,
},
period,
timingFunction: "ease",
canvasOptions: {offsetX: offset, offsetY: 220},
});
return animation;
};
const genWiggle = (offset: number, speed: number) => {
const animation = blobs2Animate.canvasPath();
blobs2Animate.wigglePreset(
animation,
{
extraPoints: 4,
randomness: 2,
seed: Math.random(),
size: 200,
},
{offsetX: offset, offsetY: 220},
{speed},
);
addInte
gitextract_u04v749x/ ├── .github/ │ └── workflows/ │ └── push.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CNAME ├── LICENSE ├── README.legacy.md ├── README.md ├── demo/ │ ├── content.ts │ ├── example.ts │ ├── index.html │ └── internal/ │ ├── canvas.ts │ ├── debug.ts │ └── layout.ts ├── examples/ │ └── corner-expand.html ├── index.html ├── internal/ │ ├── animate/ │ │ ├── frames.ts │ │ ├── interpolate.ts │ │ ├── prepare.ts │ │ ├── state.ts │ │ ├── testing/ │ │ │ ├── index.html │ │ │ └── script.ts │ │ └── timing.ts │ ├── check.ts │ ├── gen.ts │ ├── rand.ts │ ├── render/ │ │ ├── canvas.ts │ │ ├── svg.test.ts │ │ └── svg.ts │ ├── types.ts │ └── util.ts ├── package.json ├── public/ │ ├── __snapshots__/ │ │ └── legacy.test.ts.snap │ ├── animate.test.ts │ ├── animate.ts │ ├── blobs.test.ts │ ├── blobs.ts │ ├── legacy.test.ts │ └── legacy.ts ├── rollup.config.mjs └── tsconfig.json
SYMBOL INDEX (37 symbols across 12 files)
FILE: demo/internal/layout.ts
type Cell (line 10) | interface Cell {
type CellPainter (line 18) | interface CellPainter {
type AnimationPainter (line 27) | interface AnimationPainter {
FILE: internal/animate/frames.ts
type Keyframe (line 6) | interface Keyframe {
type InternalKeyframe (line 12) | interface InternalKeyframe {
type RenderCache (line 24) | interface RenderCache {
type RenderInput (line 31) | interface RenderInput {
type RenderOutput (line 37) | interface RenderOutput {
type TransitionInput (line 43) | interface TransitionInput<T extends Keyframe> extends RenderInput {
type TransitionOutput (line 48) | interface TransitionOutput {
FILE: internal/animate/state.ts
type CallbackKeyframe (line 4) | interface CallbackKeyframe extends Keyframe {
type FrameCallbackStore (line 8) | interface FrameCallbackStore {
FILE: internal/animate/timing.ts
type TimingFunc (line 1) | interface TimingFunc {
FILE: internal/render/svg.ts
type RenderOptions (line 4) | interface RenderOptions {
class XmlElement (line 133) | class XmlElement {
method constructor (line 137) | public constructor(public tag: string) {}
method render (line 139) | public render(): string {
method renderAttributes (line 148) | private renderAttributes(): string {
method renderChildren (line 158) | private renderChildren(): string {
FILE: internal/types.ts
type Coord (line 2) | interface Coord {
type Handle (line 7) | interface Handle {
type Point (line 14) | interface Point extends Coord {
FILE: internal/util.ts
type PointIteratorArgs (line 10) | interface PointIteratorArgs {
FILE: public/animate.test.ts
type TestCase (line 38) | interface TestCase {
FILE: public/animate.ts
type Keyframe (line 17) | interface Keyframe {
type CanvasKeyframe (line 45) | interface CanvasKeyframe extends Keyframe {
type CanvasCustomKeyframe (line 55) | interface CanvasCustomKeyframe extends Keyframe {
type Animation (line 60) | interface Animation {
type TimestampProvider (line 80) | interface TimestampProvider {
type WiggleOptions (line 84) | interface WiggleOptions {
FILE: public/blobs.test.ts
type TestCase (line 25) | interface TestCase<T> {
FILE: public/blobs.ts
type BlobOptions (line 7) | interface BlobOptions {
type CanvasOptions (line 19) | interface CanvasOptions {
type SvgOptions (line 25) | interface SvgOptions {
FILE: public/legacy.ts
type PathOptions (line 13) | interface PathOptions {
type BlobOptions (line 27) | interface BlobOptions extends PathOptions {
type XmlElement (line 103) | interface XmlElement {
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (292K chars).
[
{
"path": ".github/workflows/push.yml",
"chars": 249,
"preview": "on: push\nname: on-push\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n - uses: a"
},
{
"path": ".gitignore",
"chars": 110,
"preview": "coverage/\nnode_modules/\n~*\n*.js\n*.js.map\n*.d.ts\n.cache\ndist\n!rollup.config.js\ndocs/*.js\ndocs/*.svg\ndocs/*.css\n"
},
{
"path": ".npmignore",
"chars": 73,
"preview": "*\n!README.md\n!LICENSE\n!package.json\n!**/*.js\n!**/*.js.map\n!**/index.d.ts\n"
},
{
"path": "CHANGELOG.md",
"chars": 1521,
"preview": "# 2.3.0\n\n- Add `CanvasCustomKeyframe` to `v2/animate`\n- Add `wigglePreset` to `v2/animate`\n\n# 2.2.1\n\n- Add support"
},
{
"path": "CNAME",
"chars": 9,
"preview": "blobs.dev"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) Gabriel Harel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.legacy.md",
"chars": 3041,
"preview": "The legacy API exists to preserve compatibility for users importing the package\nusing a `script` tag. Because [unpkg.com"
},
{
"path": "README.md",
"chars": 6545,
"preview": "<p align=\"center\">\n <a href=\"https://github.com/g-harel/blobs/blob/master/README.legacy.md\"><b>Legacy documentation</"
},
{
"path": "demo/content.ts",
"chars": 35382,
"preview": "import {addCanvas, addTitle, colors, sizes} from \"./internal/layout\";\nimport {\n calcBouncePercentage,\n drawClosed,"
},
{
"path": "demo/example.ts",
"chars": 3483,
"preview": "import {CanvasKeyframe, canvasPath, wigglePreset} from \"../public/animate\";\nimport {drawHandles, drawPoint} from \"./inte"
},
{
"path": "demo/index.html",
"chars": 4585,
"preview": "<html>\n <head>\n <link rel=\"shortcut icon\" href=\"https://blobs.dev/assets/favicon.ico?v=3ewlwLn2WO\" />\n "
},
{
"path": "demo/internal/canvas.ts",
"chars": 6163,
"preview": "import {TimingFunc} from \"../../internal/animate/timing\";\nimport {Coord, Point} from \"../../internal/types\";\nimport {exp"
},
{
"path": "demo/internal/debug.ts",
"chars": 810,
"preview": "// If debug is initially set to false it will not be toggleable.\nlet debug = window.location.search.includes(\"debug\") &&"
},
{
"path": "demo/internal/layout.ts",
"chars": 7865,
"preview": "import {tempStyles} from \"./canvas\";\nimport {isDebug, onDebugStateChange} from \"./debug\";\n\nexport const colors = {\n d"
},
{
"path": "examples/corner-expand.html",
"chars": 3432,
"preview": "<html>\n <head>\n <title>Expand demo</title>\n <script src=\"https://unpkg.com/blobs/v2/animate\"></script>\n"
},
{
"path": "index.html",
"chars": 89690,
"preview": "<html><head><link rel=\"icon shortcut\" href=\"https://blobs.dev/assets/favicon.ico?v=3ewlwLn2WO\"><meta name=\"viewport\" con"
},
{
"path": "internal/animate/frames.ts",
"chars": 6621,
"preview": "import {TimingFunc, timingFunctions} from \"./timing\";\nimport {Point} from \"../types\";\nimport {prepare} from \"./prepare\";"
},
{
"path": "internal/animate/interpolate.ts",
"chars": 3076,
"preview": "import {Point} from \"../types\";\nimport {mapPoints, mod, smooth, split, splitLine} from \"../util\";\n\n// Interpolates betwe"
},
{
"path": "internal/animate/prepare.ts",
"chars": 4805,
"preview": "import {\n angleOf,\n coordEqual,\n distance,\n forPoints,\n insertCount,\n length,\n mapPoints,\n mod,\n"
},
{
"path": "internal/animate/state.ts",
"chars": 3068,
"preview": "import {Point} from \"../types\";\nimport {InternalKeyframe, Keyframe, RenderCache, renderFramesAt, transitionFrames} from "
},
{
"path": "internal/animate/testing/index.html",
"chars": 814,
"preview": "<html>\n <head>\n <style>\n body {\n align-items: center;\n display: flex;"
},
{
"path": "internal/animate/testing/script.ts",
"chars": 16262,
"preview": "import {interpolateBetweenSmooth} from \"../interpolate\";\nimport {divide, prepare} from \"../prepare\";\nimport {Coord, Poin"
},
{
"path": "internal/animate/timing.ts",
"chars": 857,
"preview": "export interface TimingFunc {\n (percentage: number): number;\n}\n\nconst linear: TimingFunc = (p) => {\n return p;\n};\n"
},
{
"path": "internal/check.ts",
"chars": 3494,
"preview": "import {timingFunctions} from \"./animate/timing\";\n\nconst typeCheck = (name: string, val: any, expected: string[]) => {\n "
},
{
"path": "internal/gen.ts",
"chars": 2254,
"preview": "import {rand} from \"../internal/rand\";\nimport {mapPoints} from \"../internal/util\";\nimport {BlobOptions} from \"../public/"
},
{
"path": "internal/rand.ts",
"chars": 1437,
"preview": "import {createNoise2D} from \"simplex-noise\";\n\n// Seeded random number generator.\n// https://stackoverflow.com/a/47593316"
},
{
"path": "internal/render/canvas.ts",
"chars": 2216,
"preview": "import {Coord, Point} from \"../types\";\nimport {expandHandle, forPoints} from \"../util\";\n\nconst pointSize = 2;\nconst info"
},
{
"path": "internal/render/svg.test.ts",
"chars": 953,
"preview": "import {XmlElement} from \"./svg\";\n\ndescribe(\"internal/render/svg\", () => {\n describe(\"XmlElement\", () => {\n it"
},
{
"path": "internal/render/svg.ts",
"chars": 6506,
"preview": "import {Point} from \"../types\";\nimport {expandHandle, forPoints} from \"../util\";\n\nexport interface RenderOptions {\n /"
},
{
"path": "internal/types.ts",
"chars": 415,
"preview": "// Position in a coordinate system with an origin in the top left corner.\nexport interface Coord {\n x: number;\n y:"
},
{
"path": "internal/util.ts",
"chars": 5821,
"preview": "import {Coord, Handle, Point} from \"./types\";\n\nexport const copyPoint = (p: Point): Point => ({\n x: p.x,\n y: p.y,\n"
},
{
"path": "package.json",
"chars": 1957,
"preview": "{\n \"name\": \"blobs\",\n \"version\": \"2.3.0\",\n \"description\": \"Random blob generation and animation\",\n \"author\": \"g-harel"
},
{
"path": "public/__snapshots__/legacy.test.ts.snap",
"chars": 13333,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`fill 1`] = `\"<svg width=\"109\" height=\"109\" viewBox=\"0 0 109 109\" xm"
},
{
"path": "public/animate.test.ts",
"chars": 14842,
"preview": "import {CanvasKeyframe, canvasPath} from \"./animate\";\n\nconst genKeyframe = (): CanvasKeyframe => ({\n duration: 1000 *"
},
{
"path": "public/animate.ts",
"chars": 6777,
"preview": "import {Point} from \"../internal/types\";\nimport {renderPath2D} from \"../internal/render/canvas\";\nimport {genFromOptions}"
},
{
"path": "public/blobs.test.ts",
"chars": 10531,
"preview": "import {BlobOptions, CanvasOptions, canvasPath, svg, SvgOptions, svgPath} from \"./blobs\";\n\n// @ts-ignore\nimport {Path2D,"
},
{
"path": "public/blobs.ts",
"chars": 2462,
"preview": "import {genFromOptions} from \"../internal/gen\";\nimport {renderPath} from \"../internal/render/svg\";\nimport {renderPath2D}"
},
{
"path": "public/legacy.test.ts",
"chars": 3079,
"preview": "import blobs, {BlobOptions} from \"./legacy\";\n\nconst genMinimalOptions = (): BlobOptions => ({\n size: 1000 * Math.rand"
},
{
"path": "public/legacy.ts",
"chars": 3303,
"preview": "import {rand} from \"../internal/rand\";\nimport {renderEditable, XmlElement as InternalXmlElement} from \"../internal/rende"
},
{
"path": "rollup.config.mjs",
"chars": 1275,
"preview": "import typescript from \"rollup-plugin-typescript2\";\nimport { uglify } from \"rollup-plugin-uglify\";\nimport copy from \"rol"
},
{
"path": "tsconfig.json",
"chars": 704,
"preview": "{\n \"exclude\": [\n \"example\",\n \"test\",\n \"node_modules\"\n ],\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"mod"
}
]
About this extraction
This page contains the full source code of the g-harel/blobs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (274.3 KB), approximately 80.1k tokens, and a symbol index with 37 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.