diff --git a/index.html b/index.html index 9f05c85..556cef2 100644 --- a/index.html +++ b/index.html @@ -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; +} Alas, you need JavaScript to peruse this page. diff --git a/lib/animated-gif.ts b/lib/animated-gif.ts new file mode 100644 index 0000000..938c661 --- /dev/null +++ b/lib/animated-gif.ts @@ -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, + }); +} diff --git a/lib/auto-sizing-svg.tsx b/lib/auto-sizing-svg.tsx index 25092bc..d3aea5f 100644 --- a/lib/auto-sizing-svg.tsx +++ b/lib/auto-sizing-svg.tsx @@ -69,10 +69,58 @@ export const AutoSizingSvg = React.forwardRef( ref={ref} > {bgColor && ( - + )} {props.children} ); } ); + +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; + +export const SvgWithBackground: React.FC = ({ + x, + y, + width, + height, + bgColor, + children, +}) => ( + + {bgColor && ( + + )} + {children} + +); diff --git a/lib/export-svg.tsx b/lib/export-svg.tsx index e761f93..9760ca8 100644 --- a/lib/export-svg.tsx +++ b/lib/export-svg.tsx @@ -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 [ ``, "", '', - el.outerHTML, + svgMarkup, ].join("\n"); } -type ImageExporter = (svgEl: SVGSVGElement) => Promise; +type ProgressHandler = (value: number | null) => void; + +type ImageExporter = ( + svgEl: SVGSVGElement, + onProgress: ProgressHandler +) => Promise; /** * Initiates a download on the user's browser which downloads the given @@ -19,6 +27,7 @@ async function exportImage( svgRef: React.RefObject, 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 { + 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 { + const fps = animate.fps || 15; + const msecPerFrame = 1000 / fps; + const numFrames = Math.floor(animate.duration / msecPerFrame); + const svgMeta = getSvgMetadata(svgEl); + const render = (animPct: number) => ( + + {animate.render(animPct)} + + ); + 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; + animate?: ExportableAnimation | false; basename: string; -}> = ({ svgRef, basename }) => ( - <> - exportImage(svgRef, basename, "svg", exportSvg)}> - Export SVG - {" "} - exportImage(svgRef, basename, "png", exportPng)}> - Export PNG - - > -); +}> = ({ svgRef, basename, animate }) => { + const [progress, setProgress] = useState(null); + + if (progress !== null) { + return ( + + Exporting… + + + ); + } + + return ( + <> + + exportImage(svgRef, basename, "svg", setProgress, exportSvg) + } + > + Export SVG + {" "} + + exportImage(svgRef, basename, "png", setProgress, exportPng) + } + > + Export PNG + {" "} + {animate && ( + + exportImage( + svgRef, + basename, + "gif", + setProgress, + exportGif.bind(null, animate) + ) + } + > + Export GIF + + )} + > + ); +}; diff --git a/lib/pages/mandala-page.tsx b/lib/pages/mandala-page.tsx index a034ba4..f5357b2 100644 --- a/lib/pages/mandala-page.tsx +++ b/lib/pages/mandala-page.tsx @@ -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 = [ - , - ]; - - if (useTwoCircles) { - circles.push( + const makeMandala = (animPct: number): JSX.Element => { + const circles = [ - ); - if (firstBehindSecond) { - circles.reverse(); + key="first" + {...animateMandalaCircleParams(circle1, animPct)} + {...symbolCtx} + />, + ]; + + if (useTwoCircles) { + circles.push( + + ); + if (firstBehindSecond) { + circles.reverse(); + } } - } + + return {circles}; + }; return ( @@ -301,7 +306,13 @@ export const MandalaPage: React.FC<{}> = () => { }} /> - + = () => { > - {circles} + {makeMandala(animPct)} diff --git a/vendor/gif.js/gif.d.ts b/vendor/gif.js/gif.d.ts new file mode 100644 index 0000000..e357d40 --- /dev/null +++ b/vendor/gif.js/gif.d.ts @@ -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(); +} diff --git a/vendor/gif.js/gif.js b/vendor/gif.js/gif.js new file mode 100644 index 0000000..61c08ea --- /dev/null +++ b/vendor/gif.js/gif.js @@ -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); +}); diff --git a/vendor/gif.js/gif.worker.ts b/vendor/gif.js/gif.worker.ts new file mode 100644 index 0000000..8b0c997 --- /dev/null +++ b/vendor/gif.js/gif.worker.ts @@ -0,0 +1,10 @@ +/** + * This is just a string containing the gif.js worker JS. It alleviates + * us from having to find the file at runtime. + * + * Concept taken from: https://github.com/jnordberg/gif.js/issues/115 + */ +export const GIF_WORKER_JS = ` +// gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js +(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=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j
Alas, you need JavaScript to peruse this page.
Exporting…