code:
///<reference path="model.d.ts"/>

/*
 * Created by aimozg on 29.08.2020.
 */
namespace Renderer {
	const millitime = (typeof performance === 'object' && typeof performance.now === 'function') ?
		function () {
			return performance.now()
		} : function () {
			return new Date().getTime()
		};

	export interface LayerImageLoader {
		loadImage(src: string,
		          layer: CompositeLayer,
		          successCallback: (src: string, layer: CompositeLayer, image: HTMLImageElement) => any,
		          errorCallback: (src: string, layer: CompositeLayer, error: any) => any
		);
	}
	export const DefaultImageLoader: LayerImageLoader = {
		loadImage(src: string,
		          layer: CompositeLayer,
		          successCallback: (src: string, layer: CompositeLayer, image: HTMLImageElement) => any,
		          errorCallback: (src: string, layer: CompositeLayer, error: any) => any) {
			const image = new Image();
			image.onload = () => {
				successCallback(src, layer, image);
			}
			image.onerror = (event) => {
				errorCallback(src, layer, event);
			}
			image.src = src;
		}
	}
	export let ImageLoader: LayerImageLoader = DefaultImageLoader;

	export interface RendererListener {
		error?: (error: Error, context:any) => any;

		composeLayers?: (layers: CompositeLayer[]) => any;
		loaded?: (layer: string, src: string) => any;
		loadError?: (layer: string, src: string) => any;
		loadingDone?: (time: number, count: number) => any;

		beforeRender?: (layers: CompositeLayer[]) => any;
		layerCacheMiss?: (layer: CompositeLayer) => any;
		layerCacheHit?: (layer: CompositeLayer) => any;
		processingStep?: (layer: string, processing: string, canvas: HTMLCanvasElement, dt: number) => any;
		composition?: (layer: string, result: HTMLCanvasElement) => any;
		renderingDone?: (time: number) => any;

		keyframe?: (animation: string, keyframeIndex: number, keyframe: KeyframeSpec) => any;
		keyframeRender?: (spec: string, cacheHit: boolean, cacheRenderTime: number) => any;
		animationStop?: () => any;
	}
	function rendererError(listener:RendererListener, error:Error, context?:any) {
		if (listener && listener.error) {
			listener.error(error, context);
		} else {
			console.error(error);
		}
	}

	/**
	 * Last arguments to composeLayers
	 */
	export let lastCall: any[] | undefined = undefined;
	/**
	 * Last arguments to animateLayers
	 */
	export let lastAnimateCall: any[] | undefined = undefined;
	/**
	 * Last result of animateLayers
	 */
	export let lastAnimation: AnimatingCanvas | undefined = undefined;

	/**
	 * Use "pixels" of this size when generating images.
	 */
	export let pixelSize: number = 1;

	export function emptyLayerFilter():CompositeLayerParams {
		return {
			desaturate: false,
			blend: "",
			blendMode: "",
			brightness: 0.0,
			contrast: 1.0
		}
	}

	/**
	 * 0 -> "#000000", 0.5 -> "#808080", 1.0 -> "#FFFFFF"
	 */
	export function gray(value:number): string {
		value = Math.min(1, Math.max(0, value));
		value = Math.round(value*255);
		let s = value.toString(16);
		if (value < 16) s = '0' + s;
		return '#'+s+s+s;
	}

	export function createCanvas(w: number, h: number, fill?: string): CanvasRenderingContext2D {
		let c = document.createElement("canvas");
		c.width = w;
		c.height = h;
		let c2d = c.getContext('2d')!!
		if (fill) {
			c2d.fillStyle = fill;
			c2d.fillRect(0, 0, w, h);
		}
		return c2d;
	}

	export function ensureCanvas(image: CanvasImageSource): HTMLCanvasElement {
		if (image instanceof HTMLCanvasElement) {
			return image;
		}
		let i2 = createCanvas(image.width as number, image.height as number);
		i2.drawImage(image, 0, 0);
		return i2.canvas;
	}

	/**
	 * Free to use CanvasRenderingContext2D (to create image data, gradients, patterns)
	 */
	export const globalC2D = createCanvas(1,1);

	/**
	 * Creates a cutout of color in shape of sourceImage
	 */
	export function cutout(sourceImage: CanvasImageSource,
	                       color: string,
	                       canvas: CanvasRenderingContext2D = createCanvas(sourceImage.width as number, sourceImage.height as number)): CanvasRenderingContext2D {
		let sw = sourceImage.width as number;
		let sh = sourceImage.height as number;
		canvas.clearRect(0, 0, sw, sh);
		// Fill with target color
		canvas.globalCompositeOperation = 'source-over';
		canvas.fillStyle = color;
		canvas.fillRect(0, 0, sw, sh);

		return cutoutFrom(canvas, sourceImage);
	}

	/**
	 * Cuts out from base a shape in form of stencil.
	 * Modifies and returns base.
	 */
	export function cutoutFrom(base: CanvasRenderingContext2D,
	                           stencil: CanvasImageSource): CanvasRenderingContext2D {
		base.globalCompositeOperation = 'destination-in';
		base.drawImage(stencil, 0, 0);
		return base;
	}

	/**
	 * Paints sourceImage over cutout of it filled with color.
	 */
	export function composeOverCutout(sourceImage: CanvasImageSource,
	                                  color: string,
	                                  blendMode: string = 'multiply',
	                                  canvas: CanvasRenderingContext2D = createCanvas(sourceImage.width as number, sourceImage.height as number)
	): CanvasRenderingContext2D {
		canvas = cutout(sourceImage, color, canvas);
		// Multiply cutout with original
		canvas.globalCompositeOperation = blendMode;
		canvas.drawImage(sourceImage, 0, 0);

		return canvas;
	}

	/**
	 * Repeatedly fill all sub-frames of canvas with same style.
	 * (Makes sense with gradient and pattern fills, to keep consistents across all sub-frames)
	 */
	export function fillFrames(
		fillStyle: string|CanvasGradient|CanvasPattern,
		canvas: CanvasRenderingContext2D,
		frameCount: number,
		frameWidth: number
	) {
		const frameHeight = canvas.canvas.height;
		canvas.globalCompositeOperation = 'source-over';
		canvas.fillStyle = fillStyle;
		canvas.fillRect(0, 0, frameWidth, frameHeight);
		if (pixelSize > 1) {
			// downscale, redraw on temp canvas, then draw again
			const tw = Math.floor(frameWidth/pixelSize),
				th = Math.floor(frameHeight/pixelSize);
			const tmpcanvas = createCanvas(tw, th);
			tmpcanvas.imageSmoothingEnabled = false
			canvas.imageSmoothingEnabled = false
			tmpcanvas.drawImage(
				canvas.canvas,
				0, 0, frameWidth, frameHeight,
				0, 0, tw, th
			);
			canvas.drawImage(
				tmpcanvas.canvas,
				0, 0, tw, th,
				0, 0, frameWidth, frameHeight
			);
		}
		for (let i = 1; i<frameCount; i++) {
			canvas.drawImage(canvas.canvas,
				0, 0, frameWidth, frameHeight,
				i*frameWidth, 0, frameWidth, frameHeight);
		}
	}

	export let Patterns:Dict<CanvasPattern> = {};
	/**
	 * CanvasPattern generator/provider.
	 * Default implementation looks up in the Renderer.Patterns object, can be replaced to accept complex object
	 * and generate custom pattern.
	 */
	export let PatternProvider: (spec: string|object)=>(CanvasPattern|null) = (spec)=>{
		if (typeof spec === 'string' && spec in Patterns) return Patterns[spec];
		return null;
	};

	export function createGradient(spec: BlendGradientSpec): CanvasGradient {
		let gradient: CanvasGradient;
		switch (spec.gradient) {
			case "linear":
				gradient = globalC2D.createLinearGradient(
					spec.values[0], spec.values[1],
					spec.values[2], spec.values[3]
				);
				break;
			case "radial":
				gradient = globalC2D.createRadialGradient(
					spec.values[1], spec.values[2], spec.values[3],
					spec.values[4], spec.values[5], spec.values[6]
				);
				break;
			default:
				throw new Error("Invalid gradient type: "+spec.gradient);
		}
		if (spec.colors.length < 2) throw new Error("Invalid gradient stops: "+JSON.stringify(spec.colors));
		for (let i = 0; i < spec.colors.length; i++) {
			let stop = spec.colors[i];
			let offset:number, color:string;
			if (typeof stop === 'string') {
				color = stop;
				offset =  i/(spec.colors.length-1);
			} else {
				offset = stop[0];
				color = stop[1];
			}
			gradient.addColorStop(offset, color);
		}
		return gradient
	}

	/**
	 * Paints sourceImage over same-sized canvas filled with pattern or gradient
	 */
	export function composeOverSpecialRect(
		sourceImage: CanvasImageSource,
		fillStyle: CanvasGradient|CanvasPattern,
		blendMode: string,
		frameCount: number,
		targetCanvas: CanvasRenderingContext2D = createCanvas(
			sourceImage.width as number,
			sourceImage.height as number
		)
	): CanvasRenderingContext2D {
		let fw = (sourceImage.width as number)/frameCount;
		fillFrames(fillStyle, targetCanvas, frameCount, fw);

		targetCanvas.globalCompositeOperation = blendMode;
		targetCanvas.drawImage(sourceImage, 0, 0);
		return targetCanvas
	}

	/**
	 * Paints sourceImage over same-sized canvas filled with color
	 */
	export function composeOverRect(sourceImage: CanvasImageSource,
	                                color: string,
	                                blendMode: string,
	                                targetCanvas: CanvasRenderingContext2D = createCanvas(sourceImage.width as number,
		                                sourceImage.height as number)
	): CanvasRenderingContext2D {
		// Fill with target color
		targetCanvas.globalCompositeOperation = 'source-over';
		targetCanvas.fillStyle = color;
		targetCanvas.fillRect(0, 0, sourceImage.width as number, sourceImage.height as number);

		targetCanvas.globalCompositeOperation = blendMode;
		targetCanvas.drawImage(sourceImage, 0, 0);
		return targetCanvas
	}

	/**
	 * Paints over sourceImage a cutout of it filled with color.
	 */
	export function composeUnderCutout(sourceImage: CanvasImageSource,
	                                   color: string,
	                                   blendMode: string = 'multiply',
	                                   canvas: CanvasRenderingContext2D =
		                                   createCanvas(sourceImage.width as number, sourceImage.height as number)) {
		const cut = cutout(sourceImage, color);
		// Create a copy of sourceImage
		canvas.globalCompositeOperation = 'source-over';
		canvas.drawImage(sourceImage, 0, 0);
		// Multiply with cutout
		canvas.globalCompositeOperation = blendMode;
		canvas.drawImage(cut.canvas, 0, 0);

		return canvas;
	}

	/**
	 * Paints over sourceImage a same-sized canvas filled with color
	 */
	export function composeUnderRect(sourceImage: CanvasImageSource,
	                                   color: string,
	                                   blendMode: string = 'multiply',
	                                   targetCanvas: CanvasRenderingContext2D =
		                                   createCanvas(sourceImage.width as number, sourceImage.height as number)): CanvasRenderingContext2D {
		let fill = createCanvas(sourceImage.width as number, sourceImage.height as number, color);
		targetCanvas.globalCompositeOperation = 'source-over';
		targetCanvas.drawImage(sourceImage, 0, 0);
		targetCanvas.globalCompositeOperation = blendMode;
		targetCanvas.drawImage(fill.canvas, 0, 0);
		return targetCanvas
	}

	export let ImageCaches: {
		[index: string]: HTMLImageElement
	} = {};
	export let ImageErrors: {
		[index: string]: boolean
	} = {};

	/**
	 * Switch between compose(Over|Under)(Rect|Cutout)
	 */
	export function compose(
		composeOver: boolean,
		doCutout: boolean,
		sourceImage: CanvasImageSource,
		color: string,
		blendMode: string,
		targetCanvas: CanvasRenderingContext2D = createCanvas(
			sourceImage.width as number,
			sourceImage.height as number)
	): CanvasRenderingContext2D {
		if (doCutout) {
			if (composeOver) {
				return composeOverCutout(sourceImage, color, blendMode, targetCanvas);
			} else {
				return composeUnderCutout(sourceImage, color, blendMode, targetCanvas);
			}
		} else {
			if (composeOver) {
				return composeOverRect(sourceImage, color, blendMode, targetCanvas);
			} else {
				return composeUnderRect(sourceImage, color, blendMode, targetCanvas);
			}
		}
	}

	/**
	 * Fills properties in `target` from `source`.
	 * If `overwrite` is false, only missing properties are copied.
	 * In both cases, brightness is added, contrast is multiplied.
	 * Returns target
	 */
	export function mergeLayerData(target: CompositeLayerSpec, source: CompositeLayerParams, overwrite: boolean = false): CompositeLayerSpec {
		for (let k of Object.keys(source)) {
			if (k === 'brightness' && 'brightness' in target) {
				target.brightness += source.brightness;
			} else if (k === 'contrast' && 'contrast' in target) {
				target.contrast *= source.contrast;
			} else if (overwrite || !(k in target)) {
				target[k] = source[k];
			}
		}
		return target;
	}

	export function encodeProcessing(spec: CompositeLayerSpec): string {
		return JSON.stringify({
			// z, alpha, show, and frames regulate how layer is rendered onto target canvas
			src: spec.src,
			blend: spec.blend,
			blendMode: spec.blendMode,
			desaturate: spec.desaturate,
			brightness: spec.brightness,
			contrast: spec.contrast,
			prefilter: spec.prefilter,
			masksrc: spec.masksrc
		});

	}

	export function composeLayersAgain() {
		composeLayers.apply(Renderer, lastCall);
	}

	export function desaturateImage(image: CanvasImageSource,
	                                resultCanvas?: CanvasRenderingContext2D,
	                                doCutout: boolean = true): HTMLCanvasElement {
		return compose(false, doCutout, image, '#000000', 'saturation', resultCanvas).canvas;
	}

	export function filterImage(image: CanvasImageSource, filter: string, resultCanvas?: CanvasRenderingContext2D): HTMLCanvasElement {
		if (!resultCanvas) {
			resultCanvas = createCanvas(image.width as number, image.height as number);
		}
		resultCanvas.filter = filter;
		resultCanvas.globalCompositeOperation = 'source-over';
		resultCanvas.drawImage(image, 0, 0);
		resultCanvas.filter = '';
		return resultCanvas.canvas;
	}

	export function adjustBrightness(image: CanvasImageSource,
	                                 brightness: number,
	                                 resultCanvas?: CanvasRenderingContext2D,
	                                 doCutout: boolean = true): HTMLCanvasElement {
		if (brightness > 0) {
			const value = gray(brightness);
			// color-dodge by X% gray adjusts levels 0%-(100-X)% to 0%-100%
			return compose(false, doCutout, image, value, 'color-dodge', resultCanvas).canvas;
			// Other option:
			// screen by X% gray adjust levels 0%-100% to X%-100%
			// return composeUnderCutout(image, value, 'screen').canvas;
		} else {
			// multiply by X% gray adjusts levels 0%-100% to 0%-X%
			const value = gray(1+brightness);
			return compose(false, doCutout, image, value, 'multiply', resultCanvas).canvas;
			// Other option:
			// color-burn by X% gray adjusts levels (100-X)%-100% to 0%-100%
		}
	}

	export function adjustLevels(image: CanvasImageSource,
	                             /**
	                              * scale factor, 1 - no change, >1 - higher contrast, <1 - lower contrast.
	                              */
	                             factor: number,
	                             /**
	                              * shift, 0 - no change, >0 - brighter, <0 - darker
	                              */
	                             shift: number,
	                             resultCanvas?: CanvasRenderingContext2D
	                             ): HTMLCanvasElement {
		if (factor >= 1) {
			/*
			 color-dodge ( color, X ) = color / (1 - X) ; 0..(1-X) -> 0..1, (1-X) and brighter become white
			 color-burn ( color, Y ) = 1 - (1 - color) / Y ; (1-Y)..1 -> 0..1, (1-Y) and darker become black
			 color-burn ( color-dodge ( color, X ), Y ) = (1-1/Y) + color / (Y-X*Y)
														= shift   + color * factor
			 given (shift, factor), solving for (X, Y):
			 X = 1-(1-shift)/factor
			 Y = 1/(1 - shift)
			 */
			const x = 1 - (1 - shift) / factor;
			const y = 1 / (1 - shift);

			const c1 = compose(false, false, image, gray(x), 'color-dodge');
			const c2 = compose(false, false, c1.canvas, gray(y), 'color-burn', resultCanvas);
			return c2.canvas;
		} else {
			/*
			 multiply ( color, X ) = color * X ; 0..1 -> 0..X
			 screen ( color, Y ) = 1 - (1 - color) * (1 - Y) ; 0..1 -> Y..1
			 screen ( multiply ( color, X ), Y ) = 1 - (1 - color * X ) * (1 - Y)
			                                     = Y     + color * X*(1-Y)
			                                     = shift + color * factor
			 solving for (X, Y):
			 Y = shift
			 X = factor/(1-shift)
			 */
			const x = factor/(1-shift);
			const y = shift;
			const c1 = compose(false, false, image, gray(x), 'multiply');
			const c2 = compose(false, false, c1.canvas, gray(y), 'screen');
			return c2.canvas;
		}
	}

	export function adjustContrast(image: CanvasImageSource,
	                               factor: number,
	                               resultCanvas?: CanvasRenderingContext2D
	): HTMLCanvasElement {
		/*
		 contrast is scale by F with origin at 0.5
		*/
		const shift = 0.5*(1-factor);
		return adjustLevels(image, factor, shift, resultCanvas);
	}

	export function adjustBrightnessAndContrast(
		image: CanvasImageSource,
		brightness: number,
		contrast: number,
		resultCanvas?: CanvasRenderingContext2D
	): HTMLCanvasElement {
		// = adjustContrast (color + brightness, contrast)
		const shift = brightness*contrast + 0.5*(1-contrast);
		return adjustLevels(image, contrast, shift, resultCanvas);
	}

	export interface RenderPipelineContext {
		layer: CompositeLayer;
		/**
		 * Updates along the pipeline
		 */
		image: CanvasImageSource;
		listener: RendererListener;
		rects: LayerRects;
		needsCutout: boolean;

		// extra properties are allowed
		[index:string]: any;
	}

	/**
	 * Abstraction of layer processing steps.
	 * All steps are stored in RenderingPipeline array, and can be changed externally
	 */
	export interface RenderingStep {
		name:string;

		/**
		 * Return true if this step has to be performed
		 */
		condition(layer:CompositeLayer, context: RenderPipelineContext):boolean;

		/**
		 * Rendering function, returns resulting image.
		 */
		render(image:CanvasImageSource,
		                 layer:CompositeLayer,
		                 context: RenderPipelineContext
		): HTMLCanvasElement;
	}

	const RenderingStepDesaturate:RenderingStep = {
		name: "desaturate",

		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return layer.desaturate;
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			context.needsCutout = true;
			return desaturateImage(image, undefined, false);
		}
	};

	const RenderingStepPrefilter:RenderingStep = {
		name: "prefilter",
		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return layer.prefilter && layer.prefilter !== "none";
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			return filterImage(image, layer.prefilter);
		}

	};

	const RenderingStepBrightness:RenderingStep = {
		name: "brightness",
		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return typeof layer.brightness === 'number' && layer.brightness !== 0;
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			context.needsCutout = true;
			return adjustBrightness(image, layer.brightness, undefined, false);
		}
	}

	const RenderingStepContrast:RenderingStep = {
		name: "contrast",

		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return typeof layer.contrast === 'number' && layer.contrast !== 1;
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			context.needsCutout = true;
			return adjustContrast(image, layer.contrast, undefined);
		}

	}

	const RenderingStepBlendColor:RenderingStep = {
		name: "blend:color",

		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return layer.blendMode && layer.blend && typeof layer.blend === "string";
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			context.needsCutout = true;
			return composeOverRect(image, layer.blend as string, layer.blendMode).canvas;
		}
	}

	const RenderingStepBlendGradient:RenderingStep = {
		name: "blend:gradient",

		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return layer.blendMode && layer.blend && typeof layer.blend === 'object' && 'gradient' in layer.blend;
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			context.needsCutout = true;
			let gradient = createGradient(layer.blend as BlendGradientSpec);
			return composeOverSpecialRect(image, gradient, layer.blendMode, context.rects.subspriteFrameCount).canvas;
		}
	}

	const RenderingStepBlendPattern:RenderingStep = {
		name: "blend:pattern",

		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return layer.blendMode && layer.blend && typeof layer.blend === 'object' && 'pattern' in layer.blend;
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			context.needsCutout = true;
			let pattern = PatternProvider((layer.blend as BlendPatternSpec).pattern);
			if (!pattern) {
				return ensureCanvas(image);
			}
			return composeOverSpecialRect(image, pattern, layer.blendMode, context.rects.subspriteFrameCount).canvas;
		}
	}

	const RenderingStepMask:RenderingStep = {
		name: "mask",

		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return !!layer.mask;
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			return cutoutFrom(ensureCanvas(image).getContext('2d'), layer.mask).canvas;
		}
	}

	const RenderingStepCutout:RenderingStep = {
		name: "cutout",

		condition(layer: CompositeLayer, context: Renderer.RenderPipelineContext): boolean {
			return context.needsCutout;
		},

		render(image: CanvasImageSource, layer: CompositeLayer, context: Renderer.RenderPipelineContext): HTMLCanvasElement {
			return cutoutFrom(ensureCanvas(image).getContext('2d'), layer.image!!).canvas;
		}
	}

	/**
	 * Rendering steps used. Order matters!
	 */
	export const RenderingPipeline: RenderingStep[] = [
		RenderingStepDesaturate,
		RenderingStepPrefilter,
		RenderingStepBrightness,
		RenderingStepContrast,
		RenderingStepBlendPattern,
		RenderingStepBlendGradient,
		RenderingStepBlendColor,
		RenderingStepMask,
		RenderingStepCutout
	]

	export function processLayer(
		layer: CompositeLayer,
		rects: LayerRects,
		listener: RendererListener
	) {
		let context: RenderPipelineContext = {
			layer: layer,
			image: layer.image!!,
			needsCutout: false,
			rects: rects,
			listener: listener
		}
		for (let step of RenderingPipeline) {
			if (!step.condition(context.layer, context)) continue;
			let t0 = millitime();
			let listener = context.listener;
			context.image = step.render(context.image, context.layer, context);
			if (listener && listener.processingStep) {
				listener.processingStep(context.layer.name, step.name, context.image, millitime() - t0);
			}
		}
		return context.image;
	}

	interface LayerRects {
		width: number;
		height: number;
		frameWidth: number;
		frameCount: number;
		subspriteWidth: number;
		subspriteHeight: number;
		subspriteFrameCount: number;
		dx: number;
		dy: number;
	}
	function calcLayerRects(layer: CompositeLayer,
	                        layerImageWidth: number,
	                        targetWidth: number,
	                        targetHeight: number,
	                        frameCount: number): LayerRects {
		const frameWidth = targetWidth / frameCount;
		const subspriteWidth = layer.width || frameWidth;
		const subspriteHeight = layer.height || targetHeight;
		const dx = layer.dx || 0;
		const dy = layer.dy || 0;
		const subspriteFrameCount = layerImageWidth / subspriteWidth;
		return {
			width: targetWidth,
			height: targetHeight,
			frameWidth,
			frameCount,
			subspriteWidth,
			subspriteHeight,
			subspriteFrameCount,
			dx,
			dy
		}
	}
	export function composeProcessedLayer(layer: CompositeLayer,
	                                      targetCanvas: CanvasRenderingContext2D,
	                                      rects: LayerRects) {
		const image = layer.cachedImage;
		targetCanvas.filter = 'none';
		if (typeof layer.alpha === 'number') {
			targetCanvas.globalAlpha = layer.alpha;
		} else {
			targetCanvas.globalAlpha = 1.0;
		}

		const {frameWidth, frameCount, subspriteWidth, subspriteHeight, subspriteFrameCount, dx, dy} = rects;
		if (rects.subspriteFrameCount === frameCount && !layer.frames) {
			targetCanvas.drawImage(image, dx, dy);
		} else {
			for (let i = 0; i < frameCount; i++) {
				const imageFrameIndex = Math.min(subspriteFrameCount - 1,
					layer.frames ? layer.frames[i] : Math.floor(i * subspriteFrameCount / frameCount));
				targetCanvas.drawImage(image,
					imageFrameIndex * subspriteWidth, 0, subspriteWidth, subspriteHeight,
					dx + i * frameWidth, dy, subspriteWidth, subspriteHeight);
			}
		}
	}

	export function composeLayers(targetCanvas: CanvasRenderingContext2D,
	                              layerSpecs: CompositeLayerSpec[],
	                              frameCount: number,
	                              listener: RendererListener) {
		lastCall = [targetCanvas, layerSpecs, frameCount, listener];
		const t0 = millitime();
		// Sort layers by z-index, then array index
		const layers: CompositeLayer[] = layerSpecs
			.filter(layer =>
				layer.show !== false
				&& !(typeof layer.alpha === 'number' && layer.alpha <= 0.0)
			)
			.map((layer, i) => {
				if (isNaN(layer.z)) {
					console.error("Layer "+(layer.name||layer.src)+" has z-index NaN")
					layer.z = 0;
				}
				return [layer, i] as [CompositeLayerSpec, number];
			}) // map to pairs [element, index]
			.sort((a, b) => {
				if (a[0].z === b[0].z) return a[1] - b[1];
				else return a[0].z!! - b[0].z!!;
			})
			.map(e => e[0] as CompositeLayer); // unwrap values;
		if (listener && listener.composeLayers) listener.composeLayers(layers);

		// Tricky part.
		// We add <img> elements and hook on their onload event.
		// When image loads, we put it into layer 'image' property and kick maybeRenderResult
		// When all images are loaded, we call renderResult

		let rendered = false;
		let layersLoaded = 0;

		function renderResult() {
			rendered = true;
			targetCanvas.clearRect(0, 0, targetCanvas.canvas.width, targetCanvas.canvas.height);
			if (listener && listener.beforeRender) {
				listener.beforeRender(layers);
			}
			const targetWidth = targetCanvas.canvas.width;
			const targetHeight = targetCanvas.canvas.height;
			const t1 = millitime();
			for (const layer of layers) {
				if (layer.show === false) continue; // Could be disabled due to load error
				let name = layer.name || layer.src;
				let image = layer.image!!;
				let layerRects = calcLayerRects(layer, image.width as number, targetWidth, targetHeight, frameCount);
				let currentProcessing = encodeProcessing(layer);
				if (layer.cachedProcessing && layer.cachedImage && currentProcessing === layer.cachedProcessing) {
					if (listener && listener.layerCacheHit) {
						listener.layerCacheHit(layer);
					}
					image = layer.cachedImage;
				} else {
					if (listener && listener.layerCacheMiss) {
						listener.layerCacheMiss(layer);
					}
					image = processLayer(layer, layerRects, listener);
					layer.cachedProcessing = currentProcessing;
					layer.cachedImage = image;
				}
				composeProcessedLayer(layer, targetCanvas, layerRects);
				if (listener && listener.composition) {
					listener.composition(name, targetCanvas.canvas);
				}
			}
			if (listener && listener.renderingDone) listener.renderingDone(millitime() - t1);
		}

		function maybeRenderResult() {
			if (rendered) return;
			for (const layer of layers) {
				if (layer.show !== false && !layer.image) return;
				if (layer.masksrc && !layer.mask) return;
			}
			if (listener && listener.loadingDone) listener.loadingDone(millitime() - t0, layersLoaded);
			try {
				renderResult();
			} catch (e) {
				rendererError(listener, e);
			}
		}

		function loadLayerImage(layer: CompositeLayer) {
			ImageLoader.loadImage(
				layer.src,
				layer,
				(src,layer,image)=>{
					layersLoaded++;
					if (listener && listener.loaded) {
						listener.loaded(layer.name || 'unnamed', src);
					}
					layer.image = image;
					layer.imageSrc = src;
					ImageCaches[src] = image;
					maybeRenderResult();
				},
				(src,layer,error)=>{
					// Mark this src as erroneous to avoid blinking due to reload attempts
					ImageErrors[src] = true;
					if (listener && listener.loadError) {
						listener.loadError(layer.name || 'unnamed', src);
					} else {
						console.error('Failed to load image ' + src + (layer.name ? ' for layer ' + layer.name : ''));
					}
					layer.show = false;
					maybeRenderResult();
				}
			)
		}
		function loadLayerMask(layer: CompositeLayer) {
			ImageLoader.loadImage(
				layer.masksrc,
				layer,
				(src,layer,image)=>{
					layersLoaded++;
					if (listener && listener.loaded) {
						listener.loaded(layer.name || 'unnamed', src);
					}
					layer.mask = image;
					layer.cachedMaskSrc = src;
					ImageCaches[src] = image;
					maybeRenderResult();
				},
				(src,layer,error)=>{
					// Mark this src as erroneous to avoid blinking due to reload attempts
					ImageErrors[src] = true;
					if (listener && listener.loadError) {
						listener.loadError(layer.name || 'unnamed', src);
					} else {
						console.error('Failed to load mask ' + src + (layer.name ? ' for layer ' + layer.name : ''));
					}
					delete layer.masksrc;
					maybeRenderResult();
				}
			)
		}

		for (const layer of layers) {
			let needImage = true;
			if (layer.image) {
				if (layer.imageSrc === layer.src) {
					needImage = false;
				} else {
					// Layer was loaded in previous render, but then its src was changed - purge cache
					delete layer.image;
					delete layer.imageSrc;
				}
			}
			if (needImage) {
				if (ImageErrors[layer.src]) {
					layer.show = false;
					continue;
				} else if (layer.src in ImageCaches) {
					layer.image = ImageCaches[layer.src];
					layer.imageSrc = layer.src;
				} else {
					loadLayerImage(layer);
				}
			}
			let needMask = !!layer.masksrc;
			if (layer.mask) {
				if (layer.cachedMaskSrc === layer.masksrc) {
					needMask = false;
				} else {
					// Layer mask was loaded in previous render, but then its masksrc was changed - purge cache
					delete layer.mask;
					delete layer.cachedMaskSrc;
				}
			}
			if (needMask) {
				if (ImageErrors[layer.masksrc]) {
					delete layer.masksrc;
				} else if (layer.masksrc in ImageCaches) {
					layer.mask = ImageCaches[layer.masksrc];
					layer.cachedMaskSrc = layer.masksrc;
				} else {
					loadLayerMask(layer);
				}
			}
		}

		maybeRenderResult();
	}

	export interface AnimationInfo {
		spec: KeyframeAnimationSpec;
		/**
		 * True if any layer properties other than `frame`, are animated
		 */
		complex: boolean;
		/**
		 * Affected layers
		 */
		layers: CompositeLayerSpec[];
		name: string;
		/**
		 * id from setTimeout
		 */
		timeoutId: number;
		/**
		 * Current keyframe index
		 */
		keyframeIndex: number;
		/**
		 * Current keyframe
		 */
		keyframe: KeyframeSpec;
		/**
		 * Time of current keyframe start, milliseconds
		 */
		time: number;
	}

	export interface AnimatingCanvas {
		playing: boolean;
		time: number;
		target: CanvasRenderingContext2D;
		keyframeCaches: Dict<CanvasRenderingContext2D>;
		animations: AnimationInfo[];
		/**
		 * True during rendering a frame
		 */
		busy: boolean;

		redraw(): void;

		invalidateCaches(): void;

		start(): void;

		stop(): void;
	}

	export function invalidateLayerCaches(layers: CompositeLayer[]) {
		for (let layer of layers) {
			delete layer.image;
			delete layer.imageSrc;
			delete layer.mask;
			delete layer.cachedMaskSrc;
			delete layer.cachedImage;
			delete layer.cachedProcessing;
		}
	}

	export function animateLayersAgain() {
		return animateLayers.apply(Renderer, lastAnimateCall);
	}

	const animatingCanvases = new WeakMap<CanvasRenderingContext2D, AnimatingCanvas>();

	export let Animations: Dict<AnimationSpec> = {};
	/**
	 * Animation spec provider; default implementation is look up in Renderer.Animations by layer's `animation` property.
	 *
	 * Can be overridden to auto-generate animations, for example.
	 */
	export let AnimationProvider: (layer:CompositeLayerSpec)=>(AnimationSpec|undefined) =
			layer=>Animations[layer.animation];

	/**
	 * Animatable properties of KeyframeSpec and CompositeLayer
	 */
	export const AnimatableProps = ["alpha","show","blend","brightness","contrast","dx","dy"];
	export function animateLayers(targetCanvas: CanvasRenderingContext2D,
	                              layerSpecs: CompositeLayerSpec[],
	                              listener: RendererListener,
	                              autoStop: boolean = true): AnimatingCanvas {
		lastAnimateCall = [targetCanvas, layerSpecs, listener, autoStop];
		const keyframeCaches: Dict<CanvasRenderingContext2D> = {};

		function invalidateCaches() {
			for (let key in keyframeCaches) delete keyframeCaches[key];
		}

		let schedule: {
			[index: number]: Function[]
		} = {};
		// this mess should become a class already
		const animatingCanvas: AnimatingCanvas = {
			target: targetCanvas,
			keyframeCaches: keyframeCaches,
			animations: [],
			playing: false,
			busy: false,
			start() {
				if (this.playing) this.stop();
				this.playing = true;
				// stop previous animation on this targetCanvas, if present
				let oldAnimation = animatingCanvases.get(targetCanvas);
				if (oldAnimation != null) {
					oldAnimation.stop();
				}
				animatingCanvases.set(targetCanvas, this);
				let usedAnimations: Dict<AnimationInfo> = {};
				for (let layer of layerSpecs) {
					if (!layer.src || layer.show === false) continue;
					if (layer.animation) {
						let spec = AnimationProvider(layer);
						if (!spec) {
							console.error("Layer '" + (layer.name || layer.src) + "' animation '" + layer.animation + "' not found");
							continue;
						}
						let complex = false;
						if ('frames' in spec) {
							let frames = spec.frames, duration = spec.duration;
							spec = {
								keyframes: []
							};
							for (let i = 0; i < frames; i++) {
								spec.keyframes.push({frame: i, duration: duration});
							}
						} else {
							for (let kf of spec.keyframes) {
								for (let ap of AnimatableProps) {
									if (ap in kf) {
										complex = true;
										break;
									}
								}
								if (complex) break;
							}
						}
						let animation = usedAnimations[layer.animation];
						if (!animation) {
							animation = usedAnimations[layer.animation] = {
								name: layer.animation,
								complex: complex,
								spec: spec,
								timeoutId: 0,
								keyframeIndex: 0,
								keyframe: spec.keyframes[0],
								layers: [],
								time: 0
							};
						}
						animation.layers.push(layer);
						applyKeyframe(animation.keyframe, layer);
					} else {
						layer.frames = [0];
					}
				}
				this.animations = Object.values(usedAnimations);
				for (let animation of this.animations) {
					scheduleNextKeyframe(animation);
					if (listener && listener.keyframe) listener.keyframe(animation.name, animation.keyframeIndex, animation.keyframe);
				}
				compose().catch((e)=>{if (e) console.error(e)});
			},
			stop() {
				if (!this.playing) return;
				this.playing = false;
				animatingCanvases.delete(targetCanvas);
				for (let info of this.animations) {
					if (info.timeoutId) clearTimeout(info.timeoutId);
				}
				schedule = {};
				this.animations.splice(0);
				invalidateCaches();
				if (listener && listener.animationStop) listener.animationStop();
			},
			invalidateCaches,
			time: 0,
			redraw() {
				compose().catch((e)=>{if (e) console.error(e)});
			}
		}

		function genAnimationSpec(): string {
			let j = {};
			for (let animation of animatingCanvas.animations) {
				if (animation.complex) {
					j[animation.name] = animation.keyframeIndex;
				} else {
					j[animation.name] = animation.keyframe.frame;
				}
			}
			return JSON.stringify(j);
		}

		function scheduleNextKeyframe(animation: AnimationInfo) {
			if (animation.keyframe.duration <= 0) return;
			let t1 = animation.time + animation.keyframe.duration;
			let tasks = schedule[t1];
			if (!tasks) {
				schedule[t1] = tasks = [];
				animation.timeoutId = window.setTimeout(() => {
					try {
						delete schedule[t1];
						animatingCanvas.time = Math.max(t1, animatingCanvas.time);
						for (let task of tasks) task();
						compose().catch((e)=>{if (e) console.error(e)});
					} catch (e) {
						rendererError(listener, e);
					}
				}, animation.keyframe.duration);
			} else {
				animation.timeoutId = 0;
			}
			tasks.push(() => {
				animation.time = t1;
				nextKeyframe(animation)
			});
		}

		function applyKeyframe(keyframe: KeyframeSpec, layer: CompositeLayer) {
			layer.frames = [keyframe.frame];
			for (let ap of AnimatableProps) {
				if (ap in keyframe) layer[ap] = keyframe[ap];
			}
		}

		function nextKeyframe(animation: AnimationInfo) {
			let keyframes = animation.spec.keyframes;
			animation.keyframeIndex = (animation.keyframeIndex + 1) % keyframes.length;
			animation.keyframe = keyframes[animation.keyframeIndex];
			for (let layer of animation.layers) {
				applyKeyframe(animation.keyframe, layer);
			}
			scheduleNextKeyframe(animation);
			if (listener && listener.keyframe) listener.keyframe(animation.name, animation.keyframeIndex, animation.keyframe);
		}

		function stopCheck():boolean {
			if (autoStop && animatingCanvas.time > 0 && !(document.body.contains(targetCanvas.canvas))) {
				/* the canvas was removed from DOM. we exclude frame 0 because it might not yet be added */
				animatingCanvas.stop();
				return true;
			}
			return false;
		}
		function compose(): Promise<void> {
			if (stopCheck() || animatingCanvas.busy) {
				return Promise.reject();
			}
			animatingCanvas.busy = true;
			return new Promise((resolve,reject)=>{
				requestAnimationFrame(()=>{
					animatingCanvas.busy = false;
					try {
						doCompose0();
						resolve();
					} catch (e) {
						rendererError(listener, e);
						reject(e)
					}
				})
			})
			function doCompose0() {
				let spec = genAnimationSpec();
				let cachedCanvas = keyframeCaches[spec];
				if (cachedCanvas) {
					const t0 = millitime();
					targetCanvas.clearRect(0, 0, targetCanvas.canvas.width, targetCanvas.canvas.height);
					targetCanvas.globalAlpha = 1.0;
					targetCanvas.drawImage(cachedCanvas.canvas, 0, 0);
					if (listener && listener.keyframeRender) {
						listener.keyframeRender(spec, true, millitime() - t0);
					}
				} else {
					if (listener && listener.keyframeRender) {
						listener.keyframeRender(spec, false, 0);
					}
					const myListener = Object.assign({}, listener, {
						renderingDone(time) {
							let canvas = createCanvas(targetCanvas.canvas.width, targetCanvas.canvas.height);
							canvas.drawImage(targetCanvas.canvas, 0, 0);
							keyframeCaches[genAnimationSpec()] = canvas;
							if (listener && listener.renderingDone) listener.renderingDone.apply(listener, arguments);
						}
					})
					try {
						composeLayers(targetCanvas, layerSpecs, 1, myListener);
					} catch (e) {
						animatingCanvas.stop();
						throw e;
					}
				}
			}
		}

		animatingCanvas.start();

		return (lastAnimation = animatingCanvas)
	}

	/**
	 * Linear interpolation.
	 *
	 * f(0) = min,
	 * f(1) = max.
	 */
	export function lint(value: number, min: number, max: number, allowOverflow = false): number {
		if (!allowOverflow) value = Math.min(1, Math.max(0, value));
		return value * (max - min) + min;
	}

	export function lintArray(value: number, mins: number[], maxes: number[], allowOverflow = false): number[] {
		return mins.map((min, i) => lint(value, min, maxes[i], allowOverflow));
	}

	export function lintStaged(value: number, points: number[]): number {
		value = Math.min(1, Math.max(0, value));
		const n = points.length - 1;
		let i = (value * n) | 0;
		if (i === n) i = n - 1;
		return lint(value * n - i, points[i], points[i + 1]);
	}

	export function lintRgb(value: number, min: tinycolor.ColorInput, max: tinycolor.ColorInput): tinycolor.Instance {
		min = tinycolor(min).toRgb();
		max = tinycolor(max).toRgb();
		return tinycolor({
			r: lint(value, min.r, max.r),
			g: lint(value, min.g, max.g),
			b: lint(value, min.b, max.b)
		});
	}

	export function lintRgbStaged(value: number, points: tinycolor.ColorInput[]): tinycolor.Instance {
		value = Math.min(1, Math.max(0, value));
		const n = points.length - 1;
		let i = (value * n) | 0;
		if (i === n) i = n - 1;
		return lintRgb(value * n - i, points[i], points[i + 1]);
	}

	window.Renderer = Renderer;
	// Expose library functions needed by model evaluation, to global ns
	window.lint = Renderer.lint;
	window.lintArray = Renderer.lintArray;
	window.lintStaged = Renderer.lintStaged;
	window.lintRgb = Renderer.lintRgb;
	window.lintRgbStaged = Renderer.lintRgbStaged;
}

interface Window {
	lint(value: number, min: number, max: number, allowOverflow?: boolean): number;

	lintArray(value: number, mins: number[], maxes: number[], allowOverflow?: boolean): number[];

	lintStaged(value: number, points: number[]): number;

	lintRgb(value: number, min: tinycolor.ColorInput, max: tinycolor.ColorInput): tinycolor.Instance;

	lintRgbStaged(value: number, points: tinycolor.ColorInput[]): tinycolor.Instance;
}


code:
// TinyColor v1.4.1
// https://github.com/bgrins/TinyColor
// Brian Grinstead, MIT License

(function(Math) {

    var trimLeft = /^\s+/,
        trimRight = /\s+$/,
        tinyCounter = 0,
        mathRound = Math.round,
        mathMin = Math.min,
        mathMax = Math.max,
        mathRandom = Math.random;

    function tinycolor (color, opts) {

        color = (color) ? color : '';
        opts = opts || { };

        // If input is already a tinycolor, return itself
        if (color instanceof tinycolor) {
           return color;
        }
        // If we are called as a function, call using new instead
        if (!(this instanceof tinycolor)) {
            return new tinycolor(color, opts);
        }

        var rgb = inputToRGB(color);
        this._originalInput = color,
        this._r = rgb.r,
        this._g = rgb.g,
        this._b = rgb.b,
        this._a = rgb.a,
        this._roundA = mathRound(100*this._a) / 100,
        this._format = opts.format || rgb.format;
        this._gradientType = opts.gradientType;

        // Don't let the range of [0,255] come back in [0,1].
        // Potentially lose a little bit of precision here, but will fix issues where
        // .5 gets interpreted as half of the total, instead of half of 1
        // If it was supposed to be 128, this was already taken care of by `inputToRgb`
        if (this._r < 1) { this._r = mathRound(this._r); }
        if (this._g < 1) { this._g = mathRound(this._g); }
        if (this._b < 1) { this._b = mathRound(this._b); }

        this._ok = rgb.ok;
        this._tc_id = tinyCounter++;
    }

    tinycolor.prototype = {
        isDark: function() {
            return this.getBrightness() < 128;
        },
        isLight: function() {
            return !this.isDark();
        },
        isValid: function() {
            return this._ok;
        },
        getOriginalInput: function() {
          return this._originalInput;
        },
        getFormat: function() {
            return this._format;
        },
        getAlpha: function() {
            return this._a;
        },
        getBrightness: function() {
            //http://www.w3.org/TR/AERT#color-contrast
            var rgb = this.toRgb();
            return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
        },
        getLuminance: function() {
            //http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
            var rgb = this.toRgb();
            var RsRGB, GsRGB, BsRGB, R, G, B;
            RsRGB = rgb.r/255;
            GsRGB = rgb.g/255;
            BsRGB = rgb.b/255;

            if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);}
            if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);}
            if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);}
            return (0.2126 * R) + (0.7152 * G) + (0.0722 * B);
        },
        setAlpha: function(value) {
            this._a = boundAlpha(value);
            this._roundA = mathRound(100*this._a) / 100;
            return this;
        },
        toHsv: function() {
            var hsv = rgbToHsv(this._r, this._g, this._b);
            return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a };
        },
        toHsvString: function() {
            var hsv = rgbToHsv(this._r, this._g, this._b);
            var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
            return (this._a == 1) ?
              "hsv("  + h + ", " + s + "%, " + v + "%)" :
              "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")";
        },
        toHsl: function() {
            var hsl = rgbToHsl(this._r, this._g, this._b);
            return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a };
        },
        toHslString: function() {
            var hsl = rgbToHsl(this._r, this._g, this._b);
            var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
            return (this._a == 1) ?
              "hsl("  + h + ", " + s + "%, " + l + "%)" :
              "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")";
        },
        toHex: function(allow3Char) {
            return rgbToHex(this._r, this._g, this._b, allow3Char);
        },
        toHexString: function(allow3Char) {
            return '#' + this.toHex(allow3Char);
        },
        toHex8: function(allow4Char) {
            return rgbaToHex(this._r, this._g, this._b, this._a, allow4Char);
        },
        toHex8String: function(allow4Char) {
            return '#' + this.toHex8(allow4Char);
        },
        toRgb: function() {
            return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a };
        },
        toRgbString: function() {
            return (this._a == 1) ?
              "rgb("  + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" :
              "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
        },
        toPercentageRgb: function() {
            return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a };
        },
        toPercentageRgbString: function() {
            return (this._a == 1) ?
              "rgb("  + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" :
              "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
        },
        toName: function() {
            if (this._a === 0) {
                return "transparent";
            }

            if (this._a < 1) {
                return false;
            }

            return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
        },
        toFilter: function(secondColor) {
            var hex8String = '#' + rgbaToArgbHex(this._r, this._g, this._b, this._a);
            var secondHex8String = hex8String;
            var gradientType = this._gradientType ? "GradientType = 1, " : "";

            if (secondColor) {
                var s = tinycolor(secondColor);
                secondHex8String = '#' + rgbaToArgbHex(s._r, s._g, s._b, s._a);
            }

            return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")";
        },
        toString: function(format) {
            var formatSet = !!format;
            format = format || this._format;

            var formattedString = false;
            var hasAlpha = this._a < 1 && this._a >= 0;
            var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "hex4" || format === "hex8" || format === "name");

            if (needsAlphaFormat) {
                // Special case for "transparent", all other non-alpha formats
                // will return rgba when there is transparency.
                if (format === "name" && this._a === 0) {
                    return this.toName();
                }
                return this.toRgbString();
            }
            if (format === "rgb") {
                formattedString = this.toRgbString();
            }
            if (format === "prgb") {
                formattedString = this.toPercentageRgbString();
            }
            if (format === "hex" || format === "hex6") {
                formattedString = this.toHexString();
            }
            if (format === "hex3") {
                formattedString = this.toHexString(true);
            }
            if (format === "hex4") {
                formattedString = this.toHex8String(true);
            }
            if (format === "hex8") {
                formattedString = this.toHex8String();
            }
            if (format === "name") {
                formattedString = this.toName();
            }
            if (format === "hsl") {
                formattedString = this.toHslString();
            }
            if (format === "hsv") {
                formattedString = this.toHsvString();
            }

            return formattedString || this.toHexString();
        },
        clone: function() {
            return tinycolor(this.toString());
        },

        _applyModification: function(fn, args) {
            var color = fn.apply(null, [this].concat([].slice.call(args)));
            this._r = color._r;
            this._g = color._g;
            this._b = color._b;
            this.setAlpha(color._a);
            return this;
        },
        lighten: function() {
            return this._applyModification(lighten, arguments);
        },
        brighten: function() {
            return this._applyModification(brighten, arguments);
        },
        darken: function() {
            return this._applyModification(darken, arguments);
        },
        desaturate: function() {
            return this._applyModification(desaturate, arguments);
        },
        saturate: function() {
            return this._applyModification(saturate, arguments);
        },
        greyscale: function() {
            return this._applyModification(greyscale, arguments);
        },
        spin: function() {
            return this._applyModification(spin, arguments);
        },

        _applyCombination: function(fn, args) {
            return fn.apply(null, [this].concat([].slice.call(args)));
        },
        analogous: function() {
            return this._applyCombination(analogous, arguments);
        },
        complement: function() {
            return this._applyCombination(complement, arguments);
        },
        monochromatic: function() {
            return this._applyCombination(monochromatic, arguments);
        },
        splitcomplement: function() {
            return this._applyCombination(splitcomplement, arguments);
        },
        triad: function() {
            return this._applyCombination(triad, arguments);
        },
        tetrad: function() {
            return this._applyCombination(tetrad, arguments);
        }
    };

    // If input is an object, force 1 into "1.0" to handle ratios properly
    // String input requires "1.0" as input, so 1 will be treated as 1
    tinycolor.fromRatio = function(color, opts) {
        if (typeof color == "object") {
            var newColor = {};
            for (var i in color) {
                if (color.hasOwnProperty(i)) {
                    if (i === "a") {
                        newColor[i] = color[i];
                    }
                    else {
                        newColor[i] = convertToPercentage(color[i]);
                    }
                }
            }
            color = newColor;
        }

        return tinycolor(color, opts);
    };

    // Given a string or object, convert that input to RGB
    // Possible string inputs:
    //
    //     "red"
    //     "#f00" or "f00"
    //     "#ff0000" or "ff0000"
    //     "#ff000000" or "ff000000"
    //     "rgb 255 0 0" or "rgb (255, 0, 0)"
    //     "rgb 1.0 0 0" or "rgb (1, 0, 0)"
    //     "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
    //     "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
    //     "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
    //     "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
    //     "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
    //
    function inputToRGB(color) {

        var rgb = { r: 0, g: 0, b: 0 };
        var a = 1;
        var s = null;
        var v = null;
        var l = null;
        var ok = false;
        var format = false;

        if (typeof color == "string") {
            color = stringInputToObject(color);
        }

        if (typeof color == "object") {
            if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
                rgb = rgbToRgb(color.r, color.g, color.b);
                ok = true;
                format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
            }
            else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
                s = convertToPercentage(color.s);
                v = convertToPercentage(color.v);
                rgb = hsvToRgb(color.h, s, v);
                ok = true;
                format = "hsv";
            }
            else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
                s = convertToPercentage(color.s);
                l = convertToPercentage(color.l);
                rgb = hslToRgb(color.h, s, l);
                ok = true;
                format = "hsl";
            }

            if (color.hasOwnProperty("a")) {
                a = color.a;
            }
        }

        a = boundAlpha(a);

        return {
            ok: ok,
            format: color.format || format,
            r: mathMin(255, mathMax(rgb.r, 0)),
            g: mathMin(255, mathMax(rgb.g, 0)),
            b: mathMin(255, mathMax(rgb.b, 0)),
            a: a
        };
    }

    // Conversion Functions
    // --------------------

    // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
    // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>

    // `rgbToRgb`
    // Handle bounds / percentage checking to conform to CSS color spec
    // <http://www.w3.org/TR/css3-color/>
    // *Assumes:* r, g, b in [0, 255] or [0, 1]
    // *Returns:* { r, g, b } in [0, 255]
    function rgbToRgb(r, g, b){
        return {
            r: bound01(r, 255) * 255,
            g: bound01(g, 255) * 255,
            b: bound01(b, 255) * 255
        };
    }

    // `rgbToHsl`
    // Converts an RGB color value to HSL.
    // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
    // *Returns:* { h, s, l } in [0,1]
    function rgbToHsl(r, g, b) {

        r = bound01(r, 255);
        g = bound01(g, 255);
        b = bound01(b, 255);

        var max = mathMax(r, g, b), min = mathMin(r, g, b);
        var h, s, l = (max + min) / 2;

        if(max == min) {
            h = s = 0; // achromatic
        }
        else {
            var d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            switch(max) {
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }

            h /= 6;
        }

        return { h: h, s: s, l: l };
    }

    // `hslToRgb`
    // Converts an HSL color value to RGB.
    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
    // *Returns:* { r, g, b } in the set [0, 255]
    function hslToRgb(h, s, l) {
        var r, g, b;

        h = bound01(h, 360);
        s = bound01(s, 100);
        l = bound01(l, 100);

        function hue2rgb(p, q, t) {
            if(t < 0) t += 1;
            if(t > 1) t -= 1;
            if(t < 1/6) return p + (q - p) * 6 * t;
            if(t < 1/2) return q;
            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        }

        if(s === 0) {
            r = g = b = l; // achromatic
        }
        else {
            var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            var p = 2 * l - q;
            r = hue2rgb(p, q, h + 1/3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1/3);
        }

        return { r: r * 255, g: g * 255, b: b * 255 };
    }

    // `rgbToHsv`
    // Converts an RGB color value to HSV
    // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
    // *Returns:* { h, s, v } in [0,1]
    function rgbToHsv(r, g, b) {

        r = bound01(r, 255);
        g = bound01(g, 255);
        b = bound01(b, 255);

        var max = mathMax(r, g, b), min = mathMin(r, g, b);
        var h, s, v = max;

        var d = max - min;
        s = max === 0 ? 0 : d / max;

        if(max == min) {
            h = 0; // achromatic
        }
        else {
            switch(max) {
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }
            h /= 6;
        }
        return { h: h, s: s, v: v };
    }

    // `hsvToRgb`
    // Converts an HSV color value to RGB.
    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
    // *Returns:* { r, g, b } in the set [0, 255]
     function hsvToRgb(h, s, v) {

        h = bound01(h, 360) * 6;
        s = bound01(s, 100);
        v = bound01(v, 100);

        var i = Math.floor(h),
            f = h - i,
            p = v * (1 - s),
            q = v * (1 - f * s),
            t = v * (1 - (1 - f) * s),
            mod = i % 6,
            r = [v, q, p, p, t, v][mod],
            g = [t, v, v, q, p, p][mod],
            b = [p, p, t, v, v, q][mod];

        return { r: r * 255, g: g * 255, b: b * 255 };
    }

    // `rgbToHex`
    // Converts an RGB color to hex
    // Assumes r, g, and b are contained in the set [0, 255]
    // Returns a 3 or 6 character hex
    function rgbToHex(r, g, b, allow3Char) {

        var hex = [
            pad2(mathRound(r).toString(16)),
            pad2(mathRound(g).toString(16)),
            pad2(mathRound(b).toString(16))
        ];

        // Return a 3 character hex if possible
        if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
            return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
        }

        return hex.join("");
    }

    // `rgbaToHex`
    // Converts an RGBA color plus alpha transparency to hex
    // Assumes r, g, b are contained in the set [0, 255] and
    // a in [0, 1]. Returns a 4 or 8 character rgba hex
    function rgbaToHex(r, g, b, a, allow4Char) {

        var hex = [
            pad2(mathRound(r).toString(16)),
            pad2(mathRound(g).toString(16)),
            pad2(mathRound(b).toString(16)),
            pad2(convertDecimalToHex(a))
        ];

        // Return a 4 character hex if possible
        if (allow4Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1) && hex[3].charAt(0) == hex[3].charAt(1)) {
            return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
        }

        return hex.join("");
    }

    // `rgbaToArgbHex`
    // Converts an RGBA color to an ARGB Hex8 string
    // Rarely used, but required for "toFilter()"
    function rgbaToArgbHex(r, g, b, a) {

        var hex = [
            pad2(convertDecimalToHex(a)),
            pad2(mathRound(r).toString(16)),
            pad2(mathRound(g).toString(16)),
            pad2(mathRound(b).toString(16))
        ];

        return hex.join("");
    }

    // `equals`
    // Can be called with any tinycolor input
    tinycolor.equals = function (color1, color2) {
        if (!color1 || !color2) { return false; }
        return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
    };

    tinycolor.random = function() {
        return tinycolor.fromRatio({
            r: mathRandom(),
            g: mathRandom(),
            b: mathRandom()
        });
    };

    // Modification Functions
    // ----------------------
    // Thanks to less.js for some of the basics here
    // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>

    function desaturate(color, amount) {
        amount = (amount === 0) ? 0 : (amount || 10);
        var hsl = tinycolor(color).toHsl();
        hsl.s -= amount / 100;
        hsl.s = clamp01(hsl.s);
        return tinycolor(hsl);
    }

    function saturate(color, amount) {
        amount = (amount === 0) ? 0 : (amount || 10);
        var hsl = tinycolor(color).toHsl();
        hsl.s += amount / 100;
        hsl.s = clamp01(hsl.s);
        return tinycolor(hsl);
    }

    function greyscale(color) {
        return tinycolor(color).desaturate(100);
    }

    function lighten (color, amount) {
        amount = (amount === 0) ? 0 : (amount || 10);
        var hsl = tinycolor(color).toHsl();
        hsl.l += amount / 100;
        hsl.l = clamp01(hsl.l);
        return tinycolor(hsl);
    }

    function brighten(color, amount) {
        amount = (amount === 0) ? 0 : (amount || 10);
        var rgb = tinycolor(color).toRgb();
        rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
        rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
        rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
        return tinycolor(rgb);
    }

    function darken (color, amount) {
        amount = (amount === 0) ? 0 : (amount || 10);
        var hsl = tinycolor(color).toHsl();
        hsl.l -= amount / 100;
        hsl.l = clamp01(hsl.l);
        return tinycolor(hsl);
    }

    // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
    // Values outside of this range will be wrapped into this range.
    function spin(color, amount) {
        var hsl = tinycolor(color).toHsl();
        var hue = (hsl.h + amount) % 360;
        hsl.h = hue < 0 ? 360 + hue : hue;
        return tinycolor(hsl);
    }

    // Combination Functions
    // ---------------------
    // Thanks to jQuery xColor for some of the ideas behind these
    // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>

    function complement(color) {
        var hsl = tinycolor(color).toHsl();
        hsl.h = (hsl.h + 180) % 360;
        return tinycolor(hsl);
    }

    function triad(color) {
        var hsl = tinycolor(color).toHsl();
        var h = hsl.h;
        return [
            tinycolor(color),
            tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
            tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
        ];
    }

    function tetrad(color) {
        var hsl = tinycolor(color).toHsl();
        var h = hsl.h;
        return [
            tinycolor(color),
            tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
            tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
            tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
        ];
    }

    function splitcomplement(color) {
        var hsl = tinycolor(color).toHsl();
        var h = hsl.h;
        return [
            tinycolor(color),
            tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
            tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
        ];
    }

    function analogous(color, results, slices) {
        results = results || 6;
        slices = slices || 30;

        var hsl = tinycolor(color).toHsl();
        var part = 360 / slices;
        var ret = [tinycolor(color)];

        for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
            hsl.h = (hsl.h + part) % 360;
            ret.push(tinycolor(hsl));
        }
        return ret;
    }

    function monochromatic(color, results) {
        results = results || 6;
        var hsv = tinycolor(color).toHsv();
        var h = hsv.h, s = hsv.s, v = hsv.v;
        var ret = [];
        var modification = 1 / results;

        while (results--) {
            ret.push(tinycolor({ h: h, s: s, v: v}));
            v = (v + modification) % 1;
        }

        return ret;
    }

    // Utility Functions
    // ---------------------

    tinycolor.mix = function(color1, color2, amount) {
        amount = (amount === 0) ? 0 : (amount || 50);

        var rgb1 = tinycolor(color1).toRgb();
        var rgb2 = tinycolor(color2).toRgb();

        var p = amount / 100;

        var rgba = {
            r: ((rgb2.r - rgb1.r) * p) + rgb1.r,
            g: ((rgb2.g - rgb1.g) * p) + rgb1.g,
            b: ((rgb2.b - rgb1.b) * p) + rgb1.b,
            a: ((rgb2.a - rgb1.a) * p) + rgb1.a
        };

        return tinycolor(rgba);
    };

    // Readability Functions
    // ---------------------
    // <http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef (WCAG Version 2)

    // `contrast`
    // Analyze the 2 colors and returns the color contrast defined by (WCAG Version 2)
    tinycolor.readability = function(color1, color2) {
        var c1 = tinycolor(color1);
        var c2 = tinycolor(color2);
        return (Math.max(c1.getLuminance(),c2.getLuminance())+0.05) / (Math.min(c1.getLuminance(),c2.getLuminance())+0.05);
    };

    // `isReadable`
    // Ensure that foreground and background color combinations meet WCAG2 guidelines.
    // The third argument is an optional Object.
    //      the 'level' property states 'AA' or 'AAA' - if missing or invalid, it defaults to 'AA';
    //      the 'size' property states 'large' or 'small' - if missing or invalid, it defaults to 'small'.
    // If the entire object is absent, isReadable defaults to {level:"AA",size:"small"}.

    // *Example*
    //    tinycolor.isReadable("#000", "#111") => false
    //    tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false
    tinycolor.isReadable = function(color1, color2, wcag2) {
        var readability = tinycolor.readability(color1, color2);
        var wcag2Parms, out;

        out = false;

        wcag2Parms = validateWCAG2Parms(wcag2);
        switch (wcag2Parms.level + wcag2Parms.size) {
            case "AAsmall":
            case "AAAlarge":
                out = readability >= 4.5;
                break;
            case "AAlarge":
                out = readability >= 3;
                break;
            case "AAAsmall":
                out = readability >= 7;
                break;
        }
        return out;

    };

    // `mostReadable`
    // Given a base color and a list of possible foreground or background
    // colors for that base, returns the most readable color.
    // Optionally returns Black or White if the most readable color is unreadable.
    // *Example*
    //    tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255"
    //    tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString();  // "#ffffff"
    //    tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3"
    //    tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff"
    tinycolor.mostReadable = function(baseColor, colorList, args) {
        var bestColor = null;
        var bestScore = 0;
        var readability;
        var includeFallbackColors, level, size ;
        args = args || {};
        includeFallbackColors = args.includeFallbackColors ;
        level = args.level;
        size = args.size;

        for (var i= 0; i < colorList.length ; i++) {
            readability = tinycolor.readability(baseColor, colorList[i]);
            if (readability > bestScore) {
                bestScore = readability;
                bestColor = tinycolor(colorList[i]);
            }
        }

        if (tinycolor.isReadable(baseColor, bestColor, {"level":level,"size":size}) || !includeFallbackColors) {
            return bestColor;
        }
        else {
            args.includeFallbackColors=false;
            return tinycolor.mostReadable(baseColor,["#fff", "#000"],args);
        }
    };

    // Big List of Colors
    // ------------------
    // <http://www.w3.org/TR/css3-color/#svg-color>
    var names = tinycolor.names = {
        aliceblue: "f0f8ff",
        antiquewhite: "faebd7",
        aqua: "0ff",
        aquamarine: "7fffd4",
        azure: "f0ffff",
        beige: "f5f5dc",
        bisque: "ffe4c4",
        black: "000",
        blanchedalmond: "ffebcd",
        blue: "00f",
        blueviolet: "8a2be2",
        brown: "a52a2a",
        burlywood: "deb887",
        burntsienna: "ea7e5d",
        cadetblue: "5f9ea0",
        chartreuse: "7fff00",
        chocolate: "d2691e",
        coral: "ff7f50",
        cornflowerblue: "6495ed",
        cornsilk: "fff8dc",
        crimson: "dc143c",
        cyan: "0ff",
        darkblue: "00008b",
        darkcyan: "008b8b",
        darkgoldenrod: "b8860b",
        darkgray: "a9a9a9",
        darkgreen: "006400",
        darkgrey: "a9a9a9",
        darkkhaki: "bdb76b",
        darkmagenta: "8b008b",
        darkolivegreen: "556b2f",
        darkorange: "ff8c00",
        darkorchid: "9932cc",
        darkred: "8b0000",
        darksalmon: "e9967a",
        darkseagreen: "8fbc8f",
        darkslateblue: "483d8b",
        darkslategray: "2f4f4f",
        darkslategrey: "2f4f4f",
        darkturquoise: "00ced1",
        darkviolet: "9400d3",
        deeppink: "ff1493",
        deepskyblue: "00bfff",
        dimgray: "696969",
        dimgrey: "696969",
        dodgerblue: "1e90ff",
        firebrick: "b22222",
        floralwhite: "fffaf0",
        forestgreen: "228b22",
        fuchsia: "f0f",
        gainsboro: "dcdcdc",
        ghostwhite: "f8f8ff",
        gold: "ffd700",
        goldenrod: "daa520",
        gray: "808080",
        green: "008000",
        greenyellow: "adff2f",
        grey: "808080",
        honeydew: "f0fff0",
        hotpink: "ff69b4",
        indianred: "cd5c5c",
        indigo: "4b0082",
        ivory: "fffff0",
        khaki: "f0e68c",
        lavender: "e6e6fa",
        lavenderblush: "fff0f5",
        lawngreen: "7cfc00",
        lemonchiffon: "fffacd",
        lightblue: "add8e6",
        lightcoral: "f08080",
        lightcyan: "e0ffff",
        lightgoldenrodyellow: "fafad2",
        lightgray: "d3d3d3",
        lightgreen: "90ee90",
        lightgrey: "d3d3d3",
        lightpink: "ffb6c1",
        lightsalmon: "ffa07a",
        lightseagreen: "20b2aa",
        lightskyblue: "87cefa",
        lightslategray: "789",
        lightslategrey: "789",
        lightsteelblue: "b0c4de",
        lightyellow: "ffffe0",
        lime: "0f0",
        limegreen: "32cd32",
        linen: "faf0e6",
        magenta: "f0f",
        maroon: "800000",
        mediumaquamarine: "66cdaa",
        mediumblue: "0000cd",
        mediumorchid: "ba55d3",
        mediumpurple: "9370db",
        mediumseagreen: "3cb371",
        mediumslateblue: "7b68ee",
        mediumspringgreen: "00fa9a",
        mediumturquoise: "48d1cc",
        mediumvioletred: "c71585",
        midnightblue: "191970",
        mintcream: "f5fffa",
        mistyrose: "ffe4e1",
        moccasin: "ffe4b5",
        navajowhite: "ffdead",
        navy: "000080",
        oldlace: "fdf5e6",
        olive: "808000",
        olivedrab: "6b8e23",
        orange: "ffa500",
        orangered: "ff4500",
        orchid: "da70d6",
        palegoldenrod: "eee8aa",
        palegreen: "98fb98",
        paleturquoise: "afeeee",
        palevioletred: "db7093",
        papayawhip: "ffefd5",
        peachpuff: "ffdab9",
        peru: "cd853f",
        pink: "ffc0cb",
        plum: "dda0dd",
        powderblue: "b0e0e6",
        purple: "800080",
        rebeccapurple: "663399",
        red: "f00",
        rosybrown: "bc8f8f",
        royalblue: "4169e1",
        saddlebrown: "8b4513",
        salmon: "fa8072",
        sandybrown: "f4a460",
        seagreen: "2e8b57",
        seashell: "fff5ee",
        sienna: "a0522d",
        silver: "c0c0c0",
        skyblue: "87ceeb",
        slateblue: "6a5acd",
        slategray: "708090",
        slategrey: "708090",
        snow: "fffafa",
        springgreen: "00ff7f",
        steelblue: "4682b4",
        tan: "d2b48c",
        teal: "008080",
        thistle: "d8bfd8",
        tomato: "ff6347",
        turquoise: "40e0d0",
        violet: "ee82ee",
        wheat: "f5deb3",
        white: "fff",
        whitesmoke: "f5f5f5",
        yellow: "ff0",
        yellowgreen: "9acd32"
    };

    // Make it easy to access colors via `hexNames[hex]`
    var hexNames = tinycolor.hexNames = flip(names);

    // Utilities
    // ---------

    // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
    function flip(o) {
        var flipped = { };
        for (var i in o) {
            if (o.hasOwnProperty(i)) {
                flipped[o[i]] = i;
            }
        }
        return flipped;
    }

    // Return a valid alpha value [0,1] with all invalid values being set to 1
    function boundAlpha(a) {
        a = parseFloat(a);

        if (isNaN(a) || a < 0 || a > 1) {
            a = 1;
        }

        return a;
    }

    // Take input from [0, n] and return it as [0, 1]
    function bound01(n, max) {
        if (isOnePointZero(n)) { n = "100%"; }

        var processPercent = isPercentage(n);
        n = mathMin(max, mathMax(0, parseFloat(n)));

        // Automatically convert percentage into number
        if (processPercent) {
            n = parseInt(n * max, 10) / 100;
        }

        // Handle floating point rounding errors
        if ((Math.abs(n - max) < 0.000001)) {
            return 1;
        }

        // Convert into [0, 1] range if it isn't already
        return (n % max) / parseFloat(max);
    }

    // Force a number between 0 and 1
    function clamp01(val) {
        return mathMin(1, mathMax(0, val));
    }

    // Parse a base-16 hex value into a base-10 integer
    function parseIntFromHex(val) {
        return parseInt(val, 16);
    }

    // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
    // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
    function isOnePointZero(n) {
        return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
    }

    // Check to see if string passed in is a percentage
    function isPercentage(n) {
        return typeof n === "string" && n.indexOf('%') != -1;
    }

    // Force a hex value to have 2 characters
    function pad2(c) {
        return c.length == 1 ? '0' + c : '' + c;
    }

    // Replace a decimal with it's percentage value
    function convertToPercentage(n) {
        if (n <= 1) {
            n = (n * 100) + "%";
        }

        return n;
    }

    // Converts a decimal to a hex value
    function convertDecimalToHex(d) {
        return Math.round(parseFloat(d) * 255).toString(16);
    }
    // Converts a hex value to a decimal
    function convertHexToDecimal(h) {
        return (parseIntFromHex(h) / 255);
    }

    var matchers = (function() {

        // <http://www.w3.org/TR/css3-values/#integers>
        var CSS_INTEGER = "[-\\+]?\\d+%?";

        // <http://www.w3.org/TR/css3-values/#number-value>
        var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";

        // Allow positive/negative integer/number.  Don't capture the either/or, just the entire outcome.
        var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";

        // Actual matching.
        // Parentheses and commas are optional, but not required.
        // Whitespace can take the place of commas or opening paren
        var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
        var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";

        return {
            CSS_UNIT: new RegExp(CSS_UNIT),
            rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
            rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
            hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
            hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
            hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
            hsva: new RegExp("hsva" + PERMISSIVE_MATCH4),
            hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
            hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
            hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
            hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
        };
    })();

    // `isValidCSSUnit`
    // Take in a single string / number and check to see if it looks like a CSS unit
    // (see `matchers` above for definition).
    function isValidCSSUnit(color) {
        return !!matchers.CSS_UNIT.exec(color);
    }

    // `stringInputToObject`
    // Permissive string parsing.  Take in a number of formats, and output an object
    // based on detected format.  Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
    function stringInputToObject(color) {

        color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
        var named = false;
        if (names[color]) {
            color = names[color];
            named = true;
        }
        else if (color == 'transparent') {
            return { r: 0, g: 0, b: 0, a: 0, format: "name" };
        }

        // Try to match string input using regular expressions.
        // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
        // Just return an object and let the conversion functions handle that.
        // This way the result will be the same whether the tinycolor is initialized with string or object.
        var match;
        if ((match = matchers.rgb.exec(color))) {
            return { r: match[1], g: match[2], b: match[3] };
        }
        if ((match = matchers.rgba.exec(color))) {
            return { r: match[1], g: match[2], b: match[3], a: match[4] };
        }
        if ((match = matchers.hsl.exec(color))) {
            return { h: match[1], s: match[2], l: match[3] };
        }
        if ((match = matchers.hsla.exec(color))) {
            return { h: match[1], s: match[2], l: match[3], a: match[4] };
        }
        if ((match = matchers.hsv.exec(color))) {
            return { h: match[1], s: match[2], v: match[3] };
        }
        if ((match = matchers.hsva.exec(color))) {
            return { h: match[1], s: match[2], v: match[3], a: match[4] };
        }
        if ((match = matchers.hex8.exec(color))) {
            return {
                r: parseIntFromHex(match[1]),
                g: parseIntFromHex(match[2]),
                b: parseIntFromHex(match[3]),
                a: convertHexToDecimal(match[4]),
                format: named ? "name" : "hex8"
            };
        }
        if ((match = matchers.hex6.exec(color))) {
            return {
                r: parseIntFromHex(match[1]),
                g: parseIntFromHex(match[2]),
                b: parseIntFromHex(match[3]),
                format: named ? "name" : "hex"
            };
        }
        if ((match = matchers.hex4.exec(color))) {
            return {
                r: parseIntFromHex(match[1] + '' + match[1]),
                g: parseIntFromHex(match[2] + '' + match[2]),
                b: parseIntFromHex(match[3] + '' + match[3]),
                a: convertHexToDecimal(match[4] + '' + match[4]),
                format: named ? "name" : "hex8"
            };
        }
        if ((match = matchers.hex3.exec(color))) {
            return {
                r: parseIntFromHex(match[1] + '' + match[1]),
                g: parseIntFromHex(match[2] + '' + match[2]),
                b: parseIntFromHex(match[3] + '' + match[3]),
                format: named ? "name" : "hex"
            };
        }

        return false;
    }

    function validateWCAG2Parms(parms) {
        // return valid WCAG2 parms for isReadable.
        // If input parms are invalid, return {"level":"AA", "size":"small"}
        var level, size;
        parms = parms || {"level":"AA", "size":"small"};
        level = (parms.level || "AA").toUpperCase();
        size = (parms.size || "small").toLowerCase();
        if (level !== "AA" && level !== "AAA") {
            level = "AA";
        }
        if (size !== "small" && size !== "large") {
            size = "small";
        }
        return {"level":level, "size":size};
    }

    // Node: Export function
    if (typeof module !== "undefined" && module.exports) {
        module.exports = tinycolor;
    }
    // AMD/requirejs: Define the module
    else if (typeof define === 'function' && define.amd) {
        define(function () {return tinycolor;});
    }
    // Browser: Expose to window
    else {
        window.tinycolor = tinycolor;
    }

    })(Math);

repo_name: Area/degrees

license: mit


code:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

"@types/tinycolor2@^1.4.2":
  version "1.4.2"
  resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf"
  integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw==

typescript@~3.9.3:
  version "3.9.9"
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674"
  integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==


code:
#!/usr/bin/env python3
import fileinput
import re
import sys

WARNING = '\033[93m'
ENDC = '\033[0m'

def myprint(*args):
	global filename
	print(WARNING, filename + ":", ENDC,*args)

def yield_line_and_islastline(f):
	global filename
	global linenumber
	try:
		prevline = next(f)
		filename = fileinput.filename()
		linenumber = fileinput.filelineno()
	except StopIteration:
		return
	for line in f:
		yield (prevline, f.isfirstline())
		filename = fileinput.filename()
		linenumber = fileinput.filelineno()
		prevline = line
	yield prevline, True

pattern = re.compile(r'(<<(\/?) *(if|for|else|switch|case|replace|link)[^a-zA-Z][^<>]*)')

tagfound = []
try:
	for line, isLastLine in yield_line_and_islastline(fileinput.input()):
		for (whole,end,tag) in re.findall(pattern,line):
			if tag == "else" or tag == 'case':
				if len(tagfound) == 0:
					myprint("Found", tag, "but with no opening tag:")
					myprint("  ", linenumber,":", whole)
					fileinput.nextfile()
				lasttag = tagfound[-1]
				if (tag == "else" and lasttag["tag"] != "if") or (tag == "case" and lasttag["tag"] != "switch"):
					myprint("Mismatched else: Opening tag was:")
					myprint("  ",lasttag["linenumber"],":", lasttag["whole"])
					myprint("But this tag was:")
					myprint("  ",linenumber,":", whole)
					fileinput.nextfile()
					break
			elif end != '/':
				tagfound.append({"whole": whole, "linenumber":linenumber,"tag":tag})
			else:
				if len(tagfound) == 0:
					myprint("Found closing tag but with no opening tag:")
					myprint("  ", linenumber,":", whole)
					fileinput.nextfile()
					break
				lasttag = tagfound.pop()
				if lasttag["tag"] != tag:
					myprint("Mismatched tag: Opening tag was:")
					myprint("  ",lasttag["linenumber"],":", lasttag["whole"])
					myprint("Closing tag was:")
					myprint("  ",linenumber,":", whole)
					fileinput.nextfile()
					break

		if isLastLine:
			if len(tagfound) != 0:
				myprint("End of file found but", len(tagfound), ("tag hasn't" if len(tagfound)==1 else "tags haven't"), "been closed:")
			for tag in tagfound:
				myprint("  ", tag["linenumber"],":", tag["whole"])
			tagfound = []
except UnicodeDecodeError as e:
	myprint(e)
	print("   Hint: In Linux, you can get more details about Unicode errors by running:")
	print("	 isutf8", fileinput.filename())
	print("   :Note it might be caused by ", filename)


code:
# Grayscale image converters

Utilities to convert legacy red-based sprites to grayscale sprites fit to hard-light blending in new renderer.

## Browser version

1. Open `index.html` in browser
2. Open "red" image files (can do multiple at once)
3. Adjust settings
4. Right-click on the every converted image and save to file. Don't forget to add `_gray` suffix!

## Command-line utility to mass-convert files

1. `npm install` (once, to download dependencies)
2. `node index.js`

It will convert ALL clothes items, according to setup variables.

Edit the script to process files separately.

If you get "unrecognised content at end of stream" error, see note in index.js.


code:
<!DOCTYPE html>

    <meta charset="UTF-8">
    <title>DoL sprite grayscale converter</title>

    Open image file(s):
    <input type="file" id="filectl" multiple>

    ResultR,G,B = (
    Rfactor × SrcRγ +
    Gfactor × SrcGγ +
    Bfactor × SrcBγ
    ) × Scale / ( Rfactor + Gfactor + Bfactor )

Paremeters

            R factor: <input type="number" id="param0" oninput="gsfactors[0]= +this.value;processImages()">

            G factor: <input type="number" id="param1" oninput="gsfactors[1]= +this.value;processImages()">

            B factor: <input type="number" id="param2" oninput="gsfactors[2]= +this.value;processImages()">

            Scale by: <input type="number" id="param3" oninput="gsfactors[3]= +this.value;processImages()">

            Gamma   : <input type="number" step="0.1" id="param4" oninput="gsfactors[4] = +this.value;processImages()">

            Test color: <input type="color" value="#FF0000" oninput="testcolor=this.value;makexamples()">

            Default clothing: [2, 1, 1, 1, 1]

            Hair: [2, 1, 1, 1, 1.5]

            Simple grayscale: [0.21, 0.71, 0.08, 1, 2.2]


code:
let fs = require("fs");
let PNG = require("pngjs").PNG;
const baseurl = "../../"

// The pngjs library doesn't process some files because of "unrecognised content at end of stream"
// Shamelessly commenting the throw statement in node_modules/pngjs/lib/sync-reader.js line 43 works
// I'm sorry

const clothespreset = [2,1,1,1,1];
const hairpreset = [2,1,1,1,1.5];

/**
 * In-place convert image to grayscale
 * @param image PNG image
 * @param params Processing params: [r_factor, g_factor, b_factor, scale, gamma]
 */
function desaturate(image, params=[2,1,1,1,1]) {
	const {width,height,data} = image;
	const [rf,gf,bf,sf,gamma] = params;
	const f = sf/(rf+gf+bf);
	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			let idx = (width * y + x) << 2;
			if (data[idx+3] === 0) continue;
			// brightness = 0.5*R + 0.25*G + 0.25*B
			let r = data[idx]/255;
			let g = data[idx+1]/255;
			let b = data[idx+2]/255;

			let value = (rf*r**gamma + gf*g**gamma + bf*b**gamma)*f;
			value = Math.max(0,Math.min(255,(value*255)|0));
			data[idx] = value;
			data[idx + 1] = value;
			data[idx + 2] = value;
		}
	}
}

/**
 * Convert one file, and save result with "_gray" suffix
 * @param path Relative path to PNG file
 * @param params Processing params: [r_factor, g_factor, b_factor, scale, gamma]
 * @param skipexisting Do nothing if "_gray" file exists (does not check contents)
 */
function desaturateFile(path, params=[2,1,1,1,1],skipexisting=true) {
	let newname = path.replace(".png","_gray.png");
	if (skipexisting && fs.existsSync(path)) return;
	let data = fs.readFileSync(path);
	let png = PNG.sync.read(data);
	desaturate(png,params);
	data = PNG.sync.write(png, {colorType:4});
	fs.writeFileSync(newname, data)
}

/**
 * Pick property from source file declaring JS object
 * @param json string containing JS object
 * @param propname property name
 * @return property value
 */
function lookupprop(json,propname) {
	let regex = new RegExp(`['"]?${propname}['"]?\\s*:\\s*([0-9.+\-eE]+|true|false|"[^"\\\\]*"|'[^'\\\\]*')\\s*[,}]`);
	let m = regex.exec(json);
	if (!m) return undefined;
	return JSON.parse(m[1]);
}

/**
 * Pick properties from source file declaring JS object
 * @param json string containing JS obejct
 * @param {string[]} propnames property names
 * @return {object} key-value object with all properties
 */
function lookupprops(json,propnames) {
	let object = {};
	for (let propname of propnames) object[propname] = lookupprop(json,propname);
	return object;
}

// utility function for back_img_colour
function colorByOption(optionvalue, maincolor) {
	switch (optionvalue) {
		case "":
		case undefined:
			return !!maincolor;
		case "primary":
		case "secondary":
			return true;
		default:
			return false;
	}
}

let failedFiles = [];

function processSlot(setupfile,imgdir,varname,hasIntegrity) {
	console.log("Trying " + setupfile);
	let s = fs.readFileSync(baseurl + setupfile, {encoding: "utf-8"});
	let regex = new RegExp("<<set *" + varname.replace(/\./g, '\\.') + " *to\\s*\\[((?:[^\\[]*|\\[[^\\[\\]]*])*)]", "mg")
	let m = regex.exec(s);
	if (!m) {
		console.log("Not found variable " + varname);
	} else {
		console.log("Found variable " + varname);
		let vw = m[1];
		let itemrex = /{(?:[^{}]|{[^{}]*})*}/mg;
		while (true) {
			let item = itemrex.exec(vw);
			if (!item) break;
			let json = lookupprops(item[0], [
				"variable",
				"colour_sidebar",
				"accessory_colour_sidebar",
				"accessory",
				"accessory_integrity_img",
				"breast_img",
				"breast_acc_img",
				"back_img",
				"back_img_colour",
				"sleeve_img",
				"sleeve_colour",
				"penis_img",
				"hood"
			]);
			let v = json.variable;
			console.log("Found item " + v);
			let files = [];
			if (varname === "setup.clothes.hands"){ // hack but what can I do...
				if (json.colour_sidebar) {
					files.push("left","right","left_cover","right_cover");
				}
				if (json.accessory && json.accessory_colour_sidebar) {
					files.push("left_acc","right_acc","left_cover_acc","right_cover_acc");
				}
			} else {
				if (json.back_img) {
					if (colorByOption(json.back_img_colour, json.colour_sidebar)) {
						files.push("back");
					}
				}
				if (json.colour_sidebar) {
					let ifiles = ["full"];
					if (hasIntegrity) {
						ifiles.push("frayed", "tattered", "torn");
					}
					files.push(...ifiles);
					if (json.hood) {
						for (let file of ifiles) {
							files.push(file + "_down");
						}
					}
					if (varname === "setup.clothes.neck") {
						for (let file of ifiles) {
							files.push(file + "_nocollar");
						}
					}
					if (json.breast_img === 1) {
						for (let i = 0; i <= 5; i++) {
							files.push("" + i);
						}
					}
					if (json.penis_img === 1) {
						files.push("penis");
					}
				}
				if (json.sleeve_img) {
					if (colorByOption(json.sleeve_colour, json.colour_sidebar)) {
						files.push("right", "right_cover", "left", "left_cover");
					}
				}
				if (json.accessory === 1 && json.accessory_colour_sidebar) {
					let accfiles = [];
					if (json.accessory_integrity_img) {
						accfiles.push("full_acc", "frayed_acc", "tattered_acc", "torn_acc");
					} else {
						accfiles.push("acc")
					}
					files.push(...accfiles);
					if (json.hood) {
						for (let file of accfiles) {
							files.push(file + "_down");
						}
					}
					if (json.breast_acc_img === 1) {
						for (let i = 0; i <= 5; i++) {
							files.push("" + i + "_acc");
						}
					}
					if (json.penis_img === 1) {
						files.push("acc_penis");
					}
				}
			}
			if (files.length > 0) {
				console.log("Expecting files:", ...files);
				for (let file of files) {
					let path = baseurl + imgdir + v + "/"+file +".png";
					if (fs.existsSync(path)) {
						console.log("Processing file "+path);
						try {
							desaturateFile(path, clothespreset);
						} catch (e) {
							console.error(e.message);
							failedFiles.push(path)
						}
					}
				}
			}
		}
	}
}

function convertAllClothes() {
	let slots = ["hands", "face", "feet", "genitals", "hands", "head", "legs", "lower", "neck", "over_head", "over_lower", "over_upper", "upper"];
	for (let slot of slots) {
		processSlot(`game/base-clothing/clothing-${slot}.twee`, `img/clothes/${slot}/`, `setup.clothes.${slot}`, true);
	}
	processSlot("game/base-clothing/clothing-under.twee", "img/clothes/under_lower/", "setup.clothes.under_lower", true);
	processSlot("game/base-clothing/clothing-under-upper.twee", "img/clothes/under_upper/", "setup.clothes.under_upper", true);
}

function convertAllHair() {
	let lengths = ["short","shoulder","chest","navel","thighs","feet"];
	let sidedir = baseurl+"img/hair/sides";
	let fringedir = baseurl+"img/hair/fringe";
	let files = [];
	for (let sidestyle of fs.readdirSync(sidedir)) {
		for (let l of lengths) {
			let path = sidedir+"/"+sidestyle+"/"+l+".png";
			files.push(path);
		}
	}
	for (let fringestyle of fs.readdirSync(fringedir)) {
		for (let l of lengths) {
			let path = fringedir+"/"+fringestyle+"/"+l+".png";
			files.push(path);
		}
	}
	files.push(baseurl+"img/hair/red/backhairthighsred.png");
	files.push(baseurl+"img/hair/red/backhairfeetred.png");
	for (let path of files) {
		if (!fs.existsSync(path)) continue;
		console.log("Processing ",path);
		try {
			desaturateFile(path,hairpreset)
		} catch (e) {
			console.error(e.message);
			failedFiles.push(path);
		}
	}
}

convertAllClothes();
// convertAllHair();

if (failedFiles.length>0) {
	console.warn("Failed to process: ")
	for (let f of failedFiles) {
		console.warn(f);
	}
}


code:
IMPORTANT: The release build uses a sugarcube version with all the below changes. The repo version is more up-do-date, and has other changes, but I've been unable to work out how to include all but the decompression change. Needs fixing. - Vrelnir

These are the adjustments made to the base sugarcube version used by the release build.

The adjustments are for "tweego\storyFormats\sugarcube-2\format.js"

Save system:

Find:  .append(document.createTextNode(s+1)),t.slots[s]?(d.append(
Insert:  e(\"save\",\"ui-close\",L10n.get(\"savesLabelSave\"),s,Save.slots.save),

Final result:  .append(document.createTextNode(s+1)),t.slots[s]?(d.append(e(\"save\",\"ui-close\",L10n.get(\"savesLabelSave\"),s,Save.slots.save),e(\"load\",\"ui-close\",L10n.get(\"savesLabelLoad\"),s,function(e)

Find: &&(t.expired=[].concat(_toConsumableArray(V)))
Or: H=r.expired===undefined?[]:[].concat(_toConsumableArray(r.expired))

Remove .concat(_toConsumableArray(V))
Or .concat(_toConsumableArray(r.expired))

(or the entire string)
The one you find and remove depends on the sugarcube version

Removing the concat basically saves expired as [] every time it updates. Deleting the entire string removes State.expired entirely (at least from save files). I didn't run into any errors with either,
 but I imagine if we tried to use visited, the first would assume its your first time every time, the other would throw an error that State.expired doesn't exist.

To prevent scroll to top of page on reset:

Find:  var t=jQuery(this);t.ariaIsDisabled()||(t.is("[aria-pressed]")&&t.attr("aria-pressed","true"===t.attr("aria-pressed")?"false":"true"),e.apply(this,arguments))
Or:  var t=jQuery(this);t.ariaIsDisabled()||(t.is(\"[aria-pressed]\")&&t.attr(\"aria-pressed\",\"true\"===t.attr(\"aria-pressed\")?\"false\":\"true\"),e.apply(this,arguments))

Replace with the following:

var t=jQuery(this);const dataPassage = t.attr('data-passage');const initialDataPassage = window && window.SugarCube && window.SugarCube.State && window.SugarCube.State.passage;const savedYOffset = window.pageYOffset;t.is(\"[aria-pressed]\")&&t.attr(\"aria-pressed\",\"true\"===t.attr(\"aria-pressed\")?\"false\":\"true\"),e.apply(this,arguments);const doJump = function(){ window.scrollTo(0, savedYOffset); }if ( dataPassage && (window.lastDataPassageLink === dataPassage || initialDataPassage === dataPassage))    doJump();window.lastDataPassageLink = dataPassage;

Decompresses saves and improves speed at cost of save file size:

Find: {return LZString.compressToUTF16(JSON.stringify(e))}},{key:\"_deserialize\",value:function(e){return JSON.parse(LZString.decompressFromUTF16(e))}}]),e}();

Replace: {return JSON.stringify(e)}},{key:\"_deserialize\",value:function(e){return JSON.parse((!e || e[0]==\"{\")?e:LZString.decompressFromUTF16(e))}}]),e}();

path: devTools/sugarcube edits.txt