[
  {
    "path": ".github/workflows/push.yml",
    "content": "on: push\nname: on-push\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: actions/setup-node@v1\n      with:\n        node-version: '18.x'\n    - run: yarn\n    - run: yarn run build\n    - run: yarn run test\n"
  },
  {
    "path": ".gitignore",
    "content": "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",
    "content": "*\n!README.md\n!LICENSE\n!package.json\n!**/*.js\n!**/*.js.map\n!**/index.d.ts\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 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 for custom point-based keyframes\n-   Add option to set custom timestamp provider\n-   Add module support, thank you to #4 and #7\n-   Export `Animation` and `TimestampProvider` types from `v2/animate`\n\n# 2.2.0\n\n-   Remove added points from end keyframe after interpolation completes.\n-   Add play/pause/playPause API for animations.\n\n# 2.1.0\n\n-   Improved type checks on user-provided data\n-   Added `\"blobs/v2/animate\"`\n    -   Animate between arbitrary blob keyframes\n    -   Separate import to keep main bundle small\n    -   New demo website with animated blob transitions\n    -   Supports only canvas rendering\n\n# 2.0.1\n\n-   Fix typo in code example of README\n\n# 2.0.0\n\n-   **BREAKING** Editable SVG element creation function has moved to\n    `blobs.xml(tagName)`.\n-   Added `\"blobs/v2\"`\n    -   30% smaller compressed size\n    -   Supports canvas rendering\n    -   Supports raw SVG path rendering\n\n# 1.1.0\n\n-   Add support for editable output\n\n# 1.0.5\n\n-   Fix assets in README on npmjs.com\n\n# 1.0.4\n\n-   Use snapshot tests to verify consistency\n-   Ignore unnecessary files in npm tarball\n-   Output sourcemap file\n-   Add project logo\n-   README content updates\n\n# 1.0.3\n\n-   Add link to demo page in the README\n\n# 1.0.2\n\n-   Make transpiled output minified\n-   Minor changes to the README\n\n# 1.0.1\n\n-   Remove accidental dependency\n-   Minor changes to the README\n\n# 1.0.0\n\n-   Initial release\n"
  },
  {
    "path": "CNAME",
    "content": "blobs.dev"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Gabriel Harel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.legacy.md",
    "content": "The legacy API exists to preserve compatibility for users importing the package\nusing a `script` tag. Because [unpkg.com](https://unpkg.com) serves the latest\nversion of the package if no version is specified, I can't break backwards\ncompatibility, even with a major release. This API also preserves a few features\nthat could potentially still be useful to some users (guide rendering and\neditable svg).\n\n---\n\n## Install\n\n```ts\n// $ npm install blobs\nconst blobs = require(\"blobs\");\n```\n\n```html\n<script src=\"https://unpkg.com/blobs\"></script>\n```\n\n## Usage\n\n```typescript\nconst svg = blobs(options);\n```\n\n![](https://svgsaur.us?t=&w=5&h=32&b=fdcc56)\n![](https://svgsaur.us/?t=WARNING&w=103&h=32&s=16&y=21&x=12&b=feefcd&f=arial&o=b)\n![](https://svgsaur.us?t=&w=1&h=48&)\n\n_Options are **not**\n[sanitized](https://en.wikipedia.org/wiki/HTML_sanitization). Never trust raw\nuser-submitted values in the options._\n\n## Options\n\n#### Required\n\n| Name         | Type     | Description                                  |\n| ------------ | -------- | -------------------------------------------- |\n| `size`       | `number` | Bounding box dimensions (in pixels)          |\n| `complexity` | `number` | Blob complexity (number of points)           |\n| `contrast`   | `number` | Blob contrast (randomness of point position) |\n\n#### Optional\n\n| Name           | Type       | Default    | Description                           |\n| -------------- | ---------- | ---------- | ------------------------------------- |\n| `color`        | `string?`  | `\"none\"`   | Fill color                            |\n| `stroke`       | `object?`  | `...`      | Stroke options                        |\n| `stroke.color` | `string`   | `\"none\"`   | Stroke color                          |\n| `stroke.width` | `number`   | `0`        | Stroke width (in pixels)              |\n| `seed`         | `string?`  | _`random`_ | Value to seed random number generator |\n| `guides`       | `boolean?` | `false`    | Render points, handles and stroke     |\n\n_Either `stroke` or `color` must be defined._\n\n_Guides will use stroke color and width if defined. Otherwise, they default to\n`black` stroke with width of `1`._\n\n##### Example Options Object\n\n```typescript\nconst options = {\n    size: 600,\n    complexity: 0.2,\n    contrast: 0.4,\n    color: \"#ec576b\",\n    stroke: {\n        width: 0,\n        color: \"black\",\n    },\n    guides: false,\n    seed: \"1234\",\n};\n```\n\n## Advanced\n\nIf you need to edit the output svg for your use case, blobs also allows for\n_editable_ output.\n\n```typescript\nconst editableSvg = blobs.editable(options);\n```\n\nThe output of this function is a data structure that represents a nested svg\ndocument. This structure can be changed and rendered to a string using its\n`render` function.\n\n```typescript\neditableSvg.attributes.width = 1000;\nconst svg = editableSvg.render();\n```\n\nNew elements can be added anywhere in the hierarchy.\n\n```typescript\nconst xmlChild = blobs.xml(\"path\");\nxmlChild.attributes.stroke = \"red\";\n// ...\neditableSvg.children.push(xmlChild);\n```\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <a href=\"https://github.com/g-harel/blobs/blob/master/README.legacy.md\"><b>Legacy documentation</b></a>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://www.npmjs.com/package/blobs\"><!--\n     --><img src=\"https://img.shields.io/npm/v/blobs.svg\"><!--\n --></a>\n    <a href=\"https://github.com/g-harel/blobs/actions?query=workflow%3Aon-push\"><!--\n     --><img src=\"https://img.shields.io/github/actions/workflow/status/g-harel/blobs/push.yml?event=on-push\"><!--\n --></a>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://blobs.dev\">\n        <img src=\"./assets/logo.svg?sanitize=true\">\n    </a>\n</p>\n\n## Install\n\n```bash\n$ npm install blobs\n```\n\n```ts\nimport * as blobs2 from \"blobs/v2\";\n```\n\n```ts\nimport * as blobs2Animate from \"blobs/v2/animate\";\n```\n\n<p align=\"center\">\n    OR\n</p>\n\n```html\n<script src=\"https://unpkg.com/blobs/v2\"></script>\n```\n\n```html\n<script src=\"https://unpkg.com/blobs/v2/animate\"></script>\n```\n\n## SVG Path\n\n```js\nconst svgPath = blobs2.svgPath({\n    seed: Math.random(),\n    extraPoints: 8,\n    randomness: 4,\n    size: 256,\n});\ndoSomething(svgPath);\n```\n\n## SVG\n\n```js\nconst svgString = blobs2.svg(\n    {\n        seed: Math.random(),\n        extraPoints: 8,\n        randomness: 4,\n        size: 256,\n    },\n    {\n        fill: \"white\", // 🚨 NOT SANITIZED\n        stroke: \"black\", // 🚨 NOT SANITIZED\n        strokeWidth: 4,\n    },\n);\ncontainer.innerHTML = svgString;\n```\n\n## Canvas\n\n```js\nconst path = blobs2.canvasPath(\n    {\n        seed: Math.random(),\n        extraPoints: 16,\n        randomness: 2,\n        size: 128,\n    },\n    {\n        offsetX: 16,\n        offsetY: 32,\n    },\n);\nctx.stroke(path);\n```\n\n## Canvas Animation\n\n```js\nconst ctx = /* ... */;\nconst animation = blobs2Animate.canvasPath();\n\n// Set up rendering loop.\nconst renderAnimation = () => {\n    ctx.clearRect(0, 0, width, height);\n    ctx.fill(animation.renderFrame());\n    requestAnimationFrame(renderAnimation);\n};\nrequestAnimationFrame(renderAnimation);\n\n// Transition to new blob on canvas click.\nctx.canvas.onclick = () => {\n    animation.transition({\n        duration: 4000,\n        timingFunction: \"ease\",\n        callback: loopAnimation,\n        blobOptions: {...},\n    });\n};\n```\n\n## Canvas Wiggle\n\n```js\nconst ctx = /* ... */;\nconst animation = blobs2Animate.canvasPath();\n\n// Set up rendering loop.\nconst renderAnimation = () => {\n    ctx.clearRect(0, 0, width, height);\n    ctx.fill(animation.renderFrame());\n    requestAnimationFrame(renderAnimation);\n};\nrequestAnimationFrame(renderAnimation);\n\n// Begin wiggle animation.\nblobs2Animate.wigglePreset(\n    animation\n    /* blobOptions= */ {...},\n    /* canvasOptions= */ {},\n    /* wiggleOptions= */ {speed: 2},\n)\n```\n\n## Complete API\n\n### `\"blobs/v2\"`\n\n```ts\nexport interface BlobOptions {\n    // A given seed will always produce the same blob.\n    // Use `Math.random()` for pseudorandom behavior.\n    seed: string | number;\n    // Actual number of points will be `3 + extraPoints`.\n    extraPoints: number;\n    // Increases the amount of variation in point position.\n    randomness: number;\n    // Size of the bounding box.\n    size: number;\n}\n\nexport interface CanvasOptions {\n    // Coordinates of top-left corner of the blob.\n    offsetX?: number;\n    offsetY?: number;\n}\nexport const canvasPath: (blobOptions: BlobOptions, canvasOptions?: CanvasOptions) => Path2D;\n\nexport interface SvgOptions {\n    fill?: string; // Default: \"#ec576b\".\n    stroke?: string; // Default: \"none\".\n    strokeWidth?: number; // Default: 0.\n}\nexport const svg: (blobOptions: BlobOptions, svgOptions?: SvgOptions) => string;\nexport const svgPath: (blobOptions: BlobOptions) => string;\n```\n\n### `\"blobs/v2/animate\"`\n\n```ts\ninterface Keyframe {\n    // Duration of the keyframe animation in milliseconds.\n    duration: number;\n    // Delay before animation begins in milliseconds.\n    // Default: 0.\n    delay?: number;\n    // Controls the speed of the animation over time.\n    // Default: \"linear\".\n    timingFunction?:\n        | \"linear\"\n        | \"easeEnd\"\n        | \"easeStart\"\n        | \"ease\"\n        | \"elasticEnd0\"\n        | \"elasticEnd1\"\n        | \"elasticEnd2\"\n        | \"elasticEnd3\";\n    // Called after keyframe end-state is reached or passed.\n    // Called exactly once when the keyframe end-state is rendered.\n    // Not called if the keyframe is preempted by a new transition.\n    callback?: () => void;\n    // Standard options, refer to \"blobs/v2\" documentation.\n    canvasOptions?: {\n        offsetX?: number;\n        offsetY?: number;\n    };\n}\n\nexport interface CanvasKeyframe extends Keyframe {\n    // Standard options, refer to \"blobs/v2\" documentation.\n    blobOptions: {\n        seed: number | string;\n        randomness: number;\n        extraPoints: number;\n        size: number;\n    };\n}\n\nexport interface CanvasCustomKeyframe extends Keyframe {\n    // List of point coordinates that produce a single, closed shape.\n    points: Point[];\n}\n\nexport interface Animation {\n    // Renders the current state of the animation.\n    renderFrame: () => Path2D;\n    // Renders the current state of the animation as points.\n    renderPoints: () => Point[];\n    // Immediately begin animating through the given keyframes.\n    // Non-rendered keyframes from previous transitions are cancelled.\n    transition: (...keyframes: (CanvasKeyframe | CanvasCustomKeyframe)[]) => void;\n    // Resume a paused animation. Has no effect if already playing.\n    play: () => void;\n    // Pause a playing animation. Has no effect if already paused.\n    pause: () => void;\n    // Toggle between playing and pausing the animation.\n    playPause: () => void;\n}\n\n// Function that returns the current timestamp. This value will be used for all\n// duration/delay values and will be used to interpolate between keyframes. It\n// must produce values increasing in size.\n// Default: `Date.now`.\nexport interface TimestampProvider {\n    (): number;\n}\nexport const canvasPath: (timestampProvider?: TimestampProvider) => Animation;\n\nexport interface WiggleOptions {\n    // Speed of the wiggle movement. Higher is faster.\n    speed: number;\n    // Length of the transition from the current state to the wiggle blob.\n    // Default: 0\n    initialTransition?: number;\n}\n// Preset animation that produces natural-looking random movement.\n// The wiggle animation will continue indefinitely until the next transition.\nexport const wigglePreset = (\n    animation: Animation,\n    blobOptions: BlobOptions,\n    canvasOptions: CanvasOptions,\n    wiggleOptions: WiggleOptions,\n)\n```\n\n## License\n\n[MIT](./LICENSE)\n"
  },
  {
    "path": "demo/content.ts",
    "content": "import {addCanvas, addTitle, colors, sizes} from \"./internal/layout\";\nimport {\n    calcBouncePercentage,\n    drawClosed,\n    drawHandles,\n    drawLine,\n    drawOpen,\n    drawPoint,\n    forceStyles,\n    point,\n    tempStyles,\n} from \"./internal/canvas\";\nimport {\n    coordPoint,\n    deg,\n    distance,\n    expandHandle,\n    forPoints,\n    mapPoints,\n    mod,\n    shift,\n    split,\n    splitLine,\n} from \"../internal/util\";\nimport {timingFunctions} from \"../internal/animate/timing\";\nimport {Coord, Point} from \"../internal/types\";\nimport {rand} from \"../internal/rand\";\nimport {genFromOptions, smoothBlob} from \"../internal/gen\";\nimport {BlobOptions} from \"../public/blobs\";\nimport {interpolateBetween, interpolateBetweenSmooth} from \"../internal/animate/interpolate\";\nimport {divide} from \"../internal/animate/prepare\";\nimport {statefulAnimationGenerator} from \"../internal/animate/state\";\nimport {CanvasKeyframe, canvasPath, wigglePreset} from \"../public/animate\";\n\nconst makePoly = (pointCount: number, radius: number, center: Coord): Point[] => {\n    const angle = (2 * Math.PI) / pointCount;\n    const points: Point[] = [];\n    const nullHandle = {angle: 0, length: 0};\n    for (let i = 0; i < pointCount; i++) {\n        const coord = expandHandle(center, {angle: i * angle, length: radius});\n        points.push({...coord, handleIn: nullHandle, handleOut: nullHandle});\n    }\n    return points;\n};\n\nconst centeredBlob = (options: BlobOptions, center: Coord): Point[] => {\n    return mapPoints(genFromOptions(options), ({curr}) => {\n        curr.x += center.x - options.size / 2;\n        curr.y += center.y - options.size / 2;\n        return curr;\n    });\n};\n\nconst calcFullDetails = (percentage: number, a: Point, b: Point) => {\n    const a0: Coord = a;\n    const a1 = expandHandle(a, a.handleOut);\n    const a2 = expandHandle(b, b.handleIn);\n    const a3: Coord = b;\n\n    const b0 = splitLine(percentage, a0, a1);\n    const b1 = splitLine(percentage, a1, a2);\n    const b2 = splitLine(percentage, a2, a3);\n    const c0 = splitLine(percentage, b0, b1);\n    const c1 = splitLine(percentage, b1, b2);\n    const d0 = splitLine(percentage, c0, c1);\n\n    return {a0, a1, a2, a3, b0, b1, b2, c0, c1, d0};\n};\n\naddTitle(4, \"Vector graphics\");\n\naddCanvas(\n    1.3,\n    // Pixelated circle.\n    (ctx, width, height) => {\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n        const gridSize = width * 0.01;\n        const gridCountX = width / gridSize;\n        const gridCountY = height / gridSize;\n\n        // https://www.desmos.com/calculator/psohl602g5\n        const radius = width * 0.3;\n        const falloff = width * 0.0015;\n        const thickness = width * 0.01;\n\n        for (let x = 0; x < gridCountX; x++) {\n            for (let y = 0; y < gridCountY; y++) {\n                const curr = {\n                    x: x * gridSize + gridSize / 2,\n                    y: y * gridSize + gridSize / 2,\n                };\n                const d = distance(curr, center);\n                const opacity = Math.max(\n                    0,\n                    Math.min(1, Math.abs(thickness / (d - radius)) - falloff),\n                );\n\n                tempStyles(\n                    ctx,\n                    () => {\n                        ctx.globalAlpha = opacity;\n                        ctx.fillStyle = colors.highlight;\n                    },\n                    () => ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize),\n                );\n            }\n        }\n\n        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.`;\n    },\n    // Smooth circle.\n    (ctx, width, height) => {\n        const pt = width * 0.01;\n        const shapeSize = width * 0.6;\n        const cx = width * 0.5;\n        const cy = height * 0.5;\n\n        tempStyles(\n            ctx,\n            () => {\n                ctx.lineWidth = pt;\n                ctx.strokeStyle = colors.highlight;\n            },\n            () => {\n                ctx.beginPath();\n                ctx.arc(cx, cy, shapeSize / 2, 0, 2 * Math.PI);\n                ctx.stroke();\n            },\n        );\n\n        return `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.`;\n    },\n);\n\naddCanvas(\n    1.3,\n    (ctx, width, height, animate) => {\n        const startPeriod = (1 + Math.E) * 1000;\n        const endPeriod = (1 + Math.PI) * 1000;\n\n        animate((frameTime) => {\n            const startPercentage = calcBouncePercentage(\n                startPeriod,\n                timingFunctions.ease,\n                frameTime,\n            );\n            const startLengthPercentage = calcBouncePercentage(\n                startPeriod * 0.8,\n                timingFunctions.ease,\n                frameTime,\n            );\n            const startAngle = split(startPercentage, -45, +45);\n            const startLength = width * 0.1 + width * 0.2 * startLengthPercentage;\n            const start = point(width * 0.2, height * 0.5, 0, 0, startAngle, startLength);\n\n            const endPercentage = calcBouncePercentage(endPeriod, timingFunctions.ease, frameTime);\n            const endLengthPercentage = calcBouncePercentage(\n                endPeriod * 0.8,\n                timingFunctions.ease,\n                frameTime,\n            );\n            const endAngle = split(endPercentage, 135, 225);\n            const endLength = width * 0.1 + width * 0.2 * endLengthPercentage;\n            const end = point(width * 0.8, height * 0.5, endAngle, endLength, 0, 0);\n\n            drawOpen(ctx, start, end, true);\n        });\n\n        return `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.`;\n    },\n    (ctx, width, height, animate) => {\n        const angleRange = 20;\n        const lengthRange = 40;\n        const period = 5000;\n\n        const r = rand(\"blobs\");\n        const ra = r();\n        const rb = r();\n        const rc = r();\n        const rd = r();\n\n        const wobbleHandle = (\n            frameTime: number,\n            period: number,\n            p: Point,\n            locked: boolean,\n        ): Point => {\n            const angleIn =\n                deg(p.handleIn.angle) +\n                angleRange *\n                    (0.5 - calcBouncePercentage(period * 1.1, timingFunctions.ease, frameTime));\n            const lengthIn =\n                p.handleIn.length +\n                lengthRange *\n                    (0.5 - calcBouncePercentage(period * 0.9, timingFunctions.ease, frameTime));\n            const angleOut =\n                deg(p.handleOut.angle) +\n                angleRange *\n                    (0.5 - calcBouncePercentage(period * 0.9, timingFunctions.ease, frameTime));\n            const lengthOut =\n                p.handleOut.length +\n                lengthRange *\n                    (0.5 - calcBouncePercentage(period * 1.1, timingFunctions.ease, frameTime));\n            return point(p.x, p.y, angleIn, lengthIn, locked ? angleIn + 180 : angleOut, lengthOut);\n        };\n\n        animate((frameTime) => {\n            const a = wobbleHandle(\n                frameTime,\n                period / 2 + (ra * period) / 2,\n                point(width * 0.5, height * 0.3, 210, 100, -30, 100),\n                false,\n            );\n            const b = wobbleHandle(\n                frameTime,\n                period / 2 + (rb * period) / 2,\n                point(width * 0.8, height * 0.5, -90, 100, 90, 100),\n                true,\n            );\n            const c = wobbleHandle(\n                frameTime,\n                period / 2 + (rc * period) / 2,\n                point(width * 0.5, height * 0.9, -30, 75, -150, 75),\n                false,\n            );\n            const d = wobbleHandle(\n                frameTime,\n                period / 2 + (rd * period) / 2,\n                point(width * 0.2, height * 0.5, 90, 100, -90, 100),\n                true,\n            );\n\n            drawClosed(ctx, [a, b, c, d], true);\n        });\n\n        return `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.`;\n    },\n);\n\naddCanvas(2, (ctx, width, height, animate) => {\n    const period = Math.PI * Math.E * 1000;\n    const start = point(width * 0.3, height * 0.8, 0, 0, -105, width * 0.32);\n    const end = point(width * 0.7, height * 0.8, -75, width * 0.25, 0, 0);\n\n    animate((frameTime) => {\n        const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);\n        const d = calcFullDetails(percentage, start, end);\n\n        tempStyles(\n            ctx,\n            () => {\n                ctx.fillStyle = colors.secondary;\n                ctx.strokeStyle = colors.secondary;\n            },\n            () => {\n                drawLine(ctx, d.a0, d.a1, 1);\n                drawLine(ctx, d.a1, d.a2, 1);\n                drawLine(ctx, d.a2, d.a3, 1);\n                drawLine(ctx, d.b0, d.b1, 1);\n                drawLine(ctx, d.b1, d.b2, 1);\n                drawLine(ctx, d.c0, d.c1, 1);\n\n                drawPoint(ctx, d.a0, 1.3, \"a0\");\n                drawPoint(ctx, d.a1, 1.3, \"a1\");\n                drawPoint(ctx, d.a2, 1.3, \"a2\");\n                drawPoint(ctx, d.a3, 1.3, \"a3\");\n                drawPoint(ctx, d.b0, 1.3, \"b0\");\n                drawPoint(ctx, d.b1, 1.3, \"b1\");\n                drawPoint(ctx, d.b2, 1.3, \"b2\");\n                drawPoint(ctx, d.c0, 1.3, \"c0\");\n                drawPoint(ctx, d.c1, 1.3, \"c1\");\n                drawPoint(ctx, d.d0, 1.3, \"d0\");\n            },\n        );\n\n        tempStyles(\n            ctx,\n            () => (ctx.fillStyle = colors.highlight),\n            () => drawPoint(ctx, d.d0, 3),\n        );\n\n        drawOpen(ctx, start, end, false);\n    });\n\n    return `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.`;\n});\n\naddTitle(4, \"Making a blob\");\n\naddCanvas(\n    1.3,\n    (ctx, width, height, animate) => {\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n        const radius = width * 0.3;\n        const minPoints = 3;\n        const extraPoints = 6;\n        const pointDurationMs = 2000;\n\n        animate((frameTime) => {\n            const points =\n                minPoints + extraPoints + (extraPoints / 2) * Math.sin(frameTime / pointDurationMs);\n            const shape = makePoly(points, radius, center);\n\n            // Draw lines from center to each point..\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.fillStyle = colors.secondary;\n                    ctx.strokeStyle = colors.secondary;\n                },\n                () => {\n                    drawPoint(ctx, center, 2);\n                    forPoints(shape, ({curr}) => {\n                        drawLine(ctx, center, curr, 1, 2);\n                    });\n                },\n            );\n\n            drawClosed(ctx, shape, false);\n        });\n\n        return `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.`;\n    },\n    (ctx, width, height, animate) => {\n        const period = Math.PI * 1500;\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n        const radius = width * 0.3;\n        const points = 5;\n        const randSeed = Math.random();\n        const randStrength = 0.5;\n\n        const shape = makePoly(points, radius, center);\n\n        animate((frameTime) => {\n            const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);\n            const rgen = rand(randSeed + Math.floor(frameTime / period) + \"\");\n\n            // Draw original shape.\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.fillStyle = colors.secondary;\n                    ctx.strokeStyle = colors.secondary;\n                },\n                () => {\n                    drawPoint(ctx, center, 2);\n                    forPoints(shape, ({curr, next}) => {\n                        drawLine(ctx, curr, next(), 1, 2);\n                    });\n                },\n            );\n\n            // Draw randomly shifted shape.\n            const shiftedShape = shape.map(\n                (p): Point => {\n                    const randOffset = percentage * (randStrength * rgen() - randStrength / 2);\n                    return coordPoint(splitLine(randOffset, p, center));\n                },\n            );\n\n            drawClosed(ctx, shiftedShape, true);\n        });\n\n        return `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.`;\n    },\n);\n\naddCanvas(\n    1.3,\n    (ctx, width, height, animate) => {\n        const options: BlobOptions = {\n            extraPoints: 2,\n            randomness: 6,\n            seed: \"random\",\n            size: width * 0.7,\n        };\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n        const interval = 2000;\n\n        const blob = centeredBlob(options, center);\n        const handles = mapPoints(blob, ({curr: p}) => {\n            p.handleIn.length = 150;\n            p.handleOut.length = 150;\n            return p;\n        });\n        const polyBlob = blob.map(coordPoint);\n        const pointCount = polyBlob.length;\n\n        animate((frameTime) => {\n            const activeIndex = Math.floor(frameTime / interval) % pointCount;\n            const opacity = Math.abs(Math.sin((frameTime * Math.PI) / interval));\n\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.strokeStyle = colors.secondary;\n                    ctx.globalAlpha = opacity;\n                },\n                () => {\n                    forPoints(polyBlob, ({prev, next, index}) => {\n                        if (index !== activeIndex) return;\n                        drawLine(ctx, prev(), next(), 1, 2);\n                    });\n                    forPoints(handles, ({curr, index}) => {\n                        if (index !== activeIndex) return;\n                        drawHandles(ctx, curr, 1);\n                    });\n                },\n            );\n\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.fillStyle = colors.secondary;\n                },\n                () => {\n                    drawPoint(ctx, center, 2);\n                },\n            );\n\n            drawClosed(ctx, polyBlob, false);\n        });\n\n        return `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.`;\n    },\n    (ctx, width, height, animate) => {\n        const period = Math.PI * 1500;\n        const options: BlobOptions = {\n            extraPoints: 2,\n            randomness: 6,\n            seed: \"random\",\n            size: width * 0.7,\n        };\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n\n        const blob = centeredBlob(options, center);\n\n        animate((frameTime) => {\n            const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);\n\n            // Draw original blob.\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.fillStyle = colors.secondary;\n                    ctx.strokeStyle = colors.secondary;\n                },\n                () => {\n                    drawPoint(ctx, center, 2);\n                    forPoints(blob, ({curr, next}) => {\n                        drawLine(ctx, curr, next(), 1, 2);\n                    });\n                },\n            );\n\n            // Draw animated blob.\n            const animatedBlob = mapPoints(blob, ({curr}) => {\n                curr.handleIn.length *= percentage;\n                curr.handleOut.length *= percentage;\n                return curr;\n            });\n\n            drawClosed(ctx, animatedBlob, true);\n        });\n\n        return `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.`;\n    },\n);\n\naddTitle(4, \"Interpolating between blobs\");\n\naddCanvas(2, (ctx, width, height, animate) => {\n    const period = Math.PI * 1000;\n    const center: Coord = {x: width * 0.5, y: height * 0.5};\n    const fadeSpeed = 10;\n    const fadeLead = 0.05;\n    const fadeFloor = 0.2;\n\n    const blobA = centeredBlob(\n        {\n            extraPoints: 3,\n            randomness: 6,\n            seed: \"12345\",\n            size: height * 0.8,\n        },\n        center,\n    );\n    const blobB = centeredBlob(\n        {\n            extraPoints: 3,\n            randomness: 6,\n            seed: \"abc\",\n            size: height * 0.8,\n        },\n        center,\n    );\n\n    animate((frameTime) => {\n        const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);\n\n        const shiftedFrameTime = frameTime + period * fadeLead;\n        const shiftedPercentage = calcBouncePercentage(\n            period,\n            timingFunctions.ease,\n            shiftedFrameTime,\n        );\n        const shiftedPeriodPercentage = mod(shiftedFrameTime, period) / period;\n\n        forceStyles(ctx, () => {\n            const {pt} = sizes();\n            ctx.fillStyle = \"transparent\";\n            ctx.lineWidth = pt;\n            ctx.strokeStyle = colors.secondary;\n            ctx.setLineDash([2 * pt]);\n\n            if (shiftedPeriodPercentage > 0.5) {\n                ctx.globalAlpha = fadeFloor + fadeSpeed * (1 - shiftedPercentage);\n                drawClosed(ctx, blobA, false);\n\n                ctx.globalAlpha = fadeFloor;\n                drawClosed(ctx, blobB, false);\n            } else {\n                ctx.globalAlpha = fadeFloor + fadeSpeed * shiftedPercentage;\n                drawClosed(ctx, blobB, false);\n\n                ctx.globalAlpha = fadeFloor;\n                drawClosed(ctx, blobA, false);\n            }\n        });\n\n        drawClosed(ctx, interpolateBetween(percentage, blobA, blobB), true);\n    });\n\n    return `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.`;\n});\n\naddCanvas(\n    1.3,\n    (ctx, width, height, animate) => {\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n        const maxExtraPoints = 7;\n        const period = maxExtraPoints * Math.PI * 300;\n        const {pt} = sizes();\n\n        const blob = centeredBlob(\n            {\n                extraPoints: 0,\n                randomness: 6,\n                seed: \"flip\",\n                size: height * 0.9,\n            },\n            center,\n        );\n\n        animate((frameTime) => {\n            const percentage = mod(frameTime, period) / period;\n            const extraPoints = Math.floor(percentage * (maxExtraPoints + 1));\n            drawClosed(ctx, divide(extraPoints + blob.length, blob), true);\n\n            forPoints(blob, ({curr}) => {\n                ctx.beginPath();\n                ctx.arc(curr.x, curr.y, pt * 6, 0, 2 * Math.PI);\n\n                tempStyles(\n                    ctx,\n                    () => {\n                        ctx.strokeStyle = colors.secondary;\n                        ctx.lineWidth = pt;\n                    },\n                    () => {\n                        ctx.stroke();\n                    },\n                );\n            });\n        });\n\n        return `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.`;\n    },\n    (ctx, width, height, animate) => {\n        const period = Math.PI ** Math.E * 1000;\n        const start = point(width * 0.1, height * 0.6, 0, 0, -45, width * 0.5);\n        const end = point(width * 0.9, height * 0.6, 160, width * 0.3, 0, 0);\n\n        animate((frameTime) => {\n            const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);\n            const d = calcFullDetails(percentage, start, end);\n\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.fillStyle = colors.secondary;\n                    ctx.strokeStyle = colors.secondary;\n                },\n                () => {\n                    drawLine(ctx, d.a0, d.a1, 1);\n                    drawLine(ctx, d.a1, d.a2, 1, 2);\n                    drawLine(ctx, d.a2, d.a3, 1);\n                    drawLine(ctx, d.b0, d.b1, 1, 2);\n                    drawLine(ctx, d.b1, d.b2, 1, 2);\n\n                    drawPoint(ctx, d.a0, 1.3, \"a0\");\n                    drawPoint(ctx, d.a1, 1.3, \"a1\");\n                    drawPoint(ctx, d.a2, 1.3, \"a2\");\n                    drawPoint(ctx, d.a3, 1.3, \"a3\");\n                    drawPoint(ctx, d.b1, 1.3, \"b1\");\n                },\n            );\n\n            forceStyles(ctx, () => {\n                const {pt} = sizes();\n                ctx.fillStyle = colors.secondary;\n                ctx.strokeStyle = colors.secondary;\n                ctx.lineWidth = pt;\n\n                drawOpen(ctx, start, end, false);\n            });\n\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.fillStyle = colors.highlight;\n                    ctx.strokeStyle = colors.highlight;\n                },\n                () => {\n                    drawLine(ctx, d.c0, d.c1, 1);\n                    drawLine(ctx, d.a0, d.b0, 1);\n                    drawLine(ctx, d.a3, d.b2, 1);\n\n                    drawPoint(ctx, d.b0, 1.3, \"b0\");\n                    drawPoint(ctx, d.b2, 1.3, \"b2\");\n                    drawPoint(ctx, d.c0, 1.3, \"c0\");\n                    drawPoint(ctx, d.c1, 1.3, \"c1\");\n                },\n            );\n\n            tempStyles(\n                ctx,\n                () => (ctx.fillStyle = colors.highlight),\n                () => drawPoint(ctx, d.d0, 1.3, \"d0\"),\n            );\n        });\n\n        return `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\"`;\n    },\n);\n\naddCanvas(\n    1.3,\n    (ctx, width, height, animate) => {\n        const period = (Math.E / Math.PI) * 1000;\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n\n        const blob = centeredBlob(\n            {\n                extraPoints: 3,\n                randomness: 6,\n                seed: \"shift\",\n                size: height * 0.9,\n            },\n            center,\n        );\n\n        const shiftedBlob = shift(1, blob);\n\n        let prev = 0;\n        let count = 0;\n        animate((frameTime) => {\n            const animationTime = mod(frameTime, period);\n            const percentage = timingFunctions.ease(mod(animationTime, period) / period);\n\n            // Count animation loops.\n            if (percentage < prev) count++;\n            prev = percentage;\n\n            // Draw lines points are travelling.\n            tempStyles(\n                ctx,\n                () => {\n                    ctx.fillStyle = colors.secondary;\n                    ctx.strokeStyle = colors.secondary;\n                },\n                () => {\n                    drawPoint(ctx, center, 2);\n                    forPoints(blob, ({curr, next}) => {\n                        drawLine(ctx, curr, next(), 1, 2);\n                    });\n                },\n            );\n\n            // Pause in-place every other animation loop.\n            if (count % 2 === 0) {\n                drawClosed(ctx, interpolateBetweenSmooth(2, percentage, blob, shiftedBlob), true);\n            } else {\n                drawClosed(ctx, blob, true);\n            }\n        });\n\n        return `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.`;\n    },\n    (ctx, width, height, animate) => {\n        const period = Math.PI * Math.E * 1000;\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n\n        const blob = centeredBlob(\n            {\n                extraPoints: 3,\n                randomness: 6,\n                seed: \"flip\",\n                size: height * 0.9,\n            },\n            center,\n        );\n        const reversedBlob = mapPoints(blob, ({curr}) => {\n            const temp = curr.handleIn;\n            curr.handleIn = curr.handleOut;\n            curr.handleOut = temp;\n            return curr;\n        });\n        reversedBlob.reverse();\n\n        animate((frameTime) => {\n            const percentage = calcBouncePercentage(period, timingFunctions.ease, frameTime);\n\n            forceStyles(ctx, () => {\n                const {pt} = sizes();\n                ctx.fillStyle = \"transparent\";\n                ctx.lineWidth = pt;\n                ctx.strokeStyle = colors.secondary;\n                ctx.setLineDash([2 * pt]);\n                drawClosed(ctx, blob, false);\n            });\n\n            drawClosed(ctx, interpolateBetweenSmooth(2, percentage, blob, reversedBlob), true);\n        });\n\n        return `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.`;\n    },\n);\n\naddCanvas(\n    1.3,\n    (ctx, width, height) => {\n        // Only animate in the most recent painter call.\n        const animationID = Math.random();\n        const wasReplaced = () => (ctx.canvas as any).animationID !== animationID;\n\n        const period = Math.PI * 1000;\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n        const size = Math.min(width, height) * 0.8;\n\n        const canvasBlobGenerator = (keyframe: CanvasKeyframe): Point[] => {\n            return mapPoints(genFromOptions(keyframe.blobOptions), ({curr}) => {\n                curr.x += center.x - size / 2;\n                curr.y += center.y - size / 2;\n                return curr;\n            });\n        };\n\n        const animation = statefulAnimationGenerator(\n            canvasBlobGenerator,\n            (points: Point[]) => drawClosed(ctx, points, true),\n            () => {},\n        )(Date.now);\n\n        const renderFrame = () => {\n            if (wasReplaced()) return;\n            ctx.clearRect(0, 0, width, height);\n            animation.renderFrame();\n            requestAnimationFrame(renderFrame);\n        };\n        requestAnimationFrame(renderFrame);\n\n        const loopAnimation = (): void => {\n            if (wasReplaced()) return;\n            animation.transition(genFrame());\n        };\n\n        let frameCount = -1;\n        const genFrame = (overrides: Partial<CanvasKeyframe> = {}): CanvasKeyframe => {\n            frameCount++;\n            return {\n                duration: period,\n                timingFunction: \"ease\",\n                callback: loopAnimation,\n                blobOptions: {\n                    extraPoints: Math.max(0, mod(frameCount, 4) - 1),\n                    randomness: 4,\n                    seed: Math.random(),\n                    size,\n                },\n                ...overrides,\n            };\n        };\n\n        animation.transition(genFrame({duration: 0}));\n\n        ctx.canvas.onclick = () => {\n            if (wasReplaced()) return;\n            animation.playPause();\n        };\n\n        (ctx.canvas as any).animationID = animationID;\n\n        return `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.`;\n    },\n    (ctx, width, height, animate) => {\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n        const size = Math.min(width, height) * 0.8;\n\n        const drawStar = (rays: number, od: number, id: number): Point[] => {\n            const pointCount = 2 * rays;\n            const angle = (Math.PI * 2) / pointCount;\n            const points: Point[] = [];\n            for (let i = 0; i < pointCount; i++) {\n                const pointX = Math.sin(i * angle);\n                const pointY = Math.cos(i * angle);\n                const distanceMultiplier = (i % 2 === 0 ? od : id) / 2;\n                points.push({\n                    x: center.x + pointX * distanceMultiplier,\n                    y: center.y + pointY * distanceMultiplier,\n                    handleIn: {angle: 0, length: 0},\n                    handleOut: {angle: 0, length: 0},\n                });\n            }\n            return points;\n        };\n\n        const drawPolygon = (sides: number, od: number): Point[] => {\n            const angle = (Math.PI * 2) / sides;\n            const points: Point[] = [];\n            for (let i = 0; i < sides; i++) {\n                const pointX = Math.sin(i * angle);\n                const pointY = Math.cos(i * angle);\n                const distanceMultiplier = od / 2;\n                points.push({\n                    x: center.x + pointX * distanceMultiplier,\n                    y: center.y + pointY * distanceMultiplier,\n                    handleIn: {angle: 0, length: 0},\n                    handleOut: {angle: 0, length: 0},\n                });\n            }\n            return points;\n        };\n\n        const shapes = [\n            drawStar(8, size, size * 0.7),\n            smoothBlob(drawPolygon(3, size)),\n            smoothBlob(drawStar(10, size, size * 0.9)),\n            drawPolygon(4, size),\n            smoothBlob(drawStar(3, size, size * 0.6)),\n        ];\n\n        const animation = canvasPath();\n        const genFrame = (index: number) => () => {\n            animation.transition({\n                points: shapes[index % shapes.length],\n                duration: 3000,\n                delay: 1000,\n                timingFunction: \"ease\",\n                callback: genFrame(index + 1),\n            });\n        };\n        animation.transition({\n            points: shapes[0],\n            duration: 0,\n            callback: genFrame(1),\n        });\n\n        animate(() => {\n            drawClosed(ctx, animation.renderPoints(), true);\n        });\n\n        return `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.`;\n    },\n);\n\naddTitle(4, \"Gooeyness\");\n\naddCanvas(\n    1.3,\n    (ctx, width, height, animate) => {\n        const size = Math.min(width, height) * 0.8;\n        const center: Coord = {x: (width - size) * 0.5, y: (height - size) * 0.5};\n\n        const animation = canvasPath();\n\n        const genFrame = (duration: number) => {\n            animation.transition({\n                duration: duration,\n                blobOptions: {\n                    extraPoints: 2,\n                    randomness: 3,\n                    seed: Math.random(),\n                    size,\n                },\n                callback: () => genFrame(3000),\n                timingFunction: \"ease\",\n                canvasOptions: {offsetX: center.x, offsetY: center.y},\n            });\n        };\n        genFrame(0);\n\n        animate(() => {\n            drawClosed(ctx, animation.renderPoints(), true);\n        });\n\n        return `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.`;\n    },\n    (ctx, width, height, animate) => {\n        const size = Math.min(width, height) * 0.8;\n        const center: Coord = {x: width * 0.5, y: height * 0.5};\n\n        const animation = canvasPath();\n\n        wigglePreset(\n            animation,\n            {\n                extraPoints: 2,\n                randomness: 3,\n                seed: Math.random(),\n                size,\n            },\n            {\n                offsetX: center.x - size / 2,\n                offsetY: center.y - size / 2,\n            },\n            {\n                speed: 2,\n            },\n        );\n\n        animate(() => {\n            drawClosed(ctx, animation.renderPoints(), true);\n        });\n\n        return `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.`;\n    },\n);\n"
  },
  {
    "path": "demo/example.ts",
    "content": "import {CanvasKeyframe, canvasPath, wigglePreset} from \"../public/animate\";\nimport {drawHandles, drawPoint} from \"./internal/canvas\";\nimport {isDebug} from \"./internal/debug\";\nimport {colors} from \"./internal/layout\";\n\n// Fetch reference to example container.\nconst exampleContainer = document.querySelector(\".example\")!;\n\nconst canvas = document.createElement(\"canvas\")!;\nexampleContainer.appendChild(canvas);\n\nlet size = 0;\nconst resize = () => {\n    // Set blob size relative to window, but limit to 600.\n    const rawSize = Math.min(600, Math.min(window.innerWidth - 64, window.innerHeight / 2));\n    canvas.style.width = `${rawSize}px`;\n    canvas.style.height = `${rawSize}px`;\n\n    // Scale resolution to take into account device pixel ratio.\n    size = rawSize * (window.devicePixelRatio || 1);\n\n    canvas.width = size;\n    canvas.height = size;\n};\n\n// Set blob color and set context to erase intersection of content.\nconst ctx = canvas.getContext(\"2d\")!;\n\n// Create animation and draw its frames in `requestAnimationFrame` callbacks.\nconst animation = canvasPath();\nconst renderFrame = () => {\n    ctx.clearRect(0, 0, size, size);\n    ctx.fillStyle = colors.highlight;\n    ctx.strokeStyle = colors.highlight;\n\n    if (isDebug()) {\n        const points = animation.renderPoints();\n        for (const point of points) {\n            drawPoint(ctx, point, 2);\n            drawHandles(ctx, point, 1);\n        }\n    }\n\n    ctx.fill(animation.renderFrame());\n    requestAnimationFrame(renderFrame);\n};\nrequestAnimationFrame(renderFrame);\n\n// Extra points that increases when blob gets clicked.\nlet extraPoints = 0;\n\nconst genWiggle = (transition: number) => {\n    wigglePreset(\n        animation,\n        {\n            extraPoints: 3 + extraPoints,\n            randomness: 1.5,\n            seed: Math.random(),\n            size,\n        },\n        {},\n        {speed: 2, initialTransition: transition},\n    );\n};\n\n// Generate a keyframe with overridable default values.\nconst genFrame = (overrides: any = {}): CanvasKeyframe => {\n    const blobOptions = {\n        extraPoints: 3 + extraPoints,\n        randomness: 4,\n        seed: Math.random(),\n        size,\n        ...overrides.blobOptions,\n    };\n    return {\n        duration: 4000,\n        timingFunction: \"ease\",\n        callback: loopAnimation,\n        ...overrides,\n        blobOptions,\n    };\n};\n\n// Callback for every frame which starts transition to a new frame.\nconst loopAnimation = (): void => {\n    extraPoints = 0;\n    genWiggle(5000);\n};\n\n// Quickly animate to a new frame when canvas is clicked.\ncanvas.onclick = () => {\n    extraPoints++;\n    animation.transition(\n        genFrame({\n            duration: 400,\n            timingFunction: \"elasticEnd0\",\n            blobOptions: {extraPoints},\n        }),\n    );\n};\n\n// Immediately show a new frame.\nwindow.addEventListener(\"load\", () => {\n    resize();\n    genWiggle(0);\n});\n\n// Make blob a circle while window is being resized.\nwindow.addEventListener(\"resize\", () => {\n    resize();\n    const tempSize = (size * 6) / 7;\n    animation.transition(\n        genFrame({\n            duration: 100,\n            timingFunction: \"easeEnd\",\n            blobOptions: {\n                extraPoints: 0,\n                randomness: 0,\n                seed: \"\",\n                size: tempSize,\n            },\n            canvasOptions: {\n                offsetX: (size - tempSize) / 2,\n                offsetY: (size - tempSize) / 2,\n            },\n        }),\n    );\n});\n"
  },
  {
    "path": "demo/index.html",
    "content": "<html>\n    <head>\n        <link rel=\"shortcut icon\" href=\"https://blobs.dev/assets/favicon.ico?v=3ewlwLn2WO\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <style>\n            html {\n                font-size: calc(0.1vw + 1.2rem);\n            }\n\n            body {\n                font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu,\n                    Cantarell, \"Open Sans\", \"Helvetica Neue\", sans-serif;\n                overflow-x: hidden;\n                margin: 0;\n            }\n\n            header {\n                align-items: center;\n                display: flex;\n                flex-direction: column;\n                height: 16vh;\n                padding: 5vh 0 0;\n                user-select: none;\n                -moz-user-select: none;\n            }\n\n            header img {\n                height: 16vh;\n                max-height: 6rem;\n            }\n\n            header nav a,\n            .how-it-works {\n                color: #aaa;\n                display: inline-block;\n                font-size: 0.7rem;\n                font-weight: 700;\n                padding: 0.5rem;\n                text-decoration: none;\n                text-transform: uppercase;\n            }\n\n            main {\n                align-items: center;\n                display: flex;\n                flex-direction: column;\n            }\n\n            .example {\n                align-items: center;\n                display: flex;\n                height: 50vh;\n                padding: 5vh 2rem;\n            }\n\n            .how-it-works {\n                cursor: pointer;\n                height: 10vh;\n                margin-top: 5vh;\n                transform: rotate(-2deg) translateY(-1px);\n                user-select: none;\n            }\n\n            .how-it-works.hidden {\n                display: none;\n            }\n\n            .container {\n                align-items: center;\n                display: flex;\n                flex-direction: column;\n                padding: 0 1rem 20vh;\n            }\n\n            .container:not(.open) {\n                display: none;\n            }\n\n            .container .title {\n                color: #aaa;\n                font-weight: 700;\n                margin: 1rem 0 0.5rem;\n                max-width: 1000px;\n                text-transform: uppercase;\n                user-select: none;\n                width: 100%;\n            }\n\n            .container .section {\n                border: 1px solid #eee;\n                border-radius: 0.5rem;\n                display: flex;\n                margin: 1rem 0;\n                max-width: 1000px;\n                width: 100%;\n            }\n\n            .container .section .number {\n                color: #ccc;\n                font-size: 0.6rem;\n                font-weight: 100;\n                height: 0;\n                position: relative;\n                text-decoration: none;\n                transform: translate(1rem, 0.5rem);\n                user-select: none;\n                width: 0;\n            }\n\n            .container .section .text {\n                box-sizing: border-box;\n                padding: 2rem 3rem 1.5rem;\n            }\n\n            .container .section .cell {\n                flex-grow: 1;\n            }\n\n            .container .section .cell canvas {\n                width: 100%;\n            }\n\n            .container .section .cell .label {\n                color: #555;\n                font-size: 0.6rem;\n                padding: 0 1rem 1rem;\n            }\n        </style>\n    </head>\n    <body>\n        <header>\n            <img src=\"../assets/logo.svg\" />\n            <nav>\n                <a\n                    href=\"https://github.com/g-harel/blobs\"\n                    style=\"transform: rotate(1deg) translateY(3px);\"\n                    >GITHUB</a\n                >\n                <a\n                    href=\"https://npmjs.com/package/blobs\"\n                    style=\"transform: rotate(-2deg) translateY(-1px);\"\n                    >NPM</a\n                >\n                <a\n                    href=\"mailto:gabrielj.harel@gmail.com\"\n                    style=\"transform: rotate(4deg) translateY(1px);\"\n                    >CONTACT</a\n                >\n            </nav>\n        </header>\n        <main>\n            <div class=\"example\"></div>\n            <div class=\"how-it-works\">How it works</div>\n            <div class=\"container\"></div>\n        </main>\n        <script src=\"./example.ts\"></script>\n        <script src=\"./content.ts\"></script>\n    </body>\n</html>\n"
  },
  {
    "path": "demo/internal/canvas.ts",
    "content": "import {TimingFunc} from \"../../internal/animate/timing\";\nimport {Coord, Point} from \"../../internal/types\";\nimport {expandHandle, forPoints, mod, rad} from \"../../internal/util\";\nimport {isDebug} from \"../internal/debug\";\nimport {colors, sizes} from \"../internal/layout\";\n\nexport const forceStyles = (ctx: CanvasRenderingContext2D, fn: () => void) => {\n    if (!(ctx as any).forcedStyles) (ctx as any).forcedStyles = 0;\n    (ctx as any).forcedStyles++;\n    ctx.save();\n    fn();\n    ctx.restore();\n    (ctx as any).forcedStyles--;\n};\n\nexport const tempStyles = (ctx: CanvasRenderingContext2D, style: () => void, fn: () => void) => {\n    if ((ctx as any).forcedStyles > 0) {\n        fn();\n    } else {\n        ctx.save();\n        style();\n        fn();\n        ctx.restore();\n    }\n};\n\nexport const rotateAround = (\n    options: {\n        ctx: CanvasRenderingContext2D;\n        angle: number;\n        cx: number;\n        cy: number;\n    },\n    fn: () => void,\n) => {\n    tempStyles(\n        options.ctx,\n        () => {\n            options.ctx.translate(options.cx, options.cy);\n            options.ctx.rotate(options.angle);\n        },\n        () => {\n            if (isDebug()) {\n                tempStyles(\n                    options.ctx,\n                    () => (options.ctx.fillStyle = colors.debug),\n                    () => {\n                        options.ctx.fillRect(0, -4, 1, 8);\n                        options.ctx.fillRect(-32, 0, 64, 1);\n                    },\n                );\n            }\n            fn();\n        },\n    );\n};\n\nexport const point = (\n    x: number,\n    y: number,\n    ia: number,\n    il: number,\n    oa: number,\n    ol: number,\n): Point => {\n    return {\n        x: x,\n        y: y,\n        handleIn: {angle: rad(ia), length: il},\n        handleOut: {angle: rad(oa), length: ol},\n    };\n};\n\nexport const drawPoint = (\n    ctx: CanvasRenderingContext2D,\n    coord: Coord,\n    size: number,\n    label?: string,\n) => {\n    const radius = sizes().pt * size;\n    const pointPath = new Path2D();\n    pointPath.arc(coord.x, coord.y, radius, 0, 2 * Math.PI);\n    ctx.fill(pointPath);\n\n    if (label) {\n        tempStyles(\n            ctx,\n            () => (ctx.font = `${6 * radius}px monospace`),\n            () => ctx.fillText(label, coord.x + 2 * radius, coord.y - radius),\n        );\n    }\n};\n\nexport const drawLine = (\n    ctx: CanvasRenderingContext2D,\n    a: Coord,\n    b: Coord,\n    size: number,\n    dash?: number,\n) => {\n    tempStyles(\n        ctx,\n        () => {\n            const width = sizes().pt * size;\n            if (dash) ctx.setLineDash([dash * width]);\n        },\n        () => {\n            const width = sizes().pt * size;\n            const linePath = new Path2D();\n            linePath.moveTo(a.x, a.y);\n            linePath.lineTo(b.x, b.y);\n            ctx.lineWidth = width;\n            ctx.stroke(linePath);\n        },\n    );\n};\n\nexport const drawClosed = (ctx: CanvasRenderingContext2D, points: Point[], handles?: boolean) => {\n    forPoints(points, ({curr, next}) => {\n        drawOpen(ctx, curr, next(), handles);\n    });\n};\n\nexport const drawDebugClosed = (ctx: CanvasRenderingContext2D, points: Point[], size: number) => {\n    forPoints(points, ({curr, next: nextFn}) => {\n        drawHandles(ctx, curr, size);\n\n        const next = nextFn();\n        const currHandle = expandHandle(curr, curr.handleIn);\n        const nextHandle = expandHandle(curr, curr.handleOut);\n        const curve = new Path2D();\n        curve.moveTo(curr.x, curr.y);\n        curve.bezierCurveTo(currHandle.x, currHandle.y, nextHandle.x, nextHandle.y, next.x, next.y);\n        ctx.lineWidth = sizes().pt * size * 2;\n        ctx.stroke(curve);\n\n        drawPoint(ctx, curr, size * 1.1);\n    });\n};\n\nexport const drawHandles = (ctx: CanvasRenderingContext2D, point: Point, size: number) => {\n    const inHandle = expandHandle(point, point.handleIn);\n    const outHandle = expandHandle(point, point.handleOut);\n    drawLine(ctx, point, inHandle, size);\n    drawLine(ctx, point, outHandle, size, 2);\n    drawPoint(ctx, inHandle, size * 1.4);\n    drawPoint(ctx, outHandle, size * 1.4);\n};\n\nexport const drawOpen = (\n    ctx: CanvasRenderingContext2D,\n    start: Point,\n    end: Point,\n    handles?: boolean,\n) => {\n    const width = sizes().width;\n    const startHandle = expandHandle(start, start.handleOut);\n    const endHandle = expandHandle(end, end.handleIn);\n\n    // Draw handles.\n    if (handles) {\n        tempStyles(\n            ctx,\n            () => {\n                ctx.fillStyle = colors.secondary;\n                ctx.strokeStyle = colors.secondary;\n            },\n            () => {\n                drawLine(ctx, start, startHandle, 1);\n                drawLine(ctx, end, endHandle, 1, 2);\n\n                drawPoint(ctx, startHandle, 1.4);\n                drawPoint(ctx, endHandle, 1.4);\n            },\n        );\n    }\n\n    // Draw curve.\n    tempStyles(\n        ctx,\n        () => {\n            const lineWidth = width * 0.003;\n            ctx.lineWidth = lineWidth;\n        },\n        () => {\n            const curve = new Path2D();\n            curve.moveTo(start.x, start.y);\n            curve.bezierCurveTo(\n                startHandle.x,\n                startHandle.y,\n                endHandle.x,\n                endHandle.y,\n                end.x,\n                end.y,\n            );\n\n            tempStyles(\n                ctx,\n                () => (ctx.strokeStyle = colors.highlight),\n                () => ctx.stroke(curve),\n            );\n\n            tempStyles(\n                ctx,\n                () => (ctx.fillStyle = colors.highlight),\n                () => {\n                    drawPoint(ctx, start, 2);\n                    drawPoint(ctx, end, 2);\n                },\n            );\n        },\n    );\n};\n\nexport const calcBouncePercentage = (period: number, timingFunc: TimingFunc, frameTime: number) => {\n    const halfPeriod = period / 2;\n    const animationTime = mod(frameTime, period);\n    if (animationTime <= halfPeriod) {\n        return timingFunc(animationTime / halfPeriod);\n    } else {\n        return timingFunc(1 - (animationTime - halfPeriod) / halfPeriod);\n    }\n};\n"
  },
  {
    "path": "demo/internal/debug.ts",
    "content": "// If debug is initially set to false it will not be toggleable.\nlet debug = window.location.search.includes(\"debug\") && location.hostname === \"localhost\";\nexport const isDebug = () => debug;\n\nconst debugListeners: ((debug: boolean) => void)[] = [];\nexport const onDebugStateChange = (fn: (debug: boolean) => void) => {\n    debugListeners.push(fn);\n    fn(debug);\n};\n\nif (debug && document.body) {\n    const toggleButton = document.createElement(\"button\");\n    toggleButton.innerHTML = \"debug\";\n    toggleButton.style.padding = \"2rem\";\n    toggleButton.style.position = \"fixed\";\n    toggleButton.style.top = \"0\";\n    toggleButton.onclick = () => {\n        debug = !debug;\n        for (const listener of debugListeners) {\n            listener(debug);\n        }\n    };\n    document.body.prepend(toggleButton);\n}\n"
  },
  {
    "path": "demo/internal/layout.ts",
    "content": "import {tempStyles} from \"./canvas\";\nimport {isDebug, onDebugStateChange} from \"./debug\";\n\nexport const colors = {\n    debug: \"green\",\n    highlight: \"#ec576b\",\n    secondary: \"#555\",\n};\n\ninterface Cell {\n    aspectRatio: number;\n    canvas: HTMLCanvasElement;\n    ctx: CanvasRenderingContext2D;\n    painter: CellPainter;\n    animationID: number;\n}\n\nexport interface CellPainter {\n    (\n        ctx: CanvasRenderingContext2D,\n        width: number,\n        height: number,\n        animate: (painter: AnimationPainter) => void,\n    ): string | void;\n}\n\nexport interface AnimationPainter {\n    (timestamp: number): void;\n}\n\n// Global cell state.\nconst cells = ((window as any).cells as Cell[][]) || [];\n((window as any).cells as Cell[][]) = cells;\n\nconst containerElement = document.querySelector(\".container\");\nif (!containerElement) throw \"missing container\";\n\nconst howItWorksElement = document.querySelector(\".how-it-works\");\nif (!howItWorksElement) throw \"missing container\";\n\nlet animating = false;\nconst reveal = () => {\n    containerElement.classList.add(\"open\");\n    howItWorksElement.classList.add(\"hidden\");\n    animating = true;\n    redraw();\n};\nhowItWorksElement.addEventListener(\"click\", reveal);\nif (document.location.hash || isDebug()) setTimeout(reveal);\n\nexport const sizes = (): {width: number; pt: number} => {\n    const sectionStyle = window.getComputedStyle(\n        (containerElement.lastChild as any) || document.body,\n    );\n    const sectionWidth = Number(sectionStyle.getPropertyValue(\"width\").slice(0, -2));\n    const width = sectionWidth * window.devicePixelRatio;\n    return {width, pt: width * 0.002};\n};\n\nconst createSection = (): HTMLElement => {\n    const numberLabel = (\"000\" + cells.length).substr(-3);\n\n    const sectionElement = document.createElement(\"div\");\n    sectionElement.classList.add(\"section\");\n    sectionElement.setAttribute(\"id\", numberLabel);\n    containerElement.appendChild(sectionElement);\n\n    const numberElement = document.createElement(\"a\");\n    numberElement.classList.add(\"number\");\n    numberElement.setAttribute(\"href\", \"#\" + numberLabel);\n    numberElement.appendChild(document.createTextNode(numberLabel));\n    sectionElement.appendChild(numberElement);\n\n    return sectionElement;\n};\n\n// Adds a section of text to the bottom of the layout.\nexport const addTitle = (heading: number, text: string) => {\n    const wrapperElement = document.createElement(`h${heading}`);\n    wrapperElement.classList.add(\"title\");\n    containerElement.appendChild(wrapperElement);\n\n    const textWrapperElement = document.createElement(\"div\");\n    textWrapperElement.classList.add(\"text\");\n    wrapperElement.appendChild(textWrapperElement);\n\n    text = text.replace(\"\\n\", \" \").replace(/\\s+/g, \" \").trim();\n    const textElement = document.createTextNode(text);\n    textWrapperElement.appendChild(textElement);\n};\n\nconst handleIntersection = (entries: any) => {\n    entries.map((entry: any) => {\n        entry.target.setAttribute(\"data-visible\", entry.isIntersecting);\n    });\n};\n\n// Adds a row of cells to the bottom of the layout.\nexport const addCanvas = (aspectRatio: number, ...painters: CellPainter[]) => {\n    const sectionElement = createSection();\n\n    if (painters.length == 0) {\n        painters = [() => {}];\n    }\n\n    const cellRow: Cell[] = [];\n    for (const painter of painters) {\n        const cellElement = document.createElement(\"div\");\n        cellElement.classList.add(\"cell\");\n        sectionElement.appendChild(cellElement);\n\n        const canvas = document.createElement(\"canvas\");\n        cellElement.appendChild(canvas);\n\n        const labelElement = document.createElement(\"div\");\n        labelElement.classList.add(\"label\");\n        cellElement.appendChild(labelElement);\n\n        const ctx = canvas.getContext(\"2d\");\n        if (!ctx) throw \"missing canvas context\";\n\n        const cell = {aspectRatio, canvas, ctx, painter, animationID: -1};\n        cellRow.push(cell);\n\n        new IntersectionObserver(handleIntersection, {\n            threshold: 0.1,\n        }).observe(canvas);\n    }\n    cells.push(cellRow);\n\n    redraw();\n};\n\n// Lazily redraw canvas cells to match window resolution.\nlet redrawTimeout: undefined | number = undefined;\nconst redraw = () => {\n    window.clearTimeout(redrawTimeout);\n    redrawTimeout = window.setTimeout(() => {\n        for (const cellRow of cells) {\n            const cellWidth = sizes().width / cellRow.length;\n            for (const cell of cellRow) {\n                const cellHeight = cellWidth / cell.aspectRatio;\n\n                // Resize canvas;\n                cell.canvas.width = cellWidth;\n                cell.canvas.height = cellHeight;\n\n                // Draw canvas debug info.\n                const drawDebug = () => {\n                    if (isDebug()) {\n                        tempStyles(\n                            cell.ctx,\n                            () => (cell.ctx.strokeStyle = colors.debug),\n                            () => cell.ctx.strokeRect(0, 0, cellWidth, cellHeight - 1),\n                        );\n                    }\n                };\n                drawDebug();\n\n                // Keep track of paused state.\n                let pausedAt = 0;\n                let pauseOffset = 0;\n                cell.canvas.onclick = () => {\n                    if (pausedAt === 0) {\n                        pausedAt = Date.now();\n                    } else {\n                        pauseOffset += Date.now() - pausedAt;\n                        pausedAt = 0;\n                    }\n                };\n\n                // Cell-specific callback for providing an animation painter.\n                const animate = (painter: AnimationPainter) => {\n                    if (!animating) return;\n\n                    const animationID = Math.random();\n                    const startTime = Date.now();\n                    cell.animationID = animationID;\n\n                    const drawFrame = () => {\n                        // Stop animating if cell is redrawn.\n                        if (cell.animationID !== animationID) return;\n\n                        const visible = cell.canvas.getAttribute(\"data-visible\") === \"true\";\n                        if (pausedAt === 0 && visible) {\n                            const frameTime = Date.now() - startTime - pauseOffset;\n                            cell.ctx.clearRect(0, 0, cellWidth, cellHeight);\n                            drawDebug();\n                            if (isDebug()) {\n                                tempStyles(\n                                    cell.ctx,\n                                    () => (cell.ctx.fillStyle = colors.debug),\n                                    () => cell.ctx.fillText(String(frameTime), 10, 15),\n                                );\n                            }\n                            painter(frameTime);\n                        }\n\n                        requestAnimationFrame(drawFrame);\n                    };\n                    drawFrame();\n                };\n\n                // Redraw canvas contents and replace label if changed.\n                const label = cell.painter(cell.ctx, cellWidth, cellHeight, animate);\n                if (label) {\n                    const cellElement = cell.canvas.parentElement;\n                    if (cellElement) {\n                        cellElement.style.width = `${100 / cellRow.length}%`;\n                        const labelElement = cellElement.querySelector(\".label\");\n                        if (labelElement && labelElement.innerHTML !== label) {\n                            labelElement.innerHTML = \"\";\n                            labelElement.innerHTML = label;\n                        }\n                    }\n                }\n            }\n        }\n    }, 100);\n};\n\nwindow.addEventListener(\"load\", redraw);\nwindow.addEventListener(\"resize\", redraw);\nonDebugStateChange(redraw);\n"
  },
  {
    "path": "examples/corner-expand.html",
    "content": "<html>\n    <head>\n        <title>Expand demo</title>\n        <script src=\"https://unpkg.com/blobs/v2/animate\"></script>\n        <style>\n            body {\n                background-color: #ffffff;\n                height: 100%;\n                margin: 0;\n                overflow: hidden;\n                width: 100%;\n            }\n\n            #container {\n                background-color: #ffffff;\n                border-radius: 5px;\n                box-shadow: 0 2px 10px 0 rgb(0 0 0 / 20%);\n                cursor: pointer;\n                display: flex;\n                flex-direction: column;\n                position: absolute;\n                top: 35vh;\n                left: 15vw;\n                min-height: 20%;\n                width: 400px;\n            }\n\n            canvas {\n                border: 1px solid #ec576b;\n                box-sizing: border-box;\n                height: 100%;\n                left: 0;\n                position: absolute;\n                top: 0;\n                width: 100%;\n                z-index: -1;\n            }\n        </style>\n    </head>\n    <body>\n        <div id=\"container\"></div>\n        <canvas></canvas>\n\n        <script>\n            const canvas = document.querySelector(\"canvas\");\n            const container = document.getElementById(\"container\");\n\n            const ctx = canvas.getContext(\"2d\");\n            const animation = blobs2Animate.canvasPath();\n\n            const width = canvas.clientWidth;\n            const height = canvas.clientHeight;\n            canvas.width = width;\n            canvas.height = height;\n\n            const renderAnimation = () => {\n                ctx.clearRect(0, 0, width, height);\n                ctx.fillStyle = \"#ec576b\";\n                ctx.fill(animation.renderFrame());\n                requestAnimationFrame(renderAnimation);\n            };\n            requestAnimationFrame(renderAnimation);\n\n            const size = Math.min(width, height) * 1.2;\n            const defaultOptions = () => ({\n                blobOptions: {\n                    seed: Math.random(),\n                    extraPoints: 36,\n                    randomness: 0.7,\n                    size,\n                },\n                canvasOptions: {\n                    offsetX: -size / 2.2,\n                    offsetY: -size / 2.2,\n                },\n            });\n\n            // Keyframe loop.\n            const loopAnimation = () => {\n                animation.transition({\n                    duration: 4000,\n                    timingFunction: \"ease\",\n                    callback: loopAnimation,\n                    ...defaultOptions(),\n                });\n            };\n\n            // Initial frame.\n            animation.transition({\n                duration: 0, // Render immediately.\n                callback: loopAnimation,\n                ...defaultOptions(),\n            });\n\n            container.onclick = () => {\n                const options = defaultOptions();\n                options.blobOptions.size = Math.max(width, height) * 1.6;\n                options.blobOptions.randomness = 1.4;\n                options.canvasOptions.offsetX = -size / 2;\n                options.canvasOptions.offsetY = -size / 2;\n                animation.transition({\n                    duration: 2000,\n                    timingFunction: \"elasticEnd0\",\n                    ...options,\n                });\n            };\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "index.html",
    "content": "<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) {\n\"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;\n},{}],\"5PF2\":[function(require,module,exports) {\n\"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;\n},{\"../util\":\"NSCe\"}],\"/uvX\":[function(require,module,exports) {\n\"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}\n},{}],\"BWRk\":[function(require,module,exports) {\n\"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;\n},{\"simplex-noise\":\"/uvX\"}],\"BJ3L\":[function(require,module,exports) {\n\"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;\n},{\"../internal/rand\":\"BWRk\",\"../internal/util\":\"NSCe\",\"./util\":\"NSCe\"}],\"SjCR\":[function(require,module,exports) {\n\"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;\n},{}],\"F/j+\":[function(require,module,exports) {\n\"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;\n},{\"../util\":\"NSCe\"}],\"/Sl0\":[function(require,module,exports) {\n\"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;\n},{\"../util\":\"NSCe\"}],\"bUxv\":[function(require,module,exports) {\n\"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;\n},{\"./timing\":\"SjCR\",\"./prepare\":\"F/j+\",\"./interpolate\":\"/Sl0\"}],\"+LE9\":[function(require,module,exports) {\n\"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;\n},{\"./frames\":\"bUxv\"}],\"Aed7\":[function(require,module,exports) {\n\"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;\n},{\"./animate/timing\":\"SjCR\"}],\"+HZB\":[function(require,module,exports) {\n\"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;\n},{\"../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) {\n\"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)}\n},{}],\"rSMP\":[function(require,module,exports) {\n\"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);\n},{\"./canvas\":\"PBVq\",\"./debug\":\"q9J3\"}],\"PBVq\":[function(require,module,exports) {\n\"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;\n},{\"../../internal/util\":\"NSCe\",\"../internal/debug\":\"q9J3\",\"../internal/layout\":\"rSMP\"}],\"0UHT\":[function(require,module,exports) {\n\"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}}))});\n},{\"../public/animate\":\"+HZB\",\"./internal/canvas\":\"PBVq\",\"./internal/debug\":\"q9J3\",\"./internal/layout\":\"rSMP\"}]},{},[\"0UHT\"], null)\n//# 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) {\n\"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;\n},{}],\"q9J3\":[function(require,module,exports) {\n\"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)}\n},{}],\"PBVq\":[function(require,module,exports) {\n\"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;\n},{\"../../internal/util\":\"NSCe\",\"../internal/debug\":\"q9J3\",\"../internal/layout\":\"rSMP\"}],\"rSMP\":[function(require,module,exports) {\n\"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);\n},{\"./canvas\":\"PBVq\",\"./debug\":\"q9J3\"}],\"SjCR\":[function(require,module,exports) {\n\"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;\n},{}],\"/uvX\":[function(require,module,exports) {\n\"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}\n},{}],\"BWRk\":[function(require,module,exports) {\n\"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;\n},{\"simplex-noise\":\"/uvX\"}],\"BJ3L\":[function(require,module,exports) {\n\"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;\n},{\"../internal/rand\":\"BWRk\",\"../internal/util\":\"NSCe\",\"./util\":\"NSCe\"}],\"/Sl0\":[function(require,module,exports) {\n\"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;\n},{\"../util\":\"NSCe\"}],\"F/j+\":[function(require,module,exports) {\n\"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;\n},{\"../util\":\"NSCe\"}],\"bUxv\":[function(require,module,exports) {\n\"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;\n},{\"./timing\":\"SjCR\",\"./prepare\":\"F/j+\",\"./interpolate\":\"/Sl0\"}],\"+LE9\":[function(require,module,exports) {\n\"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;\n},{\"./frames\":\"bUxv\"}],\"5PF2\":[function(require,module,exports) {\n\"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;\n},{\"../util\":\"NSCe\"}],\"Aed7\":[function(require,module,exports) {\n\"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;\n},{\"./animate/timing\":\"SjCR\"}],\"+HZB\":[function(require,module,exports) {\n\"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;\n},{\"../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) {\n\"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.\"});\n},{\"./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)\n//# sourceMappingURL=/content.a90eb1ee.js.map</script> </body></html>"
  },
  {
    "path": "internal/animate/frames.ts",
    "content": "import {TimingFunc, timingFunctions} from \"./timing\";\nimport {Point} from \"../types\";\nimport {prepare} from \"./prepare\";\nimport {interpolateBetween} from \"./interpolate\";\n\nexport interface Keyframe {\n    delay?: number;\n    duration: number;\n    timingFunction?: keyof typeof timingFunctions;\n}\n\nexport interface InternalKeyframe {\n    id: string;\n    timestamp: number;\n    timingFunction: TimingFunc;\n    initialPoints: Point[];\n    transitionSourceFrameIndex: number;\n\n    // Synthetic keyframes are generated to represent the current state when\n    // a new transition is begun.\n    isSynthetic: boolean;\n}\n\nexport interface RenderCache {\n    [frameId: string]: {\n        preparedEndPoints?: Point[];\n        preparedStartPoints?: Point[];\n    };\n}\n\nexport interface RenderInput {\n    currentFrames: InternalKeyframe[];\n    timestamp: number;\n    renderCache: RenderCache;\n}\n\nexport interface RenderOutput {\n    points: Point[];\n    lastFrameId: string | null;\n    renderCache: RenderCache;\n}\n\nexport interface TransitionInput<T extends Keyframe> extends RenderInput {\n    newFrames: T[];\n    shapeGenerator: (keyframe: T) => Point[];\n}\n\nexport interface TransitionOutput {\n    newFrames: InternalKeyframe[];\n}\n\nconst genId = (): string => {\n    return String(Math.random()).substr(2);\n};\n\nexport const renderFramesAt = (input: RenderInput): RenderOutput => {\n    const {renderCache, currentFrames} = input;\n\n    if (currentFrames.length === 0) {\n        return {renderCache, lastFrameId: null, points: []};\n    }\n\n    // Animation freezes at the final shape if there are no more keyframes.\n    if (currentFrames.length === 1) {\n        const first = currentFrames[0];\n        return {renderCache, lastFrameId: first.id, points: first.initialPoints};\n    }\n\n    // Find the start/end keyframes according to the timestamp.\n    let startKeyframe = currentFrames[0];\n    let endKeyframe = currentFrames[1];\n    for (let i = 2; i < currentFrames.length; i++) {\n        if (endKeyframe.timestamp > input.timestamp) break;\n        startKeyframe = currentFrames[i - 1];\n        endKeyframe = currentFrames[i];\n    }\n\n    // Return original end shape when past the end of the animation.\n    const endKeyframeIsLast = endKeyframe === currentFrames[currentFrames.length - 1];\n    const animationIsPastEndKeyframe = endKeyframe.timestamp < input.timestamp;\n    if (animationIsPastEndKeyframe && endKeyframeIsLast) {\n        return {\n            renderCache,\n            lastFrameId: endKeyframe.id,\n            points: endKeyframe.initialPoints,\n        };\n    }\n\n    // Use and cache prepared points for current interpolation.\n    let preparedStartPoints: Point[] | undefined =\n        renderCache[startKeyframe.id]?.preparedStartPoints;\n    let preparedEndPoints: Point[] | undefined = renderCache[endKeyframe.id]?.preparedEndPoints;\n    if (!preparedStartPoints || !preparedEndPoints) {\n        [preparedStartPoints, preparedEndPoints] = prepare(\n            startKeyframe.initialPoints,\n            endKeyframe.initialPoints,\n            {rawAngles: false, divideRatio: 1},\n        );\n\n        renderCache[startKeyframe.id] = renderCache[startKeyframe.id] || {};\n        renderCache[startKeyframe.id].preparedStartPoints = preparedStartPoints;\n\n        renderCache[endKeyframe.id] = renderCache[endKeyframe.id] || {};\n        renderCache[endKeyframe.id].preparedEndPoints = preparedEndPoints;\n    }\n\n    // Calculate progress between frames as a fraction.\n    const progress =\n        (input.timestamp - startKeyframe.timestamp) /\n        (endKeyframe.timestamp - startKeyframe.timestamp);\n\n    // Keep progress within expected range (ex. division by 0).\n    const clampedProgress = Math.max(0, Math.min(1, progress));\n\n    // Apply timing function of end frame.\n    const adjustedProgress = endKeyframe.timingFunction(clampedProgress);\n\n    return {\n        renderCache,\n        lastFrameId: clampedProgress === 1 ? endKeyframe.id : startKeyframe.id,\n        points: interpolateBetween(adjustedProgress, preparedStartPoints, preparedEndPoints),\n    };\n};\n\nexport const transitionFrames = <T extends Keyframe>(\n    input: TransitionInput<T>,\n): TransitionOutput => {\n    // Erase all old frames.\n    const newInternalFrames: InternalKeyframe[] = [];\n\n    // Reset animation when given no keyframes.\n    if (input.newFrames.length === 0) {\n        return {newFrames: newInternalFrames};\n    }\n\n    // Add current state as initial frame.\n    const currentState = renderFramesAt(input);\n    if (currentState.lastFrameId === null) {\n        // If there is currently no shape being rendered, use a point in the\n        // center of the next frame as the initial point.\n        const firstShape = input.shapeGenerator(input.newFrames[0]);\n        let firstShapeCenterPoint: Point = {\n            x: 0,\n            y: 0,\n            handleIn: {angle: 0, length: 0},\n            handleOut: {angle: 0, length: 0},\n        };\n        for (const point of firstShape) {\n            firstShapeCenterPoint.x += point.x / firstShape.length;\n            firstShapeCenterPoint.y += point.y / firstShape.length;\n        }\n        currentState.points = [firstShapeCenterPoint, firstShapeCenterPoint, firstShapeCenterPoint];\n    }\n    newInternalFrames.push({\n        id: genId(),\n        initialPoints: currentState.points,\n        timestamp: input.timestamp,\n        timingFunction: timingFunctions.linear,\n        transitionSourceFrameIndex: -1,\n        isSynthetic: true,\n    });\n\n    // Generate and add new frames.\n    let totalOffset = 0;\n    for (let i = 0; i < input.newFrames.length; i++) {\n        const keyframe = input.newFrames[i];\n\n        // Copy previous frame when current one has a delay.\n        if (keyframe.delay) {\n            totalOffset += keyframe.delay;\n            const prevFrame = newInternalFrames[newInternalFrames.length - 1];\n            newInternalFrames.push({\n                id: genId(),\n                initialPoints: prevFrame.initialPoints,\n                timestamp: input.timestamp + totalOffset,\n                timingFunction: timingFunctions.linear,\n                transitionSourceFrameIndex: i - 1,\n                isSynthetic: true,\n            });\n        }\n\n        totalOffset += keyframe.duration;\n        newInternalFrames.push({\n            id: genId(),\n            initialPoints: input.shapeGenerator(keyframe),\n            timestamp: input.timestamp + totalOffset,\n            timingFunction: timingFunctions[keyframe.timingFunction || \"linear\"],\n            transitionSourceFrameIndex: i,\n            isSynthetic: false,\n        });\n    }\n\n    return {newFrames: newInternalFrames};\n};\n"
  },
  {
    "path": "internal/animate/interpolate.ts",
    "content": "import {Point} from \"../types\";\nimport {mapPoints, mod, smooth, split, splitLine} from \"../util\";\n\n// Interpolates between angles a and b. Angles are normalized to avoid unnecessary rotation.\n// Direction is chosen to produce the smallest possible movement.\nconst interpolateAngle = (percentage: number, a: number, b: number): number => {\n    const tau = Math.PI * 2;\n    let aNorm = mod(a, tau);\n    let bNorm = mod(b, tau);\n    if (Math.abs(aNorm - bNorm) > Math.PI) {\n        if (aNorm < bNorm) {\n            aNorm += tau;\n        } else {\n            bNorm += tau;\n        }\n    }\n    return split(percentage, aNorm, bNorm);\n};\n\n// Interpolates linearly between a and b. Can only interpolate between point lists that have the\n// same number of points. Easing effects can be applied to the percentage given to this function.\n// Percentages outside the 0-1 range are supported.\nexport const interpolateBetween = (percentage: number, a: Point[], b: Point[]): Point[] => {\n    if (a.length !== b.length) {\n        throw new Error(\"must have equal number of points\");\n    }\n\n    // Clamped range for use in values that could look incorrect otherwise.\n    // ex. Handles that invert if their value goes negative (creates loops at corners).\n    const clamped = Math.min(1, Math.max(0, percentage));\n\n    const points: Point[] = [];\n    for (let i = 0; i < a.length; i++) {\n        points.push({\n            ...splitLine(percentage, a[i], b[i]),\n            handleIn: {\n                angle: interpolateAngle(percentage, a[i].handleIn.angle, b[i].handleIn.angle),\n                length: split(clamped, a[i].handleIn.length, b[i].handleIn.length),\n            },\n            handleOut: {\n                angle: interpolateAngle(percentage, a[i].handleOut.angle, b[i].handleOut.angle),\n                length: split(clamped, a[i].handleOut.length, b[i].handleOut.length),\n            },\n        });\n    }\n    return points;\n};\n\n// Interpolates between a and b while applying a smoothing effect. Smoothing effect's strength is\n// relative to how far away the percentage is from either 0 or 1. It is strongest in the middle of\n// the animation (percentage = 0.5) or when bounds are exceeded (percentage = 1.8).\nexport const interpolateBetweenSmooth = (\n    strength: number,\n    percentage: number,\n    a: Point[],\n    b: Point[],\n): Point[] => {\n    strength *= Math.min(1, Math.min(Math.abs(0 - percentage), Math.abs(1 - percentage)));\n    const interpolated = interpolateBetween(percentage, a, b);\n    const smoothed = smooth(interpolated, Math.sqrt(strength + 0.25) / 3);\n    return mapPoints(interpolated, ({index, curr}) => {\n        const sp = smoothed[index];\n        curr.handleIn.angle = interpolateAngle(strength, curr.handleIn.angle, sp.handleIn.angle);\n        curr.handleIn.length = split(strength, curr.handleIn.length, sp.handleIn.length);\n        curr.handleOut.angle = interpolateAngle(strength, curr.handleOut.angle, sp.handleOut.angle);\n        curr.handleOut.length = split(strength, curr.handleOut.length, sp.handleOut.length);\n        return curr;\n    });\n};\n"
  },
  {
    "path": "internal/animate/prepare.ts",
    "content": "import {\n    angleOf,\n    coordEqual,\n    distance,\n    forPoints,\n    insertCount,\n    length,\n    mapPoints,\n    mod,\n    reverse,\n    shift,\n} from \"../util\";\nimport {Point} from \"../types\";\n\n// Iterate through point ordering possibilities to find an option with the least\n// distance between points. Also reverse the list to try and optimize.\nconst optimizeOrder = (a: Point[], b: Point[]): Point[] => {\n    const count = a.length;\n\n    let minTotal = Infinity;\n    let minOffset = 0;\n    let minOffsetBase: Point[] = [];\n\n    const setMinOffset = (points: Point[]) => {\n        for (let i = 0; i < count; i++) {\n            let total = 0;\n            for (let j = 0; j < count; j++) {\n                total += (100 * distance(a[j], points[mod(j + i, count)])) ** 2;\n                if (total > minTotal) break;\n            }\n            if (total <= minTotal) {\n                minTotal = total;\n                minOffset = i;\n                minOffsetBase = points;\n            }\n        }\n    };\n    setMinOffset(b);\n    setMinOffset(reverse(b));\n\n    return shift(minOffset, minOffsetBase);\n};\n\n// Modify the input shape to be the exact same path visually, but with\n// additional points so that the total number of points is \"count\".\nexport const divide = (count: number, points: Point[]): Point[] => {\n    if (points.length < 3) throw new Error(\"not enough points\");\n    if (count < points.length) throw new Error(\"cannot remove points\");\n    if (count === points.length) return points.slice();\n\n    const lengths: number[] = [];\n    forPoints(points, ({curr, next}) => {\n        lengths.push(length(curr, next()));\n    });\n\n    const divisors = divideLengths(lengths, count - points.length);\n    const out: Point[] = [];\n    for (let i = 0; i < points.length; i++) {\n        const curr: Point = out[out.length - 1] || points[i];\n        const next = points[mod(i + 1, points.length)];\n        out.pop();\n        out.push(...insertCount(divisors[i], curr, next));\n    }\n\n    // Remove redundant last point to produce closed shape, but use its incoming \\\n    // handle for the first point.\n    const last = out.pop();\n    out[0] = Object.assign({}, out[0], {handleIn: last!.handleIn});\n\n    return out;\n};\n\n// If point has no handle and is on top of the point before or after it, use the\n// angle of the fixer shape's point at the same index. This is especially useful\n// when all the points of the initial shape are concentrated on the same\n// coordinates and \"expand\" into the target shape.\nconst fixAnglesWith = (fixee: Point[], fixer: Point[]): Point[] => {\n    return mapPoints(fixee, ({index, curr, prev, next}) => {\n        if (curr.handleIn.length === 0 && coordEqual(prev(), curr)) {\n            curr.handleIn.angle = fixer[index].handleIn.angle;\n        }\n        if (curr.handleOut.length === 0 && coordEqual(next(), curr)) {\n            curr.handleOut.angle = fixer[index].handleOut.angle;\n        }\n        return curr;\n    });\n};\n\n// If point has no handle, use angle between before and after points.\nconst fixAnglesSelf = (points: Point[]): Point[] => {\n    return mapPoints(points, ({curr, prev, next}) => {\n        const angle = angleOf(prev(), next());\n        if (curr.handleIn.length === 0) {\n            curr.handleIn.angle = angle + Math.PI;\n        }\n        if (curr.handleOut.length === 0) {\n            curr.handleOut.angle = angle;\n        }\n        return curr;\n    });\n};\n\n// Split the input lengths into smaller segments to add the target amount of\n// lengths while minimizing the standard deviation of the list of lengths.\nconst divideLengths = (lengths: number[], add: number): number[] => {\n    const divisors = lengths.map(() => 1);\n    const sizes = lengths.slice();\n    for (let i = 0; i < add; i++) {\n        let maxSizeIndex = 0;\n        for (let j = 1; j < sizes.length; j++) {\n            if (sizes[j] > sizes[maxSizeIndex]) {\n                maxSizeIndex = j;\n                continue;\n            }\n            if (sizes[j] === sizes[maxSizeIndex]) {\n                if (lengths[j] > lengths[maxSizeIndex]) {\n                    maxSizeIndex = j;\n                }\n            }\n        }\n        divisors[maxSizeIndex]++;\n        sizes[maxSizeIndex] = lengths[maxSizeIndex] / divisors[maxSizeIndex];\n    }\n    return divisors;\n};\n\nexport const prepare = (\n    a: Point[],\n    b: Point[],\n    options: {rawAngles: boolean; divideRatio: number},\n): [Point[], Point[]] => {\n    const pointCount = options.divideRatio * Math.max(a.length, b.length);\n    const aNorm = divide(pointCount, a);\n    const bNorm = divide(pointCount, b);\n    const bOpt = optimizeOrder(aNorm, bNorm);\n    return [\n        options.rawAngles ? aNorm : fixAnglesWith(fixAnglesSelf(aNorm), bOpt),\n        options.rawAngles ? bOpt : fixAnglesWith(fixAnglesSelf(bOpt), aNorm),\n    ];\n};\n"
  },
  {
    "path": "internal/animate/state.ts",
    "content": "import {Point} from \"../types\";\nimport {InternalKeyframe, Keyframe, RenderCache, renderFramesAt, transitionFrames} from \"./frames\";\n\ninterface CallbackKeyframe extends Keyframe {\n    callback?: () => void;\n}\n\ninterface FrameCallbackStore {\n    [frameId: string]: () => void;\n}\n\nexport const statefulAnimationGenerator = <K extends CallbackKeyframe, T>(\n    generator: (keyframe: K) => Point[],\n    renderer: (points: Point[]) => T,\n    checker: (keyframe: K, index: number) => void,\n) => (timestampProvider: () => number) => {\n    let internalFrames: InternalKeyframe[] = [];\n    let renderCache: RenderCache = {};\n    let frameCallbackStore: FrameCallbackStore = {};\n\n    // Keep track of paused state.\n    let pausedAt = 0;\n    let pauseOffset = 0;\n    const getAnimationTimestamp = () => timestampProvider() - pauseOffset;\n    const isPaused = () => pausedAt !== 0;\n\n    const play = () => {\n        if (!isPaused()) return;\n        pauseOffset += getAnimationTimestamp() - pausedAt;\n        pausedAt = 0;\n    };\n\n    const pause = () => {\n        if (isPaused()) return;\n        pausedAt = getAnimationTimestamp();\n    };\n\n    const playPause = () => {\n        if (isPaused()) {\n            play();\n        } else {\n            pause();\n        }\n    };\n\n    const renderPoints = (): Point[] => {\n        const renderOutput = renderFramesAt({\n            renderCache: renderCache,\n            timestamp: isPaused() ? pausedAt : getAnimationTimestamp(),\n            currentFrames: internalFrames,\n        });\n\n        // Update render cache with returned value.\n        renderCache = renderOutput.renderCache;\n\n        // Invoke callback if defined and the first time the frame is reached.\n        if (renderOutput.lastFrameId && frameCallbackStore[renderOutput.lastFrameId]) {\n            setTimeout(frameCallbackStore[renderOutput.lastFrameId]);\n            delete frameCallbackStore[renderOutput.lastFrameId];\n        }\n\n        return renderOutput.points;\n    };\n\n    const renderFrame = (): T => {\n        return renderer(renderPoints());\n    };\n\n    const transition = (...keyframes: K[]) => {\n        // Make sure frame info is valid.\n        for (let i = 0; i < keyframes.length; i++) {\n            checker(keyframes[i], i);\n        }\n\n        const transitionOutput = transitionFrames<K>({\n            renderCache: renderCache,\n            timestamp: getAnimationTimestamp(),\n            currentFrames: internalFrames,\n            newFrames: keyframes,\n            shapeGenerator: generator,\n        });\n\n        // Reset internal state..\n        internalFrames = transitionOutput.newFrames;\n        frameCallbackStore = {};\n        renderCache = {};\n\n        // Populate callback store using returned frame ids.\n        for (const newFrame of internalFrames) {\n            if (newFrame.isSynthetic) continue;\n            const {callback} = keyframes[newFrame.transitionSourceFrameIndex];\n            if (callback) frameCallbackStore[newFrame.id] = callback;\n        }\n    };\n\n    return {renderFrame, renderPoints, transition, play, pause, playPause};\n};\n"
  },
  {
    "path": "internal/animate/testing/index.html",
    "content": "<html>\n    <head>\n        <style>\n            body {\n                align-items: center;\n                display: flex;\n                height: 100vh;\n                justify-content: center;\n                margin: 0;\n                width: 100vw;\n            }\n\n            body * {\n                border: 1px solid #eee;\n            }\n\n            .buttons {\n                display: flex;\n                flex-direction: column;\n            }\n\n            #interact {\n                margin-top: 20px;\n                padding: 10px;\n            }\n        </style>\n    </head>\n\n    <body>\n        <div class=\"buttons\">\n            <button id=\"toggle\">TOGGLE<br />DEBUG</button>\n            <button id=\"interact\">INTERACT</button>\n        </div>\n        <script src=\"./script.ts\"></script>\n    </body>\n</html>\n"
  },
  {
    "path": "internal/animate/testing/script.ts",
    "content": "import {interpolateBetweenSmooth} from \"../interpolate\";\nimport {divide, prepare} from \"../prepare\";\nimport {Coord, Point} from \"../../types\";\nimport {forPoints, insertAt, insertCount, length, mapPoints, mod, rad} from \"../../util\";\nimport {clear, drawClosed, drawInfo} from \"../../render/canvas\";\nimport {genBlob, genFromOptions} from \"../../gen\";\nimport {rand} from \"../../rand\";\nimport * as blobs2 from \"../../../public/blobs\";\nimport * as blobs2Animate from \"../../../public/animate\";\n\nlet animationSpeed = 2;\nlet animationStart = 0.3;\nlet debug = true;\nlet size = 1300;\n\nconst canvas = document.createElement(\"canvas\");\ndocument.body.appendChild(canvas);\ncanvas.height = size;\ncanvas.width = size;\nconst temp = canvas.getContext(\"2d\");\nif (temp === null) throw new Error(\"context is null\");\nconst ctx = temp;\n\nconst toggle = document.getElementById(\"toggle\");\nif (toggle === null) throw new Error(\"no toggle\");\ntoggle.onclick = () => (debug = !debug);\n\nconst interact = document.getElementById(\"interact\") as any;\nif (toggle === null) throw new Error(\"no interact\");\nconst addInteraction = (newOnclick: () => void) => {\n    const oldOnclick = interact.onclick || (() => 0);\n    interact.onclick = () => {\n        oldOnclick();\n        newOnclick();\n    };\n};\n\nconst point = (x: number, y: number, ia: number, il: number, oa: number, ol: number): Point => {\n    return {\n        x: x * size,\n        y: y * size,\n        handleIn: {angle: rad(ia), length: il * size},\n        handleOut: {angle: rad(oa), length: ol * size},\n    };\n};\n\nconst testSplitAt = (percentage: number) => {\n    let points: Point[] = [\n        point(0.15, 0.15, 135, 0.1, 315, 0.2),\n        point(0.85, 0.15, 225, 0.1, 45, 0.2),\n        point(0.85, 0.85, 315, 0.1, 135, 0.2),\n        point(0.15, 0.85, 45, 0.1, 225, 0.2),\n    ];\n\n    const count = points.length;\n    const stop = 2 * count - 1;\n    for (let i = 0; i < count; i++) {\n        const double = i * 2;\n        const next = mod(double + 1, stop);\n        points.splice(double, 2, ...insertAt(percentage, points[double], points[next]));\n    }\n    points.splice(0, 1);\n\n    let sum = 0;\n    forPoints(points, ({curr, next}) => {\n        sum += length(curr, next());\n    });\n    drawInfo(ctx, 1, \"split at lengths sum\", sum);\n\n    drawClosed(ctx, debug, points);\n};\n\nconst testSplitBy = () => {\n    const count = 10;\n    for (let i = 0; i < count; i++) {\n        drawClosed(\n            ctx,\n            debug,\n            insertCount(\n                i + 1,\n                point(0.15, 0.2 + i * 0.06, 30, 0.04, -30, 0.04),\n                point(0.25, 0.2 + i * 0.06, 135, 0.04, 225, 0.04),\n            ),\n        );\n    }\n};\n\nconst testDividePoints = () => {\n    const count = 10;\n    for (let i = 0; i < count; i++) {\n        drawClosed(\n            ctx,\n            debug,\n            divide(i + 3, [\n                point(0.3, 0.2 + i * 0.05, -10, 0.04, -45, 0.02),\n                point(0.35, 0.2 + i * 0.05 - 0.02, 180, 0.02, 0, 0.02),\n                point(0.4, 0.2 + i * 0.05, -135, 0.02, 170, 0.04),\n            ]),\n        );\n    }\n};\n\nconst testInterpolateBetween = (percentage: number) => {\n    const a = [\n        point(0.3, 0.72, 135, 0.05, -45, 0.05),\n        point(0.4, 0.72, -135, 0.05, 45, 0.05),\n        point(0.4, 0.82, -45, 0.05, 135, 0.05),\n        point(0.3, 0.82, 45, 0.05, 225, 0.05),\n    ];\n    const b = [\n        point(0.35, 0.72, 180, 0, 0, 0),\n        point(0.4, 0.77, -90, 0, 90, 0),\n        point(0.35, 0.82, 360 * 10, 0, 180, 0),\n        point(0.3, 0.77, 90, 0, -90, 0),\n    ];\n    drawClosed(ctx, debug, loopBetween(percentage, a, b));\n};\n\nconst testPrepPointsA = (percentage: number) => {\n    const a = blob(\"a\", 6, 0.15, {x: 0.45, y: 0.1});\n    const b = blob(\"b\", 10, 0.15, {x: 0.45, y: 0.1});\n    drawClosed(\n        ctx,\n        debug,\n        loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),\n    );\n};\n\nconst testPrepPointsB = (percentage: number) => {\n    const a = blob(\"a\", 8, 0.15, {x: 0.45, y: 0.25});\n    const b: Point[] = [\n        point(0.45, 0.25, 0, 0, 0, 0),\n        point(0.6, 0.25, 0, 0, 0, 0),\n        point(0.6, 0.4, 0, 0, 0, 0),\n        point(0.45, 0.4, 0, 0, 0, 0),\n    ];\n    drawClosed(\n        ctx,\n        debug,\n        loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),\n    );\n};\n\nconst testPrepPointsC = (percentage: number) => {\n    const a = blob(\"c\", 8, 0.15, {x: 0.45, y: 0.45});\n    const b: Point[] = [\n        point(0.5, 0.45, 0, 0, 0, 0),\n        point(0.55, 0.45, 0, 0, 0, 0),\n        point(0.55, 0.5, 0, 0, 0, 0),\n        point(0.6, 0.5, 0, 0, 0, 0),\n        point(0.6, 0.55, 0, 0, 0, 0),\n        point(0.55, 0.55, 0, 0, 0, 0),\n        point(0.55, 0.6, 0, 0, 0, 0),\n        point(0.5, 0.6, 0, 0, 0, 0),\n        point(0.5, 0.55, 0, 0, 0, 0),\n        point(0.45, 0.55, 0, 0, 0, 0),\n        point(0.45, 0.5, 0, 0, 0, 0),\n        point(0.5, 0.5, 0, 0, 0, 0),\n    ];\n    drawClosed(\n        ctx,\n        debug,\n        loopBetween(percentage, ...prepare(b, a, {rawAngles: false, divideRatio: 1})),\n    );\n};\n\nconst testPrepPointsD = (percentage: number) => {\n    const a = blob(\"d\", 8, 0.15, {x: 0.45, y: 0.65});\n    const b: Point[] = [\n        point(0.525, 0.725, 0, 0, 0, 0),\n        point(0.525, 0.725, 0, 0, 0, 0),\n        point(0.525, 0.725, 0, 0, 0, 0),\n    ];\n    drawClosed(\n        ctx,\n        debug,\n        loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),\n    );\n};\n\nconst testPrepLetters = (percentage: number) => {\n    const a: Point[] = [\n        point(0.65, 0.2, 0, 0, 0, 0),\n        point(0.85, 0.2, 0, 0, 0, 0),\n        point(0.85, 0.25, 0, 0, 0, 0),\n        point(0.7, 0.25, 0, 0, 0, 0),\n        point(0.7, 0.4, 0, 0, 0, 0),\n        point(0.8, 0.4, 0, 0, 0, 0),\n        point(0.8, 0.35, 0, 0, 0, 0),\n        point(0.75, 0.35, 0, 0, 0, 0),\n        point(0.75, 0.3, 0, 0, 0, 0),\n        point(0.85, 0.3, 0, 0, 0, 0),\n        point(0.85, 0.45, 0, 0, 0, 0),\n        point(0.65, 0.45, 0, 0, 0, 0),\n    ];\n    const b: Point[] = blob(\"\", 8, 0.25, {x: 0.65, y: 0.2});\n    drawClosed(\n        ctx,\n        debug,\n        loopBetween(percentage, ...prepare(a, b, {rawAngles: false, divideRatio: 1})),\n    );\n};\n\nconst testGen = () => {\n    const cellSideCount = 16;\n    const cellSize = size / cellSideCount;\n    ctx.save();\n    ctx.strokeStyle = \"#fafafa\";\n    ctx.fillStyle = \"#f1f1f1\";\n    for (let i = 0; i < cellSideCount; i++) {\n        for (let j = 0; j < cellSideCount; j++) {\n            ctx.strokeRect(i * cellSize, j * cellSize, cellSize, cellSize);\n            ctx.fill(\n                blobs2.canvasPath(\n                    {\n                        extraPoints: j,\n                        randomness: i,\n                        seed: i + j - i * j,\n                        size: cellSize,\n                    },\n                    {\n                        offsetX: i * cellSize,\n                        offsetY: j * cellSize,\n                    },\n                ),\n            );\n        }\n    }\n    ctx.restore();\n};\n\nconst blob = (seed: string, count: number, scale: number, offset: Coord): Point[] => {\n    const rgen = rand(seed);\n    const points = genBlob(count, () => 0.3 + 0.2 * rgen());\n    return mapPoints(points, ({curr}) => {\n        curr.x *= scale * size;\n        curr.y *= scale * size;\n        curr.x += offset.x * size;\n        curr.y += offset.y * size;\n        curr.handleIn.length *= scale * size;\n        curr.handleOut.length *= scale * size;\n        return curr;\n    });\n};\n\nconst loopBetween = (percentage: number, a: Point[], b: Point[]): Point[] => {\n    // Draw before/after shapes + point path.\n    ctx.save();\n    ctx.strokeStyle = \"#ffaaaa\";\n    drawClosed(ctx, false, a);\n    ctx.strokeStyle = \"#aaaaff\";\n    drawClosed(ctx, false, b);\n    ctx.strokeStyle = \"#33ff33\";\n    for (let i = 0; i < a.length; i++) {\n        ctx.beginPath();\n        ctx.moveTo(a[i].x, a[i].y);\n        ctx.lineTo(b[i].x, b[i].y);\n        ctx.stroke();\n    }\n    ctx.restore();\n\n    if (percentage < 0.5) {\n        return interpolateBetweenSmooth(1, 2 * percentage, a, b);\n    } else {\n        return interpolateBetweenSmooth(1, -2 * percentage + 2, a, b);\n    }\n};\n\nconst genBlobAnimation = (\n    speed: number,\n    offset: number,\n    timing: blobs2Animate.CanvasKeyframe[\"timingFunction\"],\n    timeWarp: number,\n) => {\n    const animation = blobs2Animate.canvasPath(() => Date.now() * timeWarp);\n\n    const loopAnimation = () => {\n        animation.transition(\n            {\n                duration: speed,\n                delay: speed,\n                timingFunction: \"ease\",\n                blobOptions: {\n                    extraPoints: 3,\n                    randomness: 4,\n                    seed: Math.random(),\n                    size: 200,\n                },\n                canvasOptions: {\n                    offsetX: offset,\n                },\n            },\n            {\n                duration: speed,\n                timingFunction: \"ease\",\n                blobOptions: {\n                    extraPoints: 3,\n                    randomness: 4,\n                    seed: Math.random(),\n                    size: 200,\n                },\n                canvasOptions: {\n                    offsetX: offset,\n                },\n            },\n            {\n                duration: speed,\n                delay: speed,\n                timingFunction: \"ease\",\n                blobOptions: {\n                    extraPoints: 3,\n                    randomness: 4,\n                    seed: Math.random(),\n                    size: 200,\n                },\n                canvasOptions: {\n                    offsetX: offset,\n                },\n            },\n            {\n                duration: speed,\n                callback: loopAnimation,\n                timingFunction: \"ease\",\n                blobOptions: {\n                    extraPoints: 39,\n                    randomness: 2,\n                    seed: Math.random(),\n                    size: 200,\n                },\n                canvasOptions: {\n                    offsetX: offset,\n                },\n            },\n        );\n    };\n\n    animation.transition({\n        duration: 0,\n        callback: loopAnimation,\n        blobOptions: {\n            extraPoints: 1,\n            randomness: 0,\n            seed: 0,\n            size: 200,\n        },\n        canvasOptions: {\n            offsetX: offset,\n        },\n    });\n\n    addInteraction(() => {\n        animation.transition({\n            duration: speed,\n            callback: loopAnimation,\n            timingFunction: timing,\n            blobOptions: {\n                extraPoints: 30,\n                randomness: 8,\n                seed: Math.random(),\n                size: 180,\n            },\n            canvasOptions: {\n                offsetX: 10 + offset,\n                offsetY: 10,\n            },\n        });\n    });\n\n    return animation;\n};\n\nconst genCustomAnimation = (speed: number, offset: number) => {\n    const noHandles = {\n        handleIn: {angle: 0, length: 0},\n        handleOut: {angle: 0, length: 0},\n    };\n    const animation = blobs2Animate.canvasPath();\n    const loopAnimation = (immediate: boolean = false) => {\n        const size = 200;\n        animation.transition(\n            {\n                duration: immediate ? 0 : speed,\n                delay: 100,\n                timingFunction: \"elasticEnd0\",\n                blobOptions: {\n                    extraPoints: 3,\n                    randomness: 4,\n                    seed: Math.random(),\n                    size: size,\n                },\n                canvasOptions: {offsetX: offset, offsetY: 220},\n            },\n            {\n                duration: speed,\n                delay: 100,\n                timingFunction: \"elasticEnd0\",\n                points: [\n                    {x: 0, y: 0, ...noHandles},\n                    {x: 0, y: size, ...noHandles},\n                    {x: size, y: size, ...noHandles},\n                    {x: size, y: 0, ...noHandles},\n                ],\n                canvasOptions: {offsetX: offset, offsetY: 220},\n                callback: loopAnimation,\n            },\n        );\n    };\n    loopAnimation(true);\n    addInteraction(() => animation.playPause());\n    return animation;\n};\n\nconst wigglePresetBad = (\n    animation: blobs2Animate.Animation,\n    config: {\n        blobOptions: blobs2.BlobOptions;\n        period: number;\n        delay?: number;\n        timingFunction?: blobs2Animate.CanvasKeyframe[\"timingFunction\"];\n        canvasOptions?: {\n            offsetX?: number;\n            offsetY?: number;\n        };\n    },\n) => {\n    const targetBlob: Point[] = genFromOptions(config.blobOptions);\n    const numberOfPoints = 3 + config.blobOptions.extraPoints;\n    const mutatesPerPeriod = 1 * numberOfPoints;\n    const mutateInterval = config.period / mutatesPerPeriod;\n    const mutateRatio = 1 / mutatesPerPeriod;\n\n    console.log(\n        \"mutatesPerPeriod\",\n        mutatesPerPeriod,\n        \"mutateInterval\",\n        mutateInterval,\n        \"mutateRatio\",\n        mutateRatio,\n        \"config\",\n        JSON.stringify(config),\n    );\n\n    const loopAnimation = () => {\n        const newBlob = genFromOptions(Object.assign(config.blobOptions, {seed: Math.random()}));\n        for (let i = 0; i < newBlob.length; i++) {\n            if (Math.random() < mutateRatio) {\n                targetBlob[i] = newBlob[i];\n            }\n        }\n        animation.transition({\n            duration: config.period,\n            timingFunction: config.timingFunction,\n            canvasOptions: config.canvasOptions,\n            points: targetBlob,\n        });\n    };\n    animation.transition({\n        duration: 0,\n        delay: config.delay || 0,\n        timingFunction: config.timingFunction,\n        canvasOptions: config.canvasOptions,\n        points: genFromOptions(config.blobOptions),\n        callback: () => setInterval(loopAnimation, mutateInterval),\n    });\n    addInteraction(() => animation.playPause());\n};\n\nconst genBadWiggle = (period: number, offset: number) => {\n    const animation = blobs2Animate.canvasPath();\n    wigglePresetBad(animation, {\n        blobOptions: {\n            extraPoints: 1,\n            randomness: 4,\n            seed: Math.random(),\n            size: 200,\n        },\n        period,\n        timingFunction: \"ease\",\n        canvasOptions: {offsetX: offset, offsetY: 220},\n    });\n    return animation;\n};\n\nconst genWiggle = (offset: number, speed: number) => {\n    const animation = blobs2Animate.canvasPath();\n    blobs2Animate.wigglePreset(\n        animation,\n        {\n            extraPoints: 4,\n            randomness: 2,\n            seed: Math.random(),\n            size: 200,\n        },\n        {offsetX: offset, offsetY: 220},\n        {speed},\n    );\n    addInteraction(() => animation.playPause());\n    return animation;\n};\n\n(() => {\n    let percentage = animationStart;\n\n    const animations = [\n        genBlobAnimation(500, 0, \"elasticEnd0\", 1),\n        genBlobAnimation(500, 200, \"elasticEnd1\", 1),\n        genBlobAnimation(500, 400, \"elasticEnd2\", 1),\n        genBlobAnimation(500, 600, \"elasticEnd3\", 1),\n        genBlobAnimation(500, 800, \"elasticEnd3\", 0.1),\n        genCustomAnimation(1000, 0),\n        genBadWiggle(200, 200),\n        genWiggle(400, 5),\n    ];\n\n    const renderFrame = () => {\n        clear(ctx);\n\n        testGen();\n        drawInfo(ctx, 0, \"percentage\", percentage);\n        testSplitAt(percentage);\n        testSplitBy();\n        testDividePoints();\n        testInterpolateBetween(percentage);\n        testPrepPointsA(percentage);\n        testPrepPointsB(percentage);\n        testPrepPointsC(percentage);\n        testPrepPointsD(percentage);\n        testPrepLetters(percentage);\n\n        for (const animation of animations) {\n            ctx.save();\n            ctx.strokeStyle = \"orange\";\n            ctx.fillStyle = \"rgba(255, 200, 0, 0.5)\";\n            const path = animation.renderFrame();\n            ctx.stroke(path);\n            ctx.fill(path);\n            ctx.restore();\n        }\n\n        percentage += animationSpeed / 1000;\n        percentage = mod(percentage, 1);\n        if (animationSpeed > 0) requestAnimationFrame(renderFrame);\n    };\n    renderFrame();\n})();\n"
  },
  {
    "path": "internal/animate/timing.ts",
    "content": "export interface TimingFunc {\n    (percentage: number): number;\n}\n\nconst linear: TimingFunc = (p) => {\n    return p;\n};\n\nconst easeEnd: TimingFunc = (p) => {\n    return 1 - (p - 1) ** 2;\n};\n\nconst easeStart: TimingFunc = (p) => {\n    return 1 - easeEnd(1 - p);\n};\n\nconst ease: TimingFunc = (p) => {\n    return 0.5 + 0.5 * Math.sin(Math.PI * (p + 1.5));\n};\n\nconst elasticEnd = (s: number): TimingFunc => (p) => {\n    return Math.pow(2, -10 * p) * Math.sin(((p - s / 4) * (2 * Math.PI)) / s) + 1;\n};\n\n// https://www.desmos.com/calculator/fqisoq1kuw\nexport const timingFunctions = {\n    linear,\n    easeEnd,\n    easeStart,\n    ease,\n    elasticEnd0: elasticEnd(1),\n    elasticEnd1: elasticEnd(0.64),\n    elasticEnd2: elasticEnd(0.32),\n    elasticEnd3: elasticEnd(0.16),\n};\n\n// @ts-ignore: Type assertion.\nconst _: Record<string, TimingFunc> = timingFunctions;\n"
  },
  {
    "path": "internal/check.ts",
    "content": "import {timingFunctions} from \"./animate/timing\";\n\nconst typeCheck = (name: string, val: any, expected: string[]) => {\n    let actual: string = typeof val;\n    if (actual === \"number\" && isNaN(val)) actual = \"NaN\";\n    if (actual === \"object\" && val === null) actual = \"null\";\n    if (!expected.includes(actual)) {\n        throw `\"${name}\" should have type \"${expected.join(\"|\")}\" but was \"${actual}\".`;\n    }\n};\n\nexport const checkKeyframeOptions = (keyframe: any) => {\n    typeCheck(`keyframe`, keyframe, [\"object\"]);\n    const {delay, duration, timingFunction, callback} = keyframe;\n    typeCheck(`delay`, delay, [\"number\", \"undefined\"]);\n    if (delay && delay < 0) throw `delay is invalid \"${delay}\".`;\n    typeCheck(`duration`, duration, [\"number\"]);\n    if (duration && duration < 0) throw `duration is invalid \"${duration}\".`;\n    typeCheck(`timingFunction`, timingFunction, [\"string\", \"undefined\"]);\n    if (timingFunction && !(timingFunctions as any)[timingFunction]) {\n        throw `\".timingFunction\" is not recognized \"${timingFunction}\".`;\n    }\n    typeCheck(`callback`, callback, [\"function\", \"undefined\"]);\n};\n\nexport const checkBlobOptions = (blobOptions: any) => {\n    typeCheck(`blobOptions`, blobOptions, [\"object\"]);\n    const {seed, extraPoints, randomness, size} = blobOptions;\n    typeCheck(`blobOptions.seed`, seed, [\"string\", \"number\"]);\n    typeCheck(`blobOptions.extraPoints`, extraPoints, [\"number\"]);\n    if (extraPoints < 0) {\n        throw `blobOptions.extraPoints is invalid \"${extraPoints}\".`;\n    }\n    typeCheck(`blobOptions.randomness`, randomness, [\"number\"]);\n    if (randomness < 0) {\n        throw `blobOptions.randomness is invalid \"${randomness}\".`;\n    }\n    typeCheck(`blobOptions.size`, size, [\"number\"]);\n    if (size < 0) throw `blobOptions.size is invalid \"${size}\".`;\n};\n\nexport const checkCanvasOptions = (canvasOptions: any) => {\n    typeCheck(`canvasOptions`, canvasOptions, [\"object\", \"undefined\"]);\n    if (canvasOptions) {\n        const {offsetX, offsetY} = canvasOptions;\n        typeCheck(`canvasOptions.offsetX`, offsetX, [\"number\", \"undefined\"]);\n        typeCheck(`canvasOptions.offsetY`, offsetY, [\"number\", \"undefined\"]);\n    }\n};\n\nexport const checkSvgOptions = (svgOptions: any) => {\n    typeCheck(`svgOptions`, svgOptions, [\"object\", \"undefined\"]);\n    if (svgOptions) {\n        const {fill, stroke, strokeWidth} = svgOptions;\n        typeCheck(`svgOptions.fill`, fill, [\"string\", \"undefined\"]);\n        typeCheck(`svgOptions.stroke`, stroke, [\"string\", \"undefined\"]);\n        typeCheck(`svgOptions.strokeWidth`, strokeWidth, [\"number\", \"undefined\"]);\n    }\n};\n\nexport const checkPoints = (points: any) => {\n    if (!Array.isArray(points)) {\n        throw `points should be an array but was \"${typeof points}\".`;\n    }\n    if (points.length < 3) {\n        throw `expected more than two points but received \"${points.length}\".`;\n    }\n    for (const point of points) {\n        typeCheck(`point.x`, point.x, [\"number\"]);\n        typeCheck(`point.y`, point.y, [\"number\"]);\n        typeCheck(`point.handleIn`, point.handleIn, [\"object\"]);\n        typeCheck(`point.handleIn.angle`, point.handleIn.angle, [\"number\"]);\n        typeCheck(`point.handleIn.length`, point.handleIn.length, [\"number\"]);\n        typeCheck(`point.handleOut`, point.handleOut, [\"object\"]);\n        typeCheck(`point.handleOut.angle`, point.handleOut.angle, [\"number\"]);\n        typeCheck(`point.handleOut.length`, point.handleOut.length, [\"number\"]);\n    }\n};\n"
  },
  {
    "path": "internal/gen.ts",
    "content": "import {rand} from \"../internal/rand\";\nimport {mapPoints} from \"../internal/util\";\nimport {BlobOptions} from \"../public/blobs\";\nimport {Point} from \"./types\";\nimport {smooth} from \"./util\";\n\nexport const smoothBlob = (blobygon: Point[]): Point[] => {\n    // https://math.stackexchange.com/a/873589/235756\n    const angle = (Math.PI * 2) / blobygon.length;\n    const smoothingStrength = ((4 / 3) * Math.tan(angle / 4)) / Math.sin(angle / 2) / 2;\n    return smooth(blobygon, smoothingStrength);\n};\n\nexport const genBlobygon = (pointCount: number, offset: (index: number) => number): Point[] => {\n    const angle = (Math.PI * 2) / pointCount;\n    const points: Point[] = [];\n    for (let i = 0; i < pointCount; i++) {\n        const randPointOffset = offset(i);\n        const pointX = Math.sin(i * angle);\n        const pointY = Math.cos(i * angle);\n        points.push({\n            x: 0.5 + pointX * randPointOffset,\n            y: 0.5 + pointY * randPointOffset,\n            handleIn: {angle: 0, length: 0},\n            handleOut: {angle: 0, length: 0},\n        });\n    }\n    return points;\n};\n\nexport const genBlob = (pointCount: number, offset: (index: number) => number): Point[] => {\n    return smoothBlob(genBlobygon(pointCount, offset));\n};\n\nexport const genFromOptions = (\n    blobOptions: BlobOptions,\n    r?: (index: number) => number,\n): Point[] => {\n    const rgen = r || rand(String(blobOptions.seed));\n\n    // Scale of random movement increases as randomness approaches infinity.\n    // randomness = 0   -> rangeStart = 1\n    // randomness = 2   -> rangeStart = 0.8333\n    // randomness = 5   -> rangeStart = 0.6667\n    // randomness = 10  -> rangeStart = 0.5\n    // randomness = 20  -> rangeStart = 0.3333\n    // randomness = 50  -> rangeStart = 0.1667\n    // randomness = 100 -> rangeStart = 0.0909\n    const rangeStart = 1 / (1 + blobOptions.randomness / 10);\n\n    const points = genBlob(\n        3 + blobOptions.extraPoints,\n        (index) => (rangeStart + rgen(index) * (1 - rangeStart)) / 2,\n    );\n\n    const size = blobOptions.size;\n    return mapPoints(points, ({curr}) => {\n        curr.x *= size;\n        curr.y *= size;\n        curr.handleIn.length *= size;\n        curr.handleOut.length *= size;\n        return curr;\n    });\n};\n"
  },
  {
    "path": "internal/rand.ts",
    "content": "import {createNoise2D} from \"simplex-noise\";\n\n// Seeded random number generator.\n// https://stackoverflow.com/a/47593316/3053361\nexport const rand = (seed: string) => {\n    const xfnv1a = (str: string) => {\n        let h = 2166136261 >>> 0;\n        for (let i = 0; i < str.length; i++) {\n            h = Math.imul(h ^ str.charCodeAt(i), 16777619);\n        }\n        return () => {\n            h += h << 13;\n            h ^= h >>> 7;\n            h += h << 3;\n            h ^= h >>> 17;\n            return (h += h << 5) >>> 0;\n        };\n    };\n\n    const sfc32 = (a: number, b: number, c: number, d: number) => () => {\n        a >>>= 0;\n        b >>>= 0;\n        c >>>= 0;\n        d >>>= 0;\n        var t = (a + b) | 0;\n        a = b ^ (b >>> 9);\n        b = (c + (c << 3)) | 0;\n        c = (c << 21) | (c >>> 11);\n        d = (d + 1) | 0;\n        t = (t + d) | 0;\n        c = (c + t) | 0;\n        return (t >>> 0) / 4294967296;\n    };\n\n    const seedGenerator = xfnv1a(seed);\n    return sfc32(seedGenerator(), seedGenerator(), seedGenerator(), seedGenerator());\n};\n\n// Simplex noise.\n// TODO(2023-01-08) implement to remove dep\n// TODO(2023-02-16) https://asserttrue.blogspot.com/2011/12/perlin-noise-in-javascript_31.html\n// https://en.wikipedia.org/wiki/Simplex_noise\nexport const noise = (seed: string) => {\n    const noise2D = createNoise2D(rand(seed));\n    return (x: number, y: number) => {\n        return noise2D(x, y);\n    };\n};\n"
  },
  {
    "path": "internal/render/canvas.ts",
    "content": "import {Coord, Point} from \"../types\";\nimport {expandHandle, forPoints} from \"../util\";\n\nconst pointSize = 2;\nconst infoSpacing = 20;\n\nexport const clear = (ctx: CanvasRenderingContext2D) => {\n    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n};\n\nexport const drawInfo = (ctx: CanvasRenderingContext2D, pos: number, label: string, value: any) => {\n    ctx.fillText(`${label}: ${value}`, infoSpacing, (pos + 1) * infoSpacing);\n};\n\nconst drawLine = (ctx: CanvasRenderingContext2D, a: Coord, b: Coord, style: string) => {\n    const backupStrokeStyle = ctx.strokeStyle;\n    ctx.beginPath();\n    ctx.moveTo(a.x, a.y);\n    ctx.lineTo(b.x, b.y);\n    ctx.strokeStyle = style;\n    ctx.stroke();\n    ctx.strokeStyle = backupStrokeStyle;\n};\n\nconst drawPoint = (ctx: CanvasRenderingContext2D, p: Coord, style: string) => {\n    const backupFillStyle = ctx.fillStyle;\n    ctx.beginPath();\n    ctx.arc(p.x, p.y, pointSize, 0, 2 * Math.PI);\n    ctx.fillStyle = style;\n    ctx.fill();\n    ctx.fillStyle = backupFillStyle;\n};\n\nexport const drawClosed = (ctx: CanvasRenderingContext2D, debug: boolean, points: Point[]) => {\n    if (points.length < 2) throw new Error(\"not enough points\");\n\n    // Draw debug points.\n    if (debug) {\n        forPoints(points, ({curr, next: getNext}) => {\n            const next = getNext();\n\n            // Compute coordinates of handles.\n            const currHandle = expandHandle(curr, curr.handleOut);\n            const nextHandle = expandHandle(next, next.handleIn);\n\n            drawPoint(ctx, curr, \"\");\n            drawLine(ctx, curr, currHandle, \"#ccc\");\n            drawLine(ctx, next, nextHandle, \"#b6b\");\n        });\n    }\n\n    ctx.stroke(renderPath2D(points));\n};\n\nexport const renderPath2D = (points: Point[]): Path2D => {\n    const path = new Path2D();\n\n    if (points.length < 1) return path;\n    path.moveTo(points[0].x, points[0].y);\n\n    forPoints(points, ({curr, next: getNext}) => {\n        const next = getNext();\n        const currHandle = expandHandle(curr, curr.handleOut);\n        const nextHandle = expandHandle(next, next.handleIn);\n        path.bezierCurveTo(currHandle.x, currHandle.y, nextHandle.x, nextHandle.y, next.x, next.y);\n    });\n\n    return path;\n};\n"
  },
  {
    "path": "internal/render/svg.test.ts",
    "content": "import {XmlElement} from \"./svg\";\n\ndescribe(\"internal/render/svg\", () => {\n    describe(\"XmlElement\", () => {\n        it(\"should render element tags\", () => {\n            const elem = new XmlElement(\"test\");\n            expect(elem.render()).toBe(\"<test/>\");\n        });\n\n        it(\"should render element attributes\", () => {\n            const elem = new XmlElement(\"test\");\n            elem.attributes.a = 1;\n            elem.attributes[\"b-c\"] = \"d\";\n            expect(elem.render()).toBe('<test a=\"1\" b-c=\"d\"/>');\n        });\n\n        it(\"should render nested elements\", () => {\n            const a = new XmlElement(\"a\");\n            const aa = new XmlElement(\"aa\");\n            const ab = new XmlElement(\"ab\");\n            const aba = {render: () => \"aba\"};\n            a.children.push(aa);\n            a.children.push(ab);\n            ab.children.push(aba);\n            expect(a.render()).toBe(\"<a><aa/><ab>aba</ab></a>\");\n        });\n    });\n});\n"
  },
  {
    "path": "internal/render/svg.ts",
    "content": "import {Point} from \"../types\";\nimport {expandHandle, forPoints} from \"../util\";\n\nexport interface RenderOptions {\n    // Viewport size.\n    width: number;\n    height: number;\n\n    // Transformation applied to all drawn points.\n    transform?: string;\n\n    // Declare whether the path should be closed.\n    // This option is currently always true.\n    closed: true;\n\n    // Output path styling.\n    fill?: string;\n    stroke?: string;\n    strokeWidth?: number;\n\n    // Option to render guides (points, handles and viewport).\n    guides?: boolean;\n    boundingBox?: boolean;\n}\n\nexport const renderPath = (points: Point[]): string => {\n    // Render path data attribute from points and handles.\n    let path = `M${points[0].x},${points[0].y}`;\n    forPoints(points, ({curr, next: getNext}) => {\n        const next = getNext();\n        const currControl = expandHandle(curr, curr.handleOut);\n        const nextControl = expandHandle(next, next.handleIn);\n        path += `C${currControl.x},${currControl.y},${nextControl.x},${nextControl.y},${next.x},${next.y}`;\n    });\n    return path;\n};\n\n// Renders the input points to an editable data structure which can be rendered to svg.\nexport const renderEditable = (points: Point[], options: RenderOptions): XmlElement => {\n    const stroke = options.stroke || (options.guides ? \"black\" : \"none\");\n    const strokeWidth = options.strokeWidth || (options.guides ? 1 : 0);\n\n    const xmlRoot = new XmlElement(\"svg\");\n    xmlRoot.attributes.width = options.width;\n    xmlRoot.attributes.height = options.height;\n    xmlRoot.attributes.viewBox = `0 0 ${options.width} ${options.height}`;\n    xmlRoot.attributes.xmlns = \"http://www.w3.org/2000/svg\";\n\n    const xmlContentGroup = new XmlElement(\"g\");\n    xmlContentGroup.attributes.transform = options.transform || \"\";\n\n    const xmlBlobPath = new XmlElement(\"path\");\n    xmlBlobPath.attributes.stroke = stroke;\n    xmlBlobPath.attributes[\"stroke-width\"] = strokeWidth;\n    xmlBlobPath.attributes.fill = options.fill || \"none\";\n    xmlBlobPath.attributes.d = renderPath(points);\n\n    xmlContentGroup.children.push(xmlBlobPath);\n    xmlRoot.children.push(xmlContentGroup);\n\n    // Render guides if configured to do so.\n    if (options.guides) {\n        const color = options.stroke || \"black\";\n        const size = options.strokeWidth || 1;\n\n        // Bounding box.\n        if (options.boundingBox) {\n            const xmlBoundingRect = new XmlElement(\"rect\");\n            xmlBoundingRect.attributes.x = 0;\n            xmlBoundingRect.attributes.y = 0;\n            xmlBoundingRect.attributes.width = options.width;\n            xmlBoundingRect.attributes.height = options.height;\n            xmlBoundingRect.attributes.fill = \"none\";\n            xmlBoundingRect.attributes.stroke = color;\n            xmlBoundingRect.attributes[\"stroke-width\"] = 2 * size;\n            xmlBoundingRect.attributes[\"stroke-dasharray\"] = 2 * size;\n            xmlContentGroup.children.push(xmlBoundingRect);\n        }\n\n        // Points and handles.\n        forPoints(points, ({curr, next: getNext}) => {\n            const next = getNext();\n            const currControl = expandHandle(curr, curr.handleOut);\n            const nextControl = expandHandle(next, next.handleIn);\n\n            const xmlOutgoingHandleLine = new XmlElement(\"line\");\n            xmlOutgoingHandleLine.attributes.x1 = curr.x;\n            xmlOutgoingHandleLine.attributes.y1 = curr.y;\n            xmlOutgoingHandleLine.attributes.x2 = currControl.x;\n            xmlOutgoingHandleLine.attributes.y2 = currControl.y;\n            xmlOutgoingHandleLine.attributes[\"stroke-width\"] = size;\n            xmlOutgoingHandleLine.attributes.stroke = color;\n\n            const xmlIncomingHandleLine = new XmlElement(\"line\");\n            xmlIncomingHandleLine.attributes.x1 = next.x;\n            xmlIncomingHandleLine.attributes.y1 = next.y;\n            xmlIncomingHandleLine.attributes.x2 = nextControl.x;\n            xmlIncomingHandleLine.attributes.y2 = nextControl.y;\n            xmlIncomingHandleLine.attributes[\"stroke-width\"] = size;\n            xmlIncomingHandleLine.attributes.stroke = color;\n            xmlIncomingHandleLine.attributes[\"stroke-dasharray\"] = 2 * size;\n\n            const xmlOutgoingHandleCircle = new XmlElement(\"circle\");\n            xmlOutgoingHandleCircle.attributes.cx = currControl.x;\n            xmlOutgoingHandleCircle.attributes.cy = currControl.y;\n            xmlOutgoingHandleCircle.attributes.r = size;\n            xmlOutgoingHandleCircle.attributes.fill = color;\n\n            const xmlIncomingHandleCircle = new XmlElement(\"circle\");\n            xmlIncomingHandleCircle.attributes.cx = nextControl.x;\n            xmlIncomingHandleCircle.attributes.cy = nextControl.y;\n            xmlIncomingHandleCircle.attributes.r = size;\n            xmlIncomingHandleCircle.attributes.fill = color;\n\n            const xmlPointCircle = new XmlElement(\"circle\");\n            xmlPointCircle.attributes.cx = curr.x;\n            xmlPointCircle.attributes.cy = curr.y;\n            xmlPointCircle.attributes.r = 2 * size;\n            xmlPointCircle.attributes.fill = color;\n\n            xmlContentGroup.children.push(xmlOutgoingHandleLine);\n            xmlContentGroup.children.push(xmlIncomingHandleLine);\n            xmlContentGroup.children.push(xmlOutgoingHandleCircle);\n            xmlContentGroup.children.push(xmlIncomingHandleCircle);\n            xmlContentGroup.children.push(xmlPointCircle);\n        });\n    }\n\n    return xmlRoot;\n};\n\n// Structured element with tag, attributes and children.\nexport class XmlElement {\n    public attributes: Record<string, string | number> = {};\n    public children: any[] = [];\n\n    public constructor(public tag: string) {}\n\n    public render(): string {\n        const attributes = this.renderAttributes();\n        const content = this.renderChildren();\n        if (content === \"\") {\n            return `<${this.tag}${attributes}/>`;\n        }\n        return `<${this.tag}${attributes}>${content}</${this.tag}>`;\n    }\n\n    private renderAttributes(): string {\n        const attributes = Object.keys(this.attributes);\n        if (attributes.length === 0) return \"\";\n        let out = \"\";\n        for (const attribute of attributes) {\n            out += ` ${attribute}=\"${this.attributes[attribute]}\"`;\n        }\n        return out;\n    }\n\n    private renderChildren(): string {\n        let out = \"\";\n        for (const child of this.children) {\n            out += child.render();\n        }\n        return out;\n    }\n}\n"
  },
  {
    "path": "internal/types.ts",
    "content": "// Position in a coordinate system with an origin in the top left corner.\nexport interface Coord {\n    x: number;\n    y: number;\n}\n\nexport interface Handle {\n    // Angle in radians relative to the 3:00 position going clockwise.\n    angle: number;\n    // Length of the handle.\n    length: number;\n}\n\nexport interface Point extends Coord {\n    // Cubic bezier handles.\n    handleIn: Handle;\n    handleOut: Handle;\n}\n"
  },
  {
    "path": "internal/util.ts",
    "content": "import {Coord, Handle, Point} from \"./types\";\n\nexport const copyPoint = (p: Point): Point => ({\n    x: p.x,\n    y: p.y,\n    handleIn: {...p.handleIn},\n    handleOut: {...p.handleOut},\n});\n\nexport interface PointIteratorArgs {\n    curr: Point;\n    index: number;\n    sibling: (pos: number) => Point;\n    prev: () => Point;\n    next: () => Point;\n}\n\nexport const coordPoint = (coord: Coord): Point => {\n    return {\n        ...coord,\n        handleIn: {angle: 0, length: 0},\n        handleOut: {angle: 0, length: 0},\n    };\n};\n\nexport const forPoints = (points: Point[], callback: (args: PointIteratorArgs) => void) => {\n    for (let i = 0; i < points.length; i++) {\n        const sibling = (pos: number) => copyPoint(points[mod(pos, points.length)]);\n        callback({\n            curr: copyPoint(points[i]),\n            index: i,\n            sibling,\n            prev: () => sibling(i - 1),\n            next: () => sibling(i + 1),\n        });\n    }\n};\n\nexport const mapPoints = (\n    points: Point[],\n    callback: (args: PointIteratorArgs) => Point,\n): Point[] => {\n    const out: Point[] = [];\n    forPoints(points, (args) => {\n        out.push(callback(args));\n    });\n    return out;\n};\n\nexport const coordEqual = (a: Coord, b: Coord): boolean => {\n    return a.x === b.x && a.y === b.y;\n};\n\nexport const angleOf = (a: Coord, b: Coord): number => {\n    const dx = b.x - a.x;\n    const dy = -b.y + a.y;\n    const angle = Math.atan2(dy, dx);\n    if (angle < 0) {\n        return Math.abs(angle);\n    } else {\n        return 2 * Math.PI - angle;\n    }\n};\n\nexport const expandHandle = (point: Coord, handle: Handle): Coord => ({\n    x: point.x + handle.length * Math.cos(handle.angle),\n    y: point.y + handle.length * Math.sin(handle.angle),\n});\n\nconst collapseHandle = (point: Coord, handle: Coord): Handle => ({\n    angle: angleOf(point, handle),\n    length: Math.sqrt((handle.x - point.x) ** 2 + (handle.y - point.y) ** 2),\n});\n\nexport const length = (a: Point, b: Point): number => {\n    const aHandle = expandHandle(a, a.handleOut);\n    const bHandle = expandHandle(b, b.handleIn);\n    const ab = distance(a, b);\n    const abHandle = distance(aHandle, bHandle);\n    return (ab + abHandle + a.handleOut.length + b.handleIn.length) / 2;\n};\n\nexport const reverse = (points: Point[]): Point[] => {\n    return mapPoints(points, ({index, sibling}) => {\n        const point = sibling(points.length - index - 1);\n        point.handleIn.angle += Math.PI;\n        point.handleOut.angle += Math.PI;\n        return point;\n    });\n};\n\nexport const shift = (offset: number, points: Point[]): Point[] => {\n    return mapPoints(points, ({index, sibling}) => {\n        return sibling(index + offset);\n    });\n};\n\n// Add a control point to the curve between a and b.\n// Percentage [0, 1] from a to b.\n// a: original first point.\n// b: original last point.\n// c: new first point.\n// d: new added point.\n// e: new last point.\n// f: split point between a and b's handles.\n// g: split point between c's handle and f.\n// h: split point between e's handle and f.\nexport const insertAt = (percentage: number, a: Point, b: Point): [Point, Point, Point] => {\n    const c = copyPoint(a);\n    c.handleOut.length *= percentage;\n\n    const e = copyPoint(b);\n    e.handleIn.length *= 1 - percentage;\n\n    const aHandle = expandHandle(a, a.handleOut);\n    const bHandle = expandHandle(b, b.handleIn);\n    const cHandle = expandHandle(c, c.handleOut);\n    const eHandle = expandHandle(e, e.handleIn);\n    const f = splitLine(percentage, aHandle, bHandle);\n    const g = splitLine(percentage, cHandle, f);\n    const h = splitLine(1 - percentage, eHandle, f);\n    const dCoord = splitLine(percentage, g, h);\n\n    const d: Point = {\n        x: dCoord.x,\n        y: dCoord.y,\n        handleIn: collapseHandle(dCoord, g),\n        handleOut: collapseHandle(dCoord, h),\n    };\n    return [c, d, e];\n};\n\nexport const insertCount = (count: number, a: Point, b: Point): Point[] => {\n    if (count < 2) return [a, b];\n    const percentage = 1 / count;\n    const [c, d, e] = insertAt(percentage, a, b);\n    if (count === 2) return [c, d, e];\n    return [c, ...insertCount(count - 1, d, e)];\n};\n\n// Smooths out the path made up of the given points.\n// Existing handles are ignored.\nexport const smooth = (points: Point[], strength: number): Point[] => {\n    return mapPoints(points, ({curr, next, prev}) => {\n        const angle = angleOf(prev(), next());\n        return {\n            x: curr.x,\n            y: curr.y,\n            handleIn: {\n                angle: angle + Math.PI,\n                length: strength * distance(curr, prev()),\n            },\n            handleOut: {\n                angle,\n                length: strength * distance(curr, next()),\n            },\n        };\n    });\n};\n\n// Modulo operation that always produces a positive result.\n// https://stackoverflow.com/q/4467539/3053361\nexport const mod = (a: number, n: number): number => {\n    return ((a % n) + n) % n;\n};\n\n// Converts degrees to radians.\nexport const rad = (deg: number) => {\n    return (deg / 360) * 2 * Math.PI;\n};\n\n// Converts radians to degrees.\nexport const deg = (rad: number) => {\n    return (((rad / Math.PI) * 1) / 2) * 360;\n};\n\n// Calculates distance between two points.\nexport const distance = (a: Coord, b: Coord): number => {\n    return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);\n};\n\n// Calculates the angle of the line from a to b in degrees.\nexport const angle = (a: Coord, b: Coord): number => {\n    return deg(Math.atan2(b.y - a.y, b.x - a.x));\n};\n\nexport const split = (percentage: number, a: number, b: number): number => {\n    return a + percentage * (b - a);\n};\n\nexport const splitLine = (percentage: number, a: Coord, b: Coord): Coord => {\n    return {\n        x: split(percentage, a.x, b.x),\n        y: split(percentage, a.y, b.y),\n    };\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"blobs\",\n  \"version\": \"2.3.0\",\n  \"description\": \"Random blob generation and animation\",\n  \"author\": \"g-harel\",\n  \"license\": \"MIT\",\n  \"main\": \"index.js\",\n  \"module\": \"index.module.js\",\n  \"types\": \"index.d.ts\",\n  \"scripts\": {\n    \"prepack\": \"npm run build\",\n    \"postpublish\": \"npm run clean\",\n    \"build\": \"npm run clean && rollup -c rollup.config.mjs\",\n    \"clean\": \"trash '**/*.js' '**/*.js.map' '**/*.d.ts' '!**/node_modules/**/*' '!rollup.config.mjs'\",\n    \"fmt\": \"prettier --list-different --write --ignore-path .gitignore '**/*.{js,ts,md,html}' '!index.html'\",\n    \"demo:dev\": \"parcel demo/index.html --open\",\n    \"demo:build\": \"parcel build demo/index.html && move-file dist/index.html index.html\",\n    \"test\": \"jest\",\n    \"test:playground\": \"parcel internal/animate/testing/index.html --open\"\n  },\n  \"dependencies\": {\n    \"simplex-noise\": \"^4.0.1\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"@types/jest\": \"25.1.4\",\n    \"jest\": \"29.5.0\",\n    \"jest-canvas-mock\": \"2.5.0\",\n    \"move-file-cli\": \"2.0.0\",\n    \"parcel\": \"1.12.3\",\n    \"parcel-plugin-inliner\": \"1.0.14\",\n    \"path2d-polyfill\": \"^2.0.1\",\n    \"prettier\": \"2.0.2\",\n    \"rollup\": \"3.8.1\",\n    \"rollup-plugin-copy\": \"3.4.0\",\n    \"rollup-plugin-typescript2\": \"0.34.1\",\n    \"rollup-plugin-uglify\": \"6.0.1\",\n    \"trash-cli\": \"3.0.0\",\n    \"ts-jest\": \"29.1.0\",\n    \"tslib\": \"2.4.1\",\n    \"typescript\": \"4.9.4\"\n  },\n  \"homepage\": \"https://blobs.dev\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/g-harel/blobs\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/g-harel/blobs/issues\"\n  },\n  \"keywords\": [\n    \"random\",\n    \"blob\",\n    \"svg\",\n    \"path\",\n    \"canvas\",\n    \"animation\"\n  ],\n  \"prettier\": {\n    \"tabWidth\": 4,\n    \"printWidth\": 100,\n    \"trailingComma\": \"all\",\n    \"bracketSpacing\": false,\n    \"arrowParens\": \"always\"\n  },\n  \"jest\": {\n    \"preset\": \"ts-jest\",\n    \"setupFiles\": [\n      \"jest-canvas-mock\"\n    ]\n  }\n}\n"
  },
  {
    "path": "public/__snapshots__/legacy.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`fill 1`] = `\"<svg width=\"109\" height=\"109\" viewBox=\"0 0 109 109\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"rotate(29.498919679317623,54.5,54.5)\"><path stroke=\"none\" stroke-width=\"0\" fill=\"red\" d=\"M54.5,14.904644499132331C75.95401897738759,14.90464449913233,92.58177480526493,33.045981022612416,92.58177480526493,54.5C92.58177480526493,75.3898565168627,75.3898565168627,92.06532466605346,54.5,92.06532466605346C35.140079294728,92.06532466605346,22.151190205921836,73.859920705272,22.15119020592184,54.50000000000001C22.15119020592184,34.532642771227444,34.532642771227444,14.90464449913233,54.5,14.904644499132331\"/></g></svg>\"`;\n\nexports[`guides 1`] = `\"<svg width=\"781\" height=\"781\" viewBox=\"0 0 781 781\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"rotate(3.442489226513049,390.5,390.5)\"><path stroke=\"black\" stroke-width=\"1\" fill=\"yellow\" d=\"M390.5,126.33058701145353C422.7599822291789,129.8894079821013,457.1517369455021,130.12745273744568,484.7560168794348,147.19717416039634C512.0337328801013,164.0649575005215,519.8607664658696,201.0400941880963,544.5059054044544,221.56356932561312C570.4261452389635,243.14889692825656,611.7577679542321,243.3499527852702,632.3240995385551,270.0859062179718C652.5414957686334,296.3682484004677,656.0291303230811,332.78264002578373,655.5644381263628,365.9381626741163C655.1113638087595,398.264756287615,638.8841165811418,427.5606398067945,629.6826289176689,458.55332532722855C620.5587046845698,489.2847597224773,615.7027870312934,520.9907034734381,601.0633639288316,549.5100713596294C585.2723467720305,580.2728838311288,570.3294797379046,615.2818420679225,540.3690947196874,632.5468987884551C510.32201395764275,649.8619150450116,472.07720239655697,638.2572446489619,437.8101304629824,643.5868442009801C404.43907720753396,648.777084873582,373.138427800321,665.5412734118242,339.41097189375944,663.8021610840965C304.54154473726527,662.0041644964895,267.47738691065854,655.071759972366,240.1072970361148,633.3925551389201C212.71820404244946,611.6982983815018,203.52330653817333,574.7971987713082,187.7077458632539,543.6416016528067C173.62525299120972,515.9000322349515,160.14941679690554,488.38496713511927,151.34885888571608,458.54436627473683C142.50320157704357,428.55084407312614,137.3614490164037,398.0976221087325,135.5767806585398,366.87788498406366C133.6378016292758,332.9587451522112,121.30298105955639,294.07541490260604,140.3383533379282,265.9343010505837C160.16298030584315,236.62638542805374,209.36566680771864,244.31364742916202,234.98836978172793,219.91186788626771C261.3342517552232,194.82136886934256,256.65497608331873,144.23139189508154,287.8278664113615,125.47267268305961C317.404723276248,107.67438874903971,356.1890316796786,122.54550763000411,390.5,126.33058701145353\"/><line x1=\"390.5\" y1=\"126.33058701145353\" x2=\"422.7599822291789\" y2=\"129.8894079821013\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"484.7560168794348\" y1=\"147.19717416039634\" x2=\"457.1517369455021\" y2=\"130.12745273744568\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"422.7599822291789\" cy=\"129.8894079821013\" r=\"1\" fill=\"black\"/><circle cx=\"457.1517369455021\" cy=\"130.12745273744568\" r=\"1\" fill=\"black\"/><circle cx=\"390.5\" cy=\"126.33058701145353\" r=\"2\" fill=\"black\"/><line x1=\"484.7560168794348\" y1=\"147.19717416039634\" x2=\"512.0337328801013\" y2=\"164.0649575005215\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"544.5059054044544\" y1=\"221.56356932561312\" x2=\"519.8607664658696\" y2=\"201.0400941880963\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"512.0337328801013\" cy=\"164.0649575005215\" r=\"1\" fill=\"black\"/><circle cx=\"519.8607664658696\" cy=\"201.0400941880963\" r=\"1\" fill=\"black\"/><circle cx=\"484.7560168794348\" cy=\"147.19717416039634\" r=\"2\" fill=\"black\"/><line x1=\"544.5059054044544\" y1=\"221.56356932561312\" x2=\"570.4261452389635\" y2=\"243.14889692825656\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"632.3240995385551\" y1=\"270.0859062179718\" x2=\"611.7577679542321\" y2=\"243.3499527852702\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"570.4261452389635\" cy=\"243.14889692825656\" r=\"1\" fill=\"black\"/><circle cx=\"611.7577679542321\" cy=\"243.3499527852702\" r=\"1\" fill=\"black\"/><circle cx=\"544.5059054044544\" cy=\"221.56356932561312\" r=\"2\" fill=\"black\"/><line x1=\"632.3240995385551\" y1=\"270.0859062179718\" x2=\"652.5414957686334\" y2=\"296.3682484004677\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"655.5644381263628\" y1=\"365.9381626741163\" x2=\"656.0291303230811\" y2=\"332.78264002578373\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"652.5414957686334\" cy=\"296.3682484004677\" r=\"1\" fill=\"black\"/><circle cx=\"656.0291303230811\" cy=\"332.78264002578373\" r=\"1\" fill=\"black\"/><circle cx=\"632.3240995385551\" cy=\"270.0859062179718\" r=\"2\" fill=\"black\"/><line x1=\"655.5644381263628\" y1=\"365.9381626741163\" x2=\"655.1113638087595\" y2=\"398.264756287615\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"629.6826289176689\" y1=\"458.55332532722855\" x2=\"638.8841165811418\" y2=\"427.5606398067945\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"655.1113638087595\" cy=\"398.264756287615\" r=\"1\" fill=\"black\"/><circle cx=\"638.8841165811418\" cy=\"427.5606398067945\" r=\"1\" fill=\"black\"/><circle cx=\"655.5644381263628\" cy=\"365.9381626741163\" r=\"2\" fill=\"black\"/><line x1=\"629.6826289176689\" y1=\"458.55332532722855\" x2=\"620.5587046845698\" y2=\"489.2847597224773\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"601.0633639288316\" y1=\"549.5100713596294\" x2=\"615.7027870312934\" y2=\"520.9907034734381\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"620.5587046845698\" cy=\"489.2847597224773\" r=\"1\" fill=\"black\"/><circle cx=\"615.7027870312934\" cy=\"520.9907034734381\" r=\"1\" fill=\"black\"/><circle cx=\"629.6826289176689\" cy=\"458.55332532722855\" r=\"2\" fill=\"black\"/><line x1=\"601.0633639288316\" y1=\"549.5100713596294\" x2=\"585.2723467720305\" y2=\"580.2728838311288\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"540.3690947196874\" y1=\"632.5468987884551\" x2=\"570.3294797379046\" y2=\"615.2818420679225\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"585.2723467720305\" cy=\"580.2728838311288\" r=\"1\" fill=\"black\"/><circle cx=\"570.3294797379046\" cy=\"615.2818420679225\" r=\"1\" fill=\"black\"/><circle cx=\"601.0633639288316\" cy=\"549.5100713596294\" r=\"2\" fill=\"black\"/><line x1=\"540.3690947196874\" y1=\"632.5468987884551\" x2=\"510.32201395764275\" y2=\"649.8619150450116\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"437.8101304629824\" y1=\"643.5868442009801\" x2=\"472.07720239655697\" y2=\"638.2572446489619\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"510.32201395764275\" cy=\"649.8619150450116\" r=\"1\" fill=\"black\"/><circle cx=\"472.07720239655697\" cy=\"638.2572446489619\" r=\"1\" fill=\"black\"/><circle cx=\"540.3690947196874\" cy=\"632.5468987884551\" r=\"2\" fill=\"black\"/><line x1=\"437.8101304629824\" y1=\"643.5868442009801\" x2=\"404.43907720753396\" y2=\"648.777084873582\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"339.41097189375944\" y1=\"663.8021610840965\" x2=\"373.138427800321\" y2=\"665.5412734118242\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"404.43907720753396\" cy=\"648.777084873582\" r=\"1\" fill=\"black\"/><circle cx=\"373.138427800321\" cy=\"665.5412734118242\" r=\"1\" fill=\"black\"/><circle cx=\"437.8101304629824\" cy=\"643.5868442009801\" r=\"2\" fill=\"black\"/><line x1=\"339.41097189375944\" y1=\"663.8021610840965\" x2=\"304.54154473726527\" y2=\"662.0041644964895\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"240.1072970361148\" y1=\"633.3925551389201\" x2=\"267.47738691065854\" y2=\"655.071759972366\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"304.54154473726527\" cy=\"662.0041644964895\" r=\"1\" fill=\"black\"/><circle cx=\"267.47738691065854\" cy=\"655.071759972366\" r=\"1\" fill=\"black\"/><circle cx=\"339.41097189375944\" cy=\"663.8021610840965\" r=\"2\" fill=\"black\"/><line x1=\"240.1072970361148\" y1=\"633.3925551389201\" x2=\"212.71820404244946\" y2=\"611.6982983815018\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"187.7077458632539\" y1=\"543.6416016528067\" x2=\"203.52330653817333\" y2=\"574.7971987713082\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"212.71820404244946\" cy=\"611.6982983815018\" r=\"1\" fill=\"black\"/><circle cx=\"203.52330653817333\" cy=\"574.7971987713082\" r=\"1\" fill=\"black\"/><circle cx=\"240.1072970361148\" cy=\"633.3925551389201\" r=\"2\" fill=\"black\"/><line x1=\"187.7077458632539\" y1=\"543.6416016528067\" x2=\"173.62525299120972\" y2=\"515.9000322349515\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"151.34885888571608\" y1=\"458.54436627473683\" x2=\"160.14941679690554\" y2=\"488.38496713511927\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"173.62525299120972\" cy=\"515.9000322349515\" r=\"1\" fill=\"black\"/><circle cx=\"160.14941679690554\" cy=\"488.38496713511927\" r=\"1\" fill=\"black\"/><circle cx=\"187.7077458632539\" cy=\"543.6416016528067\" r=\"2\" fill=\"black\"/><line x1=\"151.34885888571608\" y1=\"458.54436627473683\" x2=\"142.50320157704357\" y2=\"428.55084407312614\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"135.5767806585398\" y1=\"366.87788498406366\" x2=\"137.3614490164037\" y2=\"398.0976221087325\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"142.50320157704357\" cy=\"428.55084407312614\" r=\"1\" fill=\"black\"/><circle cx=\"137.3614490164037\" cy=\"398.0976221087325\" r=\"1\" fill=\"black\"/><circle cx=\"151.34885888571608\" cy=\"458.54436627473683\" r=\"2\" fill=\"black\"/><line x1=\"135.5767806585398\" y1=\"366.87788498406366\" x2=\"133.6378016292758\" y2=\"332.9587451522112\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"140.3383533379282\" y1=\"265.9343010505837\" x2=\"121.30298105955639\" y2=\"294.07541490260604\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"133.6378016292758\" cy=\"332.9587451522112\" r=\"1\" fill=\"black\"/><circle cx=\"121.30298105955639\" cy=\"294.07541490260604\" r=\"1\" fill=\"black\"/><circle cx=\"135.5767806585398\" cy=\"366.87788498406366\" r=\"2\" fill=\"black\"/><line x1=\"140.3383533379282\" y1=\"265.9343010505837\" x2=\"160.16298030584315\" y2=\"236.62638542805374\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"234.98836978172793\" y1=\"219.91186788626771\" x2=\"209.36566680771864\" y2=\"244.31364742916202\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"160.16298030584315\" cy=\"236.62638542805374\" r=\"1\" fill=\"black\"/><circle cx=\"209.36566680771864\" cy=\"244.31364742916202\" r=\"1\" fill=\"black\"/><circle cx=\"140.3383533379282\" cy=\"265.9343010505837\" r=\"2\" fill=\"black\"/><line x1=\"234.98836978172793\" y1=\"219.91186788626771\" x2=\"261.3342517552232\" y2=\"194.82136886934256\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"287.8278664113615\" y1=\"125.47267268305961\" x2=\"256.65497608331873\" y2=\"144.23139189508154\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"261.3342517552232\" cy=\"194.82136886934256\" r=\"1\" fill=\"black\"/><circle cx=\"256.65497608331873\" cy=\"144.23139189508154\" r=\"1\" fill=\"black\"/><circle cx=\"234.98836978172793\" cy=\"219.91186788626771\" r=\"2\" fill=\"black\"/><line x1=\"287.8278664113615\" y1=\"125.47267268305961\" x2=\"317.404723276248\" y2=\"107.67438874903971\" stroke-width=\"1\" stroke=\"black\"/><line x1=\"390.5\" y1=\"126.33058701145353\" x2=\"356.1890316796786\" y2=\"122.54550763000411\" stroke-width=\"1\" stroke=\"black\" stroke-dasharray=\"2\"/><circle cx=\"317.404723276248\" cy=\"107.67438874903971\" r=\"1\" fill=\"black\"/><circle cx=\"356.1890316796786\" cy=\"122.54550763000411\" r=\"1\" fill=\"black\"/><circle cx=\"287.8278664113615\" cy=\"125.47267268305961\" r=\"2\" fill=\"black\"/></g></svg>\"`;\n\nexports[`stroke 1`] = `\"<svg width=\"226\" height=\"226\" viewBox=\"0 0 226 226\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"rotate(10.625043198466301,113,113)\"><path stroke=\"#ff00bb\" stroke-width=\"3.8\" fill=\"none\" d=\"M113,42.187082298945086C122.79474494878556,40.93994738588087,128.08514500519405,54.842942154431036,137.1295411257755,58.80416329407413C146.24165581029774,62.79504348485257,157.51842254183055,60.01704955511825,165.90945623567904,65.36011162379504C174.71687671447282,70.96831151907477,181.0880692974371,79.95796015403897,185.33243631773502,89.49776676385548C189.73428003886485,99.39152421352725,196.20569839281566,111.822474823727,190.74027198902974,121.17083183675264C184.275146635445,132.22912728784644,166.3394780066649,129.73602552824286,156.80578145715737,138.2912797163517C150.81862065087725,143.66397878400295,152.63580758814572,154.1776323719105,147.27490184117337,160.17535522011516C141.64626912802416,166.4726080764768,133.5690193595429,169.81059568769254,125.75894924668432,173.0261367912584C117.04502915031891,176.6138085839763,108.09560947846845,179.1793135116963,98.72369653099743,180.1647271523213C87.32619013379446,181.36312264914937,72.49281709265853,187.87055519411282,64.72599173408246,179.44347220588492C56.10054832930125,170.08478007279948,66.09720211216099,154.1178572037905,63.27873223580451,141.70658732810776C61.3893271033385,133.38649946758036,50.99025921153393,128.03545137040612,51.11446122271466,119.50443222719099C51.241275506902305,110.79398482176407,63.010835822466824,105.71867007151292,63.928797313315314,97.05579972925904C65.17258032291737,85.31812766621682,49.49602866888215,71.69698855511481,57.133958677847595,62.69799045462378C64.63143952592921,53.86446935255059,80.5830156751293,68.61242545584774,91.45947685068577,64.6191928775053C101.29841121040204,61.00688253251097,102.60284327876242,43.51092046027091,113,42.187082298945086\"/></g></svg>\"`;\n"
  },
  {
    "path": "public/animate.test.ts",
    "content": "import {CanvasKeyframe, canvasPath} from \"./animate\";\n\nconst genKeyframe = (): CanvasKeyframe => ({\n    duration: 1000 * Math.random(),\n    delay: 1000 * Math.random(),\n    timingFunction: \"linear\",\n    callback: () => {},\n    blobOptions: {\n        extraPoints: Math.floor(10 * Math.random()),\n        randomness: Math.floor(10 * Math.random()),\n        seed: Math.random(),\n        size: 100 + 200 * Math.random(),\n    },\n    canvasOptions: {\n        offsetX: 100 * Math.random(),\n        offsetY: 100 * Math.random(),\n    },\n});\n\ndescribe(\"animate\", () => {\n    describe(\"canvasPath\", () => {\n        describe(\"transition\", () => {\n            describe(\"keyframe\", () => {\n                it(\"should accept generated keyframe\", () => {\n                    const animation = canvasPath();\n                    const keyframe = genKeyframe();\n\n                    expect(() => animation.transition(keyframe)).not.toThrow();\n                });\n\n                it(\"should indicate the rejected frame index\", () => {\n                    const animation = canvasPath();\n                    const keyframes = [genKeyframe(), null as any, genKeyframe()];\n\n                    expect(() => animation.transition(...keyframes)).toThrow(/keyframe.*1/g);\n                });\n\n                interface TestCase {\n                    name: string;\n                    edit: (keyframe: CanvasKeyframe) => void;\n                    error?: RegExp;\n                }\n\n                const testCases: Array<TestCase> = [\n                    // duration\n                    {\n                        name: \"should accept valid duration\",\n                        edit: (keyframe) => (keyframe.duration = 100),\n                    },\n                    {\n                        name: \"should accept zero duration\",\n                        edit: (keyframe) => (keyframe.duration = 0),\n                    },\n                    {\n                        name: \"should reject undefined duration\",\n                        edit: (keyframe) => delete (keyframe as any).duration,\n                        error: /duration.*number.*undefined/g,\n                    },\n                    {\n                        name: \"should reject negative duration\",\n                        edit: (keyframe) => (keyframe.duration = -10),\n                        error: /duration.*invalid/g,\n                    },\n                    {\n                        name: \"should reject broken duration\",\n                        edit: (keyframe) => (keyframe.duration = NaN),\n                        error: /duration.*number.*NaN/g,\n                    },\n                    {\n                        name: \"should reject invalid duration\",\n                        edit: (keyframe) => (keyframe.duration = \"123\" as any),\n                        error: /duration.*number.*string/g,\n                    },\n                    // delay\n                    {\n                        name: \"should accept valid delay\",\n                        edit: (keyframe) => (keyframe.delay = 200),\n                    },\n                    {\n                        name: \"should accept zero delay\",\n                        edit: (keyframe) => (keyframe.delay = 0),\n                    },\n                    {\n                        name: \"should accept undefined delay\",\n                        edit: (keyframe) => delete keyframe.delay,\n                    },\n                    {\n                        name: \"should reject negative delay\",\n                        edit: (keyframe) => (keyframe.delay = -10),\n                        error: /delay.*invalid/g,\n                    },\n                    {\n                        name: \"should reject broken delay\",\n                        edit: (keyframe) => (keyframe.delay = NaN),\n                        error: /delay.*number.*NaN/g,\n                    },\n                    {\n                        name: \"should reject invalid delay\",\n                        edit: (keyframe) => (keyframe.delay = \"123\" as any),\n                        error: /delay.*number.*string/g,\n                    },\n                    // timingFunction\n                    {\n                        name: \"should accept known timingFunction\",\n                        edit: (keyframe) => (keyframe.timingFunction = \"ease\"),\n                    },\n                    {\n                        name: \"should accept undefined timingFunction\",\n                        edit: (keyframe) => delete keyframe.timingFunction,\n                    },\n                    {\n                        name: \"should reject invalid timingFunction\",\n                        edit: (keyframe) => (keyframe.timingFunction = (() => 0) as any),\n                        error: /timingFunction.*string.*function/g,\n                    },\n                    {\n                        name: \"should reject unknown timingFunction\",\n                        edit: (keyframe) => (keyframe.timingFunction = \"unknown\" as any),\n                        error: /timingFunction.*not recognized.*unknown/g,\n                    },\n                    // callback\n                    {\n                        name: \"should accept valid callback\",\n                        edit: (keyframe) => (keyframe.callback = () => console.log(\"test\")),\n                    },\n                    {\n                        name: \"should accept undefined callback\",\n                        edit: (keyframe) => delete keyframe.callback,\n                    },\n                    {\n                        name: \"should reject invalid callback\",\n                        edit: (keyframe) => (keyframe.callback = {} as any),\n                        error: /callback.*function.*object/g,\n                    },\n                    // blobOptions\n                    {\n                        name: \"should reject undefined blobOptions\",\n                        edit: (keyframe) => delete (keyframe as any).blobOptions,\n                        error: /blobOptions.*object.*undefined/g,\n                    },\n                    {\n                        name: \"should reject invalid blobOptions\",\n                        edit: (keyframe) => (keyframe.blobOptions = null as any),\n                        error: /blobOptions.*object.*null/g,\n                    },\n                    // blobOptions.seed\n                    {\n                        name: \"should accept number blobOptions seed\",\n                        edit: (keyframe) => (keyframe.blobOptions.seed = 123),\n                    },\n                    {\n                        name: \"should accept string blobOptions seed\",\n                        edit: (keyframe) => (keyframe.blobOptions.seed = \"test\"),\n                    },\n                    {\n                        name: \"should reject undefined blobOptions seed\",\n                        edit: (keyframe) => delete (keyframe as any).blobOptions.seed,\n                        error: /seed.*string.*number.*undefined/g,\n                    },\n                    {\n                        name: \"should reject broken blobOptions seed\",\n                        edit: (keyframe) => (keyframe.blobOptions.seed = NaN),\n                        error: /seed.*string.*number.*NaN/g,\n                    },\n                    // blobOptions.extraPoints\n                    {\n                        name: \"should accept valid blobOptions extraPoints\",\n                        edit: (keyframe) => (keyframe.blobOptions.extraPoints = 4),\n                    },\n                    {\n                        name: \"should reject undefined blobOptions extraPoints\",\n                        edit: (keyframe) => delete (keyframe as any).blobOptions.extraPoints,\n                        error: /blobOptions.*extraPoints.*number.*undefined/g,\n                    },\n                    {\n                        name: \"should reject broken blobOptions extraPoints\",\n                        edit: (keyframe) => (keyframe.blobOptions.extraPoints = NaN),\n                        error: /blobOptions.*extraPoints.*number.*NaN/g,\n                    },\n                    {\n                        name: \"should reject negative blobOptions extraPoints\",\n                        edit: (keyframe) => (keyframe.blobOptions.extraPoints = -2),\n                        error: /blobOptions.*extraPoints.*invalid/g,\n                    },\n                    // blobOptions.randomness\n                    {\n                        name: \"should accept valid blobOptions randomness\",\n                        edit: (keyframe) => (keyframe.blobOptions.randomness = 3),\n                    },\n                    {\n                        name: \"should reject undefined blobOptions randomness\",\n                        edit: (keyframe) => delete (keyframe as any).blobOptions.randomness,\n                        error: /blobOptions.*randomness.*number.*undefined/g,\n                    },\n                    {\n                        name: \"should reject broken blobOptions randomness\",\n                        edit: (keyframe) => (keyframe.blobOptions.randomness = NaN),\n                        error: /blobOptions.*randomness.*number.*NaN/g,\n                    },\n                    {\n                        name: \"should reject negative blobOptions randomness\",\n                        edit: (keyframe) => (keyframe.blobOptions.randomness = -10),\n                        error: /blobOptions.*randomness.*invalid/g,\n                    },\n                    // blobOptions.size\n                    {\n                        name: \"should accept valid blobOptions size\",\n                        edit: (keyframe) => (keyframe.blobOptions.size = 40),\n                    },\n                    {\n                        name: \"should reject undefined blobOptions size\",\n                        edit: (keyframe) => delete (keyframe as any).blobOptions.size,\n                        error: /blobOptions.*size.*number.*undefined/g,\n                    },\n                    {\n                        name: \"should reject broken blobOptions size\",\n                        edit: (keyframe) => (keyframe.blobOptions.size = NaN),\n                        error: /blobOptions.*size.*number.*NaN/g,\n                    },\n                    {\n                        name: \"should reject negative blobOptions size\",\n                        edit: (keyframe) => (keyframe.blobOptions.size = -1),\n                        error: /blobOptions.*size.*invalid/g,\n                    },\n                    // canvasOptions\n                    {\n                        name: \"should accept empty canvasOptions\",\n                        edit: (keyframe) => (keyframe.canvasOptions = {}),\n                    },\n                    {\n                        name: \"should accept undefined canvasOptions\",\n                        edit: (keyframe) => delete keyframe.canvasOptions,\n                    },\n                    {\n                        name: \"should reject invalid canvasOptions\",\n                        edit: (keyframe) => (keyframe.canvasOptions = null as any),\n                        error: /canvasOptions.*object.*null/g,\n                    },\n                    // canvasOptions.offsetX\n                    {\n                        name: \"should accept valid canvasOptions offsetX\",\n                        edit: (keyframe) => (keyframe.canvasOptions = {offsetX: 100}),\n                    },\n                    {\n                        name: \"should accept undefined canvasOptions offsetX\",\n                        edit: (keyframe) => delete keyframe.canvasOptions?.offsetX,\n                    },\n                    {\n                        name: \"should reject broken canvasOptions offsetX\",\n                        edit: (keyframe) => (keyframe.canvasOptions = {offsetX: NaN}),\n                        error: /canvasOptions.*offsetX.*number.*NaN/g,\n                    },\n                    // canvasOptions.offsetY\n                    {\n                        name: \"should accept valid canvasOptions offsetY\",\n                        edit: (keyframe) => (keyframe.canvasOptions = {offsetY: 222}),\n                    },\n                    {\n                        name: \"should accept undefined canvasOptions offsetY\",\n                        edit: (keyframe) => delete keyframe.canvasOptions?.offsetY,\n                    },\n                    {\n                        name: \"should reject broken canvasOptions offsetY\",\n                        edit: (keyframe) => (keyframe.canvasOptions = {offsetY: NaN}),\n                        error: /canvasOptions.*offsetY.*number.*NaN/g,\n                    },\n                ];\n\n                // Run all test cases with a configurable amount of keyframes\n                // and index of the keyframe being edited for the tests.\n                const runSuite = (keyframeCount: number, editIndex: number) => {\n                    for (const testCase of testCases) {\n                        it(testCase.name, () => {\n                            // Create blank animation.\n                            const animation = canvasPath();\n\n                            // Create keyframes to call transition with.\n                            const keyframes: CanvasKeyframe[] = [];\n                            for (let i = 0; i < keyframeCount; i++) {\n                                keyframes.push(genKeyframe());\n                            }\n\n                            // Modify selected keyframe.\n                            testCase.edit(keyframes[editIndex]);\n\n                            if (testCase.error) {\n                                // Copy regexp because they are stateful.\n                                const pattern = new RegExp(testCase.error);\n                                expect(() => animation.transition(...keyframes)).toThrow(pattern);\n                            } else {\n                                expect(() => animation.transition(...keyframes)).not.toThrow();\n                            }\n                        });\n                    }\n                };\n\n                // Run all cases when given a single test frame and asserting on it.\n                describe(\"first\", () => runSuite(1, 0));\n\n                // Run all cases when given more than one frame, asserting on last one.\n                const lastLength = 2 + Math.floor(4 * Math.random());\n                describe(\"last\", () => runSuite(lastLength, lastLength - 1));\n\n                // Run all cases when given more than one frame, asserting on a random one.\n                const nthLength = 2 + Math.floor(16 * Math.random());\n                const nthIndex = Math.floor(nthLength * Math.random());\n                describe(`nth (${nthIndex + 1}/${nthLength})`, () => runSuite(nthLength, nthIndex));\n            });\n        });\n    });\n});\n"
  },
  {
    "path": "public/animate.ts",
    "content": "import {Point} from \"../internal/types\";\nimport {renderPath2D} from \"../internal/render/canvas\";\nimport {genFromOptions} from \"../internal/gen\";\nimport {mapPoints} from \"../internal/util\";\nimport {statefulAnimationGenerator} from \"../internal/animate/state\";\nimport {\n    checkBlobOptions,\n    checkCanvasOptions,\n    checkKeyframeOptions,\n    checkPoints,\n} from \"../internal/check\";\nimport {BlobOptions, CanvasOptions} from \"./blobs\";\nimport {noise} from \"../internal/rand\";\nimport {interpolateBetween} from \"../internal/animate/interpolate\";\nimport {prepare} from \"../internal/animate/prepare\";\n\ninterface Keyframe {\n    // Duration of the keyframe animation in milliseconds.\n    duration: number;\n    // Delay before animation begins in milliseconds.\n    // Default: 0.\n    delay?: number;\n    // Controls the speed of the animation over time.\n    // Default: \"linear\".\n    timingFunction?:\n        | \"linear\"\n        | \"easeEnd\"\n        | \"easeStart\"\n        | \"ease\"\n        | \"elasticEnd0\"\n        | \"elasticEnd1\"\n        | \"elasticEnd2\"\n        | \"elasticEnd3\";\n    // Called after keyframe end-state is reached or passed.\n    // Called exactly once when the keyframe end-state is rendered.\n    // Not called if the keyframe is preempted by a new transition.\n    callback?: () => void;\n    // Standard options, refer to \"blobs/v2\" documentation.\n    canvasOptions?: {\n        offsetX?: number;\n        offsetY?: number;\n    };\n}\n\nexport interface CanvasKeyframe extends Keyframe {\n    // Standard options, refer to \"blobs/v2\" documentation.\n    blobOptions: {\n        seed: number | string;\n        randomness: number;\n        extraPoints: number;\n        size: number;\n    };\n}\n\nexport interface CanvasCustomKeyframe extends Keyframe {\n    // List of point coordinates that produce a single, closed shape.\n    points: Point[];\n}\n\nexport interface Animation {\n    // Renders the current state of the animation.\n    renderFrame: () => Path2D;\n    // Renders the current state of the animation as points.\n    renderPoints: () => Point[];\n    // Immediately begin animating through the given keyframes.\n    // Non-rendered keyframes from previous transitions are cancelled.\n    transition: (...keyframes: (CanvasKeyframe | CanvasCustomKeyframe)[]) => void;\n    // Resume a paused animation. Has no effect if already playing.\n    play: () => void;\n    // Pause a playing animation. Has no effect if already paused.\n    pause: () => void;\n    // Toggle between playing and pausing the animation.\n    playPause: () => void;\n}\n\n// Function that returns the current timestamp. This value will be used for all\n// duration/delay values and will be used to interpolate between keyframes. It\n// must produce values increasing in size.\n// Default: `Date.now`.\nexport interface TimestampProvider {\n    (): number;\n}\n\nexport interface WiggleOptions {\n    // Speed of the wiggle movement. Higher is faster.\n    speed: number;\n    // Length of the transition from the current state to the wiggle blob.\n    // Default: 0\n    initialTransition?: number;\n}\n\nconst canvasPointGenerator = (keyframe: CanvasKeyframe | CanvasCustomKeyframe): Point[] => {\n    let points: Point[];\n    if (\"points\" in keyframe) {\n        points = keyframe.points;\n    } else {\n        points = genFromOptions(keyframe.blobOptions);\n    }\n    return mapPoints(points, ({curr}) => {\n        curr.x += keyframe?.canvasOptions?.offsetX || 0;\n        curr.y += keyframe?.canvasOptions?.offsetY || 0;\n        return curr;\n    });\n};\n\nconst canvasKeyframeChecker = (keyframe: CanvasKeyframe | CanvasCustomKeyframe, index: number) => {\n    try {\n        if (\"points\" in keyframe) return checkPoints(keyframe.points);\n        checkBlobOptions(keyframe.blobOptions);\n        checkCanvasOptions(keyframe.canvasOptions);\n        checkKeyframeOptions(keyframe);\n    } catch (e) {\n        throw `(blobs2): keyframe ${index}: ${e}`;\n    }\n};\n\nexport const canvasPath = (timestampProvider?: () => number): Animation => {\n    let actualTimestampProvider = Date.now;\n\n    // Make sure timestamps are always increasing.\n    if (timestampProvider !== undefined) {\n        let lastTimestamp = 0;\n        actualTimestampProvider = () => {\n            const currentTimestamp = timestampProvider();\n            if (currentTimestamp < lastTimestamp) {\n                throw `timestamp provider generated decreasing value: ${lastTimestamp} then ${currentTimestamp}.`;\n            }\n            lastTimestamp = currentTimestamp;\n            return currentTimestamp;\n        };\n    }\n\n    return statefulAnimationGenerator(\n        canvasPointGenerator,\n        renderPath2D,\n        canvasKeyframeChecker,\n    )(actualTimestampProvider);\n};\n\nexport const wigglePreset = (\n    animation: Animation,\n    blobOptions: BlobOptions,\n    canvasOptions: CanvasOptions,\n    wiggleOptions: WiggleOptions,\n) => {\n    // Interval at which a new sample is taken.\n    // Multiple of 16 to do work every N frames.\n    const intervalMs = 16 * 10;\n    const leapSize = 0.01 * wiggleOptions.speed;\n    const noiseField = noise(String(blobOptions.seed));\n\n    const transitionFrameCount = Math.min((wiggleOptions.initialTransition || 0) / intervalMs);\n    let transitionStartFrame = animation.renderPoints();\n\n    let count = 0;\n    const loopAnimation = () => {\n        count++;\n\n        // Constantly changing blob.\n        const noiseBlob = genFromOptions(blobOptions, (index) => {\n            return noiseField(leapSize * count, index);\n        });\n\n        if (count < transitionFrameCount) {\n            // Create intermediate frame between the current state and the\n            // moving noiseBlob target.\n            const [preparedStartPoints, preparedEndPoints] = prepare(\n                transitionStartFrame,\n                noiseBlob,\n                {\n                    rawAngles: true,\n                    divideRatio: 1,\n                },\n            );\n            const progress = Math.min(1, 2 / (transitionFrameCount - count));\n            const targetPoints = interpolateBetween(\n                progress,\n                preparedStartPoints,\n                preparedEndPoints,\n            );\n            transitionStartFrame = targetPoints;\n\n            animation.transition({\n                duration: intervalMs,\n                delay: 0,\n                timingFunction: \"linear\",\n                canvasOptions,\n                points: targetPoints,\n                callback: loopAnimation,\n            });\n        } else {\n            animation.transition({\n                duration: intervalMs,\n                delay: 0,\n                timingFunction: \"linear\",\n                canvasOptions,\n                points: noiseBlob,\n                callback: loopAnimation,\n            });\n        }\n    };\n    loopAnimation();\n};\n"
  },
  {
    "path": "public/blobs.test.ts",
    "content": "import {BlobOptions, CanvasOptions, canvasPath, svg, SvgOptions, svgPath} from \"./blobs\";\n\n// @ts-ignore\nimport {Path2D, polyfillPath2D} from \"path2d-polyfill\";\nglobal.Path2D = Path2D;\n\nconst genBlobOptions = (): BlobOptions => ({\n    extraPoints: Math.floor(10 * Math.random()),\n    randomness: Math.floor(10 * Math.random()),\n    seed: Math.random(),\n    size: 100 + 200 * Math.random(),\n});\n\nconst genSvgOptions = (): SvgOptions => ({\n    fill: String(Math.random()),\n    stroke: String(Math.random()),\n    strokeWidth: 4 * Math.random(),\n});\n\nconst genCanvasOptions = (): CanvasOptions => ({\n    offsetX: 100 * Math.random(),\n    offsetY: 100 * Math.random(),\n});\n\ninterface TestCase<T> {\n    name: string;\n    edit: (options: T) => void;\n    error?: RegExp;\n}\n\nconst runSuite = <T>(t: {\n    optionsGenerator: () => T;\n    functionBeingTested: (options: any) => void;\n}) => (testCases: TestCase<T>[]) => {\n    for (const testCase of testCases) {\n        it(testCase.name, () => {\n            const options = t.optionsGenerator();\n            testCase.edit(options);\n\n            if (testCase.error) {\n                // Copy regexp because they are stateful.\n                const pattern = new RegExp(testCase.error);\n                expect(() => t.functionBeingTested(options)).toThrow(pattern);\n            } else {\n                expect(() => t.functionBeingTested(options)).not.toThrow();\n            }\n        });\n    }\n};\n\nconst testBlobOptions = (functionBeingTested: (options: any) => void) => {\n    it(\"should accept generated blobOptions\", () => {\n        expect(() => svgPath(genBlobOptions())).not.toThrow();\n    });\n\n    it(\"should reject undefined blobOptions\", () => {\n        expect(() => svgPath(undefined as any)).toThrow(/blobOptions.*object.*undefined/g);\n    });\n\n    it(\"should reject invalid blobOptions\", () => {\n        expect(() => svgPath(null as any)).toThrow(/blobOptions.*object.*null/g);\n    });\n\n    runSuite<BlobOptions>({\n        functionBeingTested,\n        optionsGenerator: genBlobOptions,\n    })([\n        // seed\n        {\n            name: \"should accept number blobOptions seed\",\n            edit: (blobOptions) => (blobOptions.seed = 123),\n        },\n        {\n            name: \"should accept string blobOptions seed\",\n            edit: (blobOptions) => (blobOptions.seed = \"test\"),\n        },\n        {\n            name: \"should reject undefined blobOptions seed\",\n            edit: (blobOptions) => delete (blobOptions as any).seed,\n            error: /seed.*string.*number.*undefined/g,\n        },\n        {\n            name: \"should reject broken blobOptions seed\",\n            edit: (blobOptions) => (blobOptions.seed = NaN),\n            error: /seed.*string.*number.*NaN/g,\n        },\n        // extraPoints\n        {\n            name: \"should accept valid blobOptions extraPoints\",\n            edit: (blobOptions) => (blobOptions.extraPoints = 4),\n        },\n        {\n            name: \"should reject undefined blobOptions extraPoints\",\n            edit: (blobOptions) => delete (blobOptions as any).extraPoints,\n            error: /blobOptions.*extraPoints.*number.*undefined/g,\n        },\n        {\n            name: \"should reject broken blobOptions extraPoints\",\n            edit: (blobOptions) => (blobOptions.extraPoints = NaN),\n            error: /blobOptions.*extraPoints.*number.*NaN/g,\n        },\n        {\n            name: \"should reject negative blobOptions extraPoints\",\n            edit: (blobOptions) => (blobOptions.extraPoints = -2),\n            error: /blobOptions.*extraPoints.*invalid/g,\n        },\n        // randomness\n        {\n            name: \"should accept valid blobOptions randomness\",\n            edit: (blobOptions) => (blobOptions.randomness = 3),\n        },\n        {\n            name: \"should reject undefined blobOptions randomness\",\n            edit: (blobOptions) => delete (blobOptions as any).randomness,\n            error: /blobOptions.*randomness.*number.*undefined/g,\n        },\n        {\n            name: \"should reject broken blobOptions randomness\",\n            edit: (blobOptions) => (blobOptions.randomness = NaN),\n            error: /blobOptions.*randomness.*number.*NaN/g,\n        },\n        {\n            name: \"should reject negative blobOptions randomness\",\n            edit: (blobOptions) => (blobOptions.randomness = -10),\n            error: /blobOptions.*randomness.*invalid/g,\n        },\n        // size\n        {\n            name: \"should accept valid blobOptions size\",\n            edit: (blobOptions) => (blobOptions.size = 40),\n        },\n        {\n            name: \"should reject undefined blobOptions size\",\n            edit: (blobOptions) => delete (blobOptions as any).size,\n            error: /blobOptions.*size.*number.*undefined/g,\n        },\n        {\n            name: \"should reject broken blobOptions size\",\n            edit: (blobOptions) => (blobOptions.size = NaN),\n            error: /blobOptions.*size.*number.*NaN/g,\n        },\n        {\n            name: \"should reject negative blobOptions size\",\n            edit: (blobOptions) => (blobOptions.size = -1),\n            error: /blobOptions.*size.*invalid/g,\n        },\n    ]);\n};\n\ndescribe(\"blobs\", () => {\n    describe(\"canvasPath\", () => {\n        describe(\"blobOptions\", () => {\n            testBlobOptions((blobOptions) => canvasPath(blobOptions, genCanvasOptions()));\n        });\n\n        describe(\"canvasOptions\", () => {\n            it(\"should accept generated canvasOptions\", () => {\n                expect(() => canvasPath(genBlobOptions(), genCanvasOptions())).not.toThrow();\n            });\n\n            it(\"should accept undefined canvasOptions\", () => {\n                expect(() => canvasPath(genBlobOptions(), undefined as any)).not.toThrow();\n            });\n\n            it(\"should reject invalid canvasOptions\", () => {\n                expect(() => canvasPath(genBlobOptions(), null as any)).toThrow(\n                    /canvasOptions.*object.*null/g,\n                );\n            });\n\n            runSuite<CanvasOptions>({\n                functionBeingTested: (canvasOptions) => canvasPath(genBlobOptions(), canvasOptions),\n                optionsGenerator: genCanvasOptions,\n            })([\n                // offsetX\n                {\n                    name: \"should accept valid canvasOptions offsetX\",\n                    edit: (canvasOptions) => (canvasOptions.offsetX = 100),\n                },\n                {\n                    name: \"should accept undefined canvasOptions offsetX\",\n                    edit: (canvasOptions) => delete canvasOptions?.offsetX,\n                },\n                {\n                    name: \"should reject broken canvasOptions offsetX\",\n                    edit: (canvasOptions) => (canvasOptions.offsetX = NaN),\n                    error: /canvasOptions.*offsetX.*number.*NaN/g,\n                },\n                // offsetY\n                {\n                    name: \"should accept valid canvasOptions offsetY\",\n                    edit: (canvasOptions) => (canvasOptions.offsetY = 222),\n                },\n                {\n                    name: \"should accept undefined canvasOptions offsetY\",\n                    edit: (canvasOptions) => delete canvasOptions?.offsetY,\n                },\n                {\n                    name: \"should reject broken canvasOptions offsetY\",\n                    edit: (canvasOptions) => (canvasOptions.offsetY = NaN),\n                    error: /canvasOptions.*offsetY.*number.*NaN/g,\n                },\n            ]);\n        });\n    });\n\n    describe(\"svg\", () => {\n        describe(\"blobOptions\", () => {\n            testBlobOptions((blobOptions) => svg(blobOptions, genSvgOptions()));\n        });\n\n        describe(\"svgOptions\", () => {\n            it(\"should accept generated svgOptions\", () => {\n                expect(() => svg(genBlobOptions(), genSvgOptions())).not.toThrow();\n            });\n\n            it(\"should accept undefined svgOptions\", () => {\n                expect(() => svg(genBlobOptions(), undefined as any)).not.toThrow();\n            });\n\n            it(\"should reject invalid svgOptions\", () => {\n                expect(() => svg(genBlobOptions(), null as any)).toThrow(\n                    /svgOptions.*object.*null/g,\n                );\n            });\n\n            runSuite<SvgOptions>({\n                functionBeingTested: (svgOptions) => svg(genBlobOptions(), svgOptions),\n                optionsGenerator: genSvgOptions,\n            })([\n                // fill\n                {\n                    name: \"should accept valid svgOptions fill\",\n                    edit: (svgOptions) => (svgOptions.fill = \"red\"),\n                },\n                {\n                    name: \"should accept undefined svgOptions fill\",\n                    edit: (svgOptions) => delete svgOptions?.fill,\n                },\n                {\n                    name: \"should reject broken svgOptions fill\",\n                    edit: (svgOptions) => (svgOptions.fill = null as any),\n                    error: /svgOptions.*fill.*string.*null/g,\n                },\n                // stroke\n                {\n                    name: \"should accept valid svgOptions stroke\",\n                    edit: (svgOptions) => (svgOptions.stroke = \"red\"),\n                },\n                {\n                    name: \"should accept undefined svgOptions stroke\",\n                    edit: (svgOptions) => delete svgOptions?.stroke,\n                },\n                {\n                    name: \"should reject broken svgOptions stroke\",\n                    edit: (svgOptions) => (svgOptions.stroke = null as any),\n                    error: /svgOptions.*stroke.*string.*null/g,\n                },\n                // strokeWidth\n                {\n                    name: \"should accept valid svgOptions strokeWidth\",\n                    edit: (svgOptions) => (svgOptions.strokeWidth = 222),\n                },\n                {\n                    name: \"should accept undefined svgOptions strokeWidth\",\n                    edit: (svgOptions) => delete svgOptions?.strokeWidth,\n                },\n                {\n                    name: \"should reject broken svgOptions strokeWidth\",\n                    edit: (svgOptions) => (svgOptions.strokeWidth = NaN),\n                    error: /svgOptions.*strokeWidth.*number.*NaN/g,\n                },\n            ]);\n        });\n    });\n\n    describe(\"svgPath\", () => {\n        describe(\"blobOptions\", () => {\n            testBlobOptions(svgPath);\n        });\n    });\n});\n"
  },
  {
    "path": "public/blobs.ts",
    "content": "import {genFromOptions} from \"../internal/gen\";\nimport {renderPath} from \"../internal/render/svg\";\nimport {renderPath2D} from \"../internal/render/canvas\";\nimport {mapPoints} from \"../internal/util\";\nimport {checkBlobOptions, checkCanvasOptions, checkSvgOptions} from \"../internal/check\";\n\nexport interface BlobOptions {\n    // A given seed will always produce the same blob.\n    // Use `Math.random()` for pseudorandom behavior.\n    seed: string | number;\n    // Actual number of points will be `3 + extraPoints`.\n    extraPoints: number;\n    // Increases the amount of variation in point position.\n    randomness: number;\n    // Size of the bounding box.\n    size: number;\n}\n\nexport interface CanvasOptions {\n    // Coordinates of top-left corner of the blob.\n    offsetX?: number;\n    offsetY?: number;\n}\n\nexport interface SvgOptions {\n    fill?: string; // Default: \"#ec576b\".\n    stroke?: string; // Default: \"none\".\n    strokeWidth?: number; // Default: 0.\n}\n\nexport const canvasPath = (blobOptions: BlobOptions, canvasOptions: CanvasOptions = {}): Path2D => {\n    try {\n        checkBlobOptions(blobOptions);\n        checkCanvasOptions(canvasOptions);\n    } catch (e) {\n        throw `(blobs2): ${e}`;\n    }\n    return renderPath2D(\n        mapPoints(genFromOptions(blobOptions), ({curr}) => {\n            curr.x += canvasOptions.offsetX || 0;\n            curr.y += canvasOptions.offsetY || 0;\n            return curr;\n        }),\n    );\n};\n\nexport const svg = (blobOptions: BlobOptions, svgOptions: SvgOptions = {}): string => {\n    try {\n        checkBlobOptions(blobOptions);\n        checkSvgOptions(svgOptions);\n    } catch (e) {\n        throw `(blobs2): ${e}`;\n    }\n    const path = svgPath(blobOptions);\n    const size = Math.floor(blobOptions.size);\n    const fill = svgOptions.fill === undefined ? \"#ec576b\" : svgOptions.fill;\n    const stroke = svgOptions.stroke === undefined ? \"none\" : svgOptions.stroke;\n    const strokeWidth = svgOptions.strokeWidth === undefined ? 0 : svgOptions.strokeWidth;\n    return `\n<svg width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path stroke=\"${stroke}\" stroke-width=\"${strokeWidth}\" fill=\"${fill}\" d=\"${path}\"/>\n</svg>`.trim();\n};\n\nexport const svgPath = (blobOptions: BlobOptions): string => {\n    try {\n        checkBlobOptions(blobOptions);\n    } catch (e) {\n        throw `(blobs2): ${e}`;\n    }\n    return renderPath(genFromOptions(blobOptions));\n};\n"
  },
  {
    "path": "public/legacy.test.ts",
    "content": "import blobs, {BlobOptions} from \"./legacy\";\n\nconst genMinimalOptions = (): BlobOptions => ({\n    size: 1000 * Math.random(),\n    complexity: 1 - Math.random(),\n    contrast: 1 - Math.random(),\n    color: \"#fff\",\n});\n\ndescribe(\"legacy\", () => {\n    it(\"should return a different result when seed is not provided\", () => {\n        const options = genMinimalOptions();\n\n        const a = blobs(options);\n        const b = blobs(options);\n\n        expect(a).not.toEqual(b);\n    });\n\n    it(\"should return the same result when the seed is provided\", () => {\n        const options = genMinimalOptions();\n\n        options.seed = \"abcde\";\n        const a = blobs(options);\n        const b = blobs(options);\n\n        expect(a).toEqual(b);\n    });\n\n    it(\"should require options be provided\", () => {\n        expect(() => (blobs as any)()).toThrow(\"options\");\n    });\n\n    it(\"should require a size be provided\", () => {\n        const options = genMinimalOptions();\n\n        delete (options as any).size;\n        expect(() => blobs(options)).toThrow(\"size\");\n    });\n\n    it(\"should reject out of range complexity values\", () => {\n        const options = genMinimalOptions();\n\n        options.complexity = 1234;\n        expect(() => blobs(options)).toThrow(\"complexity\");\n\n        options.complexity = 0;\n        expect(() => blobs(options)).toThrow(\"complexity\");\n    });\n\n    it(\"should reject out of range contrast values\", () => {\n        const options = genMinimalOptions();\n\n        options.contrast = 999;\n        expect(() => blobs(options)).toThrow(\"contrast\");\n\n        options.contrast = -1;\n        expect(() => blobs(options)).toThrow(\"contrast\");\n    });\n\n    it(\"should reject options without stroke or color\", () => {\n        const options = genMinimalOptions();\n\n        delete options.stroke;\n        delete options.color;\n        expect(() => blobs(options)).toThrow(\"stroke\");\n        expect(() => blobs(options)).toThrow(\"color\");\n    });\n});\n\ndescribe(\"editable\", () => {\n    it(\"should reflect changes when edited\", () => {\n        const options = genMinimalOptions();\n\n        const out = blobs.editable(options);\n        const initial = out.render();\n        out.attributes.id = \"test\";\n        const modified = out.render();\n\n        expect(modified).not.toBe(initial);\n    });\n});\n\n// Sanity checks to ensure the output remains consistent\n// across changes to the source.\nconst testCases: Record<string, BlobOptions> = {\n    fill: {\n        size: 109,\n        complexity: 0.1,\n        contrast: 0.331,\n        color: \"red\",\n        seed: \"fill\",\n    },\n    stroke: {\n        size: 226,\n        complexity: 0.91,\n        contrast: 0.6,\n        stroke: {\n            color: \"#ff00bb\",\n            width: 3.8,\n        },\n        seed: \"stroke\",\n    },\n    guides: {\n        size: 781,\n        complexity: 1,\n        contrast: 0.331,\n        color: \"yellow\",\n        guides: true,\n        seed: \"guides\",\n    },\n};\n\nfor (const testCase of Object.keys(testCases)) {\n    test(testCase, () => {\n        expect(blobs(testCases[testCase])).toMatchSnapshot();\n    });\n}\n"
  },
  {
    "path": "public/legacy.ts",
    "content": "import {rand} from \"../internal/rand\";\nimport {renderEditable, XmlElement as InternalXmlElement} from \"../internal/render/svg\";\nimport {genBlob} from \"../internal/gen\";\nimport {mapPoints} from \"../internal/util\";\n\nconst isBrowser = new Function(\"try {return this===window;}catch(e){ return false;}\");\nconst isLocalhost = () => location.hostname === \"localhost\" || location.hostname === \"127.0.0.1\";\nconst isFile = () => location.protocol === \"file:\";\nif (!isBrowser() || isLocalhost() || isFile()) {\n    console.warn(\"You are using the legacy blobs API!\\nPlease use 'blobs/v2' instead.\");\n}\n\nexport interface PathOptions {\n    // Bounding box dimensions.\n    size: number;\n\n    // Number of points.\n    complexity: number;\n\n    // Amount of randomness.\n    contrast: number;\n\n    // Value to seed random number generator.\n    seed?: string;\n}\n\nexport interface BlobOptions extends PathOptions {\n    // Fill color.\n    color?: string;\n\n    stroke?: {\n        // Stroke color.\n        color: string;\n\n        // Stroke width.\n        width: number;\n    };\n\n    // Render points, handles and stroke.\n    guides?: boolean;\n}\n\n// Generates an svg document string containing a randomized blob.\nconst blobs = (options: BlobOptions): string => {\n    return blobs.editable(options).render();\n};\n\n// Generates a randomized blob as an editable data structure which can be rendered to an svg document.\nblobs.editable = (options: BlobOptions): XmlElement => {\n    if (!options) {\n        throw new Error(\"no options specified\");\n    }\n\n    // Random number generator.\n    const rgen = rand(options.seed || String(Math.random()));\n\n    if (!options.size) {\n        throw new Error(\"no size specified\");\n    }\n\n    if (!options.stroke && !options.color) {\n        throw new Error(\"no color or stroke specified\");\n    }\n\n    if (options.complexity <= 0 || options.complexity > 1) {\n        throw new Error(\"complexity out of range ]0,1]\");\n    }\n\n    if (options.contrast < 0 || options.contrast > 1) {\n        throw new Error(\"contrast out of range [0,1]\");\n    }\n\n    const count = 3 + Math.floor(14 * options.complexity);\n    const offset = (): number => (1 - 0.8 * options.contrast * rgen()) / Math.E;\n\n    const points = mapPoints(genBlob(count, offset), ({curr}) => {\n        // Scale.\n        curr.x *= options.size;\n        curr.y *= options.size;\n        curr.handleIn.length *= options.size;\n        curr.handleOut.length *= options.size;\n\n        // Flip around x-axis.\n        curr.y = options.size - curr.y;\n        curr.handleIn.angle *= -1;\n        curr.handleOut.angle *= -1;\n\n        return curr;\n    });\n\n    return renderEditable(points, {\n        closed: true,\n        width: options.size,\n        height: options.size,\n        fill: options.color,\n        transform: `rotate(${rgen() * (360 / count)},${options.size / 2},${options.size / 2})`,\n        stroke: options.stroke && options.stroke.color,\n        strokeWidth: options.stroke && options.stroke.width,\n        guides: options.guides,\n    });\n};\n\nexport interface XmlElement {\n    tag: string;\n    attributes: Record<string, string | number>;\n    children: XmlElement[];\n    render(): string;\n}\n\n// Shortcut to create an XmlElement without \"new\";\nblobs.xml = (tag: string): XmlElement => new InternalXmlElement(tag);\n\nexport default blobs;\n"
  },
  {
    "path": "rollup.config.mjs",
    "content": "import typescript from \"rollup-plugin-typescript2\";\nimport { uglify } from \"rollup-plugin-uglify\";\nimport copy from \"rollup-plugin-copy\";\nimport { nodeResolve } from \"@rollup/plugin-node-resolve\";\n\nconst bundles = [\n  {\n    name: \"blobs\",\n    entry: \"public/legacy.ts\",\n    types: \"public/legacy.d.ts\",\n    output: \".\",\n  },\n  {\n    name: \"blobs\",\n    entry: \"public/legacy.ts\",\n    types: \"public/legacy.d.ts\",\n    output: \"v1\",\n  },\n  {\n    name: \"blobs2\",\n    entry: \"public/blobs.ts\",\n    types: \"public/blobs.d.ts\",\n    output: \"v2\",\n  },\n  {\n    name: \"blobs2Animate\",\n    entry: \"public/animate.ts\",\n    types: \"public/animate.d.ts\",\n    output: \"v2/animate\",\n  },\n];\n\nexport default [\"es\", \"umd\"].flatMap((format) =>\n  bundles.map((bundle) => ({\n    input: bundle.entry,\n    output: {\n      file: bundle.output + `/index${format == \"es\" ? \".module\" : \"\"}.js`,\n      format: format,\n      name: bundle.name,\n      sourcemap: true,\n    },\n    plugins: [\n      nodeResolve(),\n      typescript({ cacheRoot: \"./node_modules/.cache/rpt2\" }),\n      uglify(),\n      copy({\n        hook: \"writeBundle\",\n        targets: [{\n          src: bundle.types,\n          dest: bundle.output,\n          rename: \"index.d.ts\",\n        }],\n        verbose: true,\n      }),\n    ],\n  }))\n);\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"exclude\": [\n    \"example\",\n    \"test\",\n    \"node_modules\"\n  ],\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"module\": \"es2015\",\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"lib\": [\n      \"es2018\",\n      \"dom\"\n    ],\n    \"sourceMap\": true,\n    \"removeComments\": true,\n    \"declaration\": true,\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"strictFunctionTypes\": true,\n    \"strictPropertyInitialization\": true,\n    \"alwaysStrict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"stripInternal\": true\n  }\n}\n"
  }
]