Add animated GIF export. (#79)

This adds an "Export GIF" button _if and only if_ animation is enabled on the Mandala page.

The GIF has the same dimensions as the canvas at the time that the button was clicked.

Currently the animation is locked to 15 FPS, which I think might be the maximum rate that some platforms limit it to, but I'm not sure.  We can always adjust it in the future.
pull/88/head
Atul Varma 2021-04-10 17:11:12 -04:00 zatwierdzone przez GitHub
rodzic 7fc210f671
commit bca699018c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
8 zmienionych plików z 950 dodań i 42 usunięć

Wyświetl plik

@ -144,6 +144,20 @@ ul.navbar li:last-child {
.sidebar .numeric-slider .slider input {
flex-basis: 90%;
}
.overlay-wrapper {
position: fixed;
display: flex;
background-color: rgba(0, 0, 0, 0.9);
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
</style>
<noscript>
<p>Alas, you need JavaScript to peruse this page.</p>

Wyświetl plik

@ -0,0 +1,14 @@
import GIF from "../vendor/gif.js/gif";
import { GIF_WORKER_JS } from "../vendor/gif.js/gif.worker";
export function createGIF(): GIF {
const workerBlob = new Blob([GIF_WORKER_JS], {
type: "application/javascript",
});
return new GIF({
workers: 2,
workerScript: URL.createObjectURL(workerBlob),
quality: 10,
repeat: 0,
});
}

Wyświetl plik

@ -69,10 +69,58 @@ export const AutoSizingSvg = React.forwardRef(
ref={ref}
>
{bgColor && (
<rect x={x} y={y} width={width} height={height} fill={bgColor} />
<rect
x={x}
y={y}
width={width}
height={height}
fill={bgColor}
data-is-background
/>
)}
<g ref={gRef}>{props.children}</g>
</svg>
);
}
);
export function getSvgMetadata(svgEl: SVGSVGElement) {
let bgColor: string | undefined = undefined;
const backgroundEl = svgEl.querySelector("[data-is-background]");
if (backgroundEl) {
bgColor = backgroundEl.getAttribute("fill") ?? undefined;
}
const { x, y, width, height } = svgEl.viewBox.baseVal;
return { x, y, width, height, bgColor };
}
export type SvgMetadata = ReturnType<typeof getSvgMetadata>;
export const SvgWithBackground: React.FC<SvgMetadata & { children?: any }> = ({
x,
y,
width,
height,
bgColor,
children,
}) => (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width={`${width}px`}
height={`${height}px`}
viewBox={`${x} ${y} ${width} ${height}`}
>
{bgColor && (
<rect
x={x}
y={y}
width={width}
height={height}
fill={bgColor}
data-is-background
/>
)}
{children}
</svg>
);

Wyświetl plik

@ -1,15 +1,23 @@
import React from "react";
import React, { useState } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { createGIF } from "./animated-gif";
import { getSvgMetadata, SvgWithBackground } from "./auto-sizing-svg";
function getSvgMarkup(el: SVGSVGElement): string {
function getSvgDocument(svgMarkup: string): string {
return [
`<?xml version="1.0" encoding="utf-8"?>`,
"<!-- Generator: https://github.com/toolness/mystic-symbolic -->",
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
el.outerHTML,
svgMarkup,
].join("\n");
}
type ImageExporter = (svgEl: SVGSVGElement) => Promise<string>;
type ProgressHandler = (value: number | null) => void;
type ImageExporter = (
svgEl: SVGSVGElement,
onProgress: ProgressHandler
) => Promise<string>;
/**
* Initiates a download on the user's browser which downloads the given
@ -19,6 +27,7 @@ async function exportImage(
svgRef: React.RefObject<SVGSVGElement>,
basename: string,
ext: string,
onProgress: ProgressHandler,
exporter: ImageExporter
) {
const svgEl = svgRef.current;
@ -26,13 +35,14 @@ async function exportImage(
alert("Oops, an error occurred! Please try again later.");
return;
}
const url = await exporter(svgEl);
const url = await exporter(svgEl, onProgress);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `${basename}.${ext}`;
document.body.append(anchor);
anchor.click();
document.body.removeChild(anchor);
onProgress(null);
}
function getCanvasContext2D(
@ -43,17 +53,22 @@ function getCanvasContext2D(
return ctx;
}
function getSvgUrl(svgMarkup: string): string {
return `data:image/svg+xml;utf8,${encodeURIComponent(
getSvgDocument(svgMarkup)
)}`;
}
/**
* Exports the given SVG as an SVG in a data URL.
*/
const exportSvg: ImageExporter = async (svgEl) =>
`data:image/svg+xml;utf8,${encodeURIComponent(getSvgMarkup(svgEl))}`;
const exportSvg: ImageExporter = async (svgEl) => getSvgUrl(svgEl.outerHTML);
/**
* Exports the given SVG as a PNG in a data URL.
*/
const exportPng: ImageExporter = async (svgEl) => {
const dataURL = await exportSvg(svgEl);
const exportPng: ImageExporter = async (svgEl, onProgress) => {
const dataURL = await exportSvg(svgEl, onProgress);
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
@ -71,16 +86,113 @@ const exportPng: ImageExporter = async (svgEl) => {
});
};
function drawImage(canvas: HTMLCanvasElement, dataURL: string): Promise<void> {
return new Promise((resolve, reject) => {
const img = document.createElement("img");
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
const ctx = getCanvasContext2D(canvas);
ctx.drawImage(img, 0, 0);
resolve();
};
img.onerror = reject;
img.src = dataURL;
});
}
/**
* Exports the given SVG as a GIF in a data URL.
*/
async function exportGif(
animate: ExportableAnimation,
svgEl: SVGSVGElement,
onProgress: (value: number) => void
): Promise<string> {
const fps = animate.fps || 15;
const msecPerFrame = 1000 / fps;
const numFrames = Math.floor(animate.duration / msecPerFrame);
const svgMeta = getSvgMetadata(svgEl);
const render = (animPct: number) => (
<SvgWithBackground {...svgMeta}>
{animate.render(animPct)}
</SvgWithBackground>
);
const gif = createGIF();
for (let i = 0; i < numFrames; i++) {
onProgress(i / numFrames);
const canvas = document.createElement("canvas");
const animPct = i / numFrames;
const markup = renderToStaticMarkup(render(animPct));
const url = getSvgUrl(markup);
await drawImage(canvas, url);
gif.addFrame(canvas, { delay: msecPerFrame });
}
return new Promise((resolve, reject) => {
gif.on("finished", function (blob) {
onProgress(1);
resolve(URL.createObjectURL(blob));
});
gif.render();
});
}
export type ExportableAnimation = {
duration: number;
fps?: number;
render: (time: number) => JSX.Element;
};
export const ExportWidget: React.FC<{
svgRef: React.RefObject<SVGSVGElement>;
animate?: ExportableAnimation | false;
basename: string;
}> = ({ svgRef, basename }) => (
<>
<button onClick={() => exportImage(svgRef, basename, "svg", exportSvg)}>
Export SVG
</button>{" "}
<button onClick={() => exportImage(svgRef, basename, "png", exportPng)}>
Export PNG
</button>
</>
);
}> = ({ svgRef, basename, animate }) => {
const [progress, setProgress] = useState<number | null>(null);
if (progress !== null) {
return (
<div className="overlay-wrapper">
<p>Exporting&hellip;</p>
<progress value={progress} />
</div>
);
}
return (
<>
<button
onClick={() =>
exportImage(svgRef, basename, "svg", setProgress, exportSvg)
}
>
Export SVG
</button>{" "}
<button
onClick={() =>
exportImage(svgRef, basename, "png", setProgress, exportPng)
}
>
Export PNG
</button>{" "}
{animate && (
<button
onClick={() =>
exportImage(
svgRef,
basename,
"gif",
setProgress,
exportGif.bind(null, animate)
)
}
>
Export GIF
</button>
)}
</>
);
};

Wyświetl plik

@ -216,32 +216,37 @@ export const MandalaPage: React.FC<{}> = () => {
const [useTwoCircles, setUseTwoCircles] = useState(false);
const [invertCircle2, setInvertCircle2] = useState(true);
const [firstBehindSecond, setFirstBehindSecond] = useState(false);
const durationMsecs = durationSecs * 1000;
const isAnimated = isAnyMandalaCircleAnimated([circle1, circle2]);
const animPct = useAnimationPct(isAnimated ? durationSecs * 1000 : 0);
const animPct = useAnimationPct(isAnimated ? durationMsecs : 0);
const symbolCtx = noFillIfShowingSpecs(baseCompCtx);
const circle2SymbolCtx = invertCircle2 ? swapColors(symbolCtx) : symbolCtx;
const circles = [
<ExtendedMandalaCircle
key="first"
{...animateMandalaCircleParams(circle1, animPct)}
{...symbolCtx}
/>,
];
if (useTwoCircles) {
circles.push(
const makeMandala = (animPct: number): JSX.Element => {
const circles = [
<ExtendedMandalaCircle
key="second"
{...animateMandalaCircleParams(circle2, animPct)}
{...circle2SymbolCtx}
/>
);
if (firstBehindSecond) {
circles.reverse();
key="first"
{...animateMandalaCircleParams(circle1, animPct)}
{...symbolCtx}
/>,
];
if (useTwoCircles) {
circles.push(
<ExtendedMandalaCircle
key="second"
{...animateMandalaCircleParams(circle2, animPct)}
{...circle2SymbolCtx}
/>
);
if (firstBehindSecond) {
circles.reverse();
}
}
}
return <SvgTransform transform={svgScale(0.5)}>{circles}</SvgTransform>;
};
return (
<Page title="Mandala!">
@ -301,7 +306,13 @@ export const MandalaPage: React.FC<{}> = () => {
}}
/>
<div className="thingy">
<ExportWidget basename="mandala" svgRef={svgRef} />
<ExportWidget
basename="mandala"
svgRef={svgRef}
animate={
isAnimated && { duration: durationMsecs, render: makeMandala }
}
/>
</div>
</div>
<div
@ -311,12 +322,11 @@ export const MandalaPage: React.FC<{}> = () => {
>
<HoverDebugHelper>
<AutoSizingSvg
padding={20}
ref={svgRef}
bgColor={baseCompCtx.background}
sizeToElement={canvasRef}
>
<SvgTransform transform={svgScale(0.5)}>{circles}</SvgTransform>
{makeMandala(animPct)}
</AutoSizingSvg>
</HoverDebugHelper>
</div>

32
vendor/gif.js/gif.d.ts vendored 100644
Wyświetl plik

@ -0,0 +1,32 @@
/**
* This is fully documented here:
*
* https://github.com/jnordberg/gif.js
*
* These typings are just for the parts of the API we use, they're
* not complete at all.
*/
export default class GIF {
constructor(options: {
/** number of web workers to spawn */
workers: number;
/** url to load worker script from */
workerScript: string;
/** pixel sample interval, lower is better */
quality: number;
/** repeat count, -1 = no repeat, 0 = forever */
repeat: number;
});
addFrame(
canvas: HTMLCanvasElement,
options?: {
/** frame delay */
delay: number;
}
);
on(event: "finished", callback: (blob: Blob) => void);
render();
}

668
vendor/gif.js/gif.js vendored 100644
Wyświetl plik

@ -0,0 +1,668 @@
// gif.js 0.2.0 - https://github.com/jnordberg/gif.js
(function (f) {
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f();
} else if (typeof define === "function" && define.amd) {
define([], f);
} else {
var g;
if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
g = this;
}
g.GIF = f();
}
})(function () {
var define, module, exports;
return (function e(t, n, r) {
function s(o, u) {
if (!n[o]) {
if (!t[o]) {
var a = typeof require == "function" && require;
if (!u && a) return a(o, !0);
if (i) return i(o, !0);
var f = new Error("Cannot find module '" + o + "'");
throw ((f.code = "MODULE_NOT_FOUND"), f);
}
var l = (n[o] = { exports: {} });
t[o][0].call(
l.exports,
function (e) {
var n = t[o][1][e];
return s(n ? n : e);
},
l,
l.exports,
e,
t,
n,
r
);
}
return n[o].exports;
}
var i = typeof require == "function" && require;
for (var o = 0; o < r.length; o++) s(r[o]);
return s;
})(
{
1: [
function (require, module, exports) {
function EventEmitter() {
this._events = this._events || {};
this._maxListeners = this._maxListeners || undefined;
}
module.exports = EventEmitter;
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._maxListeners = undefined;
EventEmitter.defaultMaxListeners = 10;
EventEmitter.prototype.setMaxListeners = function (n) {
if (!isNumber(n) || n < 0 || isNaN(n))
throw TypeError("n must be a positive number");
this._maxListeners = n;
return this;
};
EventEmitter.prototype.emit = function (type) {
var er, handler, len, args, i, listeners;
if (!this._events) this._events = {};
if (type === "error") {
if (
!this._events.error ||
(isObject(this._events.error) && !this._events.error.length)
) {
er = arguments[1];
if (er instanceof Error) {
throw er;
} else {
var err = new Error(
'Uncaught, unspecified "error" event. (' + er + ")"
);
err.context = er;
throw err;
}
}
}
handler = this._events[type];
if (isUndefined(handler)) return false;
if (isFunction(handler)) {
switch (arguments.length) {
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
default:
args = Array.prototype.slice.call(arguments, 1);
handler.apply(this, args);
}
} else if (isObject(handler)) {
args = Array.prototype.slice.call(arguments, 1);
listeners = handler.slice();
len = listeners.length;
for (i = 0; i < len; i++) listeners[i].apply(this, args);
}
return true;
};
EventEmitter.prototype.addListener = function (type, listener) {
var m;
if (!isFunction(listener))
throw TypeError("listener must be a function");
if (!this._events) this._events = {};
if (this._events.newListener)
this.emit(
"newListener",
type,
isFunction(listener.listener) ? listener.listener : listener
);
if (!this._events[type]) this._events[type] = listener;
else if (isObject(this._events[type]))
this._events[type].push(listener);
else this._events[type] = [this._events[type], listener];
if (isObject(this._events[type]) && !this._events[type].warned) {
if (!isUndefined(this._maxListeners)) {
m = this._maxListeners;
} else {
m = EventEmitter.defaultMaxListeners;
}
if (m && m > 0 && this._events[type].length > m) {
this._events[type].warned = true;
console.error(
"(node) warning: possible EventEmitter memory " +
"leak detected. %d listeners added. " +
"Use emitter.setMaxListeners() to increase limit.",
this._events[type].length
);
if (typeof console.trace === "function") {
console.trace();
}
}
}
return this;
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.once = function (type, listener) {
if (!isFunction(listener))
throw TypeError("listener must be a function");
var fired = false;
function g() {
this.removeListener(type, g);
if (!fired) {
fired = true;
listener.apply(this, arguments);
}
}
g.listener = listener;
this.on(type, g);
return this;
};
EventEmitter.prototype.removeListener = function (type, listener) {
var list, position, length, i;
if (!isFunction(listener))
throw TypeError("listener must be a function");
if (!this._events || !this._events[type]) return this;
list = this._events[type];
length = list.length;
position = -1;
if (
list === listener ||
(isFunction(list.listener) && list.listener === listener)
) {
delete this._events[type];
if (this._events.removeListener)
this.emit("removeListener", type, listener);
} else if (isObject(list)) {
for (i = length; i-- > 0; ) {
if (
list[i] === listener ||
(list[i].listener && list[i].listener === listener)
) {
position = i;
break;
}
}
if (position < 0) return this;
if (list.length === 1) {
list.length = 0;
delete this._events[type];
} else {
list.splice(position, 1);
}
if (this._events.removeListener)
this.emit("removeListener", type, listener);
}
return this;
};
EventEmitter.prototype.removeAllListeners = function (type) {
var key, listeners;
if (!this._events) return this;
if (!this._events.removeListener) {
if (arguments.length === 0) this._events = {};
else if (this._events[type]) delete this._events[type];
return this;
}
if (arguments.length === 0) {
for (key in this._events) {
if (key === "removeListener") continue;
this.removeAllListeners(key);
}
this.removeAllListeners("removeListener");
this._events = {};
return this;
}
listeners = this._events[type];
if (isFunction(listeners)) {
this.removeListener(type, listeners);
} else if (listeners) {
while (listeners.length)
this.removeListener(type, listeners[listeners.length - 1]);
}
delete this._events[type];
return this;
};
EventEmitter.prototype.listeners = function (type) {
var ret;
if (!this._events || !this._events[type]) ret = [];
else if (isFunction(this._events[type])) ret = [this._events[type]];
else ret = this._events[type].slice();
return ret;
};
EventEmitter.prototype.listenerCount = function (type) {
if (this._events) {
var evlistener = this._events[type];
if (isFunction(evlistener)) return 1;
else if (evlistener) return evlistener.length;
}
return 0;
};
EventEmitter.listenerCount = function (emitter, type) {
return emitter.listenerCount(type);
};
function isFunction(arg) {
return typeof arg === "function";
}
function isNumber(arg) {
return typeof arg === "number";
}
function isObject(arg) {
return typeof arg === "object" && arg !== null;
}
function isUndefined(arg) {
return arg === void 0;
}
},
{},
],
2: [
function (require, module, exports) {
var UA, browser, mode, platform, ua;
ua = navigator.userAgent.toLowerCase();
platform = navigator.platform.toLowerCase();
UA = ua.match(
/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/
) || [null, "unknown", 0];
mode = UA[1] === "ie" && document.documentMode;
browser = {
name: UA[1] === "version" ? UA[3] : UA[1],
version:
mode || parseFloat(UA[1] === "opera" && UA[4] ? UA[4] : UA[2]),
platform: {
name: ua.match(/ip(?:ad|od|hone)/)
? "ios"
: (ua.match(/(?:webos|android)/) ||
platform.match(/mac|win|linux/) || ["other"])[0],
},
};
browser[browser.name] = true;
browser[browser.name + parseInt(browser.version, 10)] = true;
browser.platform[browser.platform.name] = true;
module.exports = browser;
},
{},
],
3: [
function (require, module, exports) {
var EventEmitter,
GIF,
browser,
extend = function (child, parent) {
for (var key in parent) {
if (hasProp.call(parent, key)) child[key] = parent[key];
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
},
hasProp = {}.hasOwnProperty,
indexOf =
[].indexOf ||
function (item) {
for (var i = 0, l = this.length; i < l; i++) {
if (i in this && this[i] === item) return i;
}
return -1;
},
slice = [].slice;
EventEmitter = require("events").EventEmitter;
browser = require("./browser.coffee");
GIF = (function (superClass) {
var defaults, frameDefaults;
extend(GIF, superClass);
defaults = {
workerScript: "gif.worker.js",
workers: 2,
repeat: 0,
background: "#fff",
quality: 10,
width: null,
height: null,
transparent: null,
debug: false,
dither: false,
};
frameDefaults = { delay: 500, copy: false };
function GIF(options) {
var base, key, value;
this.running = false;
this.options = {};
this.frames = [];
this.freeWorkers = [];
this.activeWorkers = [];
this.setOptions(options);
for (key in defaults) {
value = defaults[key];
if ((base = this.options)[key] == null) {
base[key] = value;
}
}
}
GIF.prototype.setOption = function (key, value) {
this.options[key] = value;
if (
this._canvas != null &&
(key === "width" || key === "height")
) {
return (this._canvas[key] = value);
}
};
GIF.prototype.setOptions = function (options) {
var key, results, value;
results = [];
for (key in options) {
if (!hasProp.call(options, key)) continue;
value = options[key];
results.push(this.setOption(key, value));
}
return results;
};
GIF.prototype.addFrame = function (image, options) {
var frame, key;
if (options == null) {
options = {};
}
frame = {};
frame.transparent = this.options.transparent;
for (key in frameDefaults) {
frame[key] = options[key] || frameDefaults[key];
}
if (this.options.width == null) {
this.setOption("width", image.width);
}
if (this.options.height == null) {
this.setOption("height", image.height);
}
if (
typeof ImageData !== "undefined" &&
ImageData !== null &&
image instanceof ImageData
) {
frame.data = image.data;
} else if (
(typeof CanvasRenderingContext2D !== "undefined" &&
CanvasRenderingContext2D !== null &&
image instanceof CanvasRenderingContext2D) ||
(typeof WebGLRenderingContext !== "undefined" &&
WebGLRenderingContext !== null &&
image instanceof WebGLRenderingContext)
) {
if (options.copy) {
frame.data = this.getContextData(image);
} else {
frame.context = image;
}
} else if (image.childNodes != null) {
if (options.copy) {
frame.data = this.getImageData(image);
} else {
frame.image = image;
}
} else {
throw new Error("Invalid image");
}
return this.frames.push(frame);
};
GIF.prototype.render = function () {
var i, j, numWorkers, ref;
if (this.running) {
throw new Error("Already running");
}
if (this.options.width == null || this.options.height == null) {
throw new Error(
"Width and height must be set prior to rendering"
);
}
this.running = true;
this.nextFrame = 0;
this.finishedFrames = 0;
this.imageParts = function () {
var j, ref, results;
results = [];
for (
i = j = 0, ref = this.frames.length;
0 <= ref ? j < ref : j > ref;
i = 0 <= ref ? ++j : --j
) {
results.push(null);
}
return results;
}.call(this);
numWorkers = this.spawnWorkers();
if (this.options.globalPalette === true) {
this.renderNextFrame();
} else {
for (
i = j = 0, ref = numWorkers;
0 <= ref ? j < ref : j > ref;
i = 0 <= ref ? ++j : --j
) {
this.renderNextFrame();
}
}
this.emit("start");
return this.emit("progress", 0);
};
GIF.prototype.abort = function () {
var worker;
while (true) {
worker = this.activeWorkers.shift();
if (worker == null) {
break;
}
this.log("killing active worker");
worker.terminate();
}
this.running = false;
return this.emit("abort");
};
GIF.prototype.spawnWorkers = function () {
var j, numWorkers, ref, results;
numWorkers = Math.min(this.options.workers, this.frames.length);
(function () {
results = [];
for (
var j = (ref = this.freeWorkers.length);
ref <= numWorkers ? j < numWorkers : j > numWorkers;
ref <= numWorkers ? j++ : j--
) {
results.push(j);
}
return results;
}
.apply(this)
.forEach(
(function (_this) {
return function (i) {
var worker;
_this.log("spawning worker " + i);
worker = new Worker(_this.options.workerScript);
worker.onmessage = function (event) {
_this.activeWorkers.splice(
_this.activeWorkers.indexOf(worker),
1
);
_this.freeWorkers.push(worker);
return _this.frameFinished(event.data);
};
return _this.freeWorkers.push(worker);
};
})(this)
));
return numWorkers;
};
GIF.prototype.frameFinished = function (frame) {
var i, j, ref;
this.log(
"frame " +
frame.index +
" finished - " +
this.activeWorkers.length +
" active"
);
this.finishedFrames++;
this.emit("progress", this.finishedFrames / this.frames.length);
this.imageParts[frame.index] = frame;
if (this.options.globalPalette === true) {
this.options.globalPalette = frame.globalPalette;
this.log("global palette analyzed");
if (this.frames.length > 2) {
for (
i = j = 1, ref = this.freeWorkers.length;
1 <= ref ? j < ref : j > ref;
i = 1 <= ref ? ++j : --j
) {
this.renderNextFrame();
}
}
}
if (indexOf.call(this.imageParts, null) >= 0) {
return this.renderNextFrame();
} else {
return this.finishRendering();
}
};
GIF.prototype.finishRendering = function () {
var data,
frame,
i,
image,
j,
k,
l,
len,
len1,
len2,
len3,
offset,
page,
ref,
ref1,
ref2;
len = 0;
ref = this.imageParts;
for (j = 0, len1 = ref.length; j < len1; j++) {
frame = ref[j];
len += (frame.data.length - 1) * frame.pageSize + frame.cursor;
}
len += frame.pageSize - frame.cursor;
this.log(
"rendering finished - filesize " + Math.round(len / 1e3) + "kb"
);
data = new Uint8Array(len);
offset = 0;
ref1 = this.imageParts;
for (k = 0, len2 = ref1.length; k < len2; k++) {
frame = ref1[k];
ref2 = frame.data;
for (i = l = 0, len3 = ref2.length; l < len3; i = ++l) {
page = ref2[i];
data.set(page, offset);
if (i === frame.data.length - 1) {
offset += frame.cursor;
} else {
offset += frame.pageSize;
}
}
}
image = new Blob([data], { type: "image/gif" });
return this.emit("finished", image, data);
};
GIF.prototype.renderNextFrame = function () {
var frame, task, worker;
if (this.freeWorkers.length === 0) {
throw new Error("No free workers");
}
if (this.nextFrame >= this.frames.length) {
return;
}
frame = this.frames[this.nextFrame++];
worker = this.freeWorkers.shift();
task = this.getTask(frame);
this.log(
"starting frame " +
(task.index + 1) +
" of " +
this.frames.length
);
this.activeWorkers.push(worker);
return worker.postMessage(task);
};
GIF.prototype.getContextData = function (ctx) {
return ctx.getImageData(
0,
0,
this.options.width,
this.options.height
).data;
};
GIF.prototype.getImageData = function (image) {
var ctx;
if (this._canvas == null) {
this._canvas = document.createElement("canvas");
this._canvas.width = this.options.width;
this._canvas.height = this.options.height;
}
ctx = this._canvas.getContext("2d");
ctx.setFill = this.options.background;
ctx.fillRect(0, 0, this.options.width, this.options.height);
ctx.drawImage(image, 0, 0);
return this.getContextData(ctx);
};
GIF.prototype.getTask = function (frame) {
var index, task;
index = this.frames.indexOf(frame);
task = {
index: index,
last: index === this.frames.length - 1,
delay: frame.delay,
transparent: frame.transparent,
width: this.options.width,
height: this.options.height,
quality: this.options.quality,
dither: this.options.dither,
globalPalette: this.options.globalPalette,
repeat: this.options.repeat,
canTransfer: browser.name === "chrome",
};
if (frame.data != null) {
task.data = frame.data;
} else if (frame.context != null) {
task.data = this.getContextData(frame.context);
} else if (frame.image != null) {
task.data = this.getImageData(frame.image);
} else {
throw new Error("Invalid frame");
}
return task;
};
GIF.prototype.log = function () {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
if (!this.options.debug) {
return;
}
return console.log.apply(console, args);
};
return GIF;
})(EventEmitter);
module.exports = GIF;
},
{ "./browser.coffee": 2, events: 1 },
],
},
{},
[3]
)(3);
});

10
vendor/gif.js/gif.worker.ts vendored 100644

File diff suppressed because one or more lines are too long